mirror of https://github.com/mastodon/mastodon
Refactor `<HashtagHeader>` to TypeScript (#33096)
parent
a1143c522b
commit
25387dc423
@ -0,0 +1,17 @@
|
||||
import { apiGetTag, apiFollowTag, apiUnfollowTag } from 'mastodon/api/tags';
|
||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||
|
||||
export const fetchHashtag = createDataLoadingThunk(
|
||||
'tags/fetch',
|
||||
({ tagId }: { tagId: string }) => apiGetTag(tagId),
|
||||
);
|
||||
|
||||
export const followHashtag = createDataLoadingThunk(
|
||||
'tags/follow',
|
||||
({ tagId }: { tagId: string }) => apiFollowTag(tagId),
|
||||
);
|
||||
|
||||
export const unfollowHashtag = createDataLoadingThunk(
|
||||
'tags/unfollow',
|
||||
({ tagId }: { tagId: string }) => apiUnfollowTag(tagId),
|
||||
);
|
@ -0,0 +1,11 @@
|
||||
import { apiRequestPost, apiRequestGet } from 'mastodon/api';
|
||||
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||
|
||||
export const apiGetTag = (tagId: string) =>
|
||||
apiRequestGet<ApiHashtagJSON>(`v1/tags/${tagId}`);
|
||||
|
||||
export const apiFollowTag = (tagId: string) =>
|
||||
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/follow`);
|
||||
|
||||
export const apiUnfollowTag = (tagId: string) =>
|
||||
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfollow`);
|
@ -0,0 +1,13 @@
|
||||
interface ApiHistoryJSON {
|
||||
day: string;
|
||||
accounts: string;
|
||||
uses: string;
|
||||
}
|
||||
|
||||
export interface ApiHashtagJSON {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
history: [ApiHistoryJSON, ...ApiHistoryJSON[]];
|
||||
following?: boolean;
|
||||
}
|
@ -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,
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||
|
||||
export type Hashtag = ApiHashtagJSON;
|
||||
|
||||
export const createHashtag = (serverJSON: ApiHashtagJSON): Hashtag => ({
|
||||
...serverJSON,
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
HASHTAG_FETCH_SUCCESS,
|
||||
HASHTAG_FOLLOW_REQUEST,
|
||||
HASHTAG_FOLLOW_FAIL,
|
||||
HASHTAG_UNFOLLOW_REQUEST,
|
||||
HASHTAG_UNFOLLOW_FAIL,
|
||||
} from 'mastodon/actions/tags';
|
||||
|
||||
const initialState = ImmutableMap();
|
||||
|
||||
export default function tags(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case HASHTAG_FETCH_SUCCESS:
|
||||
return state.set(action.name, fromJS(action.tag));
|
||||
case HASHTAG_FOLLOW_REQUEST:
|
||||
case HASHTAG_UNFOLLOW_FAIL:
|
||||
return state.setIn([action.name, 'following'], true);
|
||||
case HASHTAG_FOLLOW_FAIL:
|
||||
case HASHTAG_UNFOLLOW_REQUEST:
|
||||
return state.setIn([action.name, 'following'], false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue