Add featured tags selector for WebUI (#19358)

* Add featured tags selector for WebUI

* Add title to tag count
This commit is contained in:
Takeshi Umeda
2022-10-16 15:43:59 +09:00
committed by GitHub
parent c618d3a0a5
commit 4c7b5fb6c1
9 changed files with 194 additions and 20 deletions

View File

@ -0,0 +1,71 @@
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';
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}}' },
});
const mapStateToProps = (state, { account }) => ({
featuredTags: state.getIn(['user_lists', 'featured_tags', account.get('id'), 'items'], ImmutableList()),
});
export default @connect(mapStateToProps)
@injectIntl
class FeaturedTags extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
account: ImmutablePropTypes.map,
featuredTags: ImmutablePropTypes.list,
tagged: PropTypes.string,
intl: PropTypes.object.isRequired,
};
render () {
const { account, featuredTags, tagged, intl } = this.props;
if (!account || 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');
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>
</div>
);
}
}

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { Fragment } 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';
@ -27,6 +28,7 @@ export default class Header extends ImmutablePureComponent {
hideTabs: PropTypes.bool,
domain: PropTypes.string.isRequired,
hidden: PropTypes.bool,
tagged: PropTypes.string,
};
static contextTypes = {
@ -102,7 +104,7 @@ export default class Header extends ImmutablePureComponent {
}
render () {
const { account, hidden, hideTabs } = this.props;
const { account, hidden, hideTabs, tagged } = this.props;
if (account === null) {
return null;
@ -134,11 +136,15 @@ export default class Header extends ImmutablePureComponent {
/>
{!(hideTabs || hidden) && (
<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>
<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>
);

View File

@ -18,10 +18,11 @@ import { me } from 'mastodon/initial_state';
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
import LimitedAccountHint from './components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';
import { fetchFeaturedTags } from '../../actions/featured_tags';
const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) => {
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
const accountId = id || state.getIn(['accounts_map', acct]);
if (!accountId) {
@ -30,7 +31,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
};
}
const path = withReplies ? `${accountId}:with_replies` : accountId;
const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`;
return {
accountId,
@ -38,7 +39,7 @@ const mapStateToProps = (state, { params: { acct, id }, withReplies = false }) =
remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
@ -62,6 +63,7 @@ class AccountTimeline extends ImmutablePureComponent {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
tagged: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
@ -80,15 +82,16 @@ class AccountTimeline extends ImmutablePureComponent {
};
_load () {
const { accountId, withReplies, dispatch } = this.props;
const { accountId, withReplies, params: { tagged }, dispatch } = this.props;
dispatch(fetchAccount(accountId));
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId));
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(expandAccountTimeline(accountId, { withReplies }));
dispatch(fetchFeaturedTags(accountId));
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
if (accountId === me) {
dispatch(connectTimeline(`account:${me}`));
@ -106,12 +109,17 @@ class AccountTimeline extends ImmutablePureComponent {
}
componentDidUpdate (prevProps) {
const { params: { acct }, accountId, dispatch } = this.props;
const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} else if (prevProps.params.tagged !== tagged) {
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
}
if (prevProps.accountId === me && accountId !== me) {
@ -128,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
}
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies }));
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged }));
}
render () {
@ -174,7 +182,7 @@ class AccountTimeline extends ImmutablePureComponent {
<ColumnBackButton multiColumn={multiColumn} />
<StatusList
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} />}
prepend={<HeaderContainer accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
alwaysPrepend
append={remoteMessage}
scrollKey='account_timeline'

View File

@ -195,6 +195,7 @@ class SwitchingColumnsArea extends React.PureComponent {
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path={['/@:acct/followers', '/accounts/:id/followers']} component={Followers} content={children} />
<WrappedRoute path={['/@:acct/following', '/accounts/:id/following']} component={Following} content={children} />