Store all story reactions as messages

This commit is contained in:
Fedor Indutny 2022-11-02 16:48:38 -07:00 committed by GitHub
parent f13611712c
commit 54aa0d39b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 223 deletions

View file

@ -111,7 +111,7 @@ export type PropsType = {
preferredReactionEmoji: Array<string>;
recentEmojis?: Array<string>;
renderEmojiPicker: (props: RenderEmojiPickerProps) => JSX.Element;
replies: Array<ReplyType>;
replies: ReadonlyArray<ReplyType>;
skinTone?: number;
sortedGroupMembers?: Array<ConversationType>;
storyPreviewAttachment?: AttachmentType;

View file

@ -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<CallbackResultType | void>;
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<LinkPreviewType>;
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,
};
}

View file

@ -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<string>();
@ -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<MessageReactionType> =>
message.get('reactions') || [];
const getReactions = (
message: MessageModel
): ReadonlyArray<MessageReactionType> => message.get('reactions') || [];
const setReactions = (
message: MessageModel,

View file

@ -169,14 +169,15 @@ export class Reactions extends Collection<ReactionModel> {
);
// 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

8
ts/model-types.d.ts vendored
View file

@ -146,7 +146,7 @@ export type MessageAttributesType = {
messageTimer?: unknown;
profileChange?: ProfileNameChangeType;
quote?: QuotedMessageType;
reactions?: Array<MessageReactionType>;
reactions?: ReadonlyArray<MessageReactionType>;
requiredProtocolVersion?: number;
retryOptions?: RetryOptions;
sourceDevice?: number;
@ -184,7 +184,11 @@ export type MessageAttributesType = {
unidentifiedDeliveries?: Array<string>;
contact?: Array<EmbeddedContactType>;
conversationId: string;
storyReactionEmoji?: string;
storyReaction?: {
emoji: string;
targetAuthorUuid: string;
targetTimestamp: number;
};
giftBadge?: {
expiration: number;
level: number;

View file

@ -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<MessageAttributesType> {
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<MessageAttributesType> {
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<MessageAttributesType> {
}
const previousLength = (this.get('reactions') || []).length;
if (reaction.get('source') === ReactionSource.FromThisDevice) {
log.info(
`handleReaction: sending reaction to ${this.idForLogging()} from this device`
);
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 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 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'
);
const reactions = reactionUtil.addOutgoingReaction(
this.get('reactions') || [],
newReaction,
isStory(this.attributes)
newReaction
);
this.set({ reactions });
} else {
const oldReactions = this.get('reactions') || [];
let reactions: Array<MessageReactionType>;
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<MessageAttributesType> {
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<MessageAttributesType> {
);
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<MessageAttributesType> {
}
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<MessageAttributesType> {
`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<MessageAttributesType> {
} 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(),
});

View file

@ -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<MessageReactionType>,
@ -35,13 +31,8 @@ const isOutgoingReactionCompletelyUnsent = ({
export function addOutgoingReaction(
oldReactions: ReadonlyArray<MessageReactionType>,
newReaction: Readonly<MessageReactionType>,
isStoryMessage = false
): Array<MessageReactionType> {
if (isStoryMessage) {
return [...oldReactions, newReaction];
}
newReaction: Readonly<MessageReactionType>
): ReadonlyArray<MessageReactionType> {
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<MessageReactionType>,
reactions: ReadonlyArray<MessageReactionType>,
reaction: Readonly<MessageReactionType>
): Array<MessageReactionType> =>
isOutgoingReactionCompletelyUnsent(reaction) || !reaction.emoji
@ -136,8 +126,7 @@ export const markOutgoingReactionFailed = (
export const markOutgoingReactionSent = (
reactions: ReadonlyArray<MessageReactionType>,
reaction: Readonly<MessageReactionType>,
conversationIdsSentTo: Iterable<string>,
messageAttributes: MessageAttributesType
conversationIdsSentTo: Iterable<string>
): Array<MessageReactionType> => {
const result: Array<MessageReactionType> = [];
@ -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);
}

View file

@ -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)

View file

@ -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<StoriesStateType>
{ replyState }: Readonly<StoriesStateType>
): 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,
};
}
);

View file

@ -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 === '🔕'));
});
});
});

View file

@ -37,7 +37,7 @@ export type ReplyType = {
export type ReplyStateType = {
messageId: string;
replies: Array<ReplyType>;
replies: ReadonlyArray<ReplyType>;
};
export type ConversationStoryType = {

View file

@ -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<HTMLDivElement | null>(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<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-10-14T16:39:48.461Z"
},
{
"rule": "React-useRef",
"path": "ts/components/ModalHost.tsx",