Refactor <HashtagHeader> to TypeScript (#33096)

This commit is contained in:
Eugen Rochko 2024-12-06 09:42:24 +01:00 committed by GitHub
parent a1143c522b
commit 25387dc423
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 238 additions and 237 deletions

View file

@ -1,94 +0,0 @@
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { Button } from 'mastodon/components/button';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { withIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: { id: 'hashtag.unfollow', defaultMessage: 'Unfollow hashtag' },
adminModeration: { id: 'hashtag.admin_moderation', defaultMessage: 'Open moderation interface for #{name}' },
});
const usesRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const HashtagHeader = withIdentity(injectIntl(({ tag, intl, disabled, onClick, identity }) => {
if (!tag) {
return null;
}
const { signedIn, permissions } = identity;
const menu = [];
if (signedIn && (permissions & PERMISSION_MANAGE_TAXONOMIES) === PERMISSION_MANAGE_TAXONOMIES ) {
menu.push({ text: intl.formatMessage(messages.adminModeration, { name: tag.get("name") }), href: `/admin/tags/${tag.get('id')}` });
}
const [uses, people] = tag.get('history').reduce((arr, day) => [arr[0] + day.get('uses') * 1, arr[1] + day.get('accounts') * 1], [0, 0]);
const dividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.get('name')}</h1>
<div className='hashtag-header__header__buttons'>
{ menu.length > 0 && <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> }
<Button onClick={onClick} text={intl.formatMessage(tag.get('following') ? messages.unfollowHashtag : messages.followHashtag)} disabled={disabled} />
</div>
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber value={tag.getIn(['history', 0, 'uses']) * 1} renderer={usesTodayRenderer} />
</div>
</div>
);
}));
HashtagHeader.propTypes = {
tag: ImmutablePropTypes.map,
disabled: PropTypes.bool,
onClick: PropTypes.func,
intl: PropTypes.object,
};

View file

@ -0,0 +1,188 @@
import { useCallback, useMemo, useState, useEffect } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { isFulfilled } from '@reduxjs/toolkit';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import {
fetchHashtag,
followHashtag,
unfollowHashtag,
} from 'mastodon/actions/tags_typed';
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
import { Button } from 'mastodon/components/button';
import { ShortNumber } from 'mastodon/components/short_number';
import DropdownMenu from 'mastodon/containers/dropdown_menu_container';
import { useIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
import { useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
unfollowHashtag: {
id: 'hashtag.unfollow',
defaultMessage: 'Unfollow hashtag',
},
adminModeration: {
id: 'hashtag.admin_moderation',
defaultMessage: 'Open moderation interface for #{name}',
},
});
const usesRenderer = (displayNumber: React.ReactNode, pluralReady: number) => (
<FormattedMessage
id='hashtag.counter_by_uses'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const peopleRenderer = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='hashtag.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} participant} other {{counter} participants}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
const usesTodayRenderer = (
displayNumber: React.ReactNode,
pluralReady: number,
) => (
<FormattedMessage
id='hashtag.counter_by_uses_today'
defaultMessage='{count, plural, one {{counter} post} other {{counter} posts}} today'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
}}
/>
);
export const HashtagHeader: React.FC<{
tagId: string;
}> = ({ tagId }) => {
const intl = useIntl();
const { signedIn, permissions } = useIdentity();
const dispatch = useAppDispatch();
const [tag, setTag] = useState<ApiHashtagJSON>();
useEffect(() => {
void dispatch(fetchHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
}, [dispatch, tagId, setTag]);
const menu = useMemo(() => {
const tmp = [];
if (
tag &&
signedIn &&
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
PERMISSION_MANAGE_TAXONOMIES
) {
tmp.push({
text: intl.formatMessage(messages.adminModeration, { name: tag.id }),
href: `/admin/tags/${tag.id}`,
});
}
return tmp;
}, [signedIn, permissions, intl, tag]);
const handleFollow = useCallback(() => {
if (!signedIn || !tag) {
return;
}
if (tag.following) {
setTag((hashtag) => hashtag && { ...hashtag, following: false });
void dispatch(unfollowHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
} else {
setTag((hashtag) => hashtag && { ...hashtag, following: true });
void dispatch(followHashtag({ tagId })).then((result) => {
if (isFulfilled(result)) {
setTag(result.payload);
}
return '';
});
}
}, [dispatch, setTag, signedIn, tag, tagId]);
if (!tag) {
return null;
}
const [uses, people] = tag.history.reduce(
(arr, day) => [
arr[0] + parseInt(day.uses),
arr[1] + parseInt(day.accounts),
],
[0, 0],
);
const dividingCircle = <span aria-hidden>{' · '}</span>;
return (
<div className='hashtag-header'>
<div className='hashtag-header__header'>
<h1>#{tag.name}</h1>
<div className='hashtag-header__header__buttons'>
{menu.length > 0 && (
<DropdownMenu
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
size={24}
direction='right'
/>
)}
<Button
onClick={handleFollow}
text={intl.formatMessage(
tag.following ? messages.unfollowHashtag : messages.followHashtag,
)}
disabled={!signedIn}
/>
</div>
</div>
<div>
<ShortNumber value={uses} renderer={usesRenderer} />
{dividingCircle}
<ShortNumber value={people} renderer={peopleRenderer} />
{dividingCircle}
<ShortNumber
value={parseInt(tag.history[0].uses)}
renderer={usesTodayRenderer}
/>
</div>
</div>
);
};

View file

@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { isEqual } from 'lodash';
@ -13,7 +12,6 @@ import { isEqual } from 'lodash';
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
import { connectHashtagStream } from 'mastodon/actions/streaming';
import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags';
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
@ -26,7 +24,6 @@ import ColumnSettingsContainer from './containers/column_settings_container';
const mapStateToProps = (state, props) => ({
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
tag: state.getIn(['tags', props.params.id]),
});
class HashtagTimeline extends PureComponent {
@ -38,7 +35,6 @@ class HashtagTimeline extends PureComponent {
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
hasUnread: PropTypes.bool,
tag: ImmutablePropTypes.map,
multiColumn: PropTypes.bool,
};
@ -130,7 +126,6 @@ class HashtagTimeline extends PureComponent {
this._subscribe(dispatch, id, tags, local);
dispatch(expandHashtagTimeline(id, { tags, local }));
dispatch(fetchHashtag(id));
}
componentDidMount () {
@ -162,27 +157,10 @@ class HashtagTimeline extends PureComponent {
dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
};
handleFollow = () => {
const { dispatch, params, tag } = this.props;
const { id } = params;
const { signedIn } = this.props.identity;
if (!signedIn) {
return;
}
if (tag.get('following')) {
dispatch(unfollowHashtag(id));
} else {
dispatch(followHashtag(id));
}
};
render () {
const { hasUnread, columnId, multiColumn, tag } = this.props;
const { hasUnread, columnId, multiColumn } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
const { signedIn } = this.props.identity;
return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
@ -202,7 +180,7 @@ class HashtagTimeline extends PureComponent {
</ColumnHeader>
<StatusListContainer
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
prepend={pinned ? null : <HashtagHeader tagId={id} />}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`hashtag_timeline-${columnId}`}