import { InfiniteData, useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback } from 'react' import { Alert } from 'react-native' import { ApiCollection, ApiResource, MessageResource } from '../types' import { ReactionCountResource } from '../types/resources/reaction' import { updateRecordInPagesData, REACTION_EMOJIS } from '../utils' import { getMessagesQueryKey, getMessagesRequestArgs } from '../utils/request/get_messages' import { useApiClient } from './use_api_client' const REACT = 1 const UNREACT = -1 type UseMessageReactionToggleProps = { conversation_id: number message: MessageResource onSuccess?: () => void } type ToggleReactionParams = { value: ReactionCountResource['value'] mine: boolean } type QueryData = InfiniteData> export function useMessageReactionToggle({ conversation_id, message, onSuccess, }: UseMessageReactionToggleProps) { const apiClient = useApiClient() const queryClient = useQueryClient() const queryKey = getMessagesQueryKey({ conversation_id }) const message_id = message.id const toggleReaction = useCallback( ({ value, mine }: ToggleReactionParams) => { const requestParams = getMessagesRequestArgs({ conversation_id }) // If already reacted, unreact; otherwise react const endpoint = mine ? 'unreact' : 'react' const url = `/me/conversations/${conversation_id}/messages/${message_id}/${endpoint}` const fieldsWithValueJoined = Object.fromEntries( Object.entries(requestParams.data.fields).map(([k, v]) => [k, v.join(',')]) ) return apiClient.chat.post>({ url, data: { ...requestParams.data, data: { type: 'Message', attributes: { value: value }, }, fields: fieldsWithValueJoined, }, }) }, [apiClient, conversation_id, message_id] ) const { mutate: handleReactionToggle, isPending } = useMutation({ mutationFn: toggleReaction, onMutate: variables => { const previousData = queryClient.getQueryData(queryKey) if (previousData) { queryClient.setQueryData(queryKey, data => optimisticUpdate({ message, data, value: variables.value, mine: variables.mine, }) ) } return { previousData } }, onSuccess: result => { const updatedMessage = result.data queryClient.setQueryData(queryKey, data => updateRecordInPagesData({ data, record: updatedMessage, }) ) if (onSuccess) { onSuccess() } }, onError: (_data, _variables, context) => { if (context?.previousData) { queryClient.setQueryData(queryKey, context.previousData) } Alert.alert('Oops', 'We were unable to react to this message. Please try again.') }, }) return { handleReactionToggle, isPending, } } const optimisticUpdate = ({ message, data, value, mine, }: { message: MessageResource data: QueryData | undefined value: ReactionCountResource['value'] mine: boolean }) => { if (!data) return data const hasExistingReaction = message.reactionCounts.some(r => r.value === value) let updatedReactionCounts: ReactionCountResource[] if (hasExistingReaction) { const delta = mine ? UNREACT : REACT updatedReactionCounts = updateCount(message.reactionCounts, value, delta) } else { updatedReactionCounts = createCount(message.reactionCounts, value, message.id) } const updatedMessage: MessageResource = { ...message, reactionCounts: updatedReactionCounts, } return updateRecordInPagesData({ data, record: updatedMessage, }) } const updateCount = ( reactionCounts: ReactionCountResource[], value: ReactionCountResource['value'], delta: typeof REACT | typeof UNREACT ): ReactionCountResource[] => { return reactionCounts.map(reaction => { if (reaction.value === value) { return { ...reaction, mine: delta > 0 ? 1 : 0, count: reaction.count + delta, } as ReactionCountResource } return reaction }) } const createCount = ( reactionCounts: ReactionCountResource[], value: ReactionCountResource['value'], messageId: string ): ReactionCountResource[] => { const reactionOrder = Object.keys(REACTION_EMOJIS) as ReactionCountResource['value'][] const newReactionIndex = reactionOrder.indexOf(value) const insertIndex = reactionCounts.findIndex( r => reactionOrder.indexOf(r.value) > newReactionIndex ) const newReaction: ReactionCountResource = { type: 'ReactionCount', id: `${messageId}/${value}`, value, count: 1, mine: 1, messageId, authorIds: [], } return insertIndex === -1 ? [...reactionCounts, newReaction] : [...reactionCounts.slice(0, insertIndex), newReaction, ...reactionCounts.slice(insertIndex)] }