diff --git a/packages/nicolium/src/actions/interactions.ts b/packages/nicolium/src/actions/interactions.ts index 03a33959d..fa7aaaa59 100644 --- a/packages/nicolium/src/actions/interactions.ts +++ b/packages/nicolium/src/actions/interactions.ts @@ -1,57 +1,11 @@ -const REBLOG_REQUEST = 'REBLOG_REQUEST' as const; -const REBLOG_FAIL = 'REBLOG_FAIL' as const; - -const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST' as const; -const FAVOURITE_FAIL = 'FAVOURITE_FAIL' as const; - -const DISLIKE_REQUEST = 'DISLIKE_REQUEST' as const; -const DISLIKE_FAIL = 'DISLIKE_FAIL' as const; - -const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST' as const; -const UNREBLOG_FAIL = 'UNREBLOG_FAIL' as const; - -const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST' as const; - -const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST' as const; - const PIN_SUCCESS = 'PIN_SUCCESS' as const; const UNPIN_SUCCESS = 'UNPIN_SUCCESS' as const; -type InteractionsAction = - | { - type: - | typeof REBLOG_REQUEST - | typeof UNREBLOG_REQUEST - | typeof FAVOURITE_REQUEST - | typeof UNFAVOURITE_REQUEST - | typeof DISLIKE_REQUEST - | typeof UNDISLIKE_REQUEST; - statusId: string; - } - | { - type: typeof REBLOG_FAIL | typeof UNREBLOG_FAIL | typeof FAVOURITE_FAIL | typeof DISLIKE_FAIL; - statusId: string; - error: unknown; - } - | { - type: typeof PIN_SUCCESS | typeof UNPIN_SUCCESS; - statusId: string; - accountId: string; - }; - -export { - REBLOG_REQUEST, - REBLOG_FAIL, - FAVOURITE_REQUEST, - FAVOURITE_FAIL, - DISLIKE_REQUEST, - DISLIKE_FAIL, - UNREBLOG_REQUEST, - UNREBLOG_FAIL, - UNFAVOURITE_REQUEST, - UNDISLIKE_REQUEST, - PIN_SUCCESS, - UNPIN_SUCCESS, - type InteractionsAction, +type InteractionsAction = { + type: typeof PIN_SUCCESS | typeof UNPIN_SUCCESS; + statusId: string; + accountId: string; }; + +export { PIN_SUCCESS, UNPIN_SUCCESS, type InteractionsAction }; diff --git a/packages/nicolium/src/queries/statuses/use-status-interactions.ts b/packages/nicolium/src/queries/statuses/use-status-interactions.ts index 234afd7d6..8778e7357 100644 --- a/packages/nicolium/src/queries/statuses/use-status-interactions.ts +++ b/packages/nicolium/src/queries/statuses/use-status-interactions.ts @@ -15,6 +15,7 @@ import toast, { type IToastOptions } from '@/toast'; import { queryKeys } from '../keys'; import { filterById } from '../utils/filter-id'; +import type { NormalizedStatus } from '@/reducers/statuses'; import type { EmojiReaction, PaginatedResponse } from 'pl-api'; const messages = defineMessages({ @@ -47,6 +48,33 @@ const minifyEmojiReaction = ({ accounts, ...reaction }: EmojiReaction) => reacti type MinifiedEmojiReaction = ReturnType; +const updateStatus = ( + statusId: string, + changes: Partial | ((status: NormalizedStatus) => NormalizedStatus), + queryClient: ReturnType, +) => { + const previousStatus = queryClient.getQueryData( + queryKeys.statuses.show(statusId), + ); + if (!previousStatus) return; + + const newStatus = + typeof changes === 'function' ? changes(previousStatus) : { ...previousStatus, ...changes }; + queryClient.setQueryData(queryKeys.statuses.show(statusId), newStatus); + + return { previousStatus }; +}; + +const restorePreviousStatus = ( + statusId: string, + context: { previousStatus?: NormalizedStatus } | undefined, + queryClient: ReturnType, +) => { + if (context?.previousStatus) { + queryClient.setQueryData(queryKeys.statuses.show(statusId), context.previousStatus); + } +}; + const useStatusReactions = (statusId: string, emoji?: string) => { const client = useClient(); const queryClient = useQueryClient(); @@ -75,8 +103,17 @@ const useFavouriteStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'favourite', statusId], mutationFn: () => client.statuses.favouriteStatus(statusId), - onMutate: () => dispatch({ type: 'FAVOURITE_REQUEST', statusId }), - onError: () => dispatch({ type: 'UNFAVOURITE_REQUEST', statusId }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + favourited: true, + favourites_count: status.favourites_count + 1, + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ @@ -94,8 +131,17 @@ const useUnfavouriteStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'favourite', statusId], mutationFn: () => client.statuses.unfavouriteStatus(statusId), - onMutate: () => dispatch({ type: 'UNFAVOURITE_REQUEST', statusId }), - onError: () => dispatch({ type: 'FAVOURITE_REQUEST', statusId }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + favourited: false, + favourites_count: Math.max(0, status.favourites_count - 1), + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ @@ -113,8 +159,17 @@ const useDislikeStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'dislike', statusId], mutationFn: () => client.statuses.dislikeStatus(statusId), - onMutate: () => dispatch({ type: 'DISLIKE_REQUEST', statusId }), - onError: () => dispatch({ type: 'UNDISLIKE_REQUEST', statusId }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + disliked: true, + dislikes_count: status.dislikes_count + 1, + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.accountsLists.statusDislikes(statusId) }); @@ -130,8 +185,17 @@ const useUndislikeStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'dislike', statusId], mutationFn: () => client.statuses.undislikeStatus(statusId), - onMutate: () => dispatch({ type: 'UNDISLIKE_REQUEST', statusId }), - onError: () => dispatch({ type: 'DISLIKE_REQUEST', statusId }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + disliked: false, + dislikes_count: Math.max(0, status.dislikes_count - 1), + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.accountsLists.statusDislikes(statusId) }); @@ -147,8 +211,17 @@ const useReblogStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'reblog', statusId], mutationFn: (visibility?: string) => client.statuses.reblogStatus(statusId, visibility), - onMutate: () => dispatch({ type: 'REBLOG_REQUEST', statusId }), - onError: (error) => dispatch({ type: 'REBLOG_FAIL', statusId, error }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + reblogged: true, + reblogs_count: status.reblogs_count + 1, + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.accountsLists.statusReblogs(statusId) }); @@ -164,8 +237,17 @@ const useUnreblogStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'reblog', statusId], mutationFn: () => client.statuses.unreblogStatus(statusId), - onMutate: () => dispatch({ type: 'UNREBLOG_REQUEST', statusId }), - onError: (error) => dispatch({ type: 'UNREBLOG_FAIL', statusId, error }), + onMutate: () => + updateStatus( + statusId, + (status) => ({ + ...status, + reblogged: false, + reblogs_count: Math.max(0, status.reblogs_count - 1), + }), + queryClient, + ), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.accountsLists.statusReblogs(statusId) }); @@ -193,6 +275,8 @@ const useBookmarkStatus = (statusId: string) => { }); return client.statuses.bookmarkStatus(statusId, folderId); }, + onMutate: () => updateStatus(statusId, { bookmarked: true }, queryClient), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status, _, folderId) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.accountsLists.statusReblogs(statusId) }); @@ -247,6 +331,8 @@ const useUnbookmarkStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'bookmark', statusId], mutationFn: () => client.statuses.unbookmarkStatus(statusId), + onMutate: () => updateStatus(statusId, { bookmarked: false }, queryClient), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSettled: (status) => { dispatch(importEntities({ statuses: [status] })); @@ -272,6 +358,8 @@ const usePinStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'pin', statusId], mutationFn: () => client.statuses.pinStatus(statusId), + onMutate: () => updateStatus(statusId, { pinned: true }, queryClient), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSuccess: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.invalidateQueries({ queryKey: queryKeys.statusLists.pins(account!.id) }); @@ -293,6 +381,8 @@ const useUnpinStatus = (statusId: string) => { return useMutation({ mutationKey: ['statuses', 'unpin', statusId], mutationFn: () => client.statuses.unpinStatus(statusId), + onMutate: () => updateStatus(statusId, { pinned: false }, queryClient), + onError: (_, __, context) => restorePreviousStatus(statusId, context, queryClient), onSuccess: (status) => { dispatch(importEntities({ statuses: [status] })); queryClient.setQueryData(queryKeys.statusLists.pins(account!.id), filterById(statusId)); diff --git a/packages/nicolium/src/reducers/statuses.ts b/packages/nicolium/src/reducers/statuses.ts index d6344d50a..f2c215bc8 100644 --- a/packages/nicolium/src/reducers/statuses.ts +++ b/packages/nicolium/src/reducers/statuses.ts @@ -23,19 +23,6 @@ import { type EventsAction, } from '@/actions/events'; import { STATUS_IMPORT, STATUSES_IMPORT, type ImporterAction } from '@/actions/importer'; -import { - REBLOG_REQUEST, - REBLOG_FAIL, - UNREBLOG_REQUEST, - UNREBLOG_FAIL, - FAVOURITE_REQUEST, - UNFAVOURITE_REQUEST, - FAVOURITE_FAIL, - DISLIKE_REQUEST, - UNDISLIKE_REQUEST, - DISLIKE_FAIL, - type InteractionsAction, -} from '@/actions/interactions'; import { STATUS_CREATE_REQUEST, STATUS_CREATE_FAIL, @@ -259,49 +246,11 @@ const decrementReplyCount = ( return state; }; -/** Simulate favourite/unfavourite of status for optimistic interactions */ -const simulateFavourite = (state: State, statusId: string, favourited: boolean) => { - const status = state[statusId]; - if (!status) return state; - - const delta = favourited ? +1 : -1; - - const updatedStatus = { - ...status, - favourited, - favourites_count: Math.max(0, status.favourites_count + delta), - }; - - state[statusId] = updatedStatus; -}; - -/** Simulate dislike/undislike of status for optimistic interactions */ -const simulateDislike = (state: State, statusId: string, disliked: boolean) => { - const status = state[statusId]; - if (!status) return state; - - const delta = disliked ? +1 : -1; - - const updatedStatus = { - ...status, - disliked, - dislikes_count: Math.max(0, status.dislikes_count + delta), - }; - - state[statusId] = updatedStatus; -}; - const initialState: State = {}; const statuses = ( state = initialState, - action: - | EmojiReactsAction - | EventsAction - | ImporterAction - | InteractionsAction - | StatusesAction - | TimelineAction, + action: EmojiReactsAction | EventsAction | ImporterAction | StatusesAction | TimelineAction, ): State => { switch (action.type) { case STATUS_IMPORT: @@ -320,14 +269,6 @@ const statuses = ( return action.editing ? state : create(state, (draft) => decrementReplyCount(draft, action.params)); - case FAVOURITE_REQUEST: - return create(state, (draft) => simulateFavourite(draft, action.statusId, true)); - case UNFAVOURITE_REQUEST: - return create(state, (draft) => simulateFavourite(draft, action.statusId, false)); - case DISLIKE_REQUEST: - return create(state, (draft) => simulateDislike(draft, action.statusId, true)); - case UNDISLIKE_REQUEST: - return create(state, (draft) => simulateDislike(draft, action.statusId, false)); case EMOJI_REACT_REQUEST: return create(state, (draft) => { const status = draft[action.statusId]; @@ -347,50 +288,6 @@ const statuses = ( status.emoji_reactions = simulateUnEmojiReact(status.emoji_reactions, action.emoji); } }); - case FAVOURITE_FAIL: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.favourited = false; - } - }); - case DISLIKE_FAIL: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.disliked = false; - } - }); - case REBLOG_REQUEST: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.reblogs_count += 1; - status.reblogged = true; - } - }); - case REBLOG_FAIL: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.reblogged = false; - } - }); - case UNREBLOG_REQUEST: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.reblogs_count = Math.max(0, status.reblogs_count - 1); - status.reblogged = false; - } - }); - case UNREBLOG_FAIL: - return create(state, (draft) => { - const status = draft[action.statusId]; - if (status) { - status.reblogged = true; - } - }); case STATUS_MUTE_SUCCESS: return create(state, (draft) => { const status = draft[action.statusId];