diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -272,6 +272,7 @@ endsCluster: boolean, +robotext: string, +threadCreatedFromMessage: ?ThreadInfo, + +reactions: $ReadOnlyMap, }; export type ChatMessageInfoItem = | RobotextChatMessageInfoItem @@ -283,8 +284,14 @@ +startsCluster: boolean, endsCluster: boolean, +threadCreatedFromMessage: ?ThreadInfo, + +reactions: $ReadOnlyMap, }; export type ChatMessageItem = { itemType: 'loader' } | ChatMessageInfoItem; +export type MessageReactionInfo = { + +viewerReacted: boolean, + +users: $ReadOnlySet, +}; + const msInFiveMinutes = 5 * 60 * 1000; function createChatMessageItems( threadID: string, @@ -293,6 +300,7 @@ threadInfos: { +[id: string]: ThreadInfo }, threadInfoFromSourceMessageID: { +[id: string]: ThreadInfo }, additionalMessages: $ReadOnlyArray, + viewerID: string, ): ChatMessageItem[] { const thread = messageStore.threads[threadID]; @@ -304,6 +312,43 @@ ? sortMessageInfoList([...threadMessageInfos, ...additionalMessages]) : threadMessageInfos; + const targetMessageReactionsMap = new Map>>(); + + // We need to iterate backwards to put the order of messages in chronological + // order, starting with the oldest. This helps to avoid an edge case where + // the most recent message with the remove_reaction action may try to remove + // a user that hasn't been added to the usersSet, causing it to be skipped. + for (let i = messages.length - 1; i >= 0; i--) { + const messageInfo = messages[i]; + if (messageInfo.type !== messageTypes.REACTION) { + continue; + } + + if (!targetMessageReactionsMap.has(messageInfo.targetMessageID)) { + const reactsMap = new Map>(); + targetMessageReactionsMap.set(messageInfo.targetMessageID, reactsMap); + } + + const messageReactsMap = targetMessageReactionsMap.get( + messageInfo.targetMessageID, + ); + invariant(messageReactsMap, 'messageReactsInfo should be set'); + + if (!messageReactsMap.has(messageInfo.reaction)) { + const usersSet = new Set(); + messageReactsMap.set(messageInfo.reaction, usersSet); + } + + const messageReactionUsersSet = messageReactsMap.get(messageInfo.reaction); + invariant(messageReactionUsersSet, 'messageReactionUsersSet should be set'); + + if (messageInfo.action === 'add_reaction') { + messageReactionUsersSet.add(messageInfo.creator.id); + } else { + messageReactionUsersSet.delete(messageInfo.creator.id); + } + } + const chatMessageItems = []; let lastMessageInfo = null; for (let i = messages.length - 1; i >= 0; i--) { @@ -346,6 +391,43 @@ messageInfo.id && threadInfos[threadID]?.type !== threadTypes.SIDEBAR ? threadInfoFromSourceMessageID[messageInfo.id] : undefined; + + const renderedReactions: $ReadOnlyMap< + string, + MessageReactionInfo, + > = (() => { + const result = new Map(); + + let messageReactsMap; + if (originalMessageInfo.id) { + messageReactsMap = targetMessageReactionsMap.get( + originalMessageInfo.id, + ); + } + + if (!messageReactsMap) { + return result; + } + + for (const reaction of messageReactsMap.keys()) { + const reactionUsersSet = messageReactsMap.get(reaction); + invariant(reactionUsersSet, 'reactionUsersSet should be set'); + + if (reactionUsersSet.size === 0) { + continue; + } + + const messageReactionInfo = { + users: reactionUsersSet, + viewerReacted: reactionUsersSet.has(viewerID), + }; + + result.set(reaction, messageReactionInfo); + } + + return result; + })(); + if (isComposableMessageType(originalMessageInfo.type)) { // We use these invariants instead of just checking the messageInfo.type // directly in the conditional above so that isComposableMessageType can @@ -366,6 +448,7 @@ startsCluster, endsCluster: false, threadCreatedFromMessage, + reactions: renderedReactions, }); } else { invariant( @@ -386,6 +469,7 @@ endsCluster: false, threadCreatedFromMessage, robotext, + reactions: renderedReactions, }); } lastMessageInfo = originalMessageInfo; @@ -412,13 +496,16 @@ messageInfoSelector, threadInfoSelector, threadInfoFromSourceMessageIDSelector, + (state: BaseAppState<*>) => + state.currentUserInfo && state.currentUserInfo.id, ( messageStore: MessageStore, messageInfos: { +[id: string]: ?MessageInfo }, threadInfos: { +[id: string]: ThreadInfo }, threadInfoFromSourceMessageID: { +[id: string]: ThreadInfo }, + viewerID: ?string, ): ?(ChatMessageItem[]) => { - if (!threadID) { + if (!threadID || !viewerID) { return null; } return createChatMessageItems( @@ -428,6 +515,7 @@ threadInfos, threadInfoFromSourceMessageID, additionalMessages, + viewerID, ); }, );