diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index 66a3d22cd..b780e6164 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -15,7 +15,6 @@ const REBLOG_REQUEST = 'REBLOG_REQUEST' as const; const REBLOG_FAIL = 'REBLOG_FAIL' as const; const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST' as const; -const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS' as const; const FAVOURITE_FAIL = 'FAVOURITE_FAIL' as const; const DISLIKE_REQUEST = 'DISLIKE_REQUEST' as const; @@ -25,7 +24,6 @@ const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST' as const; const UNREBLOG_FAIL = 'UNREBLOG_FAIL' as const; const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST' as const; -const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS' as const; const UNDISLIKE_REQUEST = 'UNDISLIKE_REQUEST' as const; @@ -37,8 +35,6 @@ const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS' as const; const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS' as const; -const noOp = () => new Promise(f => f(undefined)); - const messages = defineMessages({ bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' }, bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' }, @@ -47,166 +43,59 @@ const messages = defineMessages({ selectFolder: { id: 'status.bookmark.select_folder', defaultMessage: 'Select folder' }, }); -const reblog = (status: Pick, visibility?: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return noOp(); +interface ReblogRequest { + type: typeof REBLOG_REQUEST; + statusId: string; +} - dispatch(reblogRequest(status.id)); +interface ReblogFail { + type: typeof REBLOG_FAIL; + statusId: string; + error: unknown; +} - return getClient(getState()).statuses.reblogStatus(status.id, visibility).then((response) => { - // The reblog API method returns a new status wrapped around the original. In this case we are only - // interested in how the original is modified, hence passing it skipping the wrapper - if (response.reblog) dispatch(importEntities({ statuses: [response.reblog] })); - }).catch(error => { - dispatch(reblogFail(status.id, error)); - }); - }; +interface UnreblogRequest { + type: typeof UNREBLOG_REQUEST; + statusId: string; +} -const unreblog = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return noOp(); +interface UnreblogFail { + type: typeof UNREBLOG_FAIL; + statusId: string; + error: unknown; +} - dispatch(unreblogRequest(status.id)); +interface FavouriteRequest { + type: typeof FAVOURITE_REQUEST; + statusId: string; +} - return getClient(getState()).statuses.unreblogStatus(status.id).catch(error => { - dispatch(unreblogFail(status.id, error)); - }); - }; +interface FavouriteFail { + type: typeof FAVOURITE_FAIL; + statusId: string; + error: unknown; +} -const toggleReblog = (status: Pick, visibility?: string) => { - if (status.reblogged) { - return unreblog(status); - } else { - return reblog(status, visibility); - } -}; +interface UnfavouriteRequest { + type: typeof UNFAVOURITE_REQUEST; + statusId: string; +} -const reblogRequest = (statusId: string) => ({ - type: REBLOG_REQUEST, - statusId, -}); +interface DislikeRequest { + type: typeof DISLIKE_REQUEST; + statusId: string; +} -const reblogFail = (statusId: string, error: unknown) => ({ - type: REBLOG_FAIL, - statusId, - error, -}); +interface DislikeFail { + type: typeof DISLIKE_FAIL; + statusId: string; + error: unknown; +} -const unreblogRequest = (statusId: string) => ({ - type: UNREBLOG_REQUEST, - statusId, -}); - -const unreblogFail = (statusId: string, error: unknown) => ({ - type: UNREBLOG_FAIL, - statusId, - error, -}); - -const favourite = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return noOp(); - - dispatch(favouriteRequest(status.id)); - - return getClient(getState()).statuses.favouriteStatus(status.id).then((response) => { - dispatch(favouriteSuccess(response)); - }).catch((error) => { - dispatch(favouriteFail(status.id, error)); - }); - }; - -const unfavourite = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return noOp(); - - dispatch(unfavouriteRequest(status.id)); - - return getClient(getState()).statuses.unfavouriteStatus(status.id).then((response) => { - dispatch(unfavouriteSuccess(response)); - }); - }; - -const toggleFavourite = (status: Pick) => { - if (status.favourited) { - return unfavourite(status); - } else { - return favourite(status); - } -}; - -const favouriteRequest = (statusId: string) => ({ - type: FAVOURITE_REQUEST, - statusId, -}); - -const favouriteSuccess = (status: Status) => ({ - type: FAVOURITE_SUCCESS, - status, - statusId: status.id, -}); - -const favouriteFail = (statusId: string, error: unknown) => ({ - type: FAVOURITE_FAIL, - statusId, - error, -}); - -const unfavouriteRequest = (statusId: string) => ({ - type: UNFAVOURITE_REQUEST, - statusId, -}); - -const unfavouriteSuccess = (status: Status) => ({ - type: UNFAVOURITE_SUCCESS, - status, - statusId: status.id, -}); - -const dislike = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(dislikeRequest(status.id)); - - return getClient(getState).statuses.dislikeStatus(status.id).catch((error) => { - dispatch(dislikeFail(status.id, error)); - }); - }; - -const undislike = (status: Pick) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch(undislikeRequest(status.id)); - - return getClient(getState).statuses.undislikeStatus(status.id); - }; - -const toggleDislike = (status: Pick) => - (dispatch: AppDispatch) => { - if (status.disliked) { - dispatch(undislike(status)); - } else { - dispatch(dislike(status)); - } - }; - -const dislikeRequest = (statusId: string) => ({ - type: DISLIKE_REQUEST, - statusId, -}); - -const dislikeFail = (statusId: string, error: unknown) => ({ - type: DISLIKE_FAIL, - statusId, - error, -}); - -const undislikeRequest = (statusId: string) => ({ - type: UNDISLIKE_REQUEST, - statusId, -}); +interface UndislikeRequest { + type: typeof UNDISLIKE_REQUEST; + statusId: string; +} const bookmark = (status: Pick, folderId?: string) => (dispatch: AppDispatch, getState: () => RootState) => { @@ -318,18 +207,16 @@ const remoteInteraction = (ap_id: string, profile: string) => getClient(getState).accounts.remoteInteraction(ap_id, profile).then((data) => data.url); type InteractionsAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType + | ReblogRequest + | ReblogFail + | UnreblogRequest + | UnreblogFail + | FavouriteRequest + | FavouriteFail + | UnfavouriteRequest + | DislikeRequest + | DislikeFail + | UndislikeRequest | ReturnType | ReturnType | ReturnType @@ -339,26 +226,17 @@ export { REBLOG_REQUEST, REBLOG_FAIL, FAVOURITE_REQUEST, - FAVOURITE_SUCCESS, FAVOURITE_FAIL, DISLIKE_REQUEST, DISLIKE_FAIL, UNREBLOG_REQUEST, UNREBLOG_FAIL, UNFAVOURITE_REQUEST, - UNFAVOURITE_SUCCESS, UNDISLIKE_REQUEST, PIN_SUCCESS, UNPIN_SUCCESS, BOOKMARK_SUCCESS, UNBOOKMARK_SUCCESS, - reblog, - unreblog, - toggleReblog, - favourite, - unfavourite, - toggleFavourite, - toggleDislike, bookmark, toggleBookmark, togglePin, diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index d4297e607..7a0a14bf4 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -6,7 +6,7 @@ import { useHistory, useRouteMatch } from 'react-router-dom'; import { blockAccount } from 'pl-fe/actions/accounts'; import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose'; import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts'; -import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'pl-fe/actions/interactions'; +import { toggleBookmark, togglePin } from 'pl-fe/actions/interactions'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { changeSetting } from 'pl-fe/actions/settings'; @@ -30,6 +30,7 @@ import { useChats } from 'pl-fe/queries/chats'; import { useBlockGroupUserMutation } from 'pl-fe/queries/groups/use-group-blocks'; import { useCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis'; import { useTranslationLanguages } from 'pl-fe/queries/instance/use-translation-languages'; +import { useDislikeStatus, useFavouriteStatus, useReblogStatus, useUndislikeStatus, useUnfavouriteStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { useModalsStore } from 'pl-fe/stores/modals'; import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; import toast from 'pl-fe/toast'; @@ -288,6 +289,9 @@ const ReblogButton: React.FC = ({ const { openModal } = useModalsStore(); const canReblog = useCanInteract(status, 'can_reblog'); + const { mutate: reblogStatus } = useReblogStatus(status.id); + const { mutate: unreblogStatus } = useUnreblogStatus(status.id); + let reblogIcon = require('@tabler/icons/outline/repeat.svg'); if (status.visibility === 'direct') { @@ -298,9 +302,17 @@ const ReblogButton: React.FC = ({ const handleReblogClick: React.EventHandler = e => { if (me) { - const modalReblog = () => dispatch(toggleReblog(status)).then(() => { - if (canReblog.approvalRequired) toast.info(messages.reblogApprovalRequired); - }); + const modalReblog = () => { + if (status.reblogged) { + unreblogStatus(); + } else { + reblogStatus(undefined, { + onSuccess: () => { + if (canReblog.approvalRequired) toast.info(messages.reblogApprovalRequired); + }, + }); + } + }; if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { @@ -377,18 +389,26 @@ const FavouriteButton: React.FC = ({ withLabels, onOpenUnauthorizedModal, }) => { - const dispatch = useAppDispatch(); const features = useFeatures(); const intl = useIntl(); const { openModal } = useModalsStore(); const canFavourite = useCanInteract(status, 'can_favourite'); + const { mutate: favouriteStatus } = useFavouriteStatus(status.id); + const { mutate: unfavouriteStatus } = useUnfavouriteStatus(status.id); + const handleFavouriteClick: React.EventHandler = (e) => { if (me) { - dispatch(toggleFavourite(status)).then(() => { - if (canFavourite.approvalRequired) toast.info(messages.favouriteApprovalRequired); - }).catch(() => {}); + if (status.favourited) { + unfavouriteStatus(); + } else { + favouriteStatus(undefined, { + onSuccess: () => { + if (canFavourite.approvalRequired) toast.info(messages.favouriteApprovalRequired); + }, + }); + } } else { onOpenUnauthorizedModal('FAVOURITE'); } @@ -431,17 +451,23 @@ const DislikeButton: React.FC = ({ me, onOpenUnauthorizedModal, }) => { - const dispatch = useAppDispatch(); const features = useFeatures(); const intl = useIntl(); const { openModal } = useModalsStore(); + const { mutate: dislikeStatus } = useDislikeStatus(status.id); + const { mutate: undislikeStatus } = useUndislikeStatus(status.id); + if (!features.statusDislikes) return; const handleDislikeClick: React.EventHandler = (e) => { if (me) { - dispatch(toggleDislike(status)); + if (status.disliked) { + undislikeStatus(); + } else { + dislikeStatus(); + } } else { onOpenUnauthorizedModal('DISLIKE'); } @@ -603,6 +629,8 @@ const MenuButton: React.FC = ({ const { autoTranslate, deleteModal, knownLanguages } = useSettings(); const { translationLanguages } = useTranslationLanguages(); + const { mutate: reblogStatus } = useReblogStatus(status.id); + const { mutate: unreblogStatus } = useUnreblogStatus(status.id); const autoTranslating = useMemo(() => { const { @@ -666,7 +694,10 @@ const MenuButton: React.FC = ({ }; const handleReblogClick = (e: React.MouseEvent | React.KeyboardEvent, visibility?: string) => { - const modalReblog = () => dispatch(toggleReblog(status, visibility)); + const modalReblog = () => { + if (status.reblogged) unreblogStatus(); + else reblogStatus(visibility); + }; if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index a06e96c82..5b7f2e00f 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -4,7 +4,6 @@ import { defineMessages, useIntl, FormattedList, FormattedMessage } from 'react- import { Link, useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'pl-fe/actions/compose'; -import { toggleFavourite, toggleReblog } from 'pl-fe/actions/interactions'; import { unfilterStatus } from 'pl-fe/actions/statuses'; import Card from 'pl-fe/components/ui/card'; import Icon from 'pl-fe/components/ui/icon'; @@ -17,6 +16,7 @@ import { HotKeys } from 'pl-fe/features/ui/components/hotkeys'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useSettings } from 'pl-fe/hooks/use-settings'; +import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { makeGetStatus, type SelectedStatus } from 'pl-fe/selectors'; import { useModalsStore } from 'pl-fe/stores/modals'; import { useStatusMetaStore } from 'pl-fe/stores/status-meta'; @@ -88,6 +88,11 @@ const Status: React.FC = (props) => { const getStatus = useMemo(makeGetStatus, []); const actualStatus = useAppSelector(state => status.reblog_id && getStatus(state, { id: status.reblog_id }) || status)!; + const { mutate: favouriteStatus } = useFavouriteStatus(actualStatus.id); + const { mutate: unfavouriteStatus } = useUnfavouriteStatus(actualStatus.id); + const { mutate: reblogStatus } = useReblogStatus(actualStatus.id); + const { mutate: unreblogStatus } = useUnreblogStatus(actualStatus.id); + const isReblog = status.reblog_id; const statusUrl = `/@${actualStatus.account.acct}/posts/${actualStatus.id}`; const group = actualStatus.group; @@ -140,11 +145,15 @@ const Status: React.FC = (props) => { const handleHotkeyFavourite = (e?: KeyboardEvent) => { e?.preventDefault(); - dispatch(toggleFavourite(actualStatus)); + if (status.favourited) unfavouriteStatus(); + else favouriteStatus(); }; const handleHotkeyBoost = (e?: KeyboardEvent) => { - const modalReblog = () => dispatch(toggleReblog(actualStatus)); + const modalReblog = () => { + if (status.reblogged) unreblogStatus(); + else reblogStatus(undefined); + }; if ((e && e.shiftKey) || !boostModal) { modalReblog(); } else { diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index c540d2b30..ff3b6b25e 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -5,7 +5,7 @@ import { Link, useHistory } from 'react-router-dom'; import { blockAccount } from 'pl-fe/actions/accounts'; import { directCompose, mentionCompose, quoteCompose } from 'pl-fe/actions/compose'; import { fetchEventIcs } from 'pl-fe/actions/events'; -import { toggleBookmark, togglePin, toggleReblog } from 'pl-fe/actions/interactions'; +import { toggleBookmark, togglePin } from 'pl-fe/actions/interactions'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation'; import { initReport, ReportableEntities } from 'pl-fe/actions/reports'; import { deleteStatus } from 'pl-fe/actions/statuses'; @@ -24,6 +24,7 @@ import { useFeatures } from 'pl-fe/hooks/use-features'; import { useOwnAccount } from 'pl-fe/hooks/use-own-account'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { useChats } from 'pl-fe/queries/chats'; +import { useReblogStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { useModalsStore } from 'pl-fe/stores/modals'; import copy from 'pl-fe/utils/copy'; import { download } from 'pl-fe/utils/download'; @@ -89,6 +90,9 @@ const EventHeader: React.FC = ({ status }) => { const isStaff = ownAccount ? ownAccount.is_admin || ownAccount.is_moderator : false; const isAdmin = ownAccount ? ownAccount.is_admin : false; + const { mutate: reblogStatus } = useReblogStatus(status?.id!); + const { mutate: unreblogStatus } = useUnreblogStatus(status?.id!); + if (!status || !status.event) { return ( <> @@ -131,7 +135,10 @@ const EventHeader: React.FC = ({ status }) => { }; const handleReblogClick = (visibility?: string) => { - const modalReblog = () => dispatch(toggleReblog(status, visibility)); + const modalReblog = () => { + if (status.reblogged) unreblogStatus(); + else reblogStatus(visibility); + }; if (!boostModal) { modalReblog(); } else { diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index be3e36243..5358efc4a 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -3,7 +3,6 @@ import { defineMessages, useIntl, FormattedList, FormattedMessage, IntlShape, Me import { Link, useHistory } from 'react-router-dom'; import { mentionCompose, replyCompose } from 'pl-fe/actions/compose'; -import { reblog, favourite, unreblog, unfavourite } from 'pl-fe/actions/interactions'; import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper'; import Icon from 'pl-fe/components/icon'; import RelativeTimestamp from 'pl-fe/components/relative-timestamp'; @@ -18,6 +17,7 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; +import { useFavouriteStatus, useUnfavouriteStatus, useReblogStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { makeGetNotification } from 'pl-fe/selectors'; import { useModalsStore } from 'pl-fe/stores/modals'; import { useSettingsStore } from 'pl-fe/stores/settings'; @@ -211,6 +211,11 @@ const Notification: React.FC = (props) => { const notification = useAppSelector((state) => getNotification(state, props.notification)); const status = getNotificationStatus(notification); + const { mutate: favouriteStatus } = useFavouriteStatus(status?.id!); + const { mutate: unfavouriteStatus } = useUnfavouriteStatus(status?.id!); + const { mutate: reblogStatus } = useReblogStatus(status?.id!); + const { mutate: unreblogStatus } = useUnreblogStatus(status?.id!); + const history = useHistory(); const intl = useIntl(); const instance = useInstance(); @@ -252,9 +257,9 @@ const Notification: React.FC = (props) => { const handleHotkeyFavourite = useCallback((e?: KeyboardEvent) => { if (status && typeof status === 'object') { if (status.favourited) { - dispatch(unfavourite(status)); + unfavouriteStatus(); } else { - dispatch(favourite(status)); + favouriteStatus(); } } }, [status]); @@ -263,15 +268,15 @@ const Notification: React.FC = (props) => { if (status && typeof status === 'object') { const boostModal = settings.boostModal; if (status.reblogged) { - dispatch(unreblog(status)); + unreblogStatus(); } else { if (e?.shiftKey || !boostModal) { - dispatch(reblog(status)); + reblogStatus(undefined); } else { openModal('BOOST', { statusId: status.id, onReblog: () => { - dispatch(reblog(status)); + reblogStatus(undefined); }, }); } diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index b135cc15d..7643bb687 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -6,7 +6,6 @@ import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; import { type ComposeReplyAction, mentionCompose, replyCompose } from 'pl-fe/actions/compose'; -import { reblog, toggleFavourite, unreblog } from 'pl-fe/actions/interactions'; import ScrollableList from 'pl-fe/components/scrollable-list'; import StatusActionBar from 'pl-fe/components/status-action-bar'; import Tombstone from 'pl-fe/components/tombstone'; @@ -16,6 +15,7 @@ import { HotKeys } from 'pl-fe/features/ui/components/hotkeys'; import PendingStatus from 'pl-fe/features/ui/components/pending-status'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus } from 'pl-fe/queries/statuses/use-status-interactions'; import { RootState } from 'pl-fe/store'; import { useModalsStore } from 'pl-fe/stores/modals'; import { useSettingsStore } from 'pl-fe/stores/settings'; @@ -117,6 +117,11 @@ const Thread: React.FC = ({ const { openModal } = useModalsStore(); const { settings } = useSettingsStore(); + const { mutate: favouriteStatus } = useFavouriteStatus(status.id); + const { mutate: unfavouriteStatus } = useUnfavouriteStatus(status.id); + const { mutate: reblogStatus } = useReblogStatus(status.id); + const { mutate: unreblogStatus } = useUnreblogStatus(status.id); + const getThread = useCallback(makeGetThread(), []); const { ancestorsIds, descendantsIds } = useAppSelector((state) => getThread(state, status.id)); @@ -135,22 +140,21 @@ const Thread: React.FC = ({ }; const handleFavouriteClick = (status: SelectedStatus) => { - dispatch(toggleFavourite(status)); + if (status.favourited) unfavouriteStatus(); + else favouriteStatus(); }; const handleReplyClick = (status: ComposeReplyAction['status']) => dispatch(replyCompose(status)); - const handleModalReblog = (status: Pick) => dispatch(reblog(status)); - const handleReblogClick = (status: SelectedStatus, e?: React.MouseEvent) => { const boostModal = settings.boostModal; if (status.reblogged) { - dispatch(unreblog(status)); + unreblogStatus(); } else { if ((e && e.shiftKey) || !boostModal) { - handleModalReblog(status); + reblogStatus(undefined); } else { - openModal('BOOST', { statusId: status.id, onReblog: () => handleModalReblog(status) }); + openModal('BOOST', { statusId: status.id, onReblog: () => reblogStatus(undefined) }); } } }; diff --git a/packages/pl-fe/src/queries/statuses/use-status-interactions.ts b/packages/pl-fe/src/queries/statuses/use-status-interactions.ts index f4b3ef0e4..911b09bc6 100644 --- a/packages/pl-fe/src/queries/statuses/use-status-interactions.ts +++ b/packages/pl-fe/src/queries/statuses/use-status-interactions.ts @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { importEntities } from 'pl-fe/actions/importer'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; @@ -6,6 +6,8 @@ import { useClient } from 'pl-fe/hooks/use-client'; import { makePaginatedResponseQuery } from 'pl-fe/queries/utils/make-paginated-response-query'; import { minifyAccountList } from 'pl-fe/queries/utils/minify-list'; +import type { InteractionsAction } from 'pl-fe/actions/interactions'; + const queryKey = { getDislikedBy: 'statusDislikes', getFavouritedBy: 'statusFavourites', @@ -36,4 +38,117 @@ const useStatusReactions = (statusId: string, emoji?: string) => { }); }; -export { useStatusDislikes, useStatusFavourites, useStatusReactions, useStatusReblogs }; +const useFavouriteStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['statuses', 'favourite', statusId], + mutationFn: () => client.statuses.favouriteStatus(statusId), + onMutate: () => dispatch({ type: 'FAVOURITE_REQUEST', statusId }), + onError: () => dispatch({ type: 'UNFAVOURITE_REQUEST', statusId }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusFavourites', statusId] }); + }, + }); +}; + +const useUnfavouriteStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['statuses', 'favourite', statusId], + mutationFn: () => client.statuses.unfavouriteStatus(statusId), + onMutate: () => dispatch({ type: 'UNFAVOURITE_REQUEST', statusId }), + onError: () => dispatch({ type: 'FAVOURITE_REQUEST', statusId }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusFavourites', statusId] }); + }, + }); +}; + +const useDislikeStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['statuses', 'dislike', statusId], + mutationFn: () => client.statuses.dislikeStatus(statusId), + onMutate: () => dispatch({ type: 'DISLIKE_REQUEST', statusId }), + onError: () => dispatch({ type: 'UNDISLIKE_REQUEST', statusId }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusDislikes', statusId] }); + }, + }); +}; + +const useUndislikeStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['statuses', 'dislike', statusId], + mutationFn: () => client.statuses.undislikeStatus(statusId), + onMutate: () => dispatch({ type: 'UNDISLIKE_REQUEST', statusId }), + onError: () => dispatch({ type: 'DISLIKE_REQUEST', statusId }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusDislikes', statusId] }); + }, + }); +}; + +const useReblogStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + 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 }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusReblogs', statusId] }); + }, + }); +}; + +const useUnreblogStatus = (statusId: string) => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + 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 }), + onSettled: (status) => { + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'statusReblogs', statusId] }); + }, + }); +}; + +export { + useStatusDislikes, + useStatusFavourites, + useStatusReactions, + useStatusReblogs, + useFavouriteStatus, + useUnfavouriteStatus, + useDislikeStatus, + useUndislikeStatus, + useReblogStatus, + useUnreblogStatus, +}; diff --git a/packages/pl-fe/src/reducers/status-lists.ts b/packages/pl-fe/src/reducers/status-lists.ts index ad12a6465..ea2c59fbb 100644 --- a/packages/pl-fe/src/reducers/status-lists.ts +++ b/packages/pl-fe/src/reducers/status-lists.ts @@ -34,8 +34,6 @@ import { type FavouritesAction, } from 'pl-fe/actions/favourites'; import { - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, BOOKMARK_SUCCESS, UNBOOKMARK_SUCCESS, PIN_SUCCESS, @@ -161,10 +159,6 @@ const statusLists = (state = initialState, action: BookmarksAction | EventsActio return create(state, draft => normalizeList(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next)); case BOOKMARKED_STATUSES_EXPAND_SUCCESS: return create(state, draft => appendToList(draft, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next)); - case FAVOURITE_SUCCESS: - return create(state, draft => prependOneToList(draft, 'favourites', action.status)); - case UNFAVOURITE_SUCCESS: - return create(state, draft => removeOneFromList(draft, 'favourites', action.status)); case BOOKMARK_SUCCESS: return create(state, draft => addBookmarkToLists(draft, action.status)); case UNBOOKMARK_SUCCESS: