Stories: Proper notifications and handling of out-of-order messages
This commit is contained in:
parent
81fc9ff94d
commit
50a0110192
12 changed files with 425 additions and 274 deletions
|
@ -3011,17 +3011,18 @@ export async function startApp(): Promise<void> {
|
||||||
log.info('Queuing incoming reaction for', reaction.targetTimestamp);
|
log.info('Queuing incoming reaction for', reaction.targetTimestamp);
|
||||||
const attributes: ReactionAttributesType = {
|
const attributes: ReactionAttributesType = {
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
|
fromId: fromConversation.id,
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
|
source: ReactionSource.FromSomeoneElse,
|
||||||
|
storyReactionMessage: message,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp: reaction.targetTimestamp,
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
fromId: fromConversation.id,
|
|
||||||
source: ReactionSource.FromSomeoneElse,
|
|
||||||
};
|
};
|
||||||
const reactionModel = Reactions.getSingleton().add(attributes);
|
const reactionModel = Reactions.getSingleton().add(attributes);
|
||||||
|
|
||||||
// Note: We do not wait for completion here
|
drop(Reactions.getSingleton().onReaction(reactionModel));
|
||||||
void Reactions.getSingleton().onReaction(reactionModel, message);
|
|
||||||
confirm();
|
confirm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -3383,16 +3384,17 @@ export async function startApp(): Promise<void> {
|
||||||
log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
log.info('Queuing sent reaction for', reaction.targetTimestamp);
|
||||||
const attributes: ReactionAttributesType = {
|
const attributes: ReactionAttributesType = {
|
||||||
emoji: reaction.emoji,
|
emoji: reaction.emoji,
|
||||||
|
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
remove: reaction.remove,
|
remove: reaction.remove,
|
||||||
|
source: ReactionSource.FromSync,
|
||||||
|
storyReactionMessage: message,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp: reaction.targetTimestamp,
|
targetTimestamp: reaction.targetTimestamp,
|
||||||
timestamp,
|
timestamp,
|
||||||
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
|
||||||
source: ReactionSource.FromSync,
|
|
||||||
};
|
};
|
||||||
const reactionModel = Reactions.getSingleton().add(attributes);
|
const reactionModel = Reactions.getSingleton().add(attributes);
|
||||||
// Note: We do not wait for completion here
|
|
||||||
void Reactions.getSingleton().onReaction(reactionModel, message);
|
drop(Reactions.getSingleton().onReaction(reactionModel));
|
||||||
|
|
||||||
event.confirm();
|
event.confirm();
|
||||||
return;
|
return;
|
||||||
|
@ -3767,7 +3769,7 @@ export async function startApp(): Promise<void> {
|
||||||
const attributes: MessageReceiptAttributesType = {
|
const attributes: MessageReceiptAttributesType = {
|
||||||
messageSentAt: timestamp,
|
messageSentAt: timestamp,
|
||||||
receiptTimestamp: envelopeTimestamp,
|
receiptTimestamp: envelopeTimestamp,
|
||||||
sourceConversationId: sourceConversation?.id,
|
sourceConversationId: sourceConversation.id,
|
||||||
sourceUuid,
|
sourceUuid,
|
||||||
sourceDevice,
|
sourceDevice,
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -326,6 +326,7 @@ export function StoryViewsNRepliesModal({
|
||||||
return (
|
return (
|
||||||
<ReplyOrReactionMessage
|
<ReplyOrReactionMessage
|
||||||
key={reply.id}
|
key={reply.id}
|
||||||
|
id={reply.id}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isInternalUser={isInternalUser}
|
isInternalUser={isInternalUser}
|
||||||
reply={reply}
|
reply={reply}
|
||||||
|
@ -504,6 +505,7 @@ export function StoryViewsNRepliesModal({
|
||||||
|
|
||||||
type ReplyOrReactionMessageProps = {
|
type ReplyOrReactionMessageProps = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
id: string;
|
||||||
isInternalUser?: boolean;
|
isInternalUser?: boolean;
|
||||||
reply: ReplyType;
|
reply: ReplyType;
|
||||||
deleteGroupStoryReply: (replyId: string) => void;
|
deleteGroupStoryReply: (replyId: string) => void;
|
||||||
|
@ -517,6 +519,7 @@ type ReplyOrReactionMessageProps = {
|
||||||
|
|
||||||
function ReplyOrReactionMessage({
|
function ReplyOrReactionMessage({
|
||||||
i18n,
|
i18n,
|
||||||
|
id,
|
||||||
isInternalUser,
|
isInternalUser,
|
||||||
reply,
|
reply,
|
||||||
deleteGroupStoryReply,
|
deleteGroupStoryReply,
|
||||||
|
@ -532,6 +535,7 @@ function ReplyOrReactionMessage({
|
||||||
<div
|
<div
|
||||||
className="StoryViewsNRepliesModal__reaction"
|
className="StoryViewsNRepliesModal__reaction"
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
data-id={id}
|
||||||
>
|
>
|
||||||
<div className="StoryViewsNRepliesModal__reaction--container">
|
<div className="StoryViewsNRepliesModal__reaction--container">
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -570,33 +574,35 @@ function ReplyOrReactionMessage({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Message
|
<div className="StoryViewsNRepliesModal__reply" data-id={id}>
|
||||||
{...MESSAGE_DEFAULT_PROPS}
|
<Message
|
||||||
author={reply.author}
|
{...MESSAGE_DEFAULT_PROPS}
|
||||||
bodyRanges={reply.bodyRanges}
|
author={reply.author}
|
||||||
contactNameColor={reply.contactNameColor}
|
bodyRanges={reply.bodyRanges}
|
||||||
containerElementRef={containerElementRef}
|
contactNameColor={reply.contactNameColor}
|
||||||
conversationColor="ultramarine"
|
containerElementRef={containerElementRef}
|
||||||
conversationId={reply.conversationId}
|
conversationColor="ultramarine"
|
||||||
conversationTitle={reply.author.title}
|
conversationId={reply.conversationId}
|
||||||
conversationType="group"
|
conversationTitle={reply.author.title}
|
||||||
direction="incoming"
|
conversationType="group"
|
||||||
deletedForEveryone={reply.deletedForEveryone}
|
direction="incoming"
|
||||||
renderMenu={undefined}
|
deletedForEveryone={reply.deletedForEveryone}
|
||||||
onContextMenu={onContextMenu}
|
renderMenu={undefined}
|
||||||
getPreferredBadge={getPreferredBadge}
|
onContextMenu={onContextMenu}
|
||||||
i18n={i18n}
|
getPreferredBadge={getPreferredBadge}
|
||||||
id={reply.id}
|
i18n={i18n}
|
||||||
interactionMode="mouse"
|
id={reply.id}
|
||||||
readStatus={reply.readStatus}
|
interactionMode="mouse"
|
||||||
renderingContext="StoryViewsNRepliesModal"
|
readStatus={reply.readStatus}
|
||||||
shouldCollapseAbove={shouldCollapseAbove}
|
renderingContext="StoryViewsNRepliesModal"
|
||||||
shouldCollapseBelow={shouldCollapseBelow}
|
shouldCollapseAbove={shouldCollapseAbove}
|
||||||
shouldHideMetadata={false}
|
shouldCollapseBelow={shouldCollapseBelow}
|
||||||
text={reply.body}
|
shouldHideMetadata={false}
|
||||||
textDirection={TextDirection.Default}
|
text={reply.body}
|
||||||
timestamp={reply.timestamp}
|
textDirection={TextDirection.Default}
|
||||||
/>
|
timestamp={reply.timestamp}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -319,7 +319,7 @@ export async function sendReaction(
|
||||||
ourUuid,
|
ourUuid,
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
}),
|
}),
|
||||||
reactionMessage.hydrateStoryContext(message),
|
reactionMessage.hydrateStoryContext(message.attributes),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
void conversation.addSingleMessage(
|
void conversation.addSingleMessage(
|
||||||
|
|
|
@ -6,11 +6,9 @@
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { Collection, Model } from 'backbone';
|
import { Collection, Model } from 'backbone';
|
||||||
|
|
||||||
import type { ConversationModel } from '../models/conversations';
|
|
||||||
import type { MessageModel } from '../models/messages';
|
import type { MessageModel } from '../models/messages';
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import { isOutgoing, isStory } from '../state/selectors/message';
|
import { isOutgoing, isStory } from '../state/selectors/message';
|
||||||
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
|
||||||
import { getOwn } from '../util/getOwn';
|
import { getOwn } from '../util/getOwn';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { createWaitBatcher } from '../util/waitBatcher';
|
import { createWaitBatcher } from '../util/waitBatcher';
|
||||||
|
@ -24,6 +22,7 @@ import {
|
||||||
import type { DeleteSentProtoRecipientOptionsType } from '../sql/Interface';
|
import type { DeleteSentProtoRecipientOptionsType } from '../sql/Interface';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
|
import { getSourceUuid } from '../messages/helpers';
|
||||||
|
|
||||||
const { deleteSentProtoRecipient } = dataInterface;
|
const { deleteSentProtoRecipient } = dataInterface;
|
||||||
|
|
||||||
|
@ -148,24 +147,20 @@ export class MessageReceipts extends Collection<MessageReceiptModel> {
|
||||||
return singleton;
|
return singleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
forMessage(
|
forMessage(message: MessageModel): Array<MessageReceiptModel> {
|
||||||
conversation: ConversationModel,
|
if (!isOutgoing(message.attributes) && !isStory(message.attributes)) {
|
||||||
message: MessageModel
|
|
||||||
): Array<MessageReceiptModel> {
|
|
||||||
if (!isOutgoing(message.attributes)) {
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
let ids: Array<string>;
|
|
||||||
if (isDirectConversation(conversation.attributes)) {
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
ids = [conversation.id];
|
const sourceUuid = getSourceUuid(message.attributes);
|
||||||
} else {
|
if (ourUuid !== sourceUuid) {
|
||||||
ids = conversation.getMemberIds();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const sentAt = message.get('sent_at');
|
const sentAt = message.get('sent_at');
|
||||||
const receipts = this.filter(
|
const receipts = this.filter(
|
||||||
receipt =>
|
receipt => receipt.get('messageSentAt') === sentAt
|
||||||
receipt.get('messageSentAt') === sentAt &&
|
|
||||||
ids.includes(receipt.get('sourceConversationId'))
|
|
||||||
);
|
);
|
||||||
if (receipts.length) {
|
if (receipts.length) {
|
||||||
log.info(`MessageReceipts: found early receipts for message ${sentAt}`);
|
log.info(`MessageReceipts: found early receipts for message ${sentAt}`);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import * as log from '../logging/log';
|
||||||
import { getContactId, getContact } from '../messages/helpers';
|
import { getContactId, getContact } from '../messages/helpers';
|
||||||
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
|
||||||
import { isOutgoing, isStory } from '../state/selectors/message';
|
import { isOutgoing, isStory } from '../state/selectors/message';
|
||||||
import { getMessageIdForLogging } from '../util/idForLogging';
|
import { strictAssert } from '../util/assert';
|
||||||
|
|
||||||
export class ReactionModel extends Model<ReactionAttributesType> {}
|
export class ReactionModel extends Model<ReactionAttributesType> {}
|
||||||
|
|
||||||
|
@ -83,10 +83,7 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onReaction(
|
async onReaction(reaction: ReactionModel): Promise<void> {
|
||||||
reaction: ReactionModel,
|
|
||||||
generatedMessage: MessageModel
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
// The conversation the target message was in; we have to find it in the database
|
// The conversation the target message was in; we have to find it in the database
|
||||||
// to to figure that out.
|
// to to figure that out.
|
||||||
|
@ -102,6 +99,11 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const generatedMessage = reaction.get('storyReactionMessage');
|
||||||
|
strictAssert(
|
||||||
|
generatedMessage,
|
||||||
|
'Story reactions must provide storyReactionMessage'
|
||||||
|
);
|
||||||
const fromConversation = window.ConversationController.get(
|
const fromConversation = window.ConversationController.get(
|
||||||
generatedMessage.get('conversationId')
|
generatedMessage.get('conversationId')
|
||||||
);
|
);
|
||||||
|
@ -115,6 +117,8 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
if (!targetMessageCheck) {
|
if (!targetMessageCheck) {
|
||||||
log.info(
|
log.info(
|
||||||
'No message for reaction',
|
'No message for reaction',
|
||||||
|
reaction.get('timestamp'),
|
||||||
|
'targeting',
|
||||||
reaction.get('targetAuthorUuid'),
|
reaction.get('targetAuthorUuid'),
|
||||||
reaction.get('targetTimestamp')
|
reaction.get('targetTimestamp')
|
||||||
);
|
);
|
||||||
|
@ -173,46 +177,14 @@ export class Reactions extends Collection<ReactionModel> {
|
||||||
|
|
||||||
// Use the generated message in ts/background.ts to create a message
|
// Use the generated message in ts/background.ts to create a message
|
||||||
// if the reaction is targeted at a story.
|
// if the reaction is targeted at a story.
|
||||||
if (isStory(targetMessage)) {
|
if (!isStory(targetMessage)) {
|
||||||
generatedMessage.set({
|
await message.handleReaction(reaction);
|
||||||
expireTimer: targetConversation.get('expireTimer'),
|
} else {
|
||||||
storyId: targetMessage.id,
|
await generatedMessage.handleReaction(reaction, {
|
||||||
storyReaction: {
|
storyMessage: targetMessage,
|
||||||
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
|
|
||||||
await Promise.all([
|
|
||||||
window.Signal.Data.saveMessage(generatedMessage.attributes, {
|
|
||||||
ourUuid: window.textsecure.storage.user
|
|
||||||
.getCheckedUuid()
|
|
||||||
.toString(),
|
|
||||||
forceSave: true,
|
|
||||||
}),
|
|
||||||
generatedMessage.hydrateStoryContext(message),
|
|
||||||
]);
|
|
||||||
|
|
||||||
log.info('Reactions.onReaction adding reaction to story', {
|
|
||||||
reactionMessageId: getMessageIdForLogging(
|
|
||||||
generatedMessage.attributes
|
|
||||||
),
|
|
||||||
storyId: getMessageIdForLogging(targetMessage),
|
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
|
||||||
timestamp: reaction.get('timestamp'),
|
|
||||||
});
|
|
||||||
|
|
||||||
const messageToAdd = window.MessageController.register(
|
|
||||||
generatedMessage.id,
|
|
||||||
generatedMessage
|
|
||||||
);
|
|
||||||
void targetConversation.addSingleMessage(messageToAdd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await message.handleReaction(reaction);
|
|
||||||
|
|
||||||
this.remove(reaction);
|
this.remove(reaction);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
7
ts/model-types.d.ts
vendored
7
ts/model-types.d.ts
vendored
|
@ -470,10 +470,13 @@ export declare class MessageModelCollectionType extends Backbone.Collection<Mess
|
||||||
|
|
||||||
export type ReactionAttributesType = {
|
export type ReactionAttributesType = {
|
||||||
emoji: string;
|
emoji: string;
|
||||||
|
fromId: string;
|
||||||
remove?: boolean;
|
remove?: boolean;
|
||||||
|
source: ReactionSource;
|
||||||
|
// Necessary to put 1:1 story replies into the right conversation - not the same
|
||||||
|
// conversation as the target message!
|
||||||
|
storyReactionMessage?: MessageModel;
|
||||||
targetAuthorUuid: string;
|
targetAuthorUuid: string;
|
||||||
targetTimestamp: number;
|
targetTimestamp: number;
|
||||||
fromId: string;
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
source: ReactionSource;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -5357,6 +5357,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationId = this.id;
|
const conversationId = this.id;
|
||||||
|
const isMessageInDirectConversation = isDirectConversation(this.attributes);
|
||||||
|
|
||||||
const sender = reaction
|
const sender = reaction
|
||||||
? window.ConversationController.get(reaction.get('fromId'))
|
? window.ConversationController.get(reaction.get('fromId'))
|
||||||
|
@ -5364,7 +5365,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const senderName = sender
|
const senderName = sender
|
||||||
? sender.getTitle()
|
? sender.getTitle()
|
||||||
: window.i18n('unknownContact');
|
: window.i18n('unknownContact');
|
||||||
const senderTitle = isDirectConversation(this.attributes)
|
const senderTitle = isMessageInDirectConversation
|
||||||
? senderName
|
? senderName
|
||||||
: window.i18n('notificationSenderInGroup', {
|
: window.i18n('notificationSenderInGroup', {
|
||||||
sender: senderName,
|
sender: senderName,
|
||||||
|
@ -5375,7 +5376,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const avatar = this.get('avatar') || this.get('profileAvatar');
|
const avatar = this.get('avatar') || this.get('profileAvatar');
|
||||||
if (avatar && avatar.path) {
|
if (avatar && avatar.path) {
|
||||||
notificationIconUrl = getAbsoluteAttachmentPath(avatar.path);
|
notificationIconUrl = getAbsoluteAttachmentPath(avatar.path);
|
||||||
} else if (isDirectConversation(this.attributes)) {
|
} else if (isMessageInDirectConversation) {
|
||||||
notificationIconUrl = await this.getIdenticon();
|
notificationIconUrl = await this.getIdenticon();
|
||||||
} else {
|
} else {
|
||||||
// Not technically needed, but helps us be explicit: we don't show an icon for a
|
// Not technically needed, but helps us be explicit: we don't show an icon for a
|
||||||
|
@ -5390,7 +5391,9 @@ export class ConversationModel extends window.Backbone
|
||||||
notificationService.add({
|
notificationService.add({
|
||||||
senderTitle,
|
senderTitle,
|
||||||
conversationId,
|
conversationId,
|
||||||
storyId: message.get('storyId'),
|
storyId: isMessageInDirectConversation
|
||||||
|
? undefined
|
||||||
|
: message.get('storyId'),
|
||||||
notificationIconUrl,
|
notificationIconUrl,
|
||||||
isExpiringMessage,
|
isExpiringMessage,
|
||||||
message: message.getNotificationText(),
|
message: message.getNotificationText(),
|
||||||
|
|
|
@ -39,7 +39,6 @@ import { softAssert, strictAssert } from '../util/assert';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { drop } from '../util/drop';
|
import { drop } from '../util/drop';
|
||||||
import { dropNull } from '../util/dropNull';
|
import { dropNull } from '../util/dropNull';
|
||||||
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
|
||||||
import type { ConversationModel } from './conversations';
|
import type { ConversationModel } from './conversations';
|
||||||
import { getCallingNotificationText } from '../util/callingNotification';
|
import { getCallingNotificationText } from '../util/callingNotification';
|
||||||
import type {
|
import type {
|
||||||
|
@ -415,20 +414,22 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async hydrateStoryContext(
|
async hydrateStoryContext(
|
||||||
inMemoryMessage?: MessageModel | null
|
inMemoryMessage?: MessageAttributesType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const storyId = this.get('storyId');
|
const storyId = this.get('storyId');
|
||||||
if (!storyId) {
|
if (!storyId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.get('storyReplyContext')) {
|
const context = this.get('storyReplyContext');
|
||||||
|
// We'll continue trying to get the attachment as long as the message still exists
|
||||||
|
if (context && (context.attachment?.url || !context.messageId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
inMemoryMessage === undefined
|
inMemoryMessage === undefined
|
||||||
? await getMessageById(storyId)
|
? (await getMessageById(storyId))?.attributes
|
||||||
: inMemoryMessage;
|
: inMemoryMessage;
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
|
@ -450,13 +451,17 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachments = getAttachmentsForMessage({ ...message.attributes });
|
const attachments = getAttachmentsForMessage({ ...message });
|
||||||
|
let attachment: AttachmentType | undefined = attachments?.[0];
|
||||||
|
if (attachment && !attachment.url) {
|
||||||
|
attachment = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
storyReplyContext: {
|
storyReplyContext: {
|
||||||
attachment: attachments ? attachments[0] : undefined,
|
attachment,
|
||||||
authorUuid: message.get('sourceUuid'),
|
authorUuid: message.sourceUuid,
|
||||||
messageId: message.get('id'),
|
messageId: message.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1078,7 +1083,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
if (this.get('storyReplyContext')) {
|
if (this.get('storyReplyContext')) {
|
||||||
this.unset('storyReplyContext');
|
this.unset('storyReplyContext');
|
||||||
}
|
}
|
||||||
await this.hydrateStoryContext(message);
|
await this.hydrateStoryContext(message.attributes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2478,16 +2483,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
findStoryMessage(conversation.id, initialMessage.storyContext),
|
findStoryMessage(conversation.id, initialMessage.storyContext),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (initialMessage.storyContext && !storyQuote) {
|
||||||
initialMessage.storyContext &&
|
if (!isDirectConversation(conversation.attributes)) {
|
||||||
!storyQuote &&
|
log.warn(
|
||||||
!isDirectConversation(conversation.attributes)
|
`${idLog}: Received storyContext message in group but no matching story. Dropping.`
|
||||||
) {
|
);
|
||||||
|
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.warn(
|
log.warn(
|
||||||
`${idLog}: Received storyContext message in group but no matching story. Dropping.`
|
`${idLog}: Received 1:1 storyContext message but no matching story. We'll try processing this message again later.`
|
||||||
);
|
);
|
||||||
|
|
||||||
confirm();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2511,10 +2519,11 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
if (
|
if (
|
||||||
storyQuoteIsFromSelf &&
|
storyQuoteIsFromSelf &&
|
||||||
sendState.isAllowedToReplyToStory === false
|
sendState.isAllowedToReplyToStory === false &&
|
||||||
|
isDirectConversation(conversation.attributes)
|
||||||
) {
|
) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`${idLog}: Received storyContext message but sender is not allowed to reply. Dropping.`
|
`${idLog}: Received 1:1 storyContext message but sender is not allowed to reply. Dropping.`
|
||||||
);
|
);
|
||||||
|
|
||||||
confirm();
|
confirm();
|
||||||
|
@ -2619,7 +2628,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (storyQuote) {
|
if (storyQuote) {
|
||||||
await this.hydrateStoryContext(storyQuote);
|
await this.hydrateStoryContext(storyQuote.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSupported = !isUnsupportedMessage(message.attributes);
|
const isSupported = !isUnsupportedMessage(message.attributes);
|
||||||
|
@ -3063,10 +3072,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const message = this;
|
const message = this;
|
||||||
const type = message.get('type');
|
const type = message.get('type');
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
const ourUuid = window.textsecure.storage.user.getCheckedUuid().toString();
|
||||||
|
const sourceUuid = getSourceUuid(message.attributes);
|
||||||
|
|
||||||
if (type === 'outgoing') {
|
if (type === 'outgoing' || (type === 'story' && ourUuid === sourceUuid)) {
|
||||||
const sendActions = MessageReceipts.getSingleton()
|
const sendActions = MessageReceipts.getSingleton()
|
||||||
.forMessage(conversation, message)
|
.forMessage(message)
|
||||||
.map(receipt => {
|
.map(receipt => {
|
||||||
let sendActionType: SendActionType;
|
let sendActionType: SendActionType;
|
||||||
const receiptType = receipt.get('type');
|
const receiptType = receipt.get('type');
|
||||||
|
@ -3252,8 +3263,20 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const reactions = Reactions.getSingleton().forMessage(message);
|
const reactions = Reactions.getSingleton().forMessage(message);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
reactions.map(async reaction => {
|
reactions.map(async reaction => {
|
||||||
await message.handleReaction(reaction, false);
|
if (isStory(this.attributes)) {
|
||||||
changed = true;
|
// We don't set changed = true here, because we don't modify the original story
|
||||||
|
const generatedMessage = reaction.get('storyReactionMessage');
|
||||||
|
strictAssert(
|
||||||
|
generatedMessage,
|
||||||
|
'Story reactions must provide storyReactionMessage'
|
||||||
|
);
|
||||||
|
await generatedMessage.handleReaction(reaction, {
|
||||||
|
storyMessage: this.attributes,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
changed = true;
|
||||||
|
await message.handleReaction(reaction, { shouldPersist: false });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3279,7 +3302,13 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
|
|
||||||
async handleReaction(
|
async handleReaction(
|
||||||
reaction: ReactionModel,
|
reaction: ReactionModel,
|
||||||
shouldPersist = true
|
{
|
||||||
|
storyMessage,
|
||||||
|
shouldPersist = true,
|
||||||
|
}: {
|
||||||
|
storyMessage?: MessageAttributesType;
|
||||||
|
shouldPersist?: boolean;
|
||||||
|
} = {}
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { attributes } = this;
|
const { attributes } = this;
|
||||||
|
|
||||||
|
@ -3305,7 +3334,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousLength = (this.get('reactions') || []).length;
|
|
||||||
const newReaction: MessageReactionType = {
|
const newReaction: MessageReactionType = {
|
||||||
emoji: reaction.get('remove') ? undefined : reaction.get('emoji'),
|
emoji: reaction.get('remove') ? undefined : reaction.get('emoji'),
|
||||||
fromId: reaction.get('fromId'),
|
fromId: reaction.get('fromId'),
|
||||||
|
@ -3328,163 +3356,221 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
'Reaction can only be from this device, from sync, or from someone else'
|
'Reaction can only be from this device, from sync, or from someone else'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isStory(this.attributes)) {
|
// Reactions to stories are saved as separate messages, and so require a totally
|
||||||
|
// different codepath.
|
||||||
|
if (storyMessage) {
|
||||||
if (isFromThisDevice) {
|
if (isFromThisDevice) {
|
||||||
log.info(
|
log.info(
|
||||||
'handleReaction: sending story reaction to ' +
|
'handleReaction: sending story reaction to ' +
|
||||||
`${this.idForLogging()} from this device`
|
`${getMessageIdForLogging(storyMessage)} from this device`
|
||||||
);
|
|
||||||
} else if (isFromSync) {
|
|
||||||
log.info(
|
|
||||||
'handleReaction: receiving story reaction to ' +
|
|
||||||
`${this.idForLogging()} from another device`
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
const generatedMessage = reaction.get('storyReactionMessage');
|
||||||
'handleReaction: receiving story reaction to ' +
|
strictAssert(
|
||||||
`${this.idForLogging()} from someone else`
|
generatedMessage,
|
||||||
|
'Story reactions must provide storyReactionMessage'
|
||||||
);
|
);
|
||||||
void conversation.notify(this, reaction);
|
const targetConversation = window.ConversationController.get(
|
||||||
}
|
generatedMessage.get('conversationId')
|
||||||
} else if (isFromThisDevice) {
|
|
||||||
log.info(
|
|
||||||
`handleReaction: sending reaction to ${this.idForLogging()} ` +
|
|
||||||
'from this device'
|
|
||||||
);
|
|
||||||
|
|
||||||
const reactions = reactionUtil.addOutgoingReaction(
|
|
||||||
this.get('reactions') || [],
|
|
||||||
newReaction
|
|
||||||
);
|
|
||||||
this.set({ reactions });
|
|
||||||
} else {
|
|
||||||
const oldReactions = this.get('reactions') || [];
|
|
||||||
let reactions: Array<MessageReactionType>;
|
|
||||||
const oldReaction = oldReactions.find(re =>
|
|
||||||
isNewReactionReplacingPrevious(re, reaction.attributes)
|
|
||||||
);
|
|
||||||
if (oldReaction) {
|
|
||||||
this.clearNotifications(oldReaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reaction.get('remove')) {
|
|
||||||
log.info(
|
|
||||||
'handleReaction: removing reaction for message',
|
|
||||||
this.idForLogging()
|
|
||||||
);
|
);
|
||||||
|
strictAssert(
|
||||||
|
targetConversation,
|
||||||
|
'handleReaction: targetConversation not found'
|
||||||
|
);
|
||||||
|
|
||||||
|
generatedMessage.set({
|
||||||
|
expireTimer: isDirectConversation(targetConversation.attributes)
|
||||||
|
? targetConversation.get('expireTimer')
|
||||||
|
: undefined,
|
||||||
|
storyId: storyMessage.id,
|
||||||
|
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
|
||||||
|
await Promise.all([
|
||||||
|
window.Signal.Data.saveMessage(generatedMessage.attributes, {
|
||||||
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
|
forceSave: true,
|
||||||
|
}),
|
||||||
|
generatedMessage.hydrateStoryContext(storyMessage),
|
||||||
|
]);
|
||||||
|
|
||||||
|
log.info('Reactions.onReaction adding reaction to story', {
|
||||||
|
reactionMessageId: getMessageIdForLogging(
|
||||||
|
generatedMessage.attributes
|
||||||
|
),
|
||||||
|
storyId: getMessageIdForLogging(storyMessage),
|
||||||
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
|
timestamp: reaction.get('timestamp'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageToAdd = window.MessageController.register(
|
||||||
|
generatedMessage.id,
|
||||||
|
generatedMessage
|
||||||
|
);
|
||||||
|
if (isDirectConversation(targetConversation.attributes)) {
|
||||||
|
await targetConversation.addSingleMessage(messageToAdd);
|
||||||
|
if (!targetConversation.get('active_at')) {
|
||||||
|
targetConversation.set({
|
||||||
|
active_at: messageToAdd.get('timestamp'),
|
||||||
|
});
|
||||||
|
window.Signal.Data.updateConversation(
|
||||||
|
targetConversation.attributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isFromSomeoneElse) {
|
||||||
|
drop(targetConversation.notify(messageToAdd));
|
||||||
|
}
|
||||||
|
|
||||||
if (isFromSync) {
|
if (isFromSync) {
|
||||||
reactions = oldReactions.filter(
|
log.info(
|
||||||
re =>
|
'handleReaction: receiving story reaction to ' +
|
||||||
!isNewReactionReplacingPrevious(re, reaction.attributes) ||
|
`${getMessageIdForLogging(storyMessage)} from another device`
|
||||||
re.timestamp > reaction.get('timestamp')
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
log.info(
|
||||||
|
'handleReaction: receiving story reaction to ' +
|
||||||
|
`${getMessageIdForLogging(storyMessage)} from someone else`
|
||||||
|
);
|
||||||
|
void conversation.notify(this, reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reactions to all messages other than stories will update the target message
|
||||||
|
const previousLength = (this.get('reactions') || []).length;
|
||||||
|
|
||||||
|
if (isFromThisDevice) {
|
||||||
|
log.info(
|
||||||
|
`handleReaction: sending reaction to ${this.idForLogging()} ` +
|
||||||
|
'from this device'
|
||||||
|
);
|
||||||
|
|
||||||
|
const reactions = reactionUtil.addOutgoingReaction(
|
||||||
|
this.get('reactions') || [],
|
||||||
|
newReaction
|
||||||
|
);
|
||||||
|
this.set({ reactions });
|
||||||
|
} else {
|
||||||
|
const oldReactions = this.get('reactions') || [];
|
||||||
|
let reactions: Array<MessageReactionType>;
|
||||||
|
const oldReaction = oldReactions.find(re =>
|
||||||
|
isNewReactionReplacingPrevious(re, newReaction)
|
||||||
|
);
|
||||||
|
if (oldReaction) {
|
||||||
|
this.clearNotifications(oldReaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reaction.get('remove')) {
|
||||||
|
log.info(
|
||||||
|
'handleReaction: removing reaction for message',
|
||||||
|
this.idForLogging()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isFromSync) {
|
||||||
|
reactions = oldReactions.filter(
|
||||||
|
re =>
|
||||||
|
!isNewReactionReplacingPrevious(re, newReaction) ||
|
||||||
|
re.timestamp > reaction.get('timestamp')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
reactions = oldReactions.filter(
|
||||||
|
re => !isNewReactionReplacingPrevious(re, newReaction)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.set({ reactions });
|
||||||
|
|
||||||
|
await window.Signal.Data.removeReactionFromConversation({
|
||||||
|
emoji: reaction.get('emoji'),
|
||||||
|
fromId: reaction.get('fromId'),
|
||||||
|
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
||||||
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'handleReaction: adding reaction for message',
|
||||||
|
this.idForLogging()
|
||||||
|
);
|
||||||
|
|
||||||
|
let reactionToAdd: MessageReactionType;
|
||||||
|
if (isFromSync) {
|
||||||
|
const ourReactions = [
|
||||||
|
newReaction,
|
||||||
|
...oldReactions.filter(
|
||||||
|
re => re.fromId === reaction.get('fromId')
|
||||||
|
),
|
||||||
|
];
|
||||||
|
reactionToAdd = maxBy(ourReactions, 'timestamp') || newReaction;
|
||||||
|
} else {
|
||||||
|
reactionToAdd = newReaction;
|
||||||
|
}
|
||||||
|
|
||||||
reactions = oldReactions.filter(
|
reactions = oldReactions.filter(
|
||||||
re => !isNewReactionReplacingPrevious(re, reaction.attributes)
|
re => !isNewReactionReplacingPrevious(re, reaction.attributes)
|
||||||
);
|
);
|
||||||
|
reactions.push(reactionToAdd);
|
||||||
|
this.set({ reactions });
|
||||||
|
|
||||||
|
if (isOutgoing(this.attributes) && isFromSomeoneElse) {
|
||||||
|
void conversation.notify(this, reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
await window.Signal.Data.addReaction({
|
||||||
|
conversationId: this.get('conversationId'),
|
||||||
|
emoji: reaction.get('emoji'),
|
||||||
|
fromId: reaction.get('fromId'),
|
||||||
|
messageId: this.id,
|
||||||
|
messageReceivedAt: this.get('received_at'),
|
||||||
|
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
||||||
|
targetTimestamp: reaction.get('targetTimestamp'),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.set({ reactions });
|
|
||||||
|
|
||||||
await window.Signal.Data.removeReactionFromConversation({
|
|
||||||
emoji: reaction.get('emoji'),
|
|
||||||
fromId: reaction.get('fromId'),
|
|
||||||
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.info(
|
|
||||||
'handleReaction: adding reaction for message',
|
|
||||||
this.idForLogging()
|
|
||||||
);
|
|
||||||
|
|
||||||
let reactionToAdd: MessageReactionType;
|
|
||||||
if (isFromSync) {
|
|
||||||
const ourReactions = [
|
|
||||||
reaction.toJSON(),
|
|
||||||
...oldReactions.filter(re => re.fromId === reaction.get('fromId')),
|
|
||||||
];
|
|
||||||
reactionToAdd = maxBy(ourReactions, 'timestamp');
|
|
||||||
} else {
|
|
||||||
reactionToAdd = reaction.toJSON();
|
|
||||||
}
|
|
||||||
|
|
||||||
reactions = oldReactions.filter(
|
|
||||||
re => !isNewReactionReplacingPrevious(re, reaction.attributes)
|
|
||||||
);
|
|
||||||
reactions.push(reactionToAdd);
|
|
||||||
this.set({ reactions });
|
|
||||||
|
|
||||||
if (isOutgoing(this.attributes) && isFromSomeoneElse) {
|
|
||||||
void conversation.notify(this, reaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.Signal.Data.addReaction({
|
|
||||||
conversationId: this.get('conversationId'),
|
|
||||||
emoji: reaction.get('emoji'),
|
|
||||||
fromId: reaction.get('fromId'),
|
|
||||||
messageId: this.id,
|
|
||||||
messageReceivedAt: this.get('received_at'),
|
|
||||||
targetAuthorUuid: reaction.get('targetAuthorUuid'),
|
|
||||||
targetTimestamp: reaction.get('targetTimestamp'),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const currentLength = (this.get('reactions') || []).length;
|
const currentLength = (this.get('reactions') || []).length;
|
||||||
log.info(
|
log.info(
|
||||||
'handleReaction:',
|
'handleReaction:',
|
||||||
`Done processing reaction for message ${this.idForLogging()}.`,
|
`Done processing reaction for message ${this.idForLogging()}.`,
|
||||||
`Went from ${previousLength} to ${currentLength} reactions.`
|
`Went from ${previousLength} to ${currentLength} reactions.`
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isFromThisDevice) {
|
if (isFromThisDevice) {
|
||||||
let jobData: ConversationQueueJobData;
|
let jobData: ConversationQueueJobData;
|
||||||
if (isStory(this.attributes)) {
|
if (storyMessage) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
newReaction.emoji !== undefined,
|
newReaction.emoji !== undefined,
|
||||||
'New story reaction must have an emoji'
|
'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,
|
|
||||||
expireTimer: conversation.get('expireTimer'),
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const generatedMessage = reaction.get('storyReactionMessage');
|
||||||
|
strictAssert(
|
||||||
|
generatedMessage,
|
||||||
|
'Story reactions must provide storyReactionmessage'
|
||||||
|
);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
await window.Signal.Data.saveMessage(reactionMessage.attributes, {
|
await window.Signal.Data.saveMessage(generatedMessage.attributes, {
|
||||||
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
|
||||||
forceSave: true,
|
forceSave: true,
|
||||||
}),
|
}),
|
||||||
reactionMessage.hydrateStoryContext(this),
|
generatedMessage.hydrateStoryContext(this.attributes),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
void conversation.addSingleMessage(
|
void conversation.addSingleMessage(
|
||||||
window.MessageController.register(reactionMessage.id, reactionMessage)
|
window.MessageController.register(
|
||||||
|
generatedMessage.id,
|
||||||
|
generatedMessage
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
jobData = {
|
jobData = {
|
||||||
type: conversationQueueJobEnum.enum.NormalMessage,
|
type: conversationQueueJobEnum.enum.NormalMessage,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
messageId: reactionMessage.id,
|
messageId: generatedMessage.id,
|
||||||
revision: conversation.get('revision'),
|
revision: conversation.get('revision'),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,8 +4,13 @@
|
||||||
import { ReactionModel } from '../messageModifiers/Reactions';
|
import { ReactionModel } from '../messageModifiers/Reactions';
|
||||||
import { ReactionSource } from './ReactionSource';
|
import { ReactionSource } from './ReactionSource';
|
||||||
import { getMessageById } from '../messages/getMessageById';
|
import { getMessageById } from '../messages/getMessageById';
|
||||||
import { getSourceUuid } from '../messages/helpers';
|
import { getSourceUuid, isStory } from '../messages/helpers';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
import { isDirectConversation } from '../util/whatTypeOfConversation';
|
||||||
|
import { incrementMessageCounter } from '../util/incrementMessageCounter';
|
||||||
|
import { repeat, zipObject } from '../util/iterables';
|
||||||
|
import { SendStatus } from '../messages/MessageSendState';
|
||||||
|
import { UUID } from '../types/UUID';
|
||||||
|
|
||||||
export async function enqueueReactionForSend({
|
export async function enqueueReactionForSend({
|
||||||
emoji,
|
emoji,
|
||||||
|
@ -31,15 +36,64 @@ export async function enqueueReactionForSend({
|
||||||
`enqueueReactionForSend: message ${message.idForLogging()} had no timestamp`
|
`enqueueReactionForSend: message ${message.idForLogging()} had no timestamp`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const messageConversation = message.getConversation();
|
||||||
|
strictAssert(
|
||||||
|
messageConversation,
|
||||||
|
'enqueueReactionForSend: No conversation extracted from target message'
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetConversation =
|
||||||
|
isStory(message.attributes) &&
|
||||||
|
isDirectConversation(messageConversation.attributes)
|
||||||
|
? window.ConversationController.get(targetAuthorUuid)
|
||||||
|
: messageConversation;
|
||||||
|
strictAssert(
|
||||||
|
targetConversation,
|
||||||
|
'enqueueReactionForSend: Did not find a targetConversation'
|
||||||
|
);
|
||||||
|
|
||||||
|
const storyMessage = isStory(message.attributes)
|
||||||
|
? message.attributes
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// Only used in story scenarios, where we use a whole message to represent the reaction
|
||||||
|
const storyReactionMessage = storyMessage
|
||||||
|
? new window.Whisper.Message({
|
||||||
|
id: UUID.generate().toString(),
|
||||||
|
type: 'outgoing',
|
||||||
|
conversationId: targetConversation.id,
|
||||||
|
sent_at: timestamp,
|
||||||
|
received_at: incrementMessageCounter(),
|
||||||
|
received_at_ms: timestamp,
|
||||||
|
timestamp,
|
||||||
|
expireTimer: targetConversation.get('expireTimer'),
|
||||||
|
sendStateByConversationId: zipObject(
|
||||||
|
targetConversation.getMemberConversationIds(),
|
||||||
|
repeat({
|
||||||
|
status: SendStatus.Pending,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
storyId: message.id,
|
||||||
|
storyReaction: {
|
||||||
|
emoji,
|
||||||
|
targetAuthorUuid,
|
||||||
|
targetTimestamp,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const reaction = new ReactionModel({
|
const reaction = new ReactionModel({
|
||||||
emoji,
|
emoji,
|
||||||
|
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
||||||
remove,
|
remove,
|
||||||
|
source: ReactionSource.FromThisDevice,
|
||||||
|
storyReactionMessage,
|
||||||
targetAuthorUuid,
|
targetAuthorUuid,
|
||||||
targetTimestamp,
|
targetTimestamp,
|
||||||
fromId: window.ConversationController.getOurConversationIdOrThrow(),
|
timestamp,
|
||||||
timestamp: Date.now(),
|
|
||||||
source: ReactionSource.FromThisDevice,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await message.handleReaction(reaction);
|
await message.handleReaction(reaction, { storyMessage });
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isPlainObject } from 'lodash';
|
import { isPlainObject } from 'lodash';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
import { isIterable } from '../util/iterables';
|
import { isIterable } from '../util/iterables';
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ export function cleanDataForIpc(data: unknown): {
|
||||||
pathsChanged: Array<string>;
|
pathsChanged: Array<string>;
|
||||||
} {
|
} {
|
||||||
const pathsChanged: Array<string> = [];
|
const pathsChanged: Array<string> = [];
|
||||||
const cleaned = cleanDataInner(data, 'root', pathsChanged);
|
const cleaned = cleanDataInner(data, 'root', pathsChanged, 0);
|
||||||
return { cleaned, pathsChanged };
|
return { cleaned, pathsChanged };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,8 +50,17 @@ interface CleanedArray extends Array<CleanedDataValue> {}
|
||||||
function cleanDataInner(
|
function cleanDataInner(
|
||||||
data: unknown,
|
data: unknown,
|
||||||
path: string,
|
path: string,
|
||||||
pathsChanged: Array<string>
|
pathsChanged: Array<string>,
|
||||||
|
depth: number
|
||||||
): CleanedDataValue {
|
): CleanedDataValue {
|
||||||
|
if (depth > 10) {
|
||||||
|
log.error(
|
||||||
|
`cleanDataInner: Reached maximum depth ${depth}; path is ${path}`
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return { cleaned: data as any, pathsChanged };
|
||||||
|
}
|
||||||
|
|
||||||
switch (typeof data) {
|
switch (typeof data) {
|
||||||
case 'undefined':
|
case 'undefined':
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
@ -77,7 +87,9 @@ function cleanDataInner(
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
pathsChanged.push(indexPath);
|
pathsChanged.push(indexPath);
|
||||||
} else {
|
} else {
|
||||||
result.push(cleanDataInner(item, indexPath, pathsChanged));
|
result.push(
|
||||||
|
cleanDataInner(item, indexPath, pathsChanged, depth + 1)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
@ -91,7 +103,8 @@ function cleanDataInner(
|
||||||
result[key] = cleanDataInner(
|
result[key] = cleanDataInner(
|
||||||
value,
|
value,
|
||||||
`${path}.<map value at ${key}>`,
|
`${path}.<map value at ${key}>`,
|
||||||
pathsChanged
|
pathsChanged,
|
||||||
|
depth + 1
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
pathsChanged.push(`${path}.<map key ${String(key)}>`);
|
pathsChanged.push(`${path}.<map key ${String(key)}>`);
|
||||||
|
@ -121,7 +134,12 @@ function cleanDataInner(
|
||||||
typeof dataAsRecord.toNumber === 'function'
|
typeof dataAsRecord.toNumber === 'function'
|
||||||
) {
|
) {
|
||||||
// We clean this just in case `toNumber` returns something bogus.
|
// We clean this just in case `toNumber` returns something bogus.
|
||||||
return cleanDataInner(dataAsRecord.toNumber(), path, pathsChanged);
|
return cleanDataInner(
|
||||||
|
dataAsRecord.toNumber(),
|
||||||
|
path,
|
||||||
|
pathsChanged,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isIterable(dataAsRecord)) {
|
if (isIterable(dataAsRecord)) {
|
||||||
|
@ -133,7 +151,8 @@ function cleanDataInner(
|
||||||
cleanDataInner(
|
cleanDataInner(
|
||||||
value,
|
value,
|
||||||
`${path}.<iterator index ${index}>`,
|
`${path}.<iterator index ${index}>`,
|
||||||
pathsChanged
|
pathsChanged,
|
||||||
|
depth + 1
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
index += 1;
|
index += 1;
|
||||||
|
@ -151,7 +170,12 @@ function cleanDataInner(
|
||||||
|
|
||||||
// Conveniently, `Object.entries` removes symbol keys.
|
// Conveniently, `Object.entries` removes symbol keys.
|
||||||
Object.entries(dataAsRecord).forEach(([key, value]) => {
|
Object.entries(dataAsRecord).forEach(([key, value]) => {
|
||||||
result[key] = cleanDataInner(value, `${path}.${key}`, pathsChanged);
|
result[key] = cleanDataInner(
|
||||||
|
value,
|
||||||
|
`${path}.${key}`,
|
||||||
|
pathsChanged,
|
||||||
|
depth + 1
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { MessageAttributesType } from '../model-types.d';
|
||||||
import { deletePackReference } from '../types/Stickers';
|
import { deletePackReference } from '../types/Stickers';
|
||||||
import { isStory } from '../messages/helpers';
|
import { isStory } from '../messages/helpers';
|
||||||
import { isDirectConversation } from './whatTypeOfConversation';
|
import { isDirectConversation } from './whatTypeOfConversation';
|
||||||
|
import { drop } from './drop';
|
||||||
|
|
||||||
export async function cleanupMessage(
|
export async function cleanupMessage(
|
||||||
message: MessageAttributesType
|
message: MessageAttributesType
|
||||||
|
@ -78,7 +79,7 @@ async function cleanupStoryReplies(
|
||||||
replies.forEach(reply => {
|
replies.forEach(reply => {
|
||||||
const model = window.MessageController.register(reply.id, reply);
|
const model = window.MessageController.register(reply.id, reply);
|
||||||
model.unset('storyReplyContext');
|
model.unset('storyReplyContext');
|
||||||
void model.hydrateStoryContext(null);
|
drop(model.hydrateStoryContext());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@
|
||||||
|
|
||||||
import type { AttachmentType } from '../types/Attachment';
|
import type { AttachmentType } from '../types/Attachment';
|
||||||
import type { MessageAttributesType } from '../model-types.d';
|
import type { MessageAttributesType } from '../model-types.d';
|
||||||
import type { SendStateByConversationId } from '../messages/MessageSendState';
|
import type {
|
||||||
|
SendState,
|
||||||
|
SendStateByConversationId,
|
||||||
|
} from '../messages/MessageSendState';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import dataInterface from '../sql/Client';
|
import dataInterface from '../sql/Client';
|
||||||
|
@ -261,27 +264,29 @@ export async function sendStoryMessage(
|
||||||
const groupTimestamp = timestamp + index + 1;
|
const groupTimestamp = timestamp + index + 1;
|
||||||
|
|
||||||
const myId = window.ConversationController.getOurConversationIdOrThrow();
|
const myId = window.ConversationController.getOurConversationIdOrThrow();
|
||||||
const sendState = {
|
const sendState: SendState = {
|
||||||
status: SendStatus.Pending,
|
status: SendStatus.Pending,
|
||||||
updatedAt: groupTimestamp,
|
updatedAt: groupTimestamp,
|
||||||
|
isAllowedToReplyToStory: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendStateByConversationId = getRecipients(group.attributes).reduce(
|
const sendStateByConversationId: SendStateByConversationId =
|
||||||
(acc, id) => {
|
getRecipients(group.attributes).reduce(
|
||||||
const conversation = window.ConversationController.get(id);
|
(acc, id) => {
|
||||||
if (!conversation) {
|
const conversation = window.ConversationController.get(id);
|
||||||
return acc;
|
if (!conversation) {
|
||||||
}
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...acc,
|
...acc,
|
||||||
[conversation.id]: sendState,
|
[conversation.id]: sendState,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[myId]: sendState,
|
[myId]: sendState,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageAttributes =
|
const messageAttributes =
|
||||||
await window.Signal.Migrations.upgradeMessageSchema({
|
await window.Signal.Migrations.upgradeMessageSchema({
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue