nicolium: migrate status optimistic responses

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-27 22:05:45 +01:00
parent badfdbf217
commit 988092c3f6
3 changed files with 109 additions and 168 deletions

View File

@ -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 };

View File

@ -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<typeof minifyEmojiReaction>;
const updateStatus = (
statusId: string,
changes: Partial<NormalizedStatus> | ((status: NormalizedStatus) => NormalizedStatus),
queryClient: ReturnType<typeof useQueryClient>,
) => {
const previousStatus = queryClient.getQueryData<NormalizedStatus>(
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<typeof useQueryClient>,
) => {
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<InteractionsAction>({ type: 'FAVOURITE_REQUEST', statusId }),
onError: () => dispatch<InteractionsAction>({ 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<InteractionsAction>({ type: 'UNFAVOURITE_REQUEST', statusId }),
onError: () => dispatch<InteractionsAction>({ 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<InteractionsAction>({ type: 'DISLIKE_REQUEST', statusId }),
onError: () => dispatch<InteractionsAction>({ 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<InteractionsAction>({ type: 'UNDISLIKE_REQUEST', statusId }),
onError: () => dispatch<InteractionsAction>({ 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<InteractionsAction>({ type: 'REBLOG_REQUEST', statusId }),
onError: (error) => dispatch<InteractionsAction>({ 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<InteractionsAction>({ type: 'UNREBLOG_REQUEST', statusId }),
onError: (error) => dispatch<InteractionsAction>({ 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));

View File

@ -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];