signal-desktop/ts/messageModifiers/Reactions.ts

198 lines
5.9 KiB
TypeScript
Raw Normal View History

2023-01-03 19:55:46 +00:00
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
2022-07-11 18:35:55 +00:00
import type { ConversationModel } from '../models/conversations';
import type { MessageModel } from '../models/messages';
2022-07-11 18:35:55 +00:00
import type {
MessageAttributesType,
ReactionAttributesType,
} from '../model-types.d';
import * as Errors from '../types/errors';
2022-07-11 18:35:55 +00:00
import * as log from '../logging/log';
import { getContactId, getContact } from '../messages/helpers';
2022-07-11 18:35:55 +00:00
import { isDirectConversation, isMe } from '../util/whatTypeOfConversation';
import { isOutgoing, isStory } from '../state/selectors/message';
import { strictAssert } from '../util/assert';
import { getMessageSentTimestampSet } from '../util/getMessageSentTimestampSet';
export class ReactionModel extends Model<ReactionAttributesType> {}
let singleton: Reactions | undefined;
2021-10-13 16:29:15 +00:00
export class Reactions extends Collection<ReactionModel> {
static getSingleton(): Reactions {
if (!singleton) {
singleton = new Reactions();
}
return singleton;
}
forMessage(message: MessageModel): Array<ReactionModel> {
const sentTimestamps = getMessageSentTimestampSet(message.attributes);
if (isOutgoing(message.attributes)) {
const outgoingReactions = this.filter(item =>
sentTimestamps.has(item.get('targetTimestamp'))
);
if (outgoingReactions.length > 0) {
log.info('Found early reaction for outgoing message');
this.remove(outgoingReactions);
return outgoingReactions;
}
}
const senderId = getContactId(message.attributes);
const reactionsBySource = this.filter(re => {
const targetSender = window.ConversationController.lookupOrCreate({
uuid: re.get('targetAuthorUuid'),
reason: 'Reactions.forMessage',
});
const targetTimestamp = re.get('targetTimestamp');
return (
targetSender?.id === senderId && sentTimestamps.has(targetTimestamp)
);
});
if (reactionsBySource.length > 0) {
log.info('Found early reaction for message');
this.remove(reactionsBySource);
return reactionsBySource;
}
return [];
}
2022-07-11 18:35:55 +00:00
private async findMessage(
targetTimestamp: number,
targetConversationId: string
): Promise<MessageAttributesType | undefined> {
const messages = await window.Signal.Data.getMessagesBySentAt(
targetTimestamp
);
return messages.find(m => {
const contact = getContact(m);
if (!contact) {
return false;
}
const mcid = contact.get('id');
return mcid === targetConversationId;
});
}
async onReaction(reaction: ReactionModel): Promise<void> {
try {
// The conversation the target message was in; we have to find it in the database
// to to figure that out.
const targetAuthorConversation =
window.ConversationController.lookupOrCreate({
uuid: reaction.get('targetAuthorUuid'),
reason: 'Reactions.onReaction',
2021-11-11 22:43:05 +00:00
});
const targetConversationId = targetAuthorConversation?.id;
if (!targetConversationId) {
throw new Error(
'onReaction: No conversationId returned from lookupOrCreate!'
);
}
const generatedMessage = reaction.get('storyReactionMessage');
strictAssert(
generatedMessage,
'Story reactions must provide storyReactionMessage'
);
2022-07-11 18:35:55 +00:00
const fromConversation = window.ConversationController.get(
generatedMessage.get('conversationId')
);
let targetConversation: ConversationModel | undefined | null;
const targetMessageCheck = await this.findMessage(
reaction.get('targetTimestamp'),
targetConversationId
);
if (!targetMessageCheck) {
log.info(
'No message for reaction',
reaction.get('timestamp'),
'targeting',
2022-07-11 18:35:55 +00:00
reaction.get('targetAuthorUuid'),
2021-11-11 22:43:05 +00:00
reaction.get('targetTimestamp')
);
2022-07-11 18:35:55 +00:00
return;
}
if (
fromConversation &&
isStory(targetMessageCheck) &&
isDirectConversation(fromConversation.attributes) &&
!isMe(fromConversation.attributes)
) {
targetConversation = fromConversation;
} else {
targetConversation =
await window.ConversationController.getConversationForTargetMessage(
targetConversationId,
reaction.get('targetTimestamp')
);
}
if (!targetConversation) {
log.info(
'No target conversation for reaction',
reaction.get('targetAuthorUuid'),
reaction.get('targetTimestamp')
);
return undefined;
}
// awaiting is safe since `onReaction` is never called from inside the queue
await targetConversation.queueJob('Reactions.onReaction', async () => {
log.info('Handling reaction for', reaction.get('targetTimestamp'));
2022-07-11 18:35:55 +00:00
// Thanks TS.
if (!targetConversation) {
return;
}
// Message is fetched inside the conversation queue so we have the
// most recent data
2022-07-11 18:35:55 +00:00
const targetMessage = await this.findMessage(
reaction.get('targetTimestamp'),
targetConversationId
);
if (!targetMessage) {
return;
}
const message = window.MessageController.register(
targetMessage.id,
targetMessage
);
// Use the generated message in ts/background.ts to create a message
2023-01-01 11:41:40 +00:00
// if the reaction is targeted at a story.
if (!isStory(targetMessage)) {
await message.handleReaction(reaction);
} else {
await generatedMessage.handleReaction(reaction, {
storyMessage: targetMessage,
});
}
this.remove(reaction);
});
} catch (error) {
log.error('Reactions.onReaction error:', Errors.toLogFormat(error));
}
}
}