Store all story reactions as messages
This commit is contained in:
parent
f13611712c
commit
54aa0d39b9
12 changed files with 237 additions and 223 deletions
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
8
ts/model-types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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 === '🔕'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,7 +37,7 @@ export type ReplyType = {
|
|||
|
||||
export type ReplyStateType = {
|
||||
messageId: string;
|
||||
replies: Array<ReplyType>;
|
||||
replies: ReadonlyArray<ReplyType>;
|
||||
};
|
||||
|
||||
export type ConversationStoryType = {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue