From afd1f49e593fd7c2541a2c3f976777fafe304877 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 19 Dec 2024 18:51:10 -0300 Subject: [PATCH] refactor: create useReaction hook, used in PureStatusReactionWrapper componet --- src/api/hooks/index.ts | 1 + src/api/hooks/statuses/useReaction.ts | 125 ++++++++++++++++++ .../pure-status-reaction-wrapper.tsx | 8 +- 3 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 src/api/hooks/statuses/useReaction.ts diff --git a/src/api/hooks/index.ts b/src/api/hooks/index.ts index 8ae1a44ff..8a958fd3c 100644 --- a/src/api/hooks/index.ts +++ b/src/api/hooks/index.ts @@ -47,6 +47,7 @@ export { useUpdateGroupTag } from './groups/useUpdateGroupTag.ts'; export { useBookmarks } from './statuses/useBookmarks.ts'; export { useBookmark } from './statuses/useBookmark.ts'; export { useFavourite } from './statuses/useFavourite.ts'; +export { useReaction } from './statuses/useReaction.ts'; // Streaming export { useUserStream } from './streaming/useUserStream.ts'; diff --git a/src/api/hooks/statuses/useReaction.ts b/src/api/hooks/statuses/useReaction.ts new file mode 100644 index 000000000..139ea977d --- /dev/null +++ b/src/api/hooks/statuses/useReaction.ts @@ -0,0 +1,125 @@ +import { useFavourite } from 'soapbox/api/hooks/index.ts'; +import { importEntities } from 'soapbox/entity-store/actions.ts'; +import { Entities } from 'soapbox/entity-store/entities.ts'; +import { useTransaction } from 'soapbox/entity-store/hooks/index.ts'; +import { useApi } from 'soapbox/hooks/useApi.ts'; +import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; +import { useGetState } from 'soapbox/hooks/useGetState.ts'; +import { EmojiReaction, Status as StatusEntity, statusSchema } from 'soapbox/schemas/index.ts'; +import { isLoggedIn } from 'soapbox/utils/auth.ts'; + +export function useReaction() { + const api = useApi(); + const getState = useGetState(); + const dispatch = useAppDispatch(); + const { transaction } = useTransaction(); + const { favourite, unfavourite } = useFavourite(); + + function emojiReactEffect(statusId: string, emoji: string) { + transaction({ + Statuses: { + [statusId]: (status) => { + // Get the emoji already present in the status reactions, if it exists. + const currentEmoji = status.reactions.find((value) => value.name === emoji); + // If the emoji doesn't exist, append it to the array and return. + if (!currentEmoji) { + return ({ + ...status, + reactions: [...status.reactions, { me: true, name: emoji, count: 1 }], + }); + } + // if the emoji exists in the status reactions, then just update the array and return. + return ({ + ...status, + reactions: status.reactions.map((val) => { + if (val.name === emoji) { + return { ...val, me: true, count: (val.count ?? 0) + 1 }; + } + return val; + }), + }); + }, + }, + }); + } + + function unemojiReactEffect(statusId: string, emoji: string) { + transaction({ + Statuses: { + [statusId]: (status) => { + return ({ + ...status, + reactions: status.reactions.map((val) => { + if (val.name === emoji && val.me === true) { + return { ...val, me: false, count: (val.count ?? 1) - 1 }; + } + return val; + }), + }); + }, + }, + }); + } + + const emojiReact = async (status: StatusEntity, emoji: string) => { // TODO: add custom emoji support + if (!isLoggedIn(getState)) return; + emojiReactEffect(status.id, emoji); + + try { + const response = await api.put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`); + const result = statusSchema.parse(await response.json()); + if (result) { + dispatch(importEntities([result], Entities.STATUSES)); + } + } catch (e) { + unemojiReactEffect(status.id, emoji); + } + }; + + const unEmojiReact = async (status: StatusEntity, emoji: string) => { + if (!isLoggedIn(getState)) return; + unemojiReactEffect(status.id, emoji); + + try { + const response = await api.delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`); + const result = statusSchema.parse(await response.json()); + if (result) { + dispatch(importEntities([result], Entities.STATUSES)); + } + } catch (e) { + emojiReactEffect(status.id, emoji); + } + }; + + const simpleEmojiReact = async (status: StatusEntity, emoji: string) => { + const emojiReacts: readonly EmojiReaction[] = status.reactions; + + // Undo a standard favourite + if (emoji === '👍' && status.favourited) return unfavourite(status.id); + + // Undo an emoji reaction + const undo = emojiReacts.filter(e => e.me === true && e.name === emoji).length > 0; + if (undo) return unEmojiReact(status, emoji); + + try { + await Promise.all([ + ...emojiReacts + .filter((emojiReact) => emojiReact.me === true) + // Remove all existing emoji reactions by the user before adding a new one. If 'emoji' is an 'apple' and the status already has 'banana' as an emoji, then remove 'banana' + .map(emojiReact => unEmojiReact(status, emojiReact.name)), + // Remove existing standard like, if it exists + status.favourited && unfavourite(status.id), + ]); + + if (emoji === '👍') { + favourite(status.id); + } else { + emojiReact(status, emoji); + } + } catch (err) { + console.error(err); + } + }; + + return { emojiReact, unEmojiReact, simpleEmojiReact }; +} diff --git a/src/components/pure-status-reaction-wrapper.tsx b/src/components/pure-status-reaction-wrapper.tsx index 63c66ec73..c242d57b6 100644 --- a/src/components/pure-status-reaction-wrapper.tsx +++ b/src/components/pure-status-reaction-wrapper.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, cloneElement } from 'react'; -import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts.ts'; import { openModal } from 'soapbox/actions/modals.ts'; +import { useReaction } from 'soapbox/api/hooks/index.ts'; import EmojiSelector from 'soapbox/components/ui/emoji-selector.tsx'; import Portal from 'soapbox/components/ui/portal.tsx'; import { Entities } from 'soapbox/entity-store/entities.ts'; @@ -10,11 +10,8 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useGetState } from 'soapbox/hooks/useGetState.ts'; import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts'; import { userTouching } from 'soapbox/is-mobile.ts'; -import { normalizeStatus } from 'soapbox/normalizers/index.ts'; import { Status as StatusEntity } from 'soapbox/schemas/index.ts'; -import type { Status as LegacyStatus } from 'soapbox/types/entities.ts'; - interface IPureStatusReactionWrapper { statusId: string; children: JSX.Element; @@ -27,6 +24,7 @@ const PureStatusReactionWrapper: React.FC = ({ statu const getState = useGetState(); const status = selectEntity(getState(), Entities.STATUSES, statusId); + const { simpleEmojiReact } = useReaction(); const timeout = useRef(); const [visible, setVisible] = useState(false); @@ -71,7 +69,7 @@ const PureStatusReactionWrapper: React.FC = ({ statu const handleReact = (emoji: string, custom?: string): void => { if (ownAccount) { - dispatch(simpleEmojiReact(normalizeStatus(status) as LegacyStatus, emoji, custom)); + simpleEmojiReact(status, emoji); } else { handleUnauthorized(); }