Receive support for DOE messages

This commit is contained in:
Ken Powers 2020-04-29 17:24:12 -04:00 committed by Scott Nonnenberg
parent b8a674bbb6
commit ba5e2ff6e5
17 changed files with 291 additions and 42 deletions

View file

@ -1691,6 +1691,10 @@
"message": "View-once Video", "message": "View-once Video",
"description": "Shown in notifications and in the left pane when a message is a view once video." "description": "Shown in notifications and in the left pane when a message is a view once video."
}, },
"message--deletedForEveryone": {
"message": "This message was deleted.",
"description": "Shown in a message's bubble when the message has been deleted for everyone."
},
"stickers--toast--InstallFailed": { "stickers--toast--InstallFailed": {
"message": "Sticker pack could not be installed", "message": "Sticker pack could not be installed",
"description": "Shown in a toast if the user attempts to install a sticker pack and it fails" "description": "Shown in a toast if the user attempts to install a sticker pack and it fails"

View file

@ -353,6 +353,7 @@
<script type='text/javascript' src='js/read_syncs.js'></script> <script type='text/javascript' src='js/read_syncs.js'></script>
<script type='text/javascript' src='js/view_syncs.js'></script> <script type='text/javascript' src='js/view_syncs.js'></script>
<script type='text/javascript' src='js/reactions.js'></script> <script type='text/javascript' src='js/reactions.js'></script>
<script type='text/javascript' src='js/deletes.js'></script>
<script type='text/javascript' src='js/libphonenumber-util.js'></script> <script type='text/javascript' src='js/libphonenumber-util.js'></script>
<script type='text/javascript' src='js/models/messages.js'></script> <script type='text/javascript' src='js/models/messages.js'></script>
<script type='text/javascript' src='js/models/conversations.js'></script> <script type='text/javascript' src='js/models/conversations.js'></script>

View file

@ -2241,7 +2241,7 @@
// Note: We do very little in this function, since everything in handleDataMessage is // Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier // inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage(). // message is processed in handleDataMessage().
async function onMessageReceived(event) { function onMessageReceived(event) {
const { data, confirm } = event; const { data, confirm } = event;
const messageDescriptor = getDescriptorForReceived(data); const messageDescriptor = getDescriptorForReceived(data);
@ -2250,17 +2250,16 @@
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) { if (isProfileUpdate) {
await handleMessageReceivedProfileUpdate({ return handleMessageReceivedProfileUpdate({
data, data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}); });
return;
} }
const message = await initIncomingMessage(data); const message = initIncomingMessage(data);
const result = await ConversationController.getOrCreateAndWait( const result = ConversationController.getOrCreate(
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
@ -2286,11 +2285,26 @@
// Note: We do not wait for completion here // Note: We do not wait for completion here
Whisper.Reactions.onReaction(reactionModel); Whisper.Reactions.onReaction(reactionModel);
confirm(); confirm();
return; return Promise.resolve();
}
if (data.message.delete) {
const { delete: del } = data.message;
const deleteModel = Whisper.Deletes.add({
targetSentTimestamp: del.targetSentTimestamp,
serverTimestamp: data.serverTimestamp,
fromId: data.source || data.sourceUuid,
});
// Note: We do not wait for completion here
Whisper.Deletes.onDelete(deleteModel);
confirm();
return Promise.resolve();
} }
// Don't wait for handleDataMessage, as it has its own per-conversation queueing // Don't wait for handleDataMessage, as it has its own per-conversation queueing
message.handleDataMessage(data.message, event.confirm); message.handleDataMessage(data.message, event.confirm);
return Promise.resolve();
} }
async function handleMessageSentProfileUpdate({ async function handleMessageSentProfileUpdate({
@ -2341,6 +2355,7 @@
sourceUuid: textsecure.storage.user.getUuid(), sourceUuid: textsecure.storage.user.getUuid(),
sourceDevice: data.device, sourceDevice: data.device,
sent_at: data.timestamp, sent_at: data.timestamp,
serverTimestamp: data.serverTimestamp,
sent_to: sentTo, sent_to: sentTo,
received_at: now, received_at: now,
conversationId: data.destination, conversationId: data.destination,
@ -2357,7 +2372,7 @@
// Note: We do very little in this function, since everything in handleDataMessage is // Note: We do very little in this function, since everything in handleDataMessage is
// inside a conversation-specific queue(). Any code here might run before an earlier // inside a conversation-specific queue(). Any code here might run before an earlier
// message is processed in handleDataMessage(). // message is processed in handleDataMessage().
async function onSentMessage(event) { function onSentMessage(event) {
const { data, confirm } = event; const { data, confirm } = event;
const messageDescriptor = getDescriptorForSent(data); const messageDescriptor = getDescriptorForSent(data);
@ -2366,20 +2381,20 @@
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE); const isProfileUpdate = Boolean(data.message.flags & PROFILE_KEY_UPDATE);
if (isProfileUpdate) { if (isProfileUpdate) {
await handleMessageSentProfileUpdate({ return handleMessageSentProfileUpdate({
data, data,
confirm, confirm,
messageDescriptor, messageDescriptor,
}); });
return;
} }
const message = await createSentMessage(data); const message = createSentMessage(data);
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
if (data.message.reaction) { if (data.message.reaction) {
const { reaction } = data.message; const { reaction } = data.message;
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const reactionModel = Whisper.Reactions.add({ const reactionModel = Whisper.Reactions.add({
emoji: reaction.emoji, emoji: reaction.emoji,
remove: reaction.remove, remove: reaction.remove,
@ -2394,10 +2409,23 @@
Whisper.Reactions.onReaction(reactionModel); Whisper.Reactions.onReaction(reactionModel);
event.confirm(); event.confirm();
return; return Promise.resolve();
} }
await ConversationController.getOrCreateAndWait( if (data.message.delete) {
const { delete: del } = data.message;
const deleteModel = Whisper.Deletes.add({
targetSentTimestamp: del.targetSentTimestamp,
serverTimestamp: del.serverTimestamp,
fromId: ourNumber || ourUuid,
});
// Note: We do not wait for completion here
Whisper.Deletes.onDelete(deleteModel);
confirm();
return Promise.resolve();
}
ConversationController.getOrCreate(
messageDescriptor.id, messageDescriptor.id,
messageDescriptor.type messageDescriptor.type
); );
@ -2406,9 +2434,11 @@
message.handleDataMessage(data.message, event.confirm, { message.handleDataMessage(data.message, event.confirm, {
data, data,
}); });
return Promise.resolve();
} }
async function initIncomingMessage(data) { function initIncomingMessage(data) {
const targetId = data.source || data.sourceUuid; const targetId = data.source || data.sourceUuid;
const conversation = ConversationController.get(targetId); const conversation = ConversationController.get(targetId);
const conversationId = conversation ? conversation.id : targetId; const conversationId = conversation ? conversation.id : targetId;
@ -2418,6 +2448,7 @@
sourceUuid: data.sourceUuid, sourceUuid: data.sourceUuid,
sourceDevice: data.sourceDevice, sourceDevice: data.sourceDevice,
sent_at: data.timestamp, sent_at: data.timestamp,
serverTimestamp: data.serverTimestamp,
received_at: Date.now(), received_at: Date.now(),
conversationId, conversationId,
unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: data.unidentifiedDeliveryReceived,
@ -2485,7 +2516,7 @@
} }
} }
async function onError(ev) { function onError(ev) {
const { error } = ev; const { error } = ev;
window.log.error('background onError:', Errors.toLogFormat(error)); window.log.error('background onError:', Errors.toLogFormat(error));
@ -2494,8 +2525,7 @@
error.name === 'HTTPError' && error.name === 'HTTPError' &&
(error.code === 401 || error.code === 403) (error.code === 401 || error.code === 403)
) { ) {
await unlinkAndDisconnect(); return unlinkAndDisconnect();
return;
} }
if ( if (
@ -2510,7 +2540,7 @@
Whisper.events.trigger('reconnectTimer'); Whisper.events.trigger('reconnectTimer');
} }
return; return Promise.resolve();
} }
if (ev.proto) { if (ev.proto) {
@ -2520,13 +2550,13 @@
} }
// Ignore this message. It is likely a duplicate delivery // Ignore this message. It is likely a duplicate delivery
// because the server lost our ack the first time. // because the server lost our ack the first time.
return; return Promise.resolve();
} }
const envelope = ev.proto; const envelope = ev.proto;
const message = await initIncomingMessage(envelope); const message = initIncomingMessage(envelope);
const conversationId = message.get('conversationId'); const conversationId = message.get('conversationId');
const conversation = await ConversationController.getOrCreateAndWait( const conversation = ConversationController.getOrCreate(
conversationId, conversationId,
'private' 'private'
); );

108
js/deletes.js Normal file
View file

@ -0,0 +1,108 @@
/* global
Backbone,
Whisper,
MessageController,
ConversationController
*/
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function() {
'use strict';
const ONE_DAY = 24 * 60 * 60 * 1000;
window.Whisper = window.Whisper || {};
Whisper.Deletes = new (Backbone.Collection.extend({
forMessage(message) {
const matchingDeletes = this.filter({
targetSentTimestamp: message.get('sent_at'),
fromId: message.getContact().get('id'),
});
if (matchingDeletes.length > 0) {
window.log.info('Found early DOE for message');
this.remove(matchingDeletes);
return matchingDeletes;
}
return [];
},
async onDelete(del) {
try {
const messages = await window.Signal.Data.getMessagesBySentAt(
del.get('targetSentTimestamp'),
{
MessageCollection: Whisper.MessageCollection,
}
);
// The contact the delete message came from
const fromContact = ConversationController.get(del.get('fromId'));
if (!fromContact) {
window.log.info(
'No contact for DOE',
del.get('fromId'),
del.get('targetSentTimestamp')
);
return;
}
const targetMessage = messages.find(m => {
const messageContact = m.getContact();
if (!messageContact) {
return false;
}
// Find messages which are from the same contact who sent the DOE
return messageContact.get('id') === fromContact.get('id');
});
if (!targetMessage) {
window.log.info(
'No message for DOE',
del.get('fromId'),
del.get('targetSentTimestamp')
);
return;
}
// Make sure the server timestamps for the DOE and the matching message
// are less than one day apart
const delta = Math.abs(
del.get('serverTimestamp') - targetMessage.get('serverTimestamp')
);
if (delta > ONE_DAY) {
window.log.info('Received late DOE. Dropping.', {
fromId: del.get('fromId'),
targetSentTimestamp: del.get('targetSentTimestamp'),
messageServerTimestamp: message.get('serverTimestamp'),
deleteServerTimestamp: del.get('serverTimestamp'),
});
this.remove(del);
return;
}
const message = MessageController.register(
targetMessage.id,
targetMessage
);
await message.handleDeleteForEveryone(del);
this.remove(del);
} catch (error) {
window.log.error(
'Deletes.onDelete error:',
error && error.stack ? error.stack : error
);
}
},
}))();
})();

View file

@ -414,6 +414,7 @@
lastMessage: { lastMessage: {
status: this.get('lastMessageStatus'), status: this.get('lastMessageStatus'),
text: this.get('lastMessage'), text: this.get('lastMessage'),
deletedForEveryone: this.get('lastMessageDeletedForEveryone'),
}, },
}; };

View file

@ -600,6 +600,8 @@
isTapToViewExpired: isTapToView && this.get('isErased'), isTapToViewExpired: isTapToView && this.get('isErased'),
isTapToViewError: isTapToViewError:
isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'), isTapToView && this.isIncoming() && this.get('isTapToViewInvalid'),
deletedForEveryone: this.get('deletedForEveryone') || false,
}; };
}, },
@ -1105,7 +1107,7 @@
isErased() { isErased() {
return Boolean(this.get('isErased')); return Boolean(this.get('isErased'));
}, },
async eraseContents() { async eraseContents(additionalProperties = {}) {
if (this.get('isErased')) { if (this.get('isErased')) {
return; return;
} }
@ -1129,6 +1131,7 @@
contact: [], contact: [],
sticker: null, sticker: null,
preview: [], preview: [],
...additionalProperties,
}); });
this.trigger('content-changed'); this.trigger('content-changed');
@ -1442,12 +1445,17 @@
const isOutgoing = this.get('type') === 'outgoing'; const isOutgoing = this.get('type') === 'outgoing';
const numDelivered = this.get('delivered'); const numDelivered = this.get('delivered');
// Case 1: We can reply if this is outgoing and delievered to at least one recipient // Case 1: We cannot reply if this message is deleted for everyone
if (this.get('deletedForEveryone')) {
return false;
}
// Case 2: We can reply if this is outgoing and delievered to at least one recipient
if (isOutgoing && numDelivered > 0) { if (isOutgoing && numDelivered > 0) {
return true; return true;
} }
// Case 2: We can reply if there are no errors // Case 3: We can reply if there are no errors
if (!errors || (errors && errors.length === 0)) { if (!errors || (errors && errors.length === 0)) {
return true; return true;
} }
@ -2457,6 +2465,11 @@
message.handleReaction(reaction); message.handleReaction(reaction);
}); });
// Does this message have any pending, previously-received associated
// delete for everyone messages?
const deletes = Whisper.Deletes.forMessage(message);
deletes.forEach(del => Whisper.Deletes.onDelete(del));
Whisper.events.trigger('incrementProgress'); Whisper.events.trigger('incrementProgress');
confirm(); confirm();
} catch (error) { } catch (error) {
@ -2473,6 +2486,10 @@
}, },
async handleReaction(reaction) { async handleReaction(reaction) {
if (this.get('deletedForEveryone')) {
return;
}
const reactions = this.get('reactions') || []; const reactions = this.get('reactions') || [];
const messageId = this.idForLogging(); const messageId = this.idForLogging();
const count = reactions.length; const count = reactions.length;
@ -2512,6 +2529,27 @@
Message: Whisper.Message, Message: Whisper.Message,
}); });
}, },
async handleDeleteForEveryone(del) {
window.log.info('Handling DOE.', {
fromId: del.get('fromId'),
targetSentTimestamp: del.get('targetSentTimestamp'),
messageServerTimestamp: this.get('serverTimestamp'),
deleteServerTimestamp: del.get('serverTimestamp'),
});
// Remove any notifications for this message
const notificationForMessage = Whisper.Notifications.findWhere({
messageId: this.get('id'),
});
Whisper.Notifications.remove(notificationForMessage);
// Erase the contents of this message
await this.eraseContents({ deletedForEveryone: true, reactions: [] });
// Update the conversation's last message in case this was the last message
this.getConversation().updateLastMessage();
},
}); });
// Receive will be enabled before we enable send // Receive will be enabled before we enable send

View file

@ -179,6 +179,10 @@ message DataMessage {
optional uint64 targetTimestamp = 5; optional uint64 targetTimestamp = 5;
} }
message Delete {
optional uint64 targetSentTimestamp = 1;
}
enum ProtocolVersion { enum ProtocolVersion {
option allow_alias = true; option allow_alias = true;
@ -206,6 +210,7 @@ message DataMessage {
optional uint32 requiredProtocolVersion = 12; optional uint32 requiredProtocolVersion = 12;
optional bool isViewOnce = 14; optional bool isViewOnce = 14;
optional Reaction reaction = 16; optional Reaction reaction = 16;
optional Delete delete = 17;
} }
message NullMessage { message NullMessage {

View file

@ -3396,9 +3396,12 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
.module-conversation-list-item__message__draft-prefix { .module-conversation-list-item__message {
font-style: italic; &__draft-prefix,
margin-right: 3px; &__deleted-for-everyone {
font-style: italic;
margin-right: 3px;
}
} }
.module-conversation-list-item__message__status-icon { .module-conversation-list-item__message__status-icon {
@ -7869,6 +7872,10 @@ button.module-image__border-overlay:focus {
&--with-reactions { &--with-reactions {
margin-bottom: 12px; margin-bottom: 12px;
} }
&--deleted-for-everyone {
font-style: italic;
}
} }
/* Spec: container > 438px and container < 593px */ /* Spec: container > 438px and container < 593px */

View file

@ -31,6 +31,7 @@ export type PropsData = {
lastMessage?: { lastMessage?: {
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
text: string; text: string;
deletedForEveryone?: boolean;
}; };
}; };
@ -152,6 +153,9 @@ export class ConversationListItem extends React.PureComponent<Props> {
} }
const showingDraft = shouldShowDraft && draftPreview; const showingDraft = shouldShowDraft && draftPreview;
const deletedForEveryone = Boolean(
lastMessage && lastMessage.deletedForEveryone
);
// Note: instead of re-using showingDraft here we explode it because // Note: instead of re-using showingDraft here we explode it because
// typescript can't tell that draftPreview is truthy otherwise // typescript can't tell that draftPreview is truthy otherwise
@ -181,6 +185,10 @@ export class ConversationListItem extends React.PureComponent<Props> {
<span className="module-conversation-list-item__message__draft-prefix"> <span className="module-conversation-list-item__message__draft-prefix">
{i18n('ConversationListItem--draft-prefix')} {i18n('ConversationListItem--draft-prefix')}
</span> </span>
) : deletedForEveryone ? (
<span className="module-conversation-list-item__message__deleted-for-everyone">
{i18n('message--deletedForEveryone')}
</span>
) : null} ) : null}
<MessageBody <MessageBody
text={text.split('\n')[0]} text={text.split('\n')[0]}

View file

@ -106,6 +106,8 @@ export type PropsData = {
reactions?: ReactionViewerProps['reactions']; reactions?: ReactionViewerProps['reactions'];
selectedReaction?: string; selectedReaction?: string;
deletedForEveryone?: boolean;
canReply: boolean; canReply: boolean;
}; };
@ -934,12 +936,20 @@ export class Message extends React.PureComponent<Props, State> {
} }
public renderText() { public renderText() {
const { text, textPending, i18n, direction, status } = this.props; const {
deletedForEveryone,
direction,
i18n,
status,
text,
textPending,
} = this.props;
const contents = const contents = deletedForEveryone
direction === 'incoming' && status === 'error' ? i18n('message--deletedForEveryone')
? i18n('incomingError') : direction === 'incoming' && status === 'error'
: text; ? i18n('incomingError')
: text;
if (!contents) { if (!contents) {
return null; return null;
@ -1677,7 +1687,11 @@ export class Message extends React.PureComponent<Props, State> {
} }
public renderContents() { public renderContents() {
const { isTapToView } = this.props; const { isTapToView, deletedForEveryone } = this.props;
if (deletedForEveryone) {
return this.renderText();
}
if (isTapToView) { if (isTapToView) {
return ( return (
@ -1863,9 +1877,11 @@ export class Message extends React.PureComponent<Props, State> {
this.handleOpen(event); this.handleOpen(event);
}; };
// tslint:disable-next-line: cyclomatic-complexity
public renderContainer() { public renderContainer() {
const { const {
authorColor, authorColor,
deletedForEveryone,
direction, direction,
isSticker, isSticker,
isTapToView, isTapToView,
@ -1903,6 +1919,9 @@ export class Message extends React.PureComponent<Props, State> {
: null, : null,
reactions && reactions.length > 0 reactions && reactions.length > 0
? 'module-message__container--with-reactions' ? 'module-message__container--with-reactions'
: null,
deletedForEveryone
? 'module-message__container--deleted-for-everyone'
: null : null
); );
const containerStyles = { const containerStyles = {

View file

@ -88,6 +88,7 @@ export type MessageType = {
phoneNumber?: string; phoneNumber?: string;
}; };
}>; }>;
deletedForEveryone?: boolean;
errors?: Array<Error>; errors?: Array<Error>;
group_update?: any; group_update?: any;
@ -627,6 +628,12 @@ function hasMessageHeightChanged(
return true; return true;
} }
const isDeletedForEveryone = message.deletedForEveryone;
const wasDeletedForEveryone = previous.deletedForEveryone;
if (isDeletedForEveryone !== wasDeletedForEveryone) {
return true;
}
return false; return false;
} }

View file

@ -38,6 +38,7 @@ describe('Conversation', () => {
const expected = { const expected = {
lastMessage: 'New outgoing message', lastMessage: 'New outgoing message',
lastMessageStatus: 'read', lastMessageStatus: 'read',
lastMessageDeletedForEveryone: undefined,
timestamp: 666, timestamp: 666,
}; };
@ -61,6 +62,7 @@ describe('Conversation', () => {
const expected = { const expected = {
lastMessage: 'xoxoxoxo', lastMessage: 'xoxoxoxo',
lastMessageStatus: null, lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555, timestamp: 555,
}; };
@ -84,6 +86,7 @@ describe('Conversation', () => {
const expected = { const expected = {
lastMessage: '', lastMessage: '',
lastMessageStatus: null, lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555, timestamp: 555,
}; };
@ -112,6 +115,7 @@ describe('Conversation', () => {
const expected = { const expected = {
lastMessage: 'Last message before expired', lastMessage: 'Last message before expired',
lastMessageStatus: null, lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555, timestamp: 555,
}; };

5
ts/textsecure.d.ts vendored
View file

@ -235,6 +235,7 @@ export declare class DataMessageClass {
requiredProtocolVersion?: number; requiredProtocolVersion?: number;
isViewOnce?: boolean; isViewOnce?: boolean;
reaction?: DataMessageClass.Reaction; reaction?: DataMessageClass.Reaction;
delete?: DataMessageClass.Delete;
} }
// Note: we need to use namespaces to express nested classes in Typescript // Note: we need to use namespaces to express nested classes in Typescript
@ -287,6 +288,10 @@ export declare namespace DataMessageClass {
targetTimestamp?: ProtoBigNumberType; targetTimestamp?: ProtoBigNumberType;
} }
class Delete {
targetSentTimestamp?: ProtoBigNumberType;
}
class Sticker { class Sticker {
packId?: ProtoBinaryType; packId?: ProtoBinaryType;
packKey?: ProtoBinaryType; packKey?: ProtoBinaryType;

View file

@ -1089,6 +1089,7 @@ class MessageReceiverInner extends EventTarget {
ev.data = { ev.data = {
destination, destination,
timestamp: timestamp.toNumber(), timestamp: timestamp.toNumber(),
serverTimestamp: envelope.serverTimestamp,
device: envelope.sourceDevice, device: envelope.sourceDevice,
unidentifiedStatus, unidentifiedStatus,
message, message,
@ -1159,6 +1160,7 @@ class MessageReceiverInner extends EventTarget {
sourceUuid: envelope.sourceUuid, sourceUuid: envelope.sourceUuid,
sourceDevice: envelope.sourceDevice, sourceDevice: envelope.sourceDevice,
timestamp: envelope.timestamp.toNumber(), timestamp: envelope.timestamp.toNumber(),
serverTimestamp: envelope.serverTimestamp,
unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived, unidentifiedDeliveryReceived: envelope.unidentifiedDeliveryReceived,
message, message,
}; };
@ -1795,6 +1797,13 @@ class MessageReceiverInner extends EventTarget {
} }
} }
const { delete: del } = decrypted;
if (del) {
if (del.targetSentTimestamp) {
del.targetSentTimestamp = del.targetSentTimestamp.toNumber();
}
}
const groupMembers = decrypted.group ? decrypted.group.members || [] : []; const groupMembers = decrypted.group ? decrypted.group.members || [] : [];
window.normalizeUuids( window.normalizeUuids(

View file

@ -4,6 +4,7 @@ interface ConversationLastMessageUpdate {
lastMessage: string; lastMessage: string;
lastMessageStatus: string | null; lastMessageStatus: string | null;
timestamp: number | null; timestamp: number | null;
lastMessageDeletedForEveryone?: boolean;
} }
export const createLastMessageUpdate = ({ export const createLastMessageUpdate = ({
@ -25,7 +26,7 @@ export const createLastMessageUpdate = ({
}; };
} }
const { type, expirationTimerUpdate } = lastMessage; const { type, expirationTimerUpdate, deletedForEveryone } = lastMessage;
const isMessageHistoryUnsynced = type === 'message-history-unsynced'; const isMessageHistoryUnsynced = type === 'message-history-unsynced';
const isVerifiedChangeMessage = type === 'verified-change'; const isVerifiedChangeMessage = type === 'verified-change';
const isExpireTimerUpdateFromSync = Boolean( const isExpireTimerUpdateFromSync = Boolean(
@ -47,8 +48,9 @@ export const createLastMessageUpdate = ({
: ''; : '';
return { return {
lastMessage: newLastMessageText || '', lastMessage: deletedForEveryone ? '' : newLastMessageText || '',
lastMessageStatus: lastMessageStatus || null, lastMessageStatus: lastMessageStatus || null,
timestamp: newTimestamp || null, timestamp: newTimestamp || null,
lastMessageDeletedForEveryone: deletedForEveryone,
}; };
}; };

View file

@ -2,10 +2,11 @@ import { Attachment } from './Attachment';
import { ContactType } from './Contact'; import { ContactType } from './Contact';
import { IndexableBoolean, IndexablePresence } from './IndexedDB'; import { IndexableBoolean, IndexablePresence } from './IndexedDB';
export type Message = export type Message = (
| UserMessage | UserMessage
| VerifiedChangeMessage | VerifiedChangeMessage
| MessageHistoryUnsyncedMessage; | MessageHistoryUnsyncedMessage
) & { deletedForEveryone?: boolean };
export type UserMessage = IncomingMessage | OutgoingMessage; export type UserMessage = IncomingMessage | OutgoingMessage;
export type IncomingMessage = Readonly< export type IncomingMessage = Readonly<

View file

@ -11648,17 +11648,17 @@
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();", "line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 179, "lineNumber": 181,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-03-03T22:30:27.594Z" "updated": "2020-04-16T19:36:47.586Z"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx", "path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();", "line": " > = React.createRef();",
"lineNumber": 183, "lineNumber": 185,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-03-03T22:30:27.594Z" "updated": "2020-04-16T19:36:47.586Z"
}, },
{ {
"rule": "React-createRef", "rule": "React-createRef",