From 31b16b01d65a9af0b17c44c4c8ce16fdbbf9aa41 Mon Sep 17 00:00:00 2001 From: mkljczk Date: Thu, 5 Dec 2024 13:11:49 +0100 Subject: [PATCH] pl-fe: migrate followed tags to tanstack query Signed-off-by: mkljczk --- packages/pl-fe/src/actions/tags.ts | 198 ------------------ .../src/features/followed-tags/index.tsx | 24 +-- .../src/features/hashtag-timeline/index.tsx | 14 +- .../src/queries/hashtags/use-followed-tags.ts | 43 ++++ .../pl-fe/src/queries/hashtags/use-hashtag.ts | 14 ++ packages/pl-fe/src/reducers/followed-tags.ts | 56 ----- packages/pl-fe/src/reducers/index.ts | 4 - packages/pl-fe/src/reducers/tags.ts | 39 ---- 8 files changed, 70 insertions(+), 322 deletions(-) delete mode 100644 packages/pl-fe/src/actions/tags.ts create mode 100644 packages/pl-fe/src/queries/hashtags/use-followed-tags.ts create mode 100644 packages/pl-fe/src/queries/hashtags/use-hashtag.ts delete mode 100644 packages/pl-fe/src/reducers/followed-tags.ts delete mode 100644 packages/pl-fe/src/reducers/tags.ts diff --git a/packages/pl-fe/src/actions/tags.ts b/packages/pl-fe/src/actions/tags.ts deleted file mode 100644 index f642f5d05..000000000 --- a/packages/pl-fe/src/actions/tags.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { getClient } from '../api'; - -import type { PaginatedResponse, Tag } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const HASHTAG_FETCH_REQUEST = 'HASHTAG_FETCH_REQUEST' as const; -const HASHTAG_FETCH_SUCCESS = 'HASHTAG_FETCH_SUCCESS' as const; -const HASHTAG_FETCH_FAIL = 'HASHTAG_FETCH_FAIL' as const; - -const HASHTAG_FOLLOW_REQUEST = 'HASHTAG_FOLLOW_REQUEST' as const; -const HASHTAG_FOLLOW_SUCCESS = 'HASHTAG_FOLLOW_SUCCESS' as const; -const HASHTAG_FOLLOW_FAIL = 'HASHTAG_FOLLOW_FAIL' as const; - -const HASHTAG_UNFOLLOW_REQUEST = 'HASHTAG_UNFOLLOW_REQUEST' as const; -const HASHTAG_UNFOLLOW_SUCCESS = 'HASHTAG_UNFOLLOW_SUCCESS' as const; -const HASHTAG_UNFOLLOW_FAIL = 'HASHTAG_UNFOLLOW_FAIL' as const; - -const FOLLOWED_HASHTAGS_FETCH_REQUEST = 'FOLLOWED_HASHTAGS_FETCH_REQUEST' as const; -const FOLLOWED_HASHTAGS_FETCH_SUCCESS = 'FOLLOWED_HASHTAGS_FETCH_SUCCESS' as const; -const FOLLOWED_HASHTAGS_FETCH_FAIL = 'FOLLOWED_HASHTAGS_FETCH_FAIL' as const; - -const FOLLOWED_HASHTAGS_EXPAND_REQUEST = 'FOLLOWED_HASHTAGS_EXPAND_REQUEST' as const; -const FOLLOWED_HASHTAGS_EXPAND_SUCCESS = 'FOLLOWED_HASHTAGS_EXPAND_SUCCESS' as const; -const FOLLOWED_HASHTAGS_EXPAND_FAIL = 'FOLLOWED_HASHTAGS_EXPAND_FAIL' as const; - -const fetchHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchHashtagRequest()); - - return getClient(getState()).myAccount.getTag(name).then((data) => { - dispatch(fetchHashtagSuccess(name, data)); - }).catch(err => { - dispatch(fetchHashtagFail(err)); - }); -}; - -const fetchHashtagRequest = () => ({ - type: HASHTAG_FETCH_REQUEST, -}); - -const fetchHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_FETCH_SUCCESS, - name, - tag, -}); - -const fetchHashtagFail = (error: unknown) => ({ - type: HASHTAG_FETCH_FAIL, - error, -}); - -const followHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(followHashtagRequest(name)); - - return getClient(getState()).myAccount.followTag(name).then((data) => { - dispatch(followHashtagSuccess(name, data)); - }).catch(err => { - dispatch(followHashtagFail(name, err)); - }); -}; - -const followHashtagRequest = (name: string) => ({ - type: HASHTAG_FOLLOW_REQUEST, - name, -}); - -const followHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_FOLLOW_SUCCESS, - name, - tag, -}); - -const followHashtagFail = (name: string, error: unknown) => ({ - type: HASHTAG_FOLLOW_FAIL, - name, - error, -}); - -const unfollowHashtag = (name: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(unfollowHashtagRequest(name)); - - return getClient(getState()).myAccount.unfollowTag(name).then((data) => { - dispatch(unfollowHashtagSuccess(name, data)); - }).catch(err => { - dispatch(unfollowHashtagFail(name, err)); - }); -}; - -const unfollowHashtagRequest = (name: string) => ({ - type: HASHTAG_UNFOLLOW_REQUEST, - name, -}); - -const unfollowHashtagSuccess = (name: string, tag: Tag) => ({ - type: HASHTAG_UNFOLLOW_SUCCESS, - name, - tag, -}); - -const unfollowHashtagFail = (name: string, error: unknown) => ({ - type: HASHTAG_UNFOLLOW_FAIL, - name, - error, -}); - -const fetchFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchFollowedHashtagsRequest()); - - return getClient(getState()).myAccount.getFollowedTags().then(response => { - dispatch(fetchFollowedHashtagsSuccess(response.items, response.next)); - }).catch(err => { - dispatch(fetchFollowedHashtagsFail(err)); - }); -}; - -const fetchFollowedHashtagsRequest = () => ({ - type: FOLLOWED_HASHTAGS_FETCH_REQUEST, -}); - -const fetchFollowedHashtagsSuccess = (followed_tags: Array, next: (() => Promise>) | null) => ({ - type: FOLLOWED_HASHTAGS_FETCH_SUCCESS, - followed_tags, - next, -}); - -const fetchFollowedHashtagsFail = (error: unknown) => ({ - type: FOLLOWED_HASHTAGS_FETCH_FAIL, - error, -}); - -const expandFollowedHashtags = () => (dispatch: AppDispatch, getState: () => RootState) => { - const next = getState().followed_tags.next; - - if (next === null) return; - - dispatch(expandFollowedHashtagsRequest()); - - return next().then(response => { - dispatch(expandFollowedHashtagsSuccess(response.items, response.next)); - }).catch(error => { - dispatch(expandFollowedHashtagsFail(error)); - }); -}; - -const expandFollowedHashtagsRequest = () => ({ - type: FOLLOWED_HASHTAGS_EXPAND_REQUEST, -}); - -const expandFollowedHashtagsSuccess = (followed_tags: Array, next: (() => Promise>) | null) => ({ - type: FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - followed_tags, - next, -}); - -const expandFollowedHashtagsFail = (error: unknown) => ({ - type: FOLLOWED_HASHTAGS_EXPAND_FAIL, - error, -}); - -type TagsAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export { - HASHTAG_FETCH_REQUEST, - HASHTAG_FETCH_SUCCESS, - HASHTAG_FETCH_FAIL, - HASHTAG_FOLLOW_REQUEST, - HASHTAG_FOLLOW_SUCCESS, - HASHTAG_FOLLOW_FAIL, - HASHTAG_UNFOLLOW_REQUEST, - HASHTAG_UNFOLLOW_SUCCESS, - HASHTAG_UNFOLLOW_FAIL, - FOLLOWED_HASHTAGS_FETCH_REQUEST, - FOLLOWED_HASHTAGS_FETCH_SUCCESS, - FOLLOWED_HASHTAGS_FETCH_FAIL, - FOLLOWED_HASHTAGS_EXPAND_REQUEST, - FOLLOWED_HASHTAGS_EXPAND_SUCCESS, - FOLLOWED_HASHTAGS_EXPAND_FAIL, - fetchHashtag, - followHashtag, - unfollowHashtag, - fetchFollowedHashtags, - expandFollowedHashtags, - type TagsAction, -}; diff --git a/packages/pl-fe/src/features/followed-tags/index.tsx b/packages/pl-fe/src/features/followed-tags/index.tsx index 2cc045093..30fe7d987 100644 --- a/packages/pl-fe/src/features/followed-tags/index.tsx +++ b/packages/pl-fe/src/features/followed-tags/index.tsx @@ -1,34 +1,20 @@ -import debounce from 'lodash/debounce'; -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { fetchFollowedHashtags, expandFollowedHashtags } from 'pl-fe/actions/tags'; import Hashtag from 'pl-fe/components/hashtag'; import ScrollableList from 'pl-fe/components/scrollable-list'; import Column from 'pl-fe/components/ui/column'; import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useFollowedTags } from 'pl-fe/queries/hashtags/use-followed-tags'; const messages = defineMessages({ heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' }, }); -const handleLoadMore = debounce((dispatch) => { - dispatch(expandFollowedHashtags()); -}, 300, { leading: true }); - const FollowedTags = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); - useEffect(() => { - dispatch(fetchFollowedHashtags()); - }, []); - - const tags = useAppSelector((state => state.followed_tags.items)); - const isLoading = useAppSelector((state => state.followed_tags.isLoading)); - const hasMore = useAppSelector((state => !!state.followed_tags.next)); + const { data: tags = [], isLoading, hasNextPage, fetchNextPage } = useFollowedTags(); const emptyMessage = ; @@ -37,8 +23,8 @@ const FollowedTags = () => { handleLoadMore(dispatch)} + hasMore={hasNextPage} + onLoadMore={fetchNextPage} placeholderComponent={PlaceholderHashtag} placeholderCount={5} itemClassName='pb-3' diff --git a/packages/pl-fe/src/features/hashtag-timeline/index.tsx b/packages/pl-fe/src/features/hashtag-timeline/index.tsx index 78596c772..a97c053a4 100644 --- a/packages/pl-fe/src/features/hashtag-timeline/index.tsx +++ b/packages/pl-fe/src/features/hashtag-timeline/index.tsx @@ -1,7 +1,6 @@ import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchHashtag, followHashtag, unfollowHashtag } from 'pl-fe/actions/tags'; import { fetchHashtagTimeline, clearTimeline } from 'pl-fe/actions/timelines'; import { useHashtagStream } from 'pl-fe/api/hooks/streaming/use-hashtag-stream'; import List, { ListItem } from 'pl-fe/components/list'; @@ -9,11 +8,12 @@ import Column from 'pl-fe/components/ui/column'; import Toggle from 'pl-fe/components/ui/toggle'; import Timeline from 'pl-fe/features/ui/components/timeline'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useIsMobile } from 'pl-fe/hooks/use-is-mobile'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; import { useTheme } from 'pl-fe/hooks/use-theme'; +import { useFollowHashtagMutation, useUnfollowHashtagMutation } from 'pl-fe/queries/hashtags/use-followed-tags'; +import { useHashtag } from 'pl-fe/queries/hashtags/use-hashtag'; interface IHashtagTimeline { params?: { @@ -26,20 +26,23 @@ const HashtagTimeline: React.FC = ({ params }) => { const features = useFeatures(); const dispatch = useAppDispatch(); - const tag = useAppSelector((state) => state.tags[tagId]); + const { data: tag } = useHashtag(tagId); const { isLoggedIn } = useLoggedIn(); const theme = useTheme(); const isMobile = useIsMobile(); + const { mutate: followHashtag } = useFollowHashtagMutation(tagId); + const { mutate: unfollowHashtag } = useUnfollowHashtagMutation(tagId); + const handleLoadMore = () => { dispatch(fetchHashtagTimeline(tagId, { }, true)); }; const handleFollow = () => { if (tag?.following) { - dispatch(unfollowHashtag(tagId)); + unfollowHashtag(); } else { - dispatch(followHashtag(tagId)); + followHashtag(); } }; @@ -47,7 +50,6 @@ const HashtagTimeline: React.FC = ({ params }) => { useEffect(() => { dispatch(clearTimeline(`hashtag:${tagId}`)); - dispatch(fetchHashtag(tagId)); dispatch(fetchHashtagTimeline(tagId)); }, [tagId]); diff --git a/packages/pl-fe/src/queries/hashtags/use-followed-tags.ts b/packages/pl-fe/src/queries/hashtags/use-followed-tags.ts new file mode 100644 index 000000000..b202ad8cd --- /dev/null +++ b/packages/pl-fe/src/queries/hashtags/use-followed-tags.ts @@ -0,0 +1,43 @@ +import { useMutation } from '@tanstack/react-query'; + +import { useClient } from 'pl-fe/hooks/use-client'; + +import { queryClient } from '../client'; +import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query'; + +const useFollowedTags = makePaginatedResponseQuery( + () => ['followedTags'], + (client) => client.myAccount.getFollowedTags(), +); + +const useFollowHashtagMutation = (tag: string) => { + const client = useClient(); + + return useMutation({ + mutationKey: ['followedTags', tag.toLocaleLowerCase()], + mutationFn: () => client.myAccount.followTag(tag), + onSuccess: (data) => { + queryClient.invalidateQueries({ + queryKey: ['followedTags'], + }); + queryClient.setQueryData(['hashtags', tag.toLocaleLowerCase()], data); + }, + }); +}; + +const useUnfollowHashtagMutation = (tag: string) => { + const client = useClient(); + + return useMutation({ + mutationKey: ['followedTags', tag.toLocaleLowerCase()], + mutationFn: () => client.myAccount.unfollowTag(tag), + onSuccess: (data) => { + queryClient.invalidateQueries({ + queryKey: ['followedTags'], + }); + queryClient.setQueryData(['hashtags', tag.toLocaleLowerCase()], data); + }, + }); +}; + +export { useFollowedTags, useFollowHashtagMutation, useUnfollowHashtagMutation }; diff --git a/packages/pl-fe/src/queries/hashtags/use-hashtag.ts b/packages/pl-fe/src/queries/hashtags/use-hashtag.ts new file mode 100644 index 000000000..a235bfb78 --- /dev/null +++ b/packages/pl-fe/src/queries/hashtags/use-hashtag.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useClient } from 'pl-fe/hooks/use-client'; + +const useHashtag = (tag: string) => { + const client = useClient(); + + return useQuery({ + queryKey: ['hashtags', tag.toLocaleLowerCase()], + queryFn: () => client.myAccount.getTag(tag), + }); +}; + +export { useHashtag }; diff --git a/packages/pl-fe/src/reducers/followed-tags.ts b/packages/pl-fe/src/reducers/followed-tags.ts deleted file mode 100644 index a99496a7b..000000000 --- a/packages/pl-fe/src/reducers/followed-tags.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { create } from 'mutative'; - -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, - TagsAction, -} from 'pl-fe/actions/tags'; - -import type { PaginatedResponse, Tag } from 'pl-api'; - -interface State { - items: Array; - isLoading: boolean; - next: (() => Promise>) | null; -} - -const initalState: State = { - items: [], - isLoading: false, - next: null, -}; - -const followed_tags = (state = initalState, action: TagsAction): State => { - switch (action.type) { - case FOLLOWED_HASHTAGS_FETCH_REQUEST: - case FOLLOWED_HASHTAGS_EXPAND_REQUEST: - return create(state, draft => { - draft.isLoading = true; - }); - case FOLLOWED_HASHTAGS_FETCH_SUCCESS: - return create(state, draft => { - draft.items = action.followed_tags; - draft.isLoading = true; - draft.next = action.next; - }); - case FOLLOWED_HASHTAGS_FETCH_FAIL: - case FOLLOWED_HASHTAGS_EXPAND_FAIL: - return create(state, draft => { - draft.isLoading = false; - }); - case FOLLOWED_HASHTAGS_EXPAND_SUCCESS: - return create(state, draft => { - draft.items = [...draft.items, ...action.followed_tags]; - draft.isLoading = true; - draft.next = action.next; - }); - default: - return state; - } -}; - -export { followed_tags as default }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index bb18ca1af..c477d3fac 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -15,7 +15,6 @@ import conversations from './conversations'; import domain_lists from './domain-lists'; import draft_statuses from './draft-statuses'; import filters from './filters'; -import followed_tags from './followed-tags'; import instance from './instance'; import listAdder from './list-adder'; import listEditor from './list-editor'; @@ -32,7 +31,6 @@ import scheduled_statuses from './scheduled-statuses'; import security from './security'; import status_lists from './status-lists'; import statuses from './statuses'; -import tags from './tags'; import timelines from './timelines'; const reducers = { @@ -48,7 +46,6 @@ const reducers = { draft_statuses, entities, filters, - followed_tags, instance, listAdder, listEditor, @@ -65,7 +62,6 @@ const reducers = { security, status_lists, statuses, - tags, timelines, }; diff --git a/packages/pl-fe/src/reducers/tags.ts b/packages/pl-fe/src/reducers/tags.ts deleted file mode 100644 index f75c92a6e..000000000 --- a/packages/pl-fe/src/reducers/tags.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { create } from 'mutative'; - -import { - HASHTAG_FETCH_SUCCESS, - HASHTAG_FOLLOW_REQUEST, - HASHTAG_FOLLOW_FAIL, - HASHTAG_UNFOLLOW_REQUEST, - HASHTAG_UNFOLLOW_FAIL, - type TagsAction, -} from 'pl-fe/actions/tags'; - -import type { Tag } from 'pl-api'; - -type State = Record; - -const initialState: State = {}; - -const tags = (state = initialState, action: TagsAction) => { - switch (action.type) { - case HASHTAG_FETCH_SUCCESS: - return create(state, (draft) => { - draft[action.name] = action.tag; - }); - case HASHTAG_FOLLOW_REQUEST: - case HASHTAG_UNFOLLOW_FAIL: - return create(state, (draft) => { - if (draft[action.name]) draft[action.name].following = true; - }); - case HASHTAG_FOLLOW_FAIL: - case HASHTAG_UNFOLLOW_REQUEST: - return create(state, (draft) => { - if (draft[action.name]) draft[action.name].following = false; - }); - default: - return state; - } -}; - -export { tags as default };