diff --git a/app/javascript/mastodon/actions/tags.js b/app/javascript/mastodon/actions/tags.js deleted file mode 100644 index 6e0c95288a..0000000000 --- a/app/javascript/mastodon/actions/tags.js +++ /dev/null @@ -1,81 +0,0 @@ -import api, { getLinks } from '../api'; - -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_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL'; - -export const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST'; -export const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS'; -export const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL'; - -export const fetchFollowedHashtags = () => (dispatch) => { - dispatch(fetchFollowedHashtagsRequest()); - - api().get('/api/v1/followed_tags').then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchFollowedHashtagsSuccess(response.data, next ? next.uri : null)); - }).catch(err => { - dispatch(fetchFollowedHashtagsFail(err)); - }); -}; - -export function fetchFollowedHashtagsRequest() { - return { - type: FOLLOWED_HASHTAGS_FETCH_REQUEST, - }; -} - -export function fetchFollowedHashtagsSuccess(followed_tags, next) { - return { - type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, - followed_tags, - next, - }; -} - -export function fetchFollowedHashtagsFail(error) { - return { - type: FOLLOWED_HASHTAGS_FETCH_FAIL, - error, - }; -} - -export function expandFollowedHashtags() { - return (dispatch, getState) => { - const url = getState().getIn(['followed_tags', 'next']); - - if (url === null) { - return; - } - - dispatch(expandFollowedHashtagsRequest()); - - api().get(url).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandFollowedHashtagsSuccess(response.data, next ? next.uri : null)); - }).catch(error => { - dispatch(expandFollowedHashtagsFail(error)); - }); - }; -} - -export function expandFollowedHashtagsRequest() { - return { - type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, - }; -} - -export function expandFollowedHashtagsSuccess(followed_tags, next) { - return { - type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - followed_tags, - next, - }; -} - -export function expandFollowedHashtagsFail(error) { - return { - type: FOLLOWED_HASHTAGS_EXPAND_FAIL, - error, - }; -} diff --git a/app/javascript/mastodon/api/tags.ts b/app/javascript/mastodon/api/tags.ts index 2cb802800c..4b111def81 100644 --- a/app/javascript/mastodon/api/tags.ts +++ b/app/javascript/mastodon/api/tags.ts @@ -1,4 +1,4 @@ -import { apiRequestPost, apiRequestGet } from 'mastodon/api'; +import api, { getLinks, apiRequestPost, apiRequestGet } from 'mastodon/api'; import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; export const apiGetTag = (tagId: string) => @@ -9,3 +9,15 @@ export const apiFollowTag = (tagId: string) => export const apiUnfollowTag = (tagId: string) => apiRequestPost(`v1/tags/${tagId}/unfollow`); + +export const apiGetFollowedTags = async (url?: string) => { + const response = await api().request({ + method: 'GET', + url: url ?? '/api/v1/followed_tags', + }); + + return { + tags: response.data, + links: getLinks(response), + }; +}; diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx index 30c20e0abd..1fe41e1e8b 100644 --- a/app/javascript/mastodon/components/hashtag.tsx +++ b/app/javascript/mastodon/components/hashtag.tsx @@ -106,6 +106,7 @@ export interface HashtagProps { to: string; uses?: number; withGraph?: boolean; + children?: React.ReactNode; } export const Hashtag: React.FC = ({ @@ -117,6 +118,7 @@ export const Hashtag: React.FC = ({ className, description, withGraph = true, + children, }) => (
@@ -158,5 +160,7 @@ export const Hashtag: React.FC = ({
)} + + {children &&
{children}
}
); diff --git a/app/javascript/mastodon/features/followed_tags/index.jsx b/app/javascript/mastodon/features/followed_tags/index.jsx deleted file mode 100644 index 21248e6de9..0000000000 --- a/app/javascript/mastodon/features/followed_tags/index.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import { Helmet } from 'react-helmet'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import { debounce } from 'lodash'; - -import TagIcon from '@/material-icons/400-24px/tag.svg?react'; -import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags'; -import ColumnHeader from 'mastodon/components/column_header'; -import { Hashtag } from 'mastodon/components/hashtag'; -import ScrollableList from 'mastodon/components/scrollable_list'; -import Column from 'mastodon/features/ui/components/column'; - -const messages = defineMessages({ - heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' }, -}); - -const mapStateToProps = state => ({ - hashtags: state.getIn(['followed_tags', 'items']), - isLoading: state.getIn(['followed_tags', 'isLoading'], true), - hasMore: !!state.getIn(['followed_tags', 'next']), -}); - -class FollowedTags extends ImmutablePureComponent { - - static propTypes = { - params: PropTypes.object.isRequired, - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - hashtags: ImmutablePropTypes.list, - isLoading: PropTypes.bool, - hasMore: PropTypes.bool, - multiColumn: PropTypes.bool, - }; - - componentDidMount() { - this.props.dispatch(fetchFollowedHashtags()); - } - - handleLoadMore = debounce(() => { - this.props.dispatch(expandFollowedHashtags()); - }, 300, { leading: true }); - - render () { - const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props; - - const emptyMessage = ; - - return ( - - - - - {hashtags.map((hashtag) => ( - day.get('uses')).toArray()} - /> - ))} - - - - - - - ); - } - -} - -export default connect(mapStateToProps)(injectIntl(FollowedTags)); diff --git a/app/javascript/mastodon/features/followed_tags/index.tsx b/app/javascript/mastodon/features/followed_tags/index.tsx new file mode 100644 index 0000000000..21d63a6fec --- /dev/null +++ b/app/javascript/mastodon/features/followed_tags/index.tsx @@ -0,0 +1,161 @@ +import { useEffect, useState, useCallback, useRef } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import { Helmet } from 'react-helmet'; + +import { isFulfilled } from '@reduxjs/toolkit'; + +import TagIcon from '@/material-icons/400-24px/tag.svg?react'; +import { unfollowHashtag } from 'mastodon/actions/tags_typed'; +import { apiGetFollowedTags } from 'mastodon/api/tags'; +import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; +import { Button } from 'mastodon/components/button'; +import { Column } from 'mastodon/components/column'; +import type { ColumnRef } from 'mastodon/components/column'; +import { ColumnHeader } from 'mastodon/components/column_header'; +import { Hashtag } from 'mastodon/components/hashtag'; +import ScrollableList from 'mastodon/components/scrollable_list'; +import { useAppDispatch } from 'mastodon/store'; + +const messages = defineMessages({ + heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' }, +}); + +const FollowedTag: React.FC<{ + tag: ApiHashtagJSON; + onUnfollow: (arg0: string) => void; +}> = ({ tag, onUnfollow }) => { + const dispatch = useAppDispatch(); + const tagId = tag.name; + + const handleClick = useCallback(() => { + void dispatch(unfollowHashtag({ tagId })).then((result) => { + if (isFulfilled(result)) { + onUnfollow(tagId); + } + + return ''; + }); + }, [dispatch, onUnfollow, tagId]); + + const people = + parseInt(tag.history[0].accounts) + + parseInt(tag.history[1]?.accounts ?? ''); + + return ( + + + + ); +}; + +const FollowedTags: React.FC<{ multiColumn: boolean }> = ({ multiColumn }) => { + const intl = useIntl(); + const [tags, setTags] = useState([]); + const [loading, setLoading] = useState(false); + const [next, setNext] = useState(); + const hasMore = !!next; + const columnRef = useRef(null); + + useEffect(() => { + setLoading(true); + + void apiGetFollowedTags() + .then(({ tags, links }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + setTags(tags); + setLoading(false); + setNext(next?.uri); + + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, [setTags, setLoading, setNext]); + + const handleLoadMore = useCallback(() => { + setLoading(true); + + void apiGetFollowedTags(next) + .then(({ tags, links }) => { + const next = links.refs.find((link) => link.rel === 'next'); + + setLoading(false); + setTags((previousTags) => [...previousTags, ...tags]); + setNext(next?.uri); + + return ''; + }) + .catch(() => { + setLoading(false); + }); + }, [setTags, setLoading, setNext, next]); + + const handleUnfollow = useCallback( + (tagId: string) => { + setTags((tags) => tags.filter((tag) => tag.name !== tagId)); + }, + [setTags], + ); + + const handleHeaderClick = useCallback(() => { + columnRef.current?.scrollTop(); + }, []); + + const emptyMessage = ( + + ); + + return ( + + + + + {tags.map((tag) => ( + + ))} + + + + {intl.formatMessage(messages.heading)} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export default FollowedTags; diff --git a/app/javascript/mastodon/reducers/followed_tags.js b/app/javascript/mastodon/reducers/followed_tags.js deleted file mode 100644 index afea8e3b35..0000000000 --- a/app/javascript/mastodon/reducers/followed_tags.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - -import { - FOLLOWED_HASHTAGS_FETCH_REQUEST, - FOLLOWED_HASHTAGS_FETCH_SUCCESS, - FOLLOWED_HASHTAGS_FETCH_FAIL, - FOLLOWED_HASHTAGS_EXPAND_REQUEST, - FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - FOLLOWED_HASHTAGS_EXPAND_FAIL, -} from 'mastodon/actions/tags'; - -const initialState = ImmutableMap({ - items: ImmutableList(), - isLoading: false, - next: null, -}); - -export default function followed_tags(state = initialState, action) { - switch(action.type) { - case FOLLOWED_HASHTAGS_FETCH_REQUEST: - return state.set('isLoading', true); - case FOLLOWED_HASHTAGS_FETCH_SUCCESS: - return state.withMutations(map => { - map.set('items', fromJS(action.followed_tags)); - map.set('isLoading', false); - map.set('next', action.next); - }); - case FOLLOWED_HASHTAGS_FETCH_FAIL: - return state.set('isLoading', false); - case FOLLOWED_HASHTAGS_EXPAND_REQUEST: - return state.set('isLoading', true); - case FOLLOWED_HASHTAGS_EXPAND_SUCCESS: - return state.withMutations(map => { - map.update('items', set => set.concat(fromJS(action.followed_tags))); - map.set('isLoading', false); - map.set('next', action.next); - }); - case FOLLOWED_HASHTAGS_EXPAND_FAIL: - return state.set('isLoading', false); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index cd5f55a868..e98d835f47 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -13,7 +13,6 @@ import conversations from './conversations'; import custom_emojis from './custom_emojis'; import { dropdownMenuReducer } from './dropdown_menu'; import filters from './filters'; -import followed_tags from './followed_tags'; import height_cache from './height_cache'; import history from './history'; import { listsReducer } from './lists'; @@ -73,7 +72,6 @@ const reducers = { markers: markersReducer, picture_in_picture: pictureInPictureReducer, history, - followed_tags, notificationPolicy: notificationPolicyReducer, notificationRequests: notificationRequestsReducer, }; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a399553f29..5d011d8f32 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8110,13 +8110,9 @@ noscript { &__item { display: flex; align-items: center; - padding: 15px; + padding: 16px; border-bottom: 1px solid var(--background-border-color); - gap: 15px; - - &:last-child { - border-bottom: 0; - } + gap: 8px; &__name { flex: 1 1 auto; @@ -8223,7 +8219,7 @@ noscript { } &--compact &__item { - padding: 10px; + padding: 12px; } }