Change featured hashtags to be displayed in navigation panel (#19382)
This commit is contained in:
		
							parent
							
								
									2690a13d0e
								
							
						
					
					
						commit
						6ab724918b
					
				
					 9 changed files with 161 additions and 101 deletions
				
			
		| 
						 | 
				
			
			@ -65,16 +65,27 @@ ImmutableHashtag.propTypes = {
 | 
			
		|||
  hashtag: ImmutablePropTypes.map.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Hashtag = ({ name, href, to, people, history, className }) => (
 | 
			
		||||
const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
 | 
			
		||||
  <div className={classNames('trends__item', className)}>
 | 
			
		||||
    <div className='trends__item__name'>
 | 
			
		||||
      <Permalink href={href} to={to}>
 | 
			
		||||
        {name ? <React.Fragment>#<span>{name}</span></React.Fragment> : <Skeleton width={50} />}
 | 
			
		||||
      </Permalink>
 | 
			
		||||
 | 
			
		||||
      {typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />}
 | 
			
		||||
      {description ? (
 | 
			
		||||
        <span>{description}</span>
 | 
			
		||||
      ) : (
 | 
			
		||||
        typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    {typeof uses !== 'undefined' && (
 | 
			
		||||
      <div className='trends__item__current'>
 | 
			
		||||
        <ShortNumber value={uses} />
 | 
			
		||||
      </div>
 | 
			
		||||
    )}
 | 
			
		||||
 | 
			
		||||
    {withGraph && (
 | 
			
		||||
      <div className='trends__item__sparkline'>
 | 
			
		||||
        <SilentErrorBoundary>
 | 
			
		||||
          <Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +93,7 @@ const Hashtag = ({ name, href, to, people, history, className }) => (
 | 
			
		|||
          </Sparklines>
 | 
			
		||||
        </SilentErrorBoundary>
 | 
			
		||||
      </div>
 | 
			
		||||
    )}
 | 
			
		||||
  </div>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -90,9 +102,15 @@ Hashtag.propTypes = {
 | 
			
		|||
  href: PropTypes.string,
 | 
			
		||||
  to: PropTypes.string,
 | 
			
		||||
  people: PropTypes.number,
 | 
			
		||||
  description: PropTypes.node,
 | 
			
		||||
  uses: PropTypes.number,
 | 
			
		||||
  history: PropTypes.arrayOf(PropTypes.number),
 | 
			
		||||
  className: PropTypes.string,
 | 
			
		||||
  withGraph: PropTypes.bool,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Hashtag.defaultProps = {
 | 
			
		||||
  withGraph: true,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Hashtag;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										30
									
								
								app/javascript/mastodon/components/navigation_portal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/javascript/mastodon/components/navigation_portal.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { Switch, Route, withRouter } from 'react-router-dom';
 | 
			
		||||
import { showTrends } from 'mastodon/initial_state';
 | 
			
		||||
import Trends from 'mastodon/features/getting_started/containers/trends_container';
 | 
			
		||||
import AccountNavigation from 'mastodon/features/account/navigation';
 | 
			
		||||
 | 
			
		||||
const DefaultNavigation = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    {showTrends && (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className='flex-spacer' />
 | 
			
		||||
        <Trends />
 | 
			
		||||
      </>
 | 
			
		||||
    )}
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default @withRouter
 | 
			
		||||
class NavigationPortal extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    return (
 | 
			
		||||
      <Switch>
 | 
			
		||||
        <Route path='/@:acct/(tagged/:tagged?)?' component={AccountNavigation} />
 | 
			
		||||
        <Route component={DefaultNavigation} />
 | 
			
		||||
      </Switch>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +1,16 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import Permalink from 'mastodon/components/permalink';
 | 
			
		||||
import ShortNumber from 'mastodon/components/short_number';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
			
		||||
import Hashtag from 'mastodon/components/hashtag';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  hashtag_all: { id: 'account.hashtag_all', defaultMessage: 'All' },
 | 
			
		||||
  hashtag_all_description: { id: 'account.hashtag_all_description', defaultMessage: 'All posts (deselect hashtags)' },
 | 
			
		||||
  hashtag_select_description: { id: 'account.hashtag_select_description', defaultMessage: 'Select hashtag #{name}' },
 | 
			
		||||
  statuses_counter: { id: 'account.statuses_counter', defaultMessage: '{count, plural, one {{counter} Post} other {{counter} Posts}}' },
 | 
			
		||||
  lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
 | 
			
		||||
  empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { account }) => ({
 | 
			
		||||
  featuredTags: state.getIn(['user_lists', 'featured_tags', account.get('id'), 'items'], ImmutableList()),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
@injectIntl
 | 
			
		||||
export default @injectIntl
 | 
			
		||||
class FeaturedTags extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,34 +25,27 @@ class FeaturedTags extends ImmutablePureComponent {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { account, featuredTags, tagged, intl } = this.props;
 | 
			
		||||
    const { account, featuredTags, intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!account || featuredTags.isEmpty()) {
 | 
			
		||||
    if (!account || account.get('suspended') || featuredTags.isEmpty()) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const suspended = account.get('suspended');
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={classNames('account__header', 'advanced', { inactive: !!account.get('moved') })}>
 | 
			
		||||
        <div className='account__header__extra'>
 | 
			
		||||
          <div className='account__header__extra__hashtag-links'>
 | 
			
		||||
            <Permalink key='all' className={classNames('account__hashtag-link', { active: !tagged })} title={intl.formatMessage(messages.hashtag_all_description)} href={account.get('url')} to={`/@${account.get('acct')}`}>{intl.formatMessage(messages.hashtag_all)}</Permalink>
 | 
			
		||||
            {!suspended && featuredTags.map(featuredTag => {
 | 
			
		||||
              const name  = featuredTag.get('name');
 | 
			
		||||
              const url   = featuredTag.get('url');
 | 
			
		||||
              const to    = `/@${account.get('acct')}/tagged/${name}`;
 | 
			
		||||
              const desc  = intl.formatMessage(messages.hashtag_select_description, { name });
 | 
			
		||||
              const count = featuredTag.get('statuses_count');
 | 
			
		||||
      <div className='getting-started__trends'>
 | 
			
		||||
        <h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
 | 
			
		||||
 | 
			
		||||
              return (
 | 
			
		||||
                <Permalink key={`#${name}`} className={classNames('account__hashtag-link', { active: this.context.router.history.location.pathname === to })} title={desc} href={url} to={to}>
 | 
			
		||||
                  #{name} <span title={intl.formatMessage(messages.statuses_counter, { count: count, counter: intl.formatNumber(count) })}>({<ShortNumber value={count} />})</span>
 | 
			
		||||
                </Permalink>
 | 
			
		||||
              );
 | 
			
		||||
            })}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {featuredTags.map(featuredTag => (
 | 
			
		||||
          <Hashtag
 | 
			
		||||
            key={featuredTag.get('name')}
 | 
			
		||||
            name={featuredTag.get('name')}
 | 
			
		||||
            href={featuredTag.get('url')}
 | 
			
		||||
            to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
 | 
			
		||||
            uses={featuredTag.get('statuses_count')}
 | 
			
		||||
            withGraph={false}
 | 
			
		||||
            description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
import { connect } from 'react-redux';
 | 
			
		||||
import FeaturedTags from '../components/featured_tags';
 | 
			
		||||
import { makeGetAccount } from 'mastodon/selectors';
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = () => {
 | 
			
		||||
  const getAccount = makeGetAccount();
 | 
			
		||||
 | 
			
		||||
  return (state, { accountId }) => ({
 | 
			
		||||
    account: getAccount(state, accountId),
 | 
			
		||||
    featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(FeaturedTags);
 | 
			
		||||
							
								
								
									
										51
									
								
								app/javascript/mastodon/features/account/navigation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/javascript/mastodon/features/account/navigation.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, { match: { params: { acct } } }) => {
 | 
			
		||||
  const accountId = state.getIn(['accounts_map', acct]);
 | 
			
		||||
 | 
			
		||||
  if (!accountId) {
 | 
			
		||||
    return {
 | 
			
		||||
      isLoading: true,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    accountId,
 | 
			
		||||
    isLoading: false,
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default @connect(mapStateToProps)
 | 
			
		||||
class AccountNavigation extends React.PureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    match: PropTypes.shape({
 | 
			
		||||
      params: PropTypes.shape({
 | 
			
		||||
        acct: PropTypes.string,
 | 
			
		||||
        tagged: PropTypes.string,
 | 
			
		||||
      }).isRequired,
 | 
			
		||||
    }).isRequired,
 | 
			
		||||
 | 
			
		||||
    accountId: PropTypes.string,
 | 
			
		||||
    isLoading: PropTypes.bool,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { accountId, isLoading, match: { params: { tagged } } } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (isLoading) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className='flex-spacer' />
 | 
			
		||||
        <FeaturedTags accountId={accountId} tagged={tagged} />
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,7 @@
 | 
			
		|||
import React, { Fragment } from 'react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import InnerHeader from '../../account/components/header';
 | 
			
		||||
import FeaturedTags from '../../account/components/featured_tags';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import MovedNote from './moved_note';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +27,6 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
    hideTabs: PropTypes.bool,
 | 
			
		||||
    domain: PropTypes.string.isRequired,
 | 
			
		||||
    hidden: PropTypes.bool,
 | 
			
		||||
    tagged: PropTypes.string,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static contextTypes = {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +102,7 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { account, hidden, hideTabs, tagged } = this.props;
 | 
			
		||||
    const { account, hidden, hideTabs } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (account === null) {
 | 
			
		||||
      return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -136,15 +134,11 @@ export default class Header extends ImmutablePureComponent {
 | 
			
		|||
        />
 | 
			
		||||
 | 
			
		||||
        {!(hideTabs || hidden) && (
 | 
			
		||||
          <Fragment>
 | 
			
		||||
          <div className='account__section-headline'>
 | 
			
		||||
            <NavLink exact to={`/@${account.get('acct')}`}><FormattedMessage id='account.posts' defaultMessage='Posts' /></NavLink>
 | 
			
		||||
            <NavLink exact to={`/@${account.get('acct')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Posts and replies' /></NavLink>
 | 
			
		||||
            <NavLink exact to={`/@${account.get('acct')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
            <FeaturedTags account={account} tagged={tagged} />
 | 
			
		||||
          </Fragment>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,13 +3,13 @@ import PropTypes from 'prop-types';
 | 
			
		|||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import Logo from 'mastodon/components/logo';
 | 
			
		||||
import TrendsContainer from 'mastodon/features/getting_started/containers/trends_container';
 | 
			
		||||
import { showTrends, timelinePreview } from 'mastodon/initial_state';
 | 
			
		||||
import { timelinePreview } from 'mastodon/initial_state';
 | 
			
		||||
import ColumnLink from './column_link';
 | 
			
		||||
import FollowRequestsColumnLink from './follow_requests_column_link';
 | 
			
		||||
import ListPanel from './list_panel';
 | 
			
		||||
import NotificationsCounterIcon from './notifications_counter_icon';
 | 
			
		||||
import SignInBanner from './sign_in_banner';
 | 
			
		||||
import NavigationPortal from 'mastodon/components/navigation_portal';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
 | 
			
		||||
| 
						 | 
				
			
			@ -93,12 +93,7 @@ class NavigationPanel extends React.Component {
 | 
			
		|||
          <ColumnLink transparent to='/about' icon='ellipsis-h' text={intl.formatMessage(messages.about)} />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {showTrends && (
 | 
			
		||||
          <React.Fragment>
 | 
			
		||||
            <div className='flex-spacer' />
 | 
			
		||||
            <TrendsContainer />
 | 
			
		||||
          </React.Fragment>
 | 
			
		||||
        )}
 | 
			
		||||
        <NavigationPortal />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7338,33 +7338,6 @@ noscript {
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__hashtag-links {
 | 
			
		||||
      overflow: hidden;
 | 
			
		||||
      padding: 10px 5px;
 | 
			
		||||
      margin: 0;
 | 
			
		||||
      color: $darker-text-color;
 | 
			
		||||
      border-bottom: 1px solid lighten($ui-base-color, 12%);
 | 
			
		||||
 | 
			
		||||
      a {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        color: $darker-text-color;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
        padding: 5px 10px;
 | 
			
		||||
        font-weight: 500;
 | 
			
		||||
 | 
			
		||||
        strong {
 | 
			
		||||
          font-weight: 700;
 | 
			
		||||
          color: $primary-text-color;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      a.active {
 | 
			
		||||
        color: darken($ui-base-color, 4%);
 | 
			
		||||
        background: $darker-text-color;
 | 
			
		||||
        border-radius: 18px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  &__account-note {
 | 
			
		||||
| 
						 | 
				
			
			@ -7482,12 +7455,6 @@ noscript {
 | 
			
		|||
      margin-left: 5px;
 | 
			
		||||
      color: $secondary-text-color;
 | 
			
		||||
      text-decoration: none;
 | 
			
		||||
 | 
			
		||||
      &__asterisk {
 | 
			
		||||
        color: $darker-text-color;
 | 
			
		||||
        font-size: 18px;
 | 
			
		||||
        vertical-align: super;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &__sparkline {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,4 +16,12 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer
 | 
			
		|||
  def name
 | 
			
		||||
    object.display_name
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def statuses_count
 | 
			
		||||
    object.statuses_count.to_s
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def last_status_at
 | 
			
		||||
    object.last_status_at&.to_date&.iso8601
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue