diff --git a/ts/components/StoryViewsNRepliesModal.tsx b/ts/components/StoryViewsNRepliesModal.tsx index 42a660dc176..9a5af141e86 100644 --- a/ts/components/StoryViewsNRepliesModal.tsx +++ b/ts/components/StoryViewsNRepliesModal.tsx @@ -111,7 +111,7 @@ export type PropsType = { preferredReactionEmoji: Array; recentEmojis?: Array; renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element; - replies: Array; + replies: ReadonlyArray; skinTone?: number; sortedGroupMembers?: Array; storyPreviewAttachment?: AttachmentType; diff --git a/ts/jobs/helpers/sendNormalMessage.ts b/ts/jobs/helpers/sendNormalMessage.ts index 4d8e77a2295..e2cdaa87b86 100644 --- a/ts/jobs/helpers/sendNormalMessage.ts +++ b/ts/jobs/helpers/sendNormalMessage.ts @@ -4,6 +4,7 @@ import { isNumber } from 'lodash'; import * as Errors from '../../types/errors'; +import { strictAssert } from '../../util/assert'; import type { MessageModel } from '../../models/messages'; import { getMessageById } from '../../messages/getMessageById'; import type { ConversationModel } from '../../models/conversations'; @@ -11,12 +12,14 @@ import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation'; import { getSendOptions } from '../../util/getSendOptions'; import { SignalService as Proto } from '../../protobuf'; import { handleMessageSend } from '../../util/handleMessageSend'; +import { findAndFormatContact } from '../../util/findAndFormatContact'; import type { CallbackResultType } from '../../textsecure/Types.d'; import { isSent } from '../../messages/MessageSendState'; -import { isOutgoing } from '../../state/selectors/message'; +import { isOutgoing, canReact } from '../../state/selectors/message'; import type { AttachmentType, ContactWithHydratedAvatar, + ReactionType, } from '../../textsecure/SendMessage'; import type { LinkPreviewType } from '../../types/message/LinkPreviews'; import type { BodyRangesType, StoryContextType } from '../../types/Util'; @@ -149,9 +152,37 @@ export async function sendNormalMessage( preview, quote, sticker, + storyMessage, storyContext, + reaction, } = await getMessageSendData({ log, message }); + if (reaction) { + strictAssert( + storyMessage, + 'Only story reactions can be sent as normal messages' + ); + + const ourConversationId = + window.ConversationController.getOurConversationIdOrThrow(); + + if ( + !canReact( + storyMessage.attributes, + ourConversationId, + findAndFormatContact + ) + ) { + log.info( + `could not react to ${messageId}. Removing this pending reaction` + ); + await markMessageFailed(message, [ + new Error('Could not react to story'), + ]); + return; + } + } + let messageSendPromise: Promise; if (recipientIdentifiersWithoutMe.length === 0) { @@ -185,6 +216,7 @@ export async function sendNormalMessage( sticker, // No storyContext; you can't reply to your own stories timestamp: messageTimestamp, + reaction, }); messageSendPromise = message.sendSyncMessageOnly(dataMessage, saveErrors); } else { @@ -228,6 +260,7 @@ export async function sendNormalMessage( quote, sticker, storyContext, + reaction, timestamp: messageTimestamp, mentions, }, @@ -280,9 +313,9 @@ export async function sendNormalMessage( preview, profileKey, quote, - reaction: undefined, sticker, storyContext, + reaction, timestamp: messageTimestamp, // Note: 1:1 story replies should not set story=true - they aren't group sends urgent: true, @@ -436,6 +469,8 @@ async function getMessageSendData({ preview: Array; quote: QuotedMessageType | null; sticker: StickerWithHydratedData | undefined; + reaction: ReactionType | undefined; + storyMessage?: MessageModel; storyContext?: StoryContextType; }> { const { @@ -488,6 +523,8 @@ async function getMessageSendData({ } ); + const storyReaction = message.get('storyReaction'); + return { attachments, body, @@ -499,12 +536,19 @@ async function getMessageSendData({ preview, quote, sticker, + storyMessage, storyContext: storyMessage ? { authorUuid: storyMessage.get('sourceUuid'), timestamp: storyMessage.get('sent_at'), } : undefined, + reaction: storyReaction + ? { + ...storyReaction, + remove: false, + } + : undefined, }; } diff --git a/ts/jobs/helpers/sendReaction.ts b/ts/jobs/helpers/sendReaction.ts index 6f8da8ec73b..fe616126e07 100644 --- a/ts/jobs/helpers/sendReaction.ts +++ b/ts/jobs/helpers/sendReaction.ts @@ -4,6 +4,7 @@ import { isNumber } from 'lodash'; import * as Errors from '../../types/errors'; +import { strictAssert } from '../../util/assert'; import { repeat, zipObject } from '../../util/iterables'; import type { CallbackResultType } from '../../textsecure/Types.d'; import type { MessageModel } from '../../models/messages'; @@ -63,11 +64,16 @@ export async function sendReaction( return; } + strictAssert( + !isStory(message.attributes), + 'Story reactions should be handled by sendStoryReaction' + ); const { pendingReaction, emojiToRemove } = reactionUtil.getNewestPendingOutgoingReaction( getReactions(message), ourConversationId ); + if (!pendingReaction) { log.info(`no pending reaction for ${messageId}. Doing nothing`); return; @@ -153,17 +159,7 @@ export async function sendReaction( ), }); - if ( - isStory(message.attributes) && - isDirectConversation(conversation.attributes) - ) { - ephemeralMessageForReactionSend.set({ - storyId: message.id, - storyReactionEmoji: reactionForSend.emoji, - }); - } else { - ephemeralMessageForReactionSend.doNotSave = true; - } + ephemeralMessageForReactionSend.doNotSave = true; let didFullySend: boolean; const successfulConversationIds = new Set(); @@ -233,12 +229,6 @@ export async function sendReaction( groupId: undefined, profileKey, options: sendOptions, - storyContext: isStory(message.attributes) - ? { - authorUuid: message.get('sourceUuid'), - timestamp: message.get('sent_at'), - } - : undefined, urgent: true, includePniSignatureMessage: true, }); @@ -271,12 +261,6 @@ export async function sendReaction( timestamp: pendingReaction.timestamp, expireTimer, profileKey, - storyContext: isStory(message.attributes) - ? { - authorUuid: message.get('sourceUuid'), - timestamp: message.get('sent_at'), - } - : undefined, }, messageId, sendOptions, @@ -346,8 +330,7 @@ export async function sendReaction( const newReactions = reactionUtil.markOutgoingReactionSent( getReactions(message), pendingReaction, - successfulConversationIds, - message.attributes + successfulConversationIds ); setReactions(message, newReactions); @@ -372,8 +355,9 @@ export async function sendReaction( } } -const getReactions = (message: MessageModel): Array => - message.get('reactions') || []; +const getReactions = ( + message: MessageModel +): ReadonlyArray => message.get('reactions') || []; const setReactions = ( message: MessageModel, diff --git a/ts/messageModifiers/Reactions.ts b/ts/messageModifiers/Reactions.ts index 24a839a102e..b81b181f10b 100644 --- a/ts/messageModifiers/Reactions.ts +++ b/ts/messageModifiers/Reactions.ts @@ -169,14 +169,15 @@ export class Reactions extends Collection { ); // Use the generated message in ts/background.ts to create a message - // if the reaction is targetted at a story on a 1:1 conversation. - if ( - isStory(targetMessage) && - isDirectConversation(targetConversation.attributes) - ) { + // if the reaction is targetted at a story. + if (isStory(targetMessage)) { generatedMessage.set({ storyId: targetMessage.id, - storyReactionEmoji: reaction.get('emoji'), + storyReaction: { + emoji: reaction.get('emoji'), + targetAuthorUuid: reaction.get('targetAuthorUuid'), + targetTimestamp: reaction.get('targetTimestamp'), + }, }); // Note: generatedMessage comes with an id, so we have to force this save diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 9f2eee7c3c4..836df44df78 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -146,7 +146,7 @@ export type MessageAttributesType = { messageTimer?: unknown; profileChange?: ProfileNameChangeType; quote?: QuotedMessageType; - reactions?: Array; + reactions?: ReadonlyArray; requiredProtocolVersion?: number; retryOptions?: RetryOptions; sourceDevice?: number; @@ -184,7 +184,11 @@ export type MessageAttributesType = { unidentifiedDeliveries?: Array; contact?: Array; conversationId: string; - storyReactionEmoji?: string; + storyReaction?: { + emoji: string; + targetAuthorUuid: string; + targetTimestamp: number; + }; giftBadge?: { expiration: number; level: number; diff --git a/ts/models/messages.ts b/ts/models/messages.ts index 3fdb6e4c176..d982f69e71f 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -33,6 +33,7 @@ import { isNormalNumber } from '../util/isNormalNumber'; import { softAssert, strictAssert } from '../util/assert'; import { missingCaseError } from '../util/missingCaseError'; import { dropNull } from '../util/dropNull'; +import { incrementMessageCounter } from '../util/incrementMessageCounter'; import type { ConversationModel } from './conversations'; import type { OwnProps as SmartMessageDetailPropsType, @@ -957,7 +958,7 @@ export class MessageModel extends window.Backbone.Model { const { text, emoji } = this.getNotificationData(); const { attributes } = this; - if (attributes.storyReactionEmoji) { + if (attributes.storyReaction) { if (attributes.type === 'outgoing') { const name = this.getConversation()?.get('profileName'); @@ -965,25 +966,25 @@ export class MessageModel extends window.Backbone.Model { return window.i18n( 'Quote__story-reaction-notification--outgoing--nameless', { - emoji: attributes.storyReactionEmoji, + emoji: attributes.storyReaction.emoji, } ); } return window.i18n('Quote__story-reaction-notification--outgoing', { - emoji: attributes.storyReactionEmoji, + emoji: attributes.storyReaction.emoji, name, }); } if (attributes.type === 'incoming') { return window.i18n('Quote__story-reaction-notification--incoming', { - emoji: attributes.storyReactionEmoji, + emoji: attributes.storyReaction.emoji, }); } if (!window.Signal.OS.isLinux()) { - return attributes.storyReactionEmoji; + return attributes.storyReaction.emoji; } return window.i18n('Quote__story-reaction--single'); @@ -3258,34 +3259,62 @@ export class MessageModel extends window.Backbone.Model { } const previousLength = (this.get('reactions') || []).length; - if (reaction.get('source') === ReactionSource.FromThisDevice) { + const newReaction: MessageReactionType = { + emoji: reaction.get('remove') ? undefined : reaction.get('emoji'), + fromId: reaction.get('fromId'), + targetAuthorUuid: reaction.get('targetAuthorUuid'), + targetTimestamp: reaction.get('targetTimestamp'), + timestamp: reaction.get('timestamp'), + isSentByConversationId: zipObject( + conversation.getMemberConversationIds(), + repeat(false) + ), + }; + + const isFromThisDevice = + reaction.get('source') === ReactionSource.FromThisDevice; + const isFromSync = reaction.get('source') === ReactionSource.FromSync; + const isFromSomeoneElse = + reaction.get('source') === ReactionSource.FromSomeoneElse; + strictAssert( + isFromThisDevice || isFromSync || isFromSomeoneElse, + 'Reaction can only be from this device, from sync, or from someone else' + ); + + if (isStory(this.attributes)) { + if (isFromThisDevice) { + log.info( + 'handleReaction: sending story reaction to ' + + `${this.idForLogging()} from this device` + ); + } else if (isFromSync) { + log.info( + 'handleReaction: receiving story reaction to ' + + `${this.idForLogging()} from another device` + ); + } else { + log.info( + 'handleReaction: receiving story reaction to ' + + `${this.idForLogging()} from someone else` + ); + conversation.notify(this, reaction); + } + } else if (isFromThisDevice) { log.info( - `handleReaction: sending reaction to ${this.idForLogging()} from this device` + `handleReaction: sending reaction to ${this.idForLogging()} ` + + 'from this device' ); - const newReaction = { - emoji: reaction.get('remove') ? undefined : reaction.get('emoji'), - fromId: reaction.get('fromId'), - targetAuthorUuid: reaction.get('targetAuthorUuid'), - targetTimestamp: reaction.get('targetTimestamp'), - timestamp: reaction.get('timestamp'), - isSentByConversationId: zipObject( - conversation.getMemberConversationIds(), - repeat(false) - ), - }; - const reactions = reactionUtil.addOutgoingReaction( this.get('reactions') || [], - newReaction, - isStory(this.attributes) + newReaction ); this.set({ reactions }); } else { const oldReactions = this.get('reactions') || []; let reactions: Array; const oldReaction = oldReactions.find(re => - isNewReactionReplacingPrevious(re, reaction.attributes, this.attributes) + isNewReactionReplacingPrevious(re, reaction.attributes) ); if (oldReaction) { this.clearNotifications(oldReaction); @@ -3297,23 +3326,15 @@ export class MessageModel extends window.Backbone.Model { this.idForLogging() ); - if (reaction.get('source') === ReactionSource.FromSync) { + if (isFromSync) { reactions = oldReactions.filter( re => - !isNewReactionReplacingPrevious( - re, - reaction.attributes, - this.attributes - ) || re.timestamp > reaction.get('timestamp') + !isNewReactionReplacingPrevious(re, reaction.attributes) || + re.timestamp > reaction.get('timestamp') ); } else { reactions = oldReactions.filter( - re => - !isNewReactionReplacingPrevious( - re, - reaction.attributes, - this.attributes - ) + re => !isNewReactionReplacingPrevious(re, reaction.attributes) ); } this.set({ reactions }); @@ -3331,7 +3352,7 @@ export class MessageModel extends window.Backbone.Model { ); let reactionToAdd: MessageReactionType; - if (reaction.get('source') === ReactionSource.FromSync) { + if (isFromSync) { const ourReactions = [ reaction.toJSON(), ...oldReactions.filter(re => re.fromId === reaction.get('fromId')), @@ -3342,20 +3363,12 @@ export class MessageModel extends window.Backbone.Model { } reactions = oldReactions.filter( - re => - !isNewReactionReplacingPrevious( - re, - reaction.attributes, - this.attributes - ) + re => !isNewReactionReplacingPrevious(re, reaction.attributes) ); reactions.push(reactionToAdd); this.set({ reactions }); - if ( - isOutgoing(this.attributes) && - reaction.get('source') === ReactionSource.FromSomeoneElse - ) { + if (isOutgoing(this.attributes) && isFromSomeoneElse) { conversation.notify(this, reaction); } @@ -3378,13 +3391,62 @@ export class MessageModel extends window.Backbone.Model { `Went from ${previousLength} to ${currentLength} reactions.` ); - if (reaction.get('source') === ReactionSource.FromThisDevice) { - const jobData: ConversationQueueJobData = { - type: conversationQueueJobEnum.enum.Reaction, - conversationId: conversation.id, - messageId: this.id, - revision: conversation.get('revision'), - }; + if (isFromThisDevice) { + let jobData: ConversationQueueJobData; + if (isStory(this.attributes)) { + strictAssert( + newReaction.emoji !== undefined, + 'New story reaction must have an emoji' + ); + const reactionMessage = new window.Whisper.Message({ + id: UUID.generate().toString(), + type: 'outgoing', + conversationId: conversation.id, + sent_at: newReaction.timestamp, + received_at: incrementMessageCounter(), + received_at_ms: newReaction.timestamp, + timestamp: newReaction.timestamp, + sendStateByConversationId: zipObject( + Object.keys(newReaction.isSentByConversationId || {}), + repeat({ + status: SendStatus.Pending, + updatedAt: Date.now(), + }) + ), + storyId: this.id, + storyReaction: { + emoji: newReaction.emoji, + targetAuthorUuid: newReaction.targetAuthorUuid, + targetTimestamp: newReaction.targetTimestamp, + }, + }); + + await Promise.all([ + await window.Signal.Data.saveMessage(reactionMessage.attributes, { + ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), + forceSave: true, + }), + reactionMessage.hydrateStoryContext(this), + ]); + + conversation.addSingleMessage( + window.MessageController.register(reactionMessage.id, reactionMessage) + ); + + jobData = { + type: conversationQueueJobEnum.enum.NormalMessage, + conversationId: conversation.id, + messageId: reactionMessage.id, + revision: conversation.get('revision'), + }; + } else { + jobData = { + type: conversationQueueJobEnum.enum.Reaction, + conversationId: conversation.id, + messageId: this.id, + revision: conversation.get('revision'), + }; + } if (shouldPersist) { await conversationJobQueue.add(jobData, async jobToInsert => { log.info( @@ -3400,7 +3462,7 @@ export class MessageModel extends window.Backbone.Model { } else { await conversationJobQueue.add(jobData); } - } else if (shouldPersist) { + } else if (shouldPersist && !isStory(this.attributes)) { await window.Signal.Data.saveMessage(this.attributes, { ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(), }); diff --git a/ts/reactions/util.ts b/ts/reactions/util.ts index d16ba9d142d..cabb80dcd09 100644 --- a/ts/reactions/util.ts +++ b/ts/reactions/util.ts @@ -2,12 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import { findLastIndex, has, identity, omit, negate } from 'lodash'; -import type { - MessageAttributesType, - MessageReactionType, -} from '../model-types.d'; +import type { MessageReactionType } from '../model-types.d'; import { areObjectEntriesEqual } from '../util/areObjectEntriesEqual'; -import { isStory } from '../state/selectors/message'; const isReactionEqual = ( a: undefined | Readonly, @@ -35,13 +31,8 @@ const isOutgoingReactionCompletelyUnsent = ({ export function addOutgoingReaction( oldReactions: ReadonlyArray, - newReaction: Readonly, - isStoryMessage = false -): Array { - if (isStoryMessage) { - return [...oldReactions, newReaction]; - } - + newReaction: Readonly +): ReadonlyArray { const pendingOutgoingReactions = new Set( oldReactions.filter(isOutgoingReactionPending) ); @@ -115,14 +106,13 @@ export function* getUnsentConversationIds({ // sender for stories. export function isNewReactionReplacingPrevious( reaction: MessageReactionType, - newReaction: MessageReactionType, - messageAttributes: MessageAttributesType + newReaction: MessageReactionType ): boolean { - return !isStory(messageAttributes) && reaction.fromId === newReaction.fromId; + return reaction.fromId === newReaction.fromId; } export const markOutgoingReactionFailed = ( - reactions: Array, + reactions: ReadonlyArray, reaction: Readonly ): Array => isOutgoingReactionCompletelyUnsent(reaction) || !reaction.emoji @@ -136,8 +126,7 @@ export const markOutgoingReactionFailed = ( export const markOutgoingReactionSent = ( reactions: ReadonlyArray, reaction: Readonly, - conversationIdsSentTo: Iterable, - messageAttributes: MessageAttributesType + conversationIdsSentTo: Iterable ): Array => { const result: Array = []; @@ -154,10 +143,14 @@ export const markOutgoingReactionSent = ( for (const re of reactions) { if (!isReactionEqual(re, reaction)) { - const shouldKeep = !isFullySent - ? true - : !isNewReactionReplacingPrevious(re, reaction, messageAttributes) || - re.timestamp > reaction.timestamp; + let shouldKeep = true; + if ( + isFullySent && + isNewReactionReplacingPrevious(re, reaction) && + re.timestamp <= reaction.timestamp + ) { + shouldKeep = false; + } if (shouldKeep) { result.push(re); } diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 0e6dd69e97e..5d87f97a6ea 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -445,7 +445,7 @@ export const getPropsForStoryReplyContext = createSelectorCreator( ( message: Pick< MessageWithUIFieldsType, - 'body' | 'conversationId' | 'storyReactionEmoji' | 'storyReplyContext' + 'body' | 'conversationId' | 'storyReaction' | 'storyReplyContext' >, { conversationSelector, @@ -455,7 +455,7 @@ export const getPropsForStoryReplyContext = createSelectorCreator( ourConversationId?: string; } ): PropsData['storyReplyContext'] => { - const { storyReactionEmoji, storyReplyContext } = message; + const { storyReaction, storyReplyContext } = message; if (!storyReplyContext) { return undefined; } @@ -474,7 +474,7 @@ export const getPropsForStoryReplyContext = createSelectorCreator( authorTitle, conversationColor, customColor, - emoji: storyReactionEmoji, + emoji: storyReaction?.emoji, isFromMe, rawAttachment: storyReplyContext.attachment ? processQuoteAttachment(storyReplyContext.attachment) diff --git a/ts/state/selectors/stories.ts b/ts/state/selectors/stories.ts index 96b8aad1c6a..72f513c5a4b 100644 --- a/ts/state/selectors/stories.ts +++ b/ts/state/selectors/stories.ts @@ -6,7 +6,6 @@ import { pick } from 'lodash'; import type { GetConversationByIdType } from './conversations'; import type { ConversationType } from '../ducks/conversations'; -import type { MessageReactionType } from '../../model-types.d'; import type { AttachmentType } from '../../types/Attachment'; import type { ConversationStoryType, @@ -64,10 +63,6 @@ export const getAddStoryData = createSelector( ({ addStoryData }): AddStoryData => addStoryData ); -function getReactionUniqueId(reaction: MessageReactionType): string { - return `${reaction.fromId}:${reaction.targetAuthorUuid}:${reaction.timestamp}`; -} - function sortByRecencyAndUnread( storyA: ConversationStoryType, storyB: ConversationStoryType @@ -273,34 +268,12 @@ export const getStoryReplies = createSelector( conversationSelector, contactNameColorSelector, me, - { stories, replyState }: Readonly + { replyState }: Readonly ): ReplyStateType | undefined => { if (!replyState) { return; } - const foundStory = stories.find( - story => story.messageId === replyState.messageId - ); - - const reactions = foundStory - ? (foundStory.reactions || []).map(reaction => { - const conversation = conversationSelector(reaction.fromId); - - return { - author: getAvatarData(conversation), - contactNameColor: contactNameColorSelector( - foundStory.conversationId, - conversation.id - ), - conversationId: reaction.fromId, - id: getReactionUniqueId(reaction), - reactionEmoji: reaction.emoji, - timestamp: reaction.timestamp, - }; - }) - : []; - const replies = replyState.replies.map(reply => { const conversation = reply.type === 'outgoing' @@ -316,6 +289,7 @@ export const getStoryReplies = createSelector( 'id', 'timestamp', ]), + reactionEmoji: reply.storyReaction?.emoji, contactNameColor: contactNameColorSelector( reply.conversationId, conversation.id @@ -325,13 +299,9 @@ export const getStoryReplies = createSelector( }; }); - const combined = [...replies, ...reactions].sort((a, b) => - a.timestamp > b.timestamp ? 1 : -1 - ); - return { messageId: replyState.messageId, - replies: combined, + replies, }; } ); diff --git a/ts/test-both/reactions/util_test.ts b/ts/test-both/reactions/util_test.ts index dd57e15e629..f9d9fc2b038 100644 --- a/ts/test-both/reactions/util_test.ts +++ b/ts/test-both/reactions/util_test.ts @@ -4,10 +4,7 @@ import { assert } from 'chai'; import { v4 as uuid } from 'uuid'; import { omit } from 'lodash'; -import type { - MessageAttributesType, - MessageReactionType, -} from '../../model-types.d'; +import type { MessageReactionType } from '../../model-types.d'; import { isEmpty } from '../../util/iterables'; import { @@ -51,18 +48,6 @@ describe('reaction utilities', () => { const newReactions = addOutgoingReaction(oldReactions, reaction); assert.deepStrictEqual(newReactions, [oldReactions[1], reaction]); }); - - it('does not remove any pending reactions if its a story', () => { - const oldReactions = [ - { ...rxn('😭', { isPending: true }), timestamp: 3 }, - { ...rxn('💬'), fromId: uuid() }, - { ...rxn('🥀', { isPending: true }), timestamp: 1 }, - { ...rxn('🌹', { isPending: true }), timestamp: 2 }, - ]; - const reaction = rxn('😀'); - const newReactions = addOutgoingReaction(oldReactions, reaction, true); - assert.deepStrictEqual(newReactions, [...oldReactions, reaction]); - }); }); describe('getNewestPendingOutgoingReaction', () => { @@ -214,36 +199,21 @@ describe('reaction utilities', () => { const reactions = [star, none, { ...rxn('🔕'), timestamp: 1 }]; - function getMessage(): MessageAttributesType { - const now = Date.now(); - return { - conversationId: uuid(), - id: uuid(), - received_at: now, - sent_at: now, - timestamp: now, - type: 'incoming', - }; - } - it("does nothing if the reaction isn't in the list", () => { const result = markOutgoingReactionSent( reactions, rxn('🥀', { isPending: true }), - [uuid()], - getMessage() + [uuid()] ); assert.deepStrictEqual(result, reactions); }); it('updates reactions to be partially sent', () => { [star, none].forEach(reaction => { - const result = markOutgoingReactionSent( - reactions, - reaction, - [uuid1, uuid2], - getMessage() - ); + const result = markOutgoingReactionSent(reactions, reaction, [ + uuid1, + uuid2, + ]); assert.deepStrictEqual( result.find(re => re.emoji === reaction.emoji) ?.isSentByConversationId, @@ -257,12 +227,11 @@ describe('reaction utilities', () => { }); it('removes sent state if a reaction with emoji is fully sent', () => { - const result = markOutgoingReactionSent( - reactions, - star, - [uuid1, uuid2, uuid3], - getMessage() - ); + const result = markOutgoingReactionSent(reactions, star, [ + uuid1, + uuid2, + uuid3, + ]); const newReaction = result.find(re => re.emoji === '⭐️'); assert.isDefined(newReaction); @@ -270,12 +239,11 @@ describe('reaction utilities', () => { }); it('removes a fully-sent reaction removal', () => { - const result = markOutgoingReactionSent( - reactions, - none, - [uuid1, uuid2, uuid3], - getMessage() - ); + const result = markOutgoingReactionSent(reactions, none, [ + uuid1, + uuid2, + uuid3, + ]); assert( result.every(({ emoji }) => typeof emoji === 'string'), @@ -284,25 +252,13 @@ describe('reaction utilities', () => { }); it('removes older reactions of mine', () => { - const result = markOutgoingReactionSent( - reactions, - star, - [uuid1, uuid2, uuid3], - getMessage() - ); + const result = markOutgoingReactionSent(reactions, star, [ + uuid1, + uuid2, + uuid3, + ]); assert.isUndefined(result.find(re => re.emoji === '🔕')); }); - - it('does not remove my older reactions if they are on a story', () => { - const result = markOutgoingReactionSent( - reactions, - star, - [uuid1, uuid2, uuid3], - { ...getMessage(), type: 'story' } - ); - - assert.isDefined(result.find(re => re.emoji === '🔕')); - }); }); }); diff --git a/ts/types/Stories.ts b/ts/types/Stories.ts index c0c2fa953bb..e580181ccd3 100644 --- a/ts/types/Stories.ts +++ b/ts/types/Stories.ts @@ -37,7 +37,7 @@ export type ReplyType = { export type ReplyStateType = { messageId: string; - replies: Array; + replies: ReadonlyArray; }; export type ConversationStoryType = { diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 9abcab33c9b..1d870ba7bfe 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -22,13 +22,6 @@ "reasonCategory": "falseMatch", "updated": "2018-09-19T18:13:29.628Z" }, - { - "rule": "React-useRef", - "path": "ts/components/ModalContainer.tsx", - "line": " const containerRef = React.useRef(null);", - "reasonCategory": "usageTrusted", - "updated": "2022-10-14T16:39:48.461Z" - }, { "rule": "jQuery-globalEval(", "path": "components/mp3lameencoder/lib/Mp3LameEncoder.js", @@ -9237,6 +9230,13 @@ "reasonCategory": "usageTrusted", "updated": "2021-09-21T01:40:08.534Z" }, + { + "rule": "React-useRef", + "path": "ts/components/ModalContainer.tsx", + "line": " const containerRef = React.useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2022-10-14T16:39:48.461Z" + }, { "rule": "React-useRef", "path": "ts/components/ModalHost.tsx",