diff --git a/lib/shared/reaction-utils.js b/lib/shared/reaction-utils.js --- a/lib/shared/reaction-utils.js +++ b/lib/shared/reaction-utils.js @@ -10,18 +10,20 @@ import { useThreadHasPermission } from './thread-utils.js'; import { threadSpecs } from './threads/thread-specs.js'; import { stringForUserExplicit } from './user-utils.js'; +import { useGetLatestMessageEdit } from '../hooks/latest-message-edit.js'; import { useSendReactionMessage } from '../hooks/message-hooks.js'; import { useResolvableNames } from '../hooks/names-cache.js'; import type { ReactionInfo } from '../selectors/chat-selectors.js'; import type { ComposableMessageInfo, + MessageInfo, RobotextMessageInfo, } from '../types/message-types.js'; import { isComposableMessageType } from '../types/message-types.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadPermissions } from '../types/thread-permission-types.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { useDispatch, useSelector } from '../utils/redux-utils.js'; function useViewerAlreadySelectedMessageReactions( reactions: ReactionInfo, @@ -116,7 +118,7 @@ } function useSendReactionBase( - messageID: ?string, + messageInfo: ?MessageInfo, threadInfo: ThreadInfo, reactions: ReactionInfo, showErrorAlert: () => mixed, @@ -129,10 +131,12 @@ const dispatchActionPromise = useDispatchActionPromise(); const processAndSendDMOperation = useProcessAndSendDMOperation(); const farcasterSendReaction = useSendFarcasterReaction(); + const dispatch = useDispatch(); + const fetchMessage = useGetLatestMessageEdit(); return React.useCallback( reaction => { - if (!messageID) { + if (!messageInfo) { return; } @@ -145,7 +149,7 @@ void threadSpecs[threadInfo.type].protocol().sendReaction( { - messageID, + messageInfo, threadInfo, reaction, action, @@ -157,19 +161,23 @@ keyserverSendReaction: callSendReactionMessage, dispatchActionPromise, farcasterSendReaction, + dispatch, + fetchMessage, }, ); }, [ - messageID, - viewerID, - reactions, - threadInfo, - showErrorAlert, - processAndSendDMOperation, callSendReactionMessage, + dispatch, dispatchActionPromise, farcasterSendReaction, + fetchMessage, + messageInfo, + processAndSendDMOperation, + reactions, + showErrorAlert, + threadInfo, + viewerID, ], ); } diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -519,8 +519,12 @@ input: ProtocolSendReactionInput, utils: SendReactionUtils, ) => { - const { threadInfo, viewerID, messageID, reaction, action } = input; + const { threadInfo, viewerID, messageInfo, reaction, action } = input; const threadID = threadInfo.id; + const messageID = messageInfo.id; + if (!messageID) { + return; + } const op: DMSendReactionMessageOperation = { type: 'send_reaction_message', diff --git a/lib/shared/threads/protocols/farcaster-thread-protocol.js b/lib/shared/threads/protocols/farcaster-thread-protocol.js --- a/lib/shared/threads/protocols/farcaster-thread-protocol.js +++ b/lib/shared/threads/protocols/farcaster-thread-protocol.js @@ -18,11 +18,13 @@ DeleteEntryResult, SaveEntryResult, } from '../../../types/entry-types.js'; +import { messageTypes } from '../../../types/message-types-enum.js'; import { defaultNumberPerThread, type SendMessagePayload, type SendMultimediaMessagePayload, } from '../../../types/message-types.js'; +import type { CompoundReactionInfo } from '../../../types/messages/compound-reaction.js'; import type { FarcasterRawThreadInfo, MemberInfoSansPermissions, @@ -46,8 +48,10 @@ ThreadJoinPayload, } from '../../../types/thread-types.js'; import { updateTypes } from '../../../types/update-types-enum.js'; +import { messageIDToCompoundReactionID } from '../../../utils/convert-farcaster-message-to-comm-messages.js'; import { farcasterThreadIDRegExp } from '../../../utils/validation-utils.js'; import { generatePendingThreadColor } from '../../color-utils.js'; +import { processFarcasterOpsActionType } from '../../farcaster/farcaster-actions.js'; import { type ModifyFarcasterMembershipInput, type SendReactionInput, @@ -270,8 +274,56 @@ input: ProtocolSendReactionInput, utils: SendReactionUtils, ) => { - const { threadInfo, reaction, action, messageID } = input; + const { threadInfo, reaction, action, messageInfo } = input; const conversationId = conversationIDFromFarcasterThreadID(threadInfo.id); + const messageID = messageInfo.id; + if (!messageID) { + return; + } + + const reactionsMessageID = messageIDToCompoundReactionID(messageID); + const currentReactionsMessage = + await utils.fetchMessage(reactionsMessageID); + const reactionCountChange = action === 'add_reaction' ? 1 : -1; + let currentCount = 0; + if ( + currentReactionsMessage && + currentReactionsMessage.type === messageTypes.COMPOUND_REACTION + ) { + currentCount = currentReactionsMessage.reactions[reaction]?.count ?? 0; + } + + const newCount = currentCount + reactionCountChange; + const updatedReaction = { + count: currentCount + reactionCountChange, + viewerReacted: action === 'add_reaction', + }; + const { [reaction]: oldReaction, ...rest } = + (currentReactionsMessage?.reactions ?? {}: { + +[reaction: string]: CompoundReactionInfo, + }); + let updatedReactions = rest; + if (newCount > 0) { + updatedReactions = { + ...currentReactionsMessage?.reactions, + [reaction]: updatedReaction, + }; + } + + const updatedMessage = { + id: reactionsMessageID, + type: messageTypes.COMPOUND_REACTION, + threadID: threadInfo.id, + creatorID: messageInfo.creator.id, + time: messageInfo.time, + targetMessageID: messageID, + reactions: updatedReactions, + }; + utils.dispatch({ + type: processFarcasterOpsActionType, + payload: { rawMessageInfos: [updatedMessage], updateInfos: [] }, + }); + const payload: SendReactionInput = { conversationId, messageId: messageID, diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -301,13 +301,17 @@ const { threadInfo, viewerID, - messageID, + messageInfo, reaction, action, showErrorAlert, } = input; const threadID = threadInfo.id; const localID = getNextLocalID(); + const messageID = messageInfo.id; + if (!messageID) { + return; + } const reactionMessagePromise = (async () => { try { diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js --- a/lib/shared/threads/thread-spec.js +++ b/lib/shared/threads/thread-spec.js @@ -53,6 +53,8 @@ FetchMessageInfosPayload, DeleteMessageRequest, DeleteMessageResponse, + RawMessageInfo, + MessageInfo, } from '../../types/message-types.js'; import type { RawTextMessageInfo } from '../../types/messages/text.js'; import type { @@ -211,7 +213,7 @@ }; export type ProtocolSendReactionInput = { - +messageID: string, + +messageInfo: MessageInfo, +threadInfo: ThreadInfo, +reaction: string, +action: 'remove_reaction' | 'add_reaction', @@ -223,6 +225,8 @@ +keyserverSendReaction: SendReactionMessageRequest => Promise, +farcasterSendReaction: SendReactionInput => Promise, +dispatchActionPromise: DispatchActionPromise, + +dispatch: Dispatch, + +fetchMessage: (messageID: string) => Promise, }; export type ProtocolAddThreadMembersInput = { diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -28,6 +28,12 @@ type RawChangeSettingsMessageInfo, rawChangeSettingsMessageInfoValidator, } from './messages/change-settings.js'; +import { + type RawCompoundReactionMessageInfo, + rawCompoundReactionMessageInfoValidator, + type CompoundReactionMessageData, + type CompoundReactionMessageInfo, +} from './messages/compound-reaction.js'; import { type CreateEntryMessageData, type CreateEntryMessageInfo, @@ -113,12 +119,6 @@ type ReactionMessageData, type ReactionMessageInfo, } from './messages/reaction.js'; -import { - type RawCompoundReactionMessageInfo, - rawCompoundReactionMessageInfoValidator, - type CompoundReactionMessageData, - type CompoundReactionMessageInfo, -} from './messages/compound-reaction.js'; import { type RawRemoveMembersMessageInfo, rawRemoveMembersMessageInfoValidator, diff --git a/lib/utils/convert-farcaster-message-to-comm-messages.js b/lib/utils/convert-farcaster-message-to-comm-messages.js --- a/lib/utils/convert-farcaster-message-to-comm-messages.js +++ b/lib/utils/convert-farcaster-message-to-comm-messages.js @@ -12,6 +12,10 @@ import type { RawMessageInfo } from '../types/message-types.js'; import type { CompoundReactionInfo } from '../types/messages/compound-reaction.js'; +function messageIDToCompoundReactionID(messageID: string): string { + return `${messageID}/reactions`; +} + function convertFarcasterMessageToCommMessages( farcasterMessage: FarcasterMessage, fcUserInfos: FCUserInfos, @@ -37,7 +41,7 @@ }; } result.push({ - id: `${farcasterMessage.messageId}/reactions`, + id: messageIDToCompoundReactionID(farcasterMessage.messageId), type: messageTypes.COMPOUND_REACTION, threadID, creatorID, // Doesn't matter - we don't use it @@ -134,4 +138,4 @@ return result; } -export { convertFarcasterMessageToCommMessages }; +export { convertFarcasterMessageToCommMessages, messageIDToCompoundReactionID }; diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -251,7 +251,7 @@ repliesText, ]); - const sendReaction = useSendReaction(messageInfo.id, threadInfo, reactions); + const sendReaction = useSendReaction(messageInfo, threadInfo, reactions); const onPressReaction = React.useCallback( (reaction: string) => sendReaction(reaction), diff --git a/native/chat/multimedia-message-tooltip-button.react.js b/native/chat/multimedia-message-tooltip-button.react.js --- a/native/chat/multimedia-message-tooltip-button.react.js +++ b/native/chat/multimedia-message-tooltip-button.react.js @@ -112,7 +112,7 @@ messageInfo, ); - const sendReaction = useSendReaction(messageInfo.id, threadInfo, reactions); + const sendReaction = useSendReaction(messageInfo, threadInfo, reactions); const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); const openEmojiPicker = React.useCallback(() => { diff --git a/native/chat/reaction-message-utils.js b/native/chat/reaction-message-utils.js --- a/native/chat/reaction-message-utils.js +++ b/native/chat/reaction-message-utils.js @@ -4,6 +4,7 @@ import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { useSendReactionBase } from 'lib/shared/reaction-utils.js'; +import type { MessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types'; import { useSelector } from '../redux/redux-utils.js'; @@ -25,12 +26,12 @@ } function useSendReaction( - messageID: ?string, + messageInfo: ?MessageInfo, threadInfo: ThreadInfo, reactions: ReactionInfo, ): (reaction: string) => mixed { return useSendReactionBase( - messageID, + messageInfo, threadInfo, reactions, showReactionErrorAlert, diff --git a/native/chat/robotext-message-tooltip-button.react.js b/native/chat/robotext-message-tooltip-button.react.js --- a/native/chat/robotext-message-tooltip-button.react.js +++ b/native/chat/robotext-message-tooltip-button.react.js @@ -81,7 +81,7 @@ ); const sendReaction = useSendReaction( - engagementTargetMessageInfo?.id, + engagementTargetMessageInfo, threadInfo, reactions, ); diff --git a/native/chat/text-message-tooltip-button.react.js b/native/chat/text-message-tooltip-button.react.js --- a/native/chat/text-message-tooltip-button.react.js +++ b/native/chat/text-message-tooltip-button.react.js @@ -109,7 +109,7 @@ messageInfo, ); - const sendReaction = useSendReaction(messageInfo.id, threadInfo, reactions); + const sendReaction = useSendReaction(messageInfo, threadInfo, reactions); const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false); const openEmojiPicker = React.useCallback(() => { diff --git a/web/chat/inline-engagement.react.js b/web/chat/inline-engagement.react.js --- a/web/chat/inline-engagement.react.js +++ b/web/chat/inline-engagement.react.js @@ -88,12 +88,12 @@ )); - }, [reactions, deleted, messageInfo.id, threadInfo]); + }, [reactions, deleted, messageInfo, threadInfo]); const containerClasses = classNames([ css.inlineEngagementContainer, diff --git a/web/chat/reaction-message-utils.js b/web/chat/reaction-message-utils.js --- a/web/chat/reaction-message-utils.js +++ b/web/chat/reaction-message-utils.js @@ -5,6 +5,7 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { useSendReactionBase } from 'lib/shared/reaction-utils.js'; +import type { MessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import Alert from '../modals/alert.react.js'; @@ -15,7 +16,7 @@ import { getAppContainerPositionInfo } from '../utils/window-utils.js'; function useSendReaction( - messageID: ?string, + messageInfo: ?MessageInfo, threadInfo: ThreadInfo, reactions: ReactionInfo, ): (reaction: string) => mixed { @@ -30,7 +31,12 @@ ), [pushModal], ); - return useSendReactionBase(messageID, threadInfo, reactions, showErrorAlert); + return useSendReactionBase( + messageInfo, + threadInfo, + reactions, + showErrorAlert, + ); } type EmojiKeyboardPosition = { diff --git a/web/chat/reaction-pill.react.js b/web/chat/reaction-pill.react.js --- a/web/chat/reaction-pill.react.js +++ b/web/chat/reaction-pill.react.js @@ -4,6 +4,7 @@ import * as React from 'react'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; +import type { MessageInfo } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import { useSendReaction } from './reaction-message-utils.js'; @@ -18,15 +19,15 @@ type Props = { +reaction: string, - +messageID: ?string, + +messageInfo: MessageInfo, +threadInfo: ThreadInfo, +reactions: ReactionInfo, }; function ReactionPill(props: Props): React.Node { - const { reaction, messageID, threadInfo, reactions } = props; + const { reaction, messageInfo, threadInfo, reactions } = props; - const sendReaction = useSendReaction(messageID, threadInfo, reactions); + const sendReaction = useSendReaction(messageInfo, threadInfo, reactions); const onClickReaction = React.useCallback( (event: SyntheticEvent) => { diff --git a/web/tooltips/message-tooltip.react.js b/web/tooltips/message-tooltip.react.js --- a/web/tooltips/message-tooltip.react.js +++ b/web/tooltips/message-tooltip.react.js @@ -173,7 +173,7 @@ const engagementTargetMessageInfo = chatMessageItemEngagementTargetMessageInfo(item); const sendReaction = useSendReaction( - engagementTargetMessageInfo?.id, + engagementTargetMessageInfo, threadInfo, reactions, );