signal-desktop/ts/messageModifiers/Reactions.ts

160 lines
4.8 KiB
TypeScript
Raw Normal View History

// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable max-classes-per-file */
import { Collection, Model } from 'backbone';
import { MessageModel } from '../models/messages';
import { isOutgoing } from '../state/selectors/message';
import { ReactionAttributesType } from '../model-types.d';
export class ReactionModel extends Model<ReactionAttributesType> {}
let singleton: Reactions | undefined;
export class Reactions extends Collection {
static getSingleton(): Reactions {
if (!singleton) {
singleton = new Reactions();
}
return singleton;
}
forMessage(message: MessageModel): Array<ReactionModel> {
if (isOutgoing(message.attributes)) {
const outgoingReactions = this.filter(
item => item.get('targetTimestamp') === message.get('sent_at')
);
if (outgoingReactions.length > 0) {
window.log.info('Found early reaction for outgoing message');
this.remove(outgoingReactions);
return outgoingReactions;
}
}
const senderId = message.getContactId();
const sentAt = message.get('sent_at');
const reactionsBySource = this.filter(re => {
const targetSenderId = window.ConversationController.ensureContactIds({
uuid: re.get('targetAuthorUuid'),
});
const targetTimestamp = re.get('targetTimestamp');
return targetSenderId === senderId && targetTimestamp === sentAt;
});
if (reactionsBySource.length > 0) {
window.log.info('Found early reaction for message');
this.remove(reactionsBySource);
return reactionsBySource;
}
return [];
}
async onReaction(
reaction: ReactionModel
): Promise<ReactionAttributesType | undefined> {
try {
// The conversation the target message was in; we have to find it in the database
// to to figure that out.
const targetConversationId = window.ConversationController.ensureContactIds(
{
uuid: reaction.get('targetAuthorUuid'),
}
);
if (!targetConversationId) {
throw new Error(
'onReaction: No conversationId returned from ensureContactIds!'
);
}
const targetConversation = await window.ConversationController.getConversationForTargetMessage(
targetConversationId,
reaction.get('targetTimestamp')
);
if (!targetConversation) {
window.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
return await targetConversation.queueJob(
'Reactions.onReaction',
async () => {
window.log.info(
'Handling reaction for',
reaction.get('targetTimestamp')
);
const messages = await window.Signal.Data.getMessagesBySentAt(
reaction.get('targetTimestamp'),
{
MessageCollection: window.Whisper.MessageCollection,
}
);
// Message is fetched inside the conversation queue so we have the
// most recent data
const targetMessage = messages.find(m => {
const contact = m.getContact();
if (!contact) {
return false;
}
const mcid = contact.get('id');
const recid = window.ConversationController.ensureContactIds({
uuid: reaction.get('targetAuthorUuid'),
});
return mcid === recid;
});
if (!targetMessage) {
window.log.info(
'No message for reaction',
reaction.get('targetAuthorUuid'),
reaction.get('targetTimestamp')
);
// Since we haven't received the message for which we are removing a
// reaction, we can just remove those pending reactions
if (reaction.get('remove')) {
this.remove(reaction);
const oldReaction = this.where({
targetAuthorUuid: reaction.get('targetAuthorUuid'),
targetTimestamp: reaction.get('targetTimestamp'),
emoji: reaction.get('emoji'),
});
oldReaction.forEach(r => this.remove(r));
}
return undefined;
}
const message = window.MessageController.register(
targetMessage.id,
targetMessage
);
const oldReaction = await message.handleReaction(reaction);
this.remove(reaction);
return oldReaction;
}
);
} catch (error) {
window.log.error(
'Reactions.onReaction error:',
error && error.stack ? error.stack : error
);
return undefined;
}
}
}