From bd900128f45b0cdb26fc4e1d0a880c95fc6f2620 Mon Sep 17 00:00:00 2001 From: William Grant Date: Mon, 3 Apr 2023 14:09:05 +0200 Subject: [PATCH] feat: added hasOutdated Client to convo model, use feature release check to process disappearing messages trigger banner when receiving a legacy disappearing message via hasOutdatedClient --- .../conversation/SessionConversation.tsx | 2 +- ts/models/conversation.ts | 12 +++++ ts/models/conversationAttributes.ts | 5 ++ ts/node/database_utility.ts | 3 ++ ts/node/migration/sessionMigrations.ts | 4 ++ ts/node/sql.ts | 5 ++ ts/receiver/contentMessage.ts | 41 ++++++++++----- ts/receiver/dataMessage.ts | 50 +++++++++++++++---- ts/receiver/queuedJob.ts | 47 ++++++----------- ts/state/ducks/conversations.ts | 2 +- ts/util/expiringMessages.ts | 2 + 11 files changed, 116 insertions(+), 57 deletions(-) diff --git a/ts/components/conversation/SessionConversation.tsx b/ts/components/conversation/SessionConversation.tsx index 720098561..71ea0bee7 100644 --- a/ts/components/conversation/SessionConversation.tsx +++ b/ts/components/conversation/SessionConversation.tsx @@ -255,7 +255,7 @@ export class SessionConversation extends React.Component {
- {selectedConversation.expirationType === 'legacy' && ( + {selectedConversation?.hasOutdatedClient && ( { const left = !!this.get('left'); const expirationType = this.get('expirationType'); const expireTimer = this.get('expireTimer'); + const lastDisappearingMessageChangeTimestamp = this.get( + 'lastDisappearingMessageChangeTimestamp' + ); + const hasOutdatedClient = this.get('hasOutdatedClient'); const currentNotificationSetting = this.get('triggerNotificationsFor'); const displayNameInProfile = this.get('displayNameInProfile'); const nickname = this.get('nickname'); @@ -414,6 +418,14 @@ export class ConversationModel extends Backbone.Model { toRet.expireTimer = expireTimer; } + if (lastDisappearingMessageChangeTimestamp) { + toRet.lastDisappearingMessageChangeTimestamp = lastDisappearingMessageChangeTimestamp; + } + + if (hasOutdatedClient) { + toRet.hasOutdatedClient = hasOutdatedClient; + } + if ( currentNotificationSetting && currentNotificationSetting !== ConversationNotificationSetting[0] diff --git a/ts/models/conversationAttributes.ts b/ts/models/conversationAttributes.ts index 375070595..70db848bd 100644 --- a/ts/models/conversationAttributes.ts +++ b/ts/models/conversationAttributes.ts @@ -35,6 +35,8 @@ export interface ConversationAttributes { expireTimer: number; lastDisappearingMessageChangeTimestamp: number; + hasOutdatedClient: boolean; + mentionedUs: boolean; unreadCount: number; lastMessageStatus: LastMessageStatusType; @@ -89,9 +91,12 @@ export const fillConvoAttributesWithDefaults = ( unreadCount: 0, lastJoinedTimestamp: 0, subscriberCount: 0, + expirationType: 'off', expireTimer: 0, lastDisappearingMessageChangeTimestamp: 0, + + hasOutdatedClient: false, active_at: 0, lastMessageStatus: undefined, diff --git a/ts/node/database_utility.ts b/ts/node/database_utility.ts index 440196900..e1ecd7784 100644 --- a/ts/node/database_utility.ts +++ b/ts/node/database_utility.ts @@ -78,6 +78,7 @@ const allowedKeysFormatRowOfConversation = [ 'conversationIdOrigin', 'expirationType', 'lastDisappearingMessageChangeTimestamp', + 'hasOutdatedClient', ]; export function formatRowOfConversation(row?: Record): ConversationAttributes | null { @@ -133,6 +134,7 @@ export function formatRowOfConversation(row?: Record): Conversation convo.readCapability = Boolean(convo.readCapability); convo.writeCapability = Boolean(convo.writeCapability); convo.uploadCapability = Boolean(convo.uploadCapability); + convo.hasOutdatedClient = Boolean(convo.hasOutdatedClient); if (!convo.conversationIdOrigin) { convo.conversationIdOrigin = undefined; @@ -208,6 +210,7 @@ const allowedKeysOfConversationAttributes = [ 'conversationIdOrigin', 'expirationType', 'lastDisappearingMessageChangeTimestamp', + 'hasOutdatedClient', ]; /** diff --git a/ts/node/migration/sessionMigrations.ts b/ts/node/migration/sessionMigrations.ts index a04b6f841..3b377bda4 100644 --- a/ts/node/migration/sessionMigrations.ts +++ b/ts/node/migration/sessionMigrations.ts @@ -1223,6 +1223,10 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN lastDisappearingMessageChangeTimestamp INTEGER DEFAULT 0;` ).run(); + db.prepare( + `ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN hasOutdatedClient BOOLEAN DEFAULT false;` + ).run(); + // same value in ts/util/releaseFeature.ts but we cannot import since window doesn't exist yet. // TODO update to agreed value between platforms const featureReleaseTimestamp = 1677574800000; // unix 28/02/2023 09:00 diff --git a/ts/node/sql.ts b/ts/node/sql.ts index fabaddef0..f0456555a 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -440,6 +440,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 // TODO rename expireTimer to expirationTimer expireTimer, lastDisappearingMessageChangeTimestamp, + hasOutdatedClient, mentionedUs, unreadCount, lastMessageStatus, @@ -486,6 +487,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 expirationType, expireTimer, lastDisappearingMessageChangeTimestamp, + hasOutdatedClient, mentionedUs, unreadCount, lastMessageStatus, @@ -521,6 +523,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 $expirationType, $expireTimer, $lastDisappearingMessageChangeTimestamp, + $hasOutdatedClient, $mentionedUs, $unreadCount, $lastMessageStatus, @@ -558,6 +561,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 expirationType, expireTimer, lastDisappearingMessageChangeTimestamp, + hasOutdatedClient: toSqliteBoolean(hasOutdatedClient), mentionedUs: toSqliteBoolean(mentionedUs), unreadCount, lastMessageStatus, @@ -2352,6 +2356,7 @@ function fillWithTestData(numConvosToAdd: number, numMsgsToAdd: number) { expirationType: 'off', expireTimer: 0, lastDisappearingMessageChangeTimestamp: 0, + hasOutdatedClient: false, groupAdmins: [], groupModerators: [], isApproved: false, diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 6083e10b4..1aa54d6bc 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -33,6 +33,7 @@ import { DisappearingMessageUpdate, setExpirationStartTimestamp, } from '../util/expiringMessages'; +import { checkIsFeatureReleased } from '../util/releaseFeature'; export async function handleSwarmContentMessage(envelope: EnvelopePlus, messageHash: string) { try { @@ -407,24 +408,40 @@ export async function innerHandleSwarmContentMessage( perfStart(`handleSwarmDataMessage-${envelope.id}`); - // TODO Trigger banner in UI? - // TODO maybe trigger the banner later based on the expirationType for the conversation - const isLegacyMode = dataMessage.expireTimer && dataMessage.expireTimer > 0; - - if (isLegacyMode) { - window.log.info('WIP: Received legacy disappearing message', content); + // We will only support legacy disappearing messages for a short period before disappearing messages v2 is unlocked + const isDisappearingMessagesV2Released = await checkIsFeatureReleased( + 'Disappearing Messages V2' + ); + const isLegacyMessage = Boolean(dataMessage.expireTimer && dataMessage.expireTimer > 0); + + // TODO account for outdated groups separately probably + if (isLegacyMessage) { + window.log.info('WIP: Received a legacy disappearing message', content); + // trigger notice banner + if (!senderConversationModel.get('hasOutdatedClient')) { + senderConversationModel.set({ hasOutdatedClient: true }); + } + } else { + if (senderConversationModel.get('hasOutdatedClient')) { + senderConversationModel.set({ hasOutdatedClient: false }); + } } + // TODO legacy messages support will be removed in a future release const expireUpdate: DisappearingMessageUpdate = { - // TODO When sending a message if it's a legacy message then we need to set the type to the default for compatiblity reasons - expirationType: isLegacyMode - ? DisappearingMessageConversationSetting[3] - : DisappearingMessageConversationSetting[content.expirationType] || 'off', - // TODO in the future we will remove the dataMessage expireTimer - expireTimer: isLegacyMode ? Number(dataMessage.expireTimer) : content.expirationTimer, + expirationType: + !isDisappearingMessagesV2Released && isLegacyMessage + ? DisappearingMessageConversationSetting[3] + : DisappearingMessageConversationSetting[content.expirationType] || 'off', + expireTimer: + !isDisappearingMessagesV2Released && isLegacyMessage + ? Number(dataMessage.expireTimer) + : content.expirationTimer, lastDisappearingMessageChangeTimestamp: content.lastDisappearingMessageChangeTimestamp ? Number(content.lastDisappearingMessageChangeTimestamp) : undefined, + isLegacyMessage, + isDisappearingMessagesV2Released, }; await handleSwarmDataMessage( diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index b5eb185dc..33426b685 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -247,22 +247,52 @@ export async function handleSwarmDataMessage( // TODO handle sync messages separately window.log.info('WIP: Sync Message dropping'); } else { - const { expirationType, expireTimer, lastDisappearingMessageChangeTimestamp } = expireUpdate; - - msgModel.set({ + const { expirationType, expireTimer, + lastDisappearingMessageChangeTimestamp, + isLegacyMessage, + isDisappearingMessagesV2Released, + } = expireUpdate; + + // TODO legacy messages support will be removed in a future release + msgModel.set({ + expirationType: isDisappearingMessagesV2Released + ? senderConversationModel.get('expirationType') + : expirationType, + expireTimer: isDisappearingMessagesV2Released + ? senderConversationModel.get('expireTimer') + : expireTimer, }); + // TODO legacy messages support will be removed in a future release // This message is conversation setting change message - if (lastDisappearingMessageChangeTimestamp) { + if ( + lastDisappearingMessageChangeTimestamp || + (isLegacyMessage && + rawDataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) + ) { + const expirationTimerUpdate = { + expirationType, + expireTimer, + lastDisappearingMessageChangeTimestamp: isLegacyMessage + ? Date.now() + : Number(lastDisappearingMessageChangeTimestamp), + source: msgModel.get('source'), + }; + + if (isLegacyMessage && isDisappearingMessagesV2Released) { + // if it is a legacy message then we ignore it and use the local client's conversation settings + expirationTimerUpdate.expirationType = senderConversationModel.get('expirationType'); + expirationTimerUpdate.expireTimer = senderConversationModel.get('expireTimer'); + + expirationTimerUpdate.lastDisappearingMessageChangeTimestamp = senderConversationModel.get( + 'lastDisappearingMessageChangeTimestamp' + ); + } + msgModel.set({ - expirationTimerUpdate: { - expirationType, - expireTimer, - lastDisappearingMessageChangeTimestamp, - source: msgModel.get('source'), - }, + expirationTimerUpdate, }); } } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 856a3960d..14ec983e8 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -16,10 +16,7 @@ import { GoogleChrome } from '../util'; import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; import { ConversationTypeEnum } from '../models/conversationAttributes'; import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; -import { - DisappearingMessageConversationType, - setExpirationStartTimestamp, -} from '../util/expiringMessages'; +import { setExpirationStartTimestamp } from '../util/expiringMessages'; import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; function contentTypeSupported(type: string): boolean { @@ -309,25 +306,6 @@ async function handleRegularMessage( }); } -async function handleExpirationTimerUpdateNoCommit( - conversation: ConversationModel, - message: MessageModel, - source: string, - expirationType: DisappearingMessageConversationType, - expireTimer: number, - lastDisappearingMessageChangeTimestamp: number -) { - await conversation.updateExpireTimer({ - providedExpirationType: expirationType, - providedExpireTimer: expireTimer, - providedChangeTimestamp: lastDisappearingMessageChangeTimestamp, - providedSource: source, - receivedAt: message.get('received_at'), - shouldCommit: false, - existingMessage: message, - }); -} - export async function handleMessageJob( messageModel: MessageModel, conversation: ConversationModel, @@ -349,10 +327,12 @@ export async function handleMessageJob( try { messageModel.set({ flags: regularDataMessage.flags }); + // TODO legacy messages support will be removed in a future release if ( messageModel.isIncoming() && - messageModel.get('expirationType') === 'deleteAfterSend' && - Boolean(messageModel.get('expirationStartTimestamp')) === false + Boolean(messageModel.get('expirationStartTimestamp')) === false && + ((messageModel.get('expirationType') === 'legacy' && conversation.isGroup()) || + messageModel.get('expirationType') === 'deleteAfterSend') ) { messageModel.set({ expirationStartTimestamp: setExpirationStartTimestamp( @@ -385,14 +365,15 @@ export async function handleMessageJob( return; } - await handleExpirationTimerUpdateNoCommit( - conversation, - messageModel, - source, - expirationType, - expireTimer, - lastDisappearingMessageChangeTimestamp - ); + await conversation.updateExpireTimer({ + providedExpirationType: expirationType, + providedExpireTimer: expireTimer, + providedChangeTimestamp: lastDisappearingMessageChangeTimestamp, + providedSource: source, + receivedAt: messageModel.get('received_at'), + shouldCommit: false, + existingMessage: messageModel, + }); } else { // this does not commit to db nor UI unless we need to approve a convo await handleRegularMessage( diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 8f384fec0..73a38cf96 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -262,7 +262,7 @@ export interface ReduxConversationType { expirationType?: DisappearingMessageConversationType; expireTimer?: number; lastDisappearingMessageChangeTimestamp?: number; - + hasOutdatedClient?: boolean; isTyping?: boolean; isBlocked?: boolean; isKickedFromGroup?: boolean; diff --git a/ts/util/expiringMessages.ts b/ts/util/expiringMessages.ts index 8dbe9744f..4679fdb41 100644 --- a/ts/util/expiringMessages.ts +++ b/ts/util/expiringMessages.ts @@ -22,6 +22,8 @@ export type DisappearingMessageUpdate = { expireTimer: number; // This is used for the expirationTimerUpdate lastDisappearingMessageChangeTimestamp?: number; + isLegacyMessage?: boolean; + isDisappearingMessagesV2Released?: boolean; }; export async function destroyMessagesAndUpdateRedux(