mirror of
https://github.com/mastodon/mastodon.git
synced 2024-12-22 18:45:41 +01:00
Refactor <HashtagHeader>
to TypeScript (#33096)
This commit is contained in:
parent
a1143c522b
commit
25387dc423
10 changed files with 238 additions and 237 deletions
|
@ -1,9 +1,5 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
|
|
||||||
export const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST';
|
|
||||||
export const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS';
|
|
||||||
export const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
|
export const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST';
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
|
export const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS';
|
||||||
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
|
export const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL';
|
||||||
|
@ -12,39 +8,6 @@ export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUES
|
||||||
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
|
export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS';
|
||||||
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL';
|
||||||
|
|
||||||
export const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST';
|
|
||||||
export const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS';
|
|
||||||
export const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL';
|
|
||||||
|
|
||||||
export const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST';
|
|
||||||
export const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS';
|
|
||||||
export const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL';
|
|
||||||
|
|
||||||
export const fetchHashtag = name => (dispatch) => {
|
|
||||||
dispatch(fetchHashtagRequest());
|
|
||||||
|
|
||||||
api().get(`/api/v1/tags/${name}`).then(({ data }) => {
|
|
||||||
dispatch(fetchHashtagSuccess(name, data));
|
|
||||||
}).catch(err => {
|
|
||||||
dispatch(fetchHashtagFail(err));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchHashtagRequest = () => ({
|
|
||||||
type: HASHTAG_FETCH_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchHashtagSuccess = (name, tag) => ({
|
|
||||||
type: HASHTAG_FETCH_SUCCESS,
|
|
||||||
name,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchHashtagFail = error => ({
|
|
||||||
type: HASHTAG_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchFollowedHashtags = () => (dispatch) => {
|
export const fetchFollowedHashtags = () => (dispatch) => {
|
||||||
dispatch(fetchFollowedHashtagsRequest());
|
dispatch(fetchFollowedHashtagsRequest());
|
||||||
|
|
||||||
|
@ -116,57 +79,3 @@ export function expandFollowedHashtagsFail(error) {
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const followHashtag = name => (dispatch) => {
|
|
||||||
dispatch(followHashtagRequest(name));
|
|
||||||
|
|
||||||
api().post(`/api/v1/tags/${name}/follow`).then(({ data }) => {
|
|
||||||
dispatch(followHashtagSuccess(name, data));
|
|
||||||
}).catch(err => {
|
|
||||||
dispatch(followHashtagFail(name, err));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const followHashtagRequest = name => ({
|
|
||||||
type: HASHTAG_FOLLOW_REQUEST,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const followHashtagSuccess = (name, tag) => ({
|
|
||||||
type: HASHTAG_FOLLOW_SUCCESS,
|
|
||||||
name,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const followHashtagFail = (name, error) => ({
|
|
||||||
type: HASHTAG_FOLLOW_FAIL,
|
|
||||||
name,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unfollowHashtag = name => (dispatch) => {
|
|
||||||
dispatch(unfollowHashtagRequest(name));
|
|
||||||
|
|
||||||
api().post(`/api/v1/tags/${name}/unfollow`).then(({ data }) => {
|
|
||||||
dispatch(unfollowHashtagSuccess(name, data));
|
|
||||||
}).catch(err => {
|
|
||||||
dispatch(unfollowHashtagFail(name, err));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const unfollowHashtagRequest = name => ({
|
|
||||||
type: HASHTAG_UNFOLLOW_REQUEST,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unfollowHashtagSuccess = (name, tag) => ({
|
|
||||||
type: HASHTAG_UNFOLLOW_SUCCESS,
|
|
||||||
name,
|
|
||||||
tag,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unfollowHashtagFail = (name, error) => ({
|
|
||||||
type: HASHTAG_UNFOLLOW_FAIL,
|
|
||||||
name,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
17
app/javascript/mastodon/actions/tags_typed.ts
Normal file
17
app/javascript/mastodon/actions/tags_typed.ts
Normal file
|
@ -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),
|
||||||
|
);
|
11
app/javascript/mastodon/api/tags.ts
Normal file
11
app/javascript/mastodon/api/tags.ts
Normal file
|
@ -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`);
|
13
app/javascript/mastodon/api_types/tags.ts
Normal file
13
app/javascript/mastodon/api_types/tags.ts
Normal file
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,7 +5,6 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
@ -13,7 +12,6 @@ import { isEqual } from 'lodash';
|
||||||
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||||
import { connectHashtagStream } from 'mastodon/actions/streaming';
|
import { connectHashtagStream } from 'mastodon/actions/streaming';
|
||||||
import { fetchHashtag, followHashtag, unfollowHashtag } from 'mastodon/actions/tags';
|
|
||||||
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
|
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
|
@ -26,7 +24,6 @@ import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
hasUnread: state.getIn(['timelines', `hashtag:${props.params.id}${props.params.local ? ':local' : ''}`, 'unread']) > 0,
|
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 {
|
class HashtagTimeline extends PureComponent {
|
||||||
|
@ -38,7 +35,6 @@ class HashtagTimeline extends PureComponent {
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
tag: ImmutablePropTypes.map,
|
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -130,7 +126,6 @@ class HashtagTimeline extends PureComponent {
|
||||||
|
|
||||||
this._subscribe(dispatch, id, tags, local);
|
this._subscribe(dispatch, id, tags, local);
|
||||||
dispatch(expandHashtagTimeline(id, { tags, local }));
|
dispatch(expandHashtagTimeline(id, { tags, local }));
|
||||||
dispatch(fetchHashtag(id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
|
@ -162,27 +157,10 @@ class HashtagTimeline extends PureComponent {
|
||||||
dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
|
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 () {
|
render () {
|
||||||
const { hasUnread, columnId, multiColumn, tag } = this.props;
|
const { hasUnread, columnId, multiColumn } = this.props;
|
||||||
const { id, local } = this.props.params;
|
const { id, local } = this.props.params;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
const { signedIn } = this.props.identity;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
|
<Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
|
||||||
|
@ -202,7 +180,7 @@ class HashtagTimeline extends PureComponent {
|
||||||
</ColumnHeader>
|
</ColumnHeader>
|
||||||
|
|
||||||
<StatusListContainer
|
<StatusListContainer
|
||||||
prepend={pinned ? null : <HashtagHeader tag={tag} disabled={!signedIn} onClick={this.handleFollow} />}
|
prepend={pinned ? null : <HashtagHeader tagId={id} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
scrollKey={`hashtag_timeline-${columnId}`}
|
scrollKey={`hashtag_timeline-${columnId}`}
|
||||||
|
|
7
app/javascript/mastodon/models/tags.ts
Normal file
7
app/javascript/mastodon/models/tags.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||||
|
|
||||||
|
export type Hashtag = ApiHashtagJSON;
|
||||||
|
|
||||||
|
export const createHashtag = (serverJSON: ApiHashtagJSON): Hashtag => ({
|
||||||
|
...serverJSON,
|
||||||
|
});
|
|
@ -36,7 +36,6 @@ import settings from './settings';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
import { suggestionsReducer } from './suggestions';
|
import { suggestionsReducer } from './suggestions';
|
||||||
import tags from './tags';
|
|
||||||
import timelines from './timelines';
|
import timelines from './timelines';
|
||||||
import trends from './trends';
|
import trends from './trends';
|
||||||
import user_lists from './user_lists';
|
import user_lists from './user_lists';
|
||||||
|
@ -76,7 +75,6 @@ const reducers = {
|
||||||
markers: markersReducer,
|
markers: markersReducer,
|
||||||
picture_in_picture: pictureInPictureReducer,
|
picture_in_picture: pictureInPictureReducer,
|
||||||
history,
|
history,
|
||||||
tags,
|
|
||||||
followed_tags,
|
followed_tags,
|
||||||
notificationPolicy: notificationPolicyReducer,
|
notificationPolicy: notificationPolicyReducer,
|
||||||
notificationRequests: notificationRequestsReducer,
|
notificationRequests: notificationRequestsReducer,
|
||||||
|
|
|
@ -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 a new issue