diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index f2c845508..95773dc7c 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -384,6 +384,7 @@ export async function setDisappearingMessagesByConvoId( providedExpireTimer: 0, fromSync: false, fromCurrentDevice: true, + fromConfigMessage: false, }); } else { await conversation.updateExpireTimer({ @@ -391,6 +392,7 @@ export async function setDisappearingMessagesByConvoId( providedExpireTimer: seconds, fromSync: false, fromCurrentDevice: true, + fromConfigMessage: false, }); } } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 54715e239..087d2c2d5 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -17,6 +17,7 @@ import { xor, } from 'lodash'; +import { v4 } from 'uuid'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; import { getConversationController } from '../session/conversations'; @@ -117,7 +118,6 @@ import { getSubscriberCountOutsideRedux, } from '../state/selectors/sogsRoomInfo'; // decide it it makes sense to move this to a redux slice? -import { v4 } from 'uuid'; import { DisappearingMessages } from '../session/disappearing_messages'; import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types'; import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; @@ -826,7 +826,8 @@ export class ConversationModel extends Backbone.Model { providedExpireTimer, providedSource, receivedAt, // is set if it comes from outside - fromSync, // if the update comes from a config or sync message + fromSync, // if the update comes from sync message ONLY + fromConfigMessage, // if the update comes from a libsession config message ONLY fromCurrentDevice, shouldCommitConvo = true, existingMessage, @@ -837,10 +838,16 @@ export class ConversationModel extends Backbone.Model { receivedAt?: number; // is set if it comes from outside fromSync: boolean; fromCurrentDevice: boolean; + fromConfigMessage: boolean; shouldCommitConvo?: boolean; existingMessage?: MessageModel; }): Promise { - const isRemoteChange = Boolean((receivedAt || fromSync) && !fromCurrentDevice); + const isRemoteChange = Boolean( + (receivedAt || fromSync || fromConfigMessage) && !fromCurrentDevice + ); + + // we don't add an update message when this comes from a config message, as we already have the SyncedMessage itself with the right timestamp to display + const shouldAddExpireUpdateMessage = !fromConfigMessage; if (this.isPublic()) { throw new Error("updateExpireTimer() Disappearing messages aren't supported in communities"); @@ -887,6 +894,16 @@ export class ConversationModel extends Backbone.Model { }); } + if (!shouldAddExpireUpdateMessage) { + await Conversation.cleanUpExpireHistoryFromConvo(this.id, this.isPrivate()); + + if (shouldCommitConvo) { + // tell the UI this conversation was updated + await this.commit(); + } + return false; + } + let message = existingMessage || undefined; const expirationType = DisappearingMessages.changeToDisappearingMessageType( this, @@ -922,12 +939,13 @@ export class ConversationModel extends Backbone.Model { }); } } - // force that message to expire with the old disappear setting when the setting was turned off. - // this is to make the update to 'off' disappear with the previous disappearing message setting + + // Note: we agreed that a closed group ControlMessage message does not expire. message.set({ - expirationType, - expireTimer, + expirationType: this.isClosedGroup() ? 'unknown' : expirationType, + expireTimer: this.isClosedGroup() ? 0 : expireTimer, }); + if (!message.get('id')) { message.set({ id: v4() }); } @@ -950,7 +968,9 @@ export class ConversationModel extends Backbone.Model { ); if (!message.getExpirationStartTimestamp()) { - const canBeDeleteAfterSend = this.isMe() || this.isGroup(); + // Note: we agreed that a closed group ControlMessage message does not expire. + + const canBeDeleteAfterSend = this.isMe() || !(this.isGroup() && message.isControlMessage()); if ( (canBeDeleteAfterSend && expirationMode === 'legacy') || expirationMode === 'deleteAfterSend' @@ -1009,13 +1029,13 @@ export class ConversationModel extends Backbone.Model { } if (this.isClosedGroup()) { if (this.isAdmin(UserUtils.getOurPubKeyStrFromCache())) { + // NOTE: we agreed that outgoing ExpirationTimerUpdate **for groups** are not expiring, + // but they still need the content to be right(as this is what we use for the change itself) + const expireUpdateForGroup = { ...expireUpdate, groupId: this.get('id'), }; - // NOTE: we agreed that outgoing ExpirationTimerUpdate **for groups** are not expiring. - expireUpdate.expirationType = 'unknown'; - expireUpdate.expireTimer = 0; const expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdateForGroup); diff --git a/ts/models/message.ts b/ts/models/message.ts index 8e65eaec8..fbb533016 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -198,6 +198,15 @@ export class MessageModel extends Backbone.Model { return Boolean(flags & expirationTimerFlag) && !isEmpty(this.getExpirationTimerUpdate()); } + public isControlMessage() { + return ( + this.isExpirationTimerUpdate() || + this.isDataExtractionNotification() || + this.isMessageRequestResponse || + this.isGroupUpdate() + ); + } + public isIncoming() { return this.get('type') === 'incoming'; } @@ -1232,6 +1241,10 @@ export class MessageModel extends Backbone.Model { throttledAllMessagesDispatch(); } + private isGroupUpdate() { + return !isEmpty(this.get('group_update')); + } + /** * Before, group_update attributes could be just the string 'You' and not an array. * Using this method to get the group update makes sure than the joined, kicked, or left are always an array of string, or undefined diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 1b7217d20..2478f1e0a 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -325,6 +325,7 @@ export async function handleNewClosedGroup( receivedAt: GetNetworkTime.getNowWithNetworkOffset(), fromSync: false, fromCurrentDevice: false, + fromConfigMessage: false, }); await removeFromCache(envelope); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index b5f4824ea..a7a8c8e75 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -248,6 +248,7 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise { // we currently don't care about the timestamp included in the field itself, just the timestamp of the envelope const { type, timestamp: referencedAttachment } = dataNotificationMessage; @@ -852,9 +848,8 @@ export async function handleDataExtractionNotification( const convo = getConversationController().get(source); if (!convo || !convo.isPrivate()) { - window?.log?.info( - 'Got DataNotification for unknown or non private convo or read receipt not enabled' - ); + window?.log?.info('Got DataNotification for unknown or non-private convo'); + return; } @@ -866,34 +861,8 @@ export async function handleDataExtractionNotification( const envelopeTimestamp = toNumber(timestamp); const referencedAttachmentTimestamp = toNumber(referencedAttachment); - const expireTimer = content.expirationTimer || 0; - - const expirationMode = DisappearingMessages.changeToDisappearingConversationMode( - convo, - DisappearingMessageMode[content.expirationType], - expireTimer - ); - let expirationType; - let expirationStartTimestamp; - - if (convo && expirationMode && expireTimer > 0) { - expirationType = - expirationMode !== 'off' - ? DisappearingMessages.changeToDisappearingMessageType(convo, expireTimer, expirationMode) - : undefined; - - // NOTE Triggers disappearing for an incoming DataExtractionNotification message - // TODO legacy messages support will be removed in a future release - if (expirationMode === 'legacy' || expirationMode === 'deleteAfterSend') { - expirationStartTimestamp = DisappearingMessages.setExpirationStartTimestamp( - expirationMode, - undefined, - 'handleDataExtractionNotification' - ); - } - } - await convo.addSingleIncomingMessage({ + let created = await convo.addSingleIncomingMessage({ source, sent_at: envelopeTimestamp, dataExtractionNotification: { @@ -901,12 +870,13 @@ export async function handleDataExtractionNotification( referencedAttachmentTimestamp, // currently unused source, }, - - unread: READ_MESSAGE_STATE.unread, - expirationType, - expireTimer, - expirationStartTimestamp, }); - + created = DisappearingMessages.getMessageReadyToDisappear( + convo, + created, + 0, + expireUpdate || undefined + ); + await created.commit(); convo.updateLastMessage(); } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index a217d0a7a..1badce164 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -434,6 +434,7 @@ export async function handleMessageJob( existingMessage: messageModel, shouldCommitConvo: false, fromCurrentDevice: false, + fromConfigMessage: false, // NOTE we don't commit yet because we want to get the message id, see below }); } else { diff --git a/ts/session/disappearing_messages/index.ts b/ts/session/disappearing_messages/index.ts index 298966c1b..5144eea2c 100644 --- a/ts/session/disappearing_messages/index.ts +++ b/ts/session/disappearing_messages/index.ts @@ -399,11 +399,15 @@ function checkForExpiringOutgoingMessage(message: MessageModel, location?: strin const expireTimer = message.getExpireTimerSeconds(); const expirationType = message.getExpirationType(); + const isGroupConvo = !!convo?.isClosedGroup(); + const isControlMessage = message.isControlMessage(); + if ( convo && expirationType && expireTimer > 0 && - Boolean(message.getExpirationStartTimestamp()) === false + !message.getExpirationStartTimestamp() && + !(isGroupConvo && isControlMessage) ) { const expirationMode = changeToDisappearingConversationMode(convo, expirationType, expireTimer); @@ -444,6 +448,24 @@ function getMessageReadyToDisappear( messageExpirationFromRetrieve, } = expireUpdate; + // This message is an ExpirationTimerUpdate + if (messageFlags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) { + const expirationTimerUpdate = { + expirationType, + expireTimer, + source: messageModel.get('source'), + }; + + messageModel.set({ + expirationTimerUpdate, + }); + } + + // Note: We agreed that a control message for legacy groups does not expire + if (conversationModel.isClosedGroup() && messageModel.isControlMessage()) { + return messageModel; + } + /** * This is quite tricky, but when we receive a message from the network, it might be a disappearing after read one, which was already read by another device. * If that's the case, we need to not only mark the message as read, but also mark it as read at the right time. @@ -496,19 +518,6 @@ function getMessageReadyToDisappear( }); } - // This message is an ExpirationTimerUpdate - if (messageFlags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) { - const expirationTimerUpdate = { - expirationType, - expireTimer, - source: messageModel.get('source'), - }; - - messageModel.set({ - expirationTimerUpdate, - }); - } - return messageModel; } diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index dc001c681..af716aeb7 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -109,12 +109,8 @@ export async function initiateClosedGroupUpdate( const sharedDetails = { sender: UserUtils.getOurPubKeyStrFromCache(), sentAt: Date.now(), - - expireUpdate: { - expirationType: groupDetails.expirationType || ('unknown' as const), - expirationTimer: expireTimer || 0, - messageExpirationFromRetrieve: GetNetworkTime.getNowWithNetworkOffset() + expireTimer * 1000, - }, + // Note: we agreed that legacy group control messages do not expire + expireUpdate: null, convo, }; diff --git a/ts/test/session/unit/disappearing_messages/DisappearingMessage_test.ts b/ts/test/session/unit/disappearing_messages/DisappearingMessage_test.ts index e0b19aa6c..8101c87dc 100644 --- a/ts/test/session/unit/disappearing_messages/DisappearingMessage_test.ts +++ b/ts/test/session/unit/disappearing_messages/DisappearingMessage_test.ts @@ -561,6 +561,7 @@ describe('DisappearingMessage', () => { shouldCommitConvo: false, existingMessage: undefined, fromCurrentDevice: false, + fromConfigMessage: false, }); await expect(promise).is.rejectedWith( "updateExpireTimer() Disappearing messages aren't supported in communities" @@ -590,6 +591,7 @@ describe('DisappearingMessage', () => { shouldCommitConvo: false, existingMessage: undefined, fromCurrentDevice: false, + fromConfigMessage: false, }); expect(updateSuccess, 'should be true').to.be.true; }); @@ -613,6 +615,7 @@ describe('DisappearingMessage', () => { shouldCommitConvo: false, existingMessage: undefined, fromCurrentDevice: false, + fromConfigMessage: false, }); expect(updateSuccess, 'should be true').to.be.true; expect(