Render incoming Reactions
This commit is contained in:
parent
b113eb19f0
commit
6cc0f2abce
25 changed files with 1411 additions and 134 deletions
|
@ -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,
|
||||
|
|
|
@ -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 = {}) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
98
js/reactions.js
Normal 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
|
||||
);
|
||||
}
|
||||
},
|
||||
}))();
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue