diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 79e5a0c0c..c23feed5e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -462,5 +462,7 @@ "reactionPopupOne": "$name$", "reactionPopupTwo": "$name$ & $name2$", "reactionPopupThree": "$name$, $name2$ & $name3$", - "reactionPopupMany": "$name$, $name2$, $name3$ &" + "reactionPopupMany": "$name$, $name2$, $name3$ &", + "reactionListCountSingular": "And $otherSingular$ has reacted to this message", + "reactionListCountPlural": "And $otherPlural$ have reacted to this message" } diff --git a/ts/components/dialog/ReactListModal.tsx b/ts/components/dialog/ReactListModal.tsx index fc1787297..1d73740e4 100644 --- a/ts/components/dialog/ReactListModal.tsx +++ b/ts/components/dialog/ReactListModal.tsx @@ -14,7 +14,7 @@ import { } from '../../state/ducks/modalDialog'; import { SortedReactionList } from '../../types/Reaction'; import { nativeEmojiData } from '../../util/emoji'; -import { sendMessageReaction } from '../../util/reactions'; +import { sendMessageReaction, SOGSReactorsFetchCount } from '../../util/reactions'; import { Avatar, AvatarSize } from '../avatar/Avatar'; import { Flex } from '../basic/Flex'; import { ContactName } from '../conversation/ContactName'; @@ -36,12 +36,12 @@ const StyledReactionsContainer = styled.div` const StyledSendersContainer = styled(Flex)` width: 100%; - min-height: 350px; + min-height: 332px; height: 100%; max-height: 496px; overflow-x: hidden; overflow-y: auto; - padding: 0 16px 32px; + padding: 0 16px 16px; `; const StyledReactionBar = styled(Flex)` @@ -159,6 +159,26 @@ const ReactionSenders = (props: ReactionSendersProps) => { ); }; +const StyledCountText = styled.p` + color: var(--color-text-subtle); + text-align: center; + margin: 16px auto 0; +`; + +const CountText = ({ count }: { count: number }) => { + return ( + <StyledCountText> + {count > SOGSReactorsFetchCount + 1 + ? window.i18n('reactionListCountPlural', [ + window.i18n('otherPlural', [String(count - SOGSReactorsFetchCount)]), + ]) + : window.i18n('reactionListCountSingular', [ + window.i18n('otherSingular', [String(count - SOGSReactorsFetchCount)]), + ])} + </StyledCountText> + ); +}; + type Props = { reaction: string; messageId: string; @@ -182,6 +202,7 @@ const handleSenders = (senders: Array<string>, me: string) => { return updatedSenders; }; +// tslint:disable-next-line: max-func-body-length export const ReactListModal = (props: Props): ReactElement => { const { reaction, messageId } = props; @@ -189,6 +210,7 @@ export const ReactListModal = (props: Props): ReactElement => { const reactionsMap = (reactions && Object.fromEntries(reactions)) || {}; const [currentReact, setCurrentReact] = useState(''); const [reactAriaLabel, setReactAriaLabel] = useState<string | undefined>(); + const [count, setCount] = useState<number | null>(null); const [senders, setSenders] = useState<Array<string>>([]); const me = UserUtils.getOurPubKeyStrFromCache(); @@ -230,7 +252,20 @@ export const ReactListModal = (props: Props): ReactElement => { if (senders.length > 0 && (!reactionsMap[currentReact]?.senders || isEmpty(_senders))) { setSenders([]); } - }, [currentReact, me, reaction, msgProps?.sortedReacts, reactionsMap, senders]); + + if (reactionsMap[currentReact]?.count && count !== reactionsMap[currentReact]?.count) { + setCount(reactionsMap[currentReact].count); + } + }, [ + count, + currentReact, + me, + reaction, + reactionsMap[currentReact]?.count, + msgProps?.sortedReacts, + reactionsMap, + senders, + ]); if (!msgProps) { return <></>; @@ -320,6 +355,7 @@ export const ReactListModal = (props: Props): ReactElement => { handleClose={handleClose} /> )} + {isPublic && count && count > SOGSReactorsFetchCount && <CountText count={count} />} </StyledSendersContainer> )} </StyledReactListContainer> diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index d1fff96b4..5719dfcca 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -93,6 +93,7 @@ import { } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { sogsV3FetchPreviewAndSaveIt } from '../session/apis/open_group_api/sogsv3/sogsV3FetchFile'; import { Reaction } from '../types/Reaction'; +import { handleMessageReaction } from '../util/reactions'; export class ConversationModel extends Backbone.Model<ConversationAttributes> { public updateLastMessage: () => any; @@ -736,7 +737,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> { const chatMessagePrivate = new VisibleMessage(chatMessageParams); await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate); - + await handleMessageReaction(reaction, UserUtils.getOurPubKeyStrFromCache(), true); return; } @@ -748,6 +749,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> { }); // we need the return await so that errors are caught in the catch {} await getMessageQueue().sendToGroup(closedGroupVisibleMessage); + await handleMessageReaction(reaction, UserUtils.getOurPubKeyStrFromCache(), true); return; } diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 4ed6222df..3c0ccdac0 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -321,7 +321,11 @@ async function handleSwarmMessage( // this call has to be made inside the queueJob! // We handle reaction DataMessages separately if (!msgModel.get('isPublic') && rawDataMessage.reaction) { - await handleMessageReaction(rawDataMessage.reaction, msgModel.get('source')); + await handleMessageReaction( + rawDataMessage.reaction, + msgModel.get('source'), + isUsFromCache(msgModel.get('source')) + ); confirm(); return; } diff --git a/ts/test/session/unit/reactions/ReactionMessage_test.ts b/ts/test/session/unit/reactions/ReactionMessage_test.ts index dfd2fd1aa..63bfa3748 100644 --- a/ts/test/session/unit/reactions/ReactionMessage_test.ts +++ b/ts/test/session/unit/reactions/ReactionMessage_test.ts @@ -54,7 +54,8 @@ describe('ReactionMessage', () => { // Handling reaction const updatedMessage = await handleMessageReaction( reaction as SignalService.DataMessage.IReaction, - ourNumber + ourNumber, + true ); expect(updatedMessage?.get('reacts'), 'original message should have reacts').to.not.be @@ -85,7 +86,8 @@ describe('ReactionMessage', () => { // Handling reaction const updatedMessage = await handleMessageReaction( reaction as SignalService.DataMessage.IReaction, - ourNumber + ourNumber, + true ); expect(updatedMessage?.get('reacts'), 'original message reacts should be undefined').to.be diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 03e4521b4..766227d00 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -69,6 +69,7 @@ export type LocalizerKeys = | 'notificationsSettingsTitle' | 'ringing' | 'tookAScreenshot' + | 'reactionListCountPlural' | 'from' | 'thisMonth' | 'next' @@ -277,6 +278,7 @@ export type LocalizerKeys = | 'mainMenuFile' | 'callMissed' | 'getStarted' + | 'reactionListCountSingular' | 'unblockUser' | 'blockUser' | 'clearAllConfirmationTitle' diff --git a/ts/types/Reaction.ts b/ts/types/Reaction.ts index 7357e54aa..d9788743e 100644 --- a/ts/types/Reaction.ts +++ b/ts/types/Reaction.ts @@ -124,7 +124,7 @@ export type ReactionList = Record< count: number; index: number; // relies on reactsIndex in the message model senders: Array<string>; - you?: boolean; // whether we are in the senders because sometimes we dont have the full list of senders yet. + you: boolean; // whether we are in the senders list, used within 1-1 and closed groups for ignoring duplicate data messages, used within opengroups since we dont always have the full list of senders. } >; diff --git a/ts/util/reactions.ts b/ts/util/reactions.ts index fb0943c96..1d1b50a05 100644 --- a/ts/util/reactions.ts +++ b/ts/util/reactions.ts @@ -139,7 +139,8 @@ export const sendMessageReaction = async (messageId: string, emoji: string) => { */ export const handleMessageReaction = async ( reaction: SignalService.DataMessage.IReaction, - sender: string + sender: string, + you: boolean ) => { if (!reaction.emoji) { window?.log?.warn(`There is no emoji for the reaction ${reaction}.`); @@ -151,22 +152,27 @@ export const handleMessageReaction = async ( return; } - if (originalMessage.get('isPublic')) { - window.log.warn("handleMessageReaction() shouldn't be used in opengroups"); - return; - } - const reacts: ReactionList = originalMessage.get('reacts') ?? {}; reacts[reaction.emoji] = reacts[reaction.emoji] || { count: null, senders: [] }; const details = reacts[reaction.emoji] ?? {}; const senders = details.senders; let count = details.count || 0; - window.log.info( - `${sender} ${reaction.action === Action.REACT ? 'added' : 'removed'} a ${ - reaction.emoji - } reaction` - ); + if (originalMessage.get('isPublic')) { + window.log.warn("handleMessageReaction() shouldn't be used in opengroups"); + return; + } else { + if (details.you && senders.includes(sender)) { + if (reaction.action === Action.REACT) { + window.log.warn('Received duplicate message for your reaction. Ignoring it'); + return; + } else { + details.you = false; + } + } else { + details.you = you; + } + } switch (reaction.action) { case Action.REACT: @@ -191,6 +197,7 @@ export const handleMessageReaction = async ( if (count > 0) { reacts[reaction.emoji].count = count; reacts[reaction.emoji].senders = details.senders; + reacts[reaction.emoji].you = details.you; if (details && details.index === undefined) { reacts[reaction.emoji].index = originalMessage.get('reactsIndex') ?? 0; @@ -206,6 +213,14 @@ export const handleMessageReaction = async ( }); await originalMessage.commit(); + + if (!you) { + window.log.info( + `${sender} ${reaction.action === Action.REACT ? 'added' : 'removed'} a ${ + reaction.emoji + } reaction` + ); + } return originalMessage; };