Render incoming Reactions

This commit is contained in:
Ken Powers 2020-01-17 17:23:19 -05:00 committed by Scott Nonnenberg
parent b113eb19f0
commit 6cc0f2abce
25 changed files with 1411 additions and 134 deletions

View file

@ -8,7 +8,7 @@
Signal,
storage,
textsecure,
WebAPI
WebAPI,
Whisper,
*/
@ -850,6 +850,13 @@
if (stickerPreview) {
return;
}
const reactionViewer = document.querySelector(
'.module-reaction-viewer'
);
if (reactionViewer) {
return;
}
}
// Close Backbone-based confirmation dialog
@ -2070,6 +2077,23 @@
messageDescriptor.type
);
if (data.message.reaction) {
const { reaction } = data.message;
const reactionModel = Whisper.Reactions.add({
emoji: reaction.emoji,
remove: reaction.remove,
targetAuthorE164: reaction.targetAuthorE164,
targetAuthorUuid: reaction.targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp.toNumber(),
timestamp: Date.now(),
fromId: messageDescriptor.id,
});
// Note: We do not wait for completion here
Whisper.Reactions.onReaction(reactionModel);
confirm();
return;
}
// Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm, {
initialLoadComplete,
@ -2188,6 +2212,20 @@
`onSentMessage: Received duplicate transcript for message ${message.idForLogging()}, but it was not an update transcript. Dropping.`
);
event.confirm();
} else if (data.message.reaction) {
const { reaction } = data.message;
const reactionModel = Whisper.Reactions.add({
emoji: reaction.emoji,
remove: reaction.remove,
targetAuthorE164: reaction.targetAuthorE164,
targetAuthorUuid: reaction.targetAuthorUuid,
targetTimestamp: reaction.targetTimestamp.toNumber(),
timestamp: Date.now(),
fromId: messageDescriptor.id,
});
// Note: We do not wait for completion here
Whisper.Reactions.onReaction(reactionModel);
event.confirm();
} else {
await ConversationController.getOrCreateAndWait(
messageDescriptor.id,

View file

@ -2070,33 +2070,35 @@
});
},
notify(message) {
if (!message.isIncoming()) {
return Promise.resolve();
async notify(message, reaction) {
if (!message.isIncoming() && !reaction) {
return;
}
const conversationId = this.id;
return ConversationController.getOrCreateAndWait(
message.get('source'),
const sender = await ConversationController.getOrCreateAndWait(
reaction ? reaction.get('fromId') : message.get('source'),
'private'
).then(sender =>
sender.getNotificationIcon().then(iconUrl => {
const messageJSON = message.toJSON();
const messageSentAt = messageJSON.sent_at;
const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON);
Whisper.Notifications.add({
conversationId,
iconUrl,
isExpiringMessage,
message: message.getNotificationText(),
messageId,
messageSentAt,
title: sender.getTitle(),
});
})
);
const iconUrl = await sender.getNotificationIcon();
const messageJSON = message.toJSON();
const messageSentAt = messageJSON.sent_at;
const messageId = message.id;
const isExpiringMessage = Message.hasExpiration(messageJSON);
Whisper.Notifications.add({
conversationId,
iconUrl,
isExpiringMessage,
message: message.getNotificationText(),
messageId,
messageSentAt,
title: sender.getTitle(),
reaction: reaction ? reaction.toJSON() : null,
});
},
notifyTyping(options = {}) {

View file

@ -496,6 +496,25 @@
const isTapToView = this.isTapToView();
const reactions = (this.get('reactions') || []).map(re => {
const c = this.findAndFormatContact(re.fromId);
if (!c) {
return {
emoji: re.emoji,
from: {
id: re.fromId,
},
};
}
return {
emoji: re.emoji,
timestamp: re.timestamp,
from: c,
};
});
return {
text: this.createNonBreakingLastSeparator(this.get('body')),
textPending: this.get('bodyPending'),
@ -518,6 +537,7 @@
isExpired: this.hasExpired,
expirationLength,
expirationTimestamp,
reactions,
isTapToView,
isTapToViewExpired: isTapToView && this.get('isErased'),
@ -1841,13 +1861,6 @@
`Starting handleDataMessage for message ${message.idForLogging()} in conversation ${conversation.idForLogging()}`
);
// Drop reaction messages at this time
if (initialMessage.reaction) {
window.log.info('Dropping reaction message', this.idForLogging());
confirm();
return;
}
// First, check for duplicates. If we find one, stop processing here.
const existingMessage = await getMessageBySender(this.attributes, {
Message: Whisper.Message,
@ -2173,6 +2186,12 @@
await conversation.notify(message);
}
// Does this message have a pending, previously-received associated reaction?
const reaction = Whisper.Reactions.forMessage(message);
if (reaction) {
message.handleReaction(reaction);
}
Whisper.events.trigger('incrementProgress');
confirm();
} catch (error) {
@ -2187,6 +2206,38 @@
}
});
},
async handleReaction(reaction) {
const reactions = this.get('reactions') || [];
if (reaction.get('remove')) {
const newReactions = reactions.filter(
re =>
re.emoji !== reaction.get('emoji') ||
re.fromId !== reaction.get('fromId')
);
this.set({ reactions: newReactions });
} else {
const newReactions = reactions.filter(
re => re.fromId !== reaction.get('fromId')
);
newReactions.push(reaction.toJSON());
this.set({ reactions: newReactions });
const conversation = ConversationController.get(
this.get('conversationId')
);
// Only notify for reactions to our own messages
if (conversation && this.isOutgoing()) {
conversation.notify(this, reaction);
}
}
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
},
});
// Receive will be enabled before we enable send

View file

@ -101,7 +101,19 @@
// eslint-disable-next-line prefer-destructuring
iconUrl = last.iconUrl;
if (numNotifications === 1) {
message = `${i18n('notificationFrom')} ${lastMessageTitle}`;
if (last.reaction) {
message = i18n('notificationReaction', [
lastMessageTitle,
last.reaction.emoji,
]);
} else {
message = `${i18n('notificationFrom')} ${lastMessageTitle}`;
}
} else if (last.reaction) {
message = i18n('notificationReactionMostRecent', [
lastMessageTitle,
last.reaction.emoji,
]);
} else {
message = `${i18n(
'notificationMostRecentFrom'
@ -113,8 +125,21 @@
if (numNotifications === 1) {
// eslint-disable-next-line prefer-destructuring
title = last.title;
// eslint-disable-next-line prefer-destructuring
message = last.message;
if (last.reaction) {
message = i18n('notificationReaction', [
last.title,
last.reaction.emoji,
]);
} else {
// eslint-disable-next-line prefer-destructuring
message = last.message;
}
} else if (last.reaction) {
title = newMessageCountLabel;
message = i18n('notificationReactionMostRecent', [
last.title,
last.reaction.emoji,
]);
} else {
title = newMessageCountLabel;
message = `${i18n('notificationMostRecent')} ${last.message}`;

98
js/reactions.js Normal file
View file

@ -0,0 +1,98 @@
/* global
Backbone,
Whisper,
MessageController
*/
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.Reactions = new (Backbone.Collection.extend({
forMessage(message) {
if (message.isOutgoing()) {
const outgoingReaction = this.findWhere({
targetTimestamp: message.get('sent_at'),
});
if (outgoingReaction) {
window.log.info('Found early reaction for outgoing message');
this.remove(outgoingReaction);
return outgoingReaction;
}
}
const reactionBySource = this.findWhere({
targetAuthorE164: message.get('source'),
targetTimestamp: message.get('sent_at'),
});
if (reactionBySource) {
window.log.info('Found early reaction for message');
this.remove(reactionBySource);
return reactionBySource;
}
return null;
},
async onReaction(reaction) {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
reaction.get('targetTimestamp'),
{
MessageCollection: Whisper.MessageCollection,
}
);
const targetMessage = messages.find(
m =>
m.get('source') === reaction.get('targetAuthorE164') ||
// Outgoing messages don't have a source and are extremely unlikely
// to have the same timestamp
m.isOutgoing()
);
if (!targetMessage) {
window.log.info(
'No message for reaction',
reaction.get('targetAuthorE164'),
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 reaction
if (reaction.get('remove')) {
this.remove(reaction);
const oldReaction = this.where({
targetAuthorE164: reaction.get('targetAuthorE164'),
targetAuthorUuid: reaction.get('targetAuthorUuid'),
targetTimestamp: reaction.get('targetTimestamp'),
emoji: reaction.get('emoji'),
});
oldReaction.forEach(r => this.remove(r));
}
return;
}
const message = MessageController.register(
targetMessage.id,
targetMessage
);
await message.handleReaction(reaction);
this.remove(reaction);
} catch (error) {
window.log.error(
'Reactions.onReaction error:',
error && error.stack ? error.stack : error
);
}
},
}))();
})();