From 7d0673f7f227d13b3215f9dc6f0bfabe0784731c Mon Sep 17 00:00:00 2001 From: William Grant Date: Mon, 3 Apr 2023 14:09:05 +0200 Subject: [PATCH] wip: added new legacy mode to panel, improved backwards compatibility legacy mode support in conversation header, added typing for the expireUpdate, next is sending support --- _locales/en/messages.json | 2 ++ .../conversation/ConversationHeaderTitle.tsx | 7 +++- .../overlay/OverlayDisappearingMessages.tsx | 29 +++++++++++---- ts/models/conversation.ts | 6 +++- ts/models/message.ts | 20 ++++++----- ts/receiver/contentMessage.ts | 35 +++++++++---------- ts/receiver/dataMessage.ts | 6 ++-- ts/receiver/queuedJob.ts | 19 +++++----- ts/session/utils/syncUtils.ts | 25 ++++++------- ts/state/selectors/conversations.ts | 8 +++-- ts/types/LocalizerKeys.ts | 2 ++ ts/util/expiringMessages.ts | 10 +++++- 12 files changed, 103 insertions(+), 66 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f45075211..3a9623b89 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -217,6 +217,8 @@ "disappearingMessagesModeAfterReadSubtitle": "Messages delete after they have been read.", "disappearingMessagesModeAfterSend": "Disappear After Send", "disappearingMessagesModeAfterSendSubtitle": "Messages delete after they have been sent.", + "disappearingMessagesModeLegacy": "Legacy", + "disappearingMessagesModeLegacySubtitle": "Original version of disappearing messages.", "disappearingMessagesDisabled": "Disappearing messages disabled", "disabledDisappearingMessages": "$name$ has turned off disappearing messages.", "youDisabledDisappearingMessages": "You have turned off disappearing messages.", diff --git a/ts/components/conversation/ConversationHeaderTitle.tsx b/ts/components/conversation/ConversationHeaderTitle.tsx index 23536336d..5e037693f 100644 --- a/ts/components/conversation/ConversationHeaderTitle.tsx +++ b/ts/components/conversation/ConversationHeaderTitle.tsx @@ -139,7 +139,12 @@ export const ConversationHeaderTitle = () => { ? null : expirationType === 'deleteAfterRead' ? window.i18n('disappearingMessagesModeAfterRead') - : window.i18n('disappearingMessagesModeAfterSend'); + : expirationType === 'deleteAfterSend' + ? window.i18n('disappearingMessagesModeAfterSend') + : // legacy mode support + isGroup + ? window.i18n('disappearingMessagesModeAfterSend') + : window.i18n('disappearingMessagesModeAfterRead'); const abbreviatedExpireTime = Boolean(expireTimer) ? ExpirationTimerOptions.getAbbreviated(expireTimer) : null; diff --git a/ts/components/conversation/right-panel/overlay/OverlayDisappearingMessages.tsx b/ts/components/conversation/right-panel/overlay/OverlayDisappearingMessages.tsx index 68dfba450..9a05fa132 100644 --- a/ts/components/conversation/right-panel/overlay/OverlayDisappearingMessages.tsx +++ b/ts/components/conversation/right-panel/overlay/OverlayDisappearingMessages.tsx @@ -16,7 +16,10 @@ import { getSelectedConversationExpirationSettings, getSelectedConversationKey, } from '../../../../state/selectors/conversations'; -import { DisappearingMessageConversationType } from '../../../../util/expiringMessages'; +import { + DisappearingMessageConversationSetting, + DisappearingMessageConversationType, +} from '../../../../util/expiringMessages'; import { TimerOptionsArray } from '../../../../state/ducks/timerOptions'; import { useTimerOptionsByMode } from '../../../../hooks/useParamSelector'; import { isEmpty } from 'lodash'; @@ -108,14 +111,18 @@ const DisappearingModes = (props: DisappearingModesProps) => { {options.map((option: DisappearingMessageConversationType) => { const optionI18n = - option === 'off' - ? window.i18n('disappearingMessagesModeOff') + option === 'legacy' + ? window.i18n('disappearingMessagesModeLegacy') : option === 'deleteAfterRead' ? window.i18n('disappearingMessagesModeAfterRead') - : window.i18n('disappearingMessagesModeAfterSend'); + : option === 'deleteAfterSend' + ? window.i18n('disappearingMessagesModeAfterSend') + : window.i18n('disappearingMessagesModeOff'); const subtitleI18n = - option === 'deleteAfterRead' + option === 'legacy' + ? window.i18n('disappearingMessagesModeLegacySubtitle') + : option === 'deleteAfterRead' ? window.i18n('disappearingMessagesModeAfterReadSubtitle') : option === 'deleteAfterSend' ? window.i18n('disappearingMessagesModeAfterSendSubtitle') @@ -185,9 +192,19 @@ export const OverlayDisappearingMessages = () => { return null; } + const { isGroup } = convoProps; + const [modeSelected, setModeSelected] = useState(convoProps.expirationType); const [timeSelected, setTimeSelected] = useState(convoProps.expireTimer); - const timerOptions = useTimerOptionsByMode(modeSelected); + // Legacy mode uses the default timer options depending on the conversation type + // TODO verify that this if fine compared to updating in the useEffect + const timerOptions = useTimerOptionsByMode( + modeSelected === 'legacy' + ? isGroup + ? DisappearingMessageConversationSetting[2] + : DisappearingMessageConversationSetting[1] + : modeSelected + ); useEffect(() => { if (modeSelected !== convoProps.expirationType) { diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 889297b1f..f91c711b7 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1061,7 +1061,6 @@ export class ConversationModel extends Backbone.Model { expireTimer = 0; } - // TODO does this actually work? if ( this.get('lastDisappearingMessageChangeTimestamp') > lastDisappearingMessageChangeTimestamp ) { @@ -1079,6 +1078,11 @@ export class ConversationModel extends Backbone.Model { return; } + if (expirationType === 'legacy') { + // TODO If we are the new client then we ignore these updates + // TODO trigger UI + } + const isOutgoing = Boolean(!receivedAt); source = source || UserUtils.getOurPubKeyStrFromCache(); diff --git a/ts/models/message.ts b/ts/models/message.ts index b47ddcbdb..731d93963 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -81,7 +81,11 @@ import { loadPreviewData, loadQuoteData, } from '../types/MessageAttachment'; -import { ExpirationTimerOptions, setExpirationStartTimestamp } from '../util/expiringMessages'; +import { + DisappearingMessageUpdate, + ExpirationTimerOptions, + setExpirationStartTimestamp, +} from '../util/expiringMessages'; import { Notifications } from '../util/notifications'; import { Storage } from '../util/storage'; import { LinkPreviews } from '../util/linkPreviews'; @@ -250,8 +254,6 @@ export class MessageModel extends Backbone.Model { return window.i18n('mediaMessage'); } if (this.isExpirationTimerUpdate()) { - // TODO Backwards compatibility for Disappearing Messages in old clients - // TODO What does this comment refer to mean? const expireTimerUpdate = this.get('expirationTimerUpdate'); const expirationType = expireTimerUpdate?.expirationType; const expireTimer = expireTimerUpdate?.expireTimer; @@ -1070,28 +1072,28 @@ export class MessageModel extends Backbone.Model { sent: true, }); - let expireUpdate = null; + let expireUpdate: DisappearingMessageUpdate | null = null; const expirationType = dataMessage.getDisappearingMessageType(); if (expirationType && contentMessage.expirationTimer) { expireUpdate = { expirationType, expireTimer: contentMessage.expirationTimer, - lastDisappearingMessageChangeTimestamp: - contentMessage.lastDisappearingMessageChangeTimestamp, + lastDisappearingMessageChangeTimestamp: Number( + contentMessage.lastDisappearingMessageChangeTimestamp + ), }; } await this.commit(); - await this.sendSyncMessage(dataMessage, now, expireUpdate); + await this.sendSyncMessage(dataMessage, now, expireUpdate || undefined); } public async sendSyncMessage( data: DataMessage | SignalService.DataMessage, sentTimestamp: number, - // TODO add proper types - expireUpdate?: any + expireUpdate?: DisappearingMessageUpdate ) { if (this.get('synced') || this.get('sentSync')) { return; diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 7577e29cd..00e2ca415 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -3,7 +3,7 @@ import { handleSwarmDataMessage } from './dataMessage'; import { removeFromCache, updateCache } from './cache'; import { SignalService } from '../protobuf'; -import { compact, flatten, identity, isEmpty, isEqual, pickBy, toNumber } from 'lodash'; +import { compact, flatten, identity, isEmpty, pickBy, toNumber } from 'lodash'; import { KeyPrefixType, PubKey } from '../session/types'; import { BlockedNumberController } from '../util/blockedNumberController'; @@ -30,6 +30,7 @@ import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; import { DisappearingMessageConversationSetting, + DisappearingMessageUpdate, setExpirationStartTimestamp, } from '../util/expiringMessages'; @@ -406,28 +407,26 @@ export async function innerHandleSwarmContentMessage( perfStart(`handleSwarmDataMessage-${envelope.id}`); - const expireUpdate = { - expirationType: DisappearingMessageConversationSetting[content.expirationType] || 'off', - // TODO rename to expirationTimer? - expireTimer: content.expirationTimer || 0, - // This is used for the expirationTimerUpdate + // 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); + } + + 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, lastDisappearingMessageChangeTimestamp: content.lastDisappearingMessageChangeTimestamp ? Number(content.lastDisappearingMessageChangeTimestamp) : undefined, }; - // TODO in the future we will remove the dataMessage expireTimer - // Backwards compatibility for Disappearing Messages in old clients - if ( - expireUpdate.expireTimer > 0 && - dataMessage.expireTimer && - !isEqual(expireUpdate.expireTimer, dataMessage.expireTimer) - ) { - // TODO Trigger banner in UI? - expireUpdate.expireTimer = dataMessage.expireTimer; - window.log.info('WIP: Received outdated disappearing message data message', content); - } - await handleSwarmDataMessage( envelope, sentAtTimestamp, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 92cac808f..b5eb185dc 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -23,6 +23,7 @@ import { toLogFormat } from '../types/attachments/Errors'; import { ConversationTypeEnum } from '../models/conversationAttributes'; import { Reactions } from '../util/reactions'; import { Action, Reaction } from '../types/Reaction'; +import { DisappearingMessageUpdate } from '../util/expiringMessages'; function cleanAttachment(attachment: any) { return { @@ -154,8 +155,7 @@ export async function handleSwarmDataMessage( rawDataMessage: SignalService.DataMessage, messageHash: string, senderConversationModel: ConversationModel, - // TODO add proper types - expireUpdate?: any + expireUpdate: DisappearingMessageUpdate ): Promise { window.log.info('handleSwarmDataMessage'); @@ -255,7 +255,7 @@ export async function handleSwarmDataMessage( }); // This message is conversation setting change message - if (expireUpdate.lastDisappearingMessageChangeTimestamp) { + if (lastDisappearingMessageChangeTimestamp) { msgModel.set({ expirationTimerUpdate: { expirationType, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 79bf62912..856a3960d 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -1,7 +1,7 @@ import { queueAttachmentDownloads } from './attachments'; import { Quote } from './types'; -import _, { isEqual } from 'lodash'; +import _, { isEmpty, isEqual } from 'lodash'; import { getConversationController } from '../session/conversations'; import { ConversationModel } from '../models/conversation'; import { MessageModel, sliceQuoteText } from '../models/message'; @@ -364,18 +364,17 @@ export async function handleMessageJob( if (messageModel.isExpirationTimerUpdate()) { const expirationTimerUpdate = messageModel.get('expirationTimerUpdate'); - let expirationType = expirationTimerUpdate?.expirationType; - const expireTimer = expirationTimerUpdate?.expireTimer || 0; - const lastDisappearingMessageChangeTimestamp = - expirationTimerUpdate?.lastDisappearingMessageChangeTimestamp || getNowWithNetworkOffset(); - - // TODO This could happen when we receive a legacy disappearing message - if (!expirationType) { - expirationType = conversation.isPrivate() ? 'deleteAfterRead' : 'deleteAfterSend'; + if (!expirationTimerUpdate || isEmpty(expirationTimerUpdate)) { + window.log.info(`WIP: There is a problem with the expiration timer update`, messageModel); + return; } - // Compare mode and timestamp + const expirationType = expirationTimerUpdate.expirationType || 'off'; + const expireTimer = expirationTimerUpdate.expireTimer; + const lastDisappearingMessageChangeTimestamp = + expirationTimerUpdate.lastDisappearingMessageChangeTimestamp || getNowWithNetworkOffset(); + // Compare mode and timestamp const oldTypeValue = conversation.get('expirationType'); const oldTimerValue = conversation.get('expireTimer'); if (isEqual(expirationType, oldTypeValue) && isEqual(expireTimer, oldTimerValue)) { diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index 45d88cc82..f4e02fd39 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -27,7 +27,7 @@ import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage import { MessageRequestResponse } from '../messages/outgoing/controlMessage/MessageRequestResponse'; import { PubKey } from '../types'; import { DataMessage } from '../messages/outgoing'; -import { DisappearingMessageType } from '../../util/expiringMessages'; +import { DisappearingMessageUpdate } from '../../util/expiringMessages'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -296,16 +296,16 @@ const buildSyncVisibleMessage = ( const buildSyncExpireTimerMessage = ( identifier: string, - expirationType: DisappearingMessageType, - expireTimer: number, - lastDisappearingMessageChangeTimestamp: number | null, + expireUpdate: DisappearingMessageUpdate, timestamp: number, syncTarget: string ) => { + const { expirationType, expireTimer, lastDisappearingMessageChangeTimestamp } = expireUpdate; + return new ExpirationTimerUpdateMessage({ identifier, timestamp, - expirationType: expirationType || null, + expirationType, expireTimer, lastDisappearingMessageChangeTimestamp: lastDisappearingMessageChangeTimestamp || null, syncTarget, @@ -324,8 +324,7 @@ export const buildSyncMessage = ( data: DataMessage | SignalService.DataMessage, syncTarget: string, sentTimestamp: number, - // TODO add proper types - expireUpdate?: any + expireUpdate?: DisappearingMessageUpdate ): VisibleMessage | ExpirationTimerUpdateMessage => { if ( (data as any).constructor.name !== 'DataMessage' && @@ -342,17 +341,13 @@ export const buildSyncMessage = ( // don't include our profileKey on syncing message. This is to be done by a ConfigurationMessage now const timestamp = _.toNumber(sentTimestamp); if ( + expireUpdate && !isEmpty(expireUpdate) && dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) { - return buildSyncExpireTimerMessage( - identifier, - expireUpdate.expirationType, - expireUpdate.expireTimer, - expireUpdate.lastDisappearingMessageChangeTimestamp, - timestamp, - syncTarget - ); + return buildSyncExpireTimerMessage(identifier, expireUpdate, timestamp, syncTarget); + } else { + window.log.info(`WIP: Something went wrong when syncing a disappearing message`); } return buildSyncVisibleMessage(identifier, dataMessage, timestamp, syncTarget); }; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 9116d61c7..524be7036 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -1184,12 +1184,15 @@ export const getSelectedConversationExpirationModes = createSelector( (convo: ReduxConversationType | undefined) => { let modes = DisappearingMessageConversationSetting; - // Note to Self and Closed Groups only support deleteAfterSend + // Note to Self and Closed Groups only support deleteAfterSend and legacy modes if (convo?.isMe || (convo?.isGroup && !convo.isPublic)) { // only deleteAfterSend - modes = [modes[0], modes[2]]; + modes = [modes[0], modes[2], modes[modes.length - 1]]; } + // Legacy mode is the 2nd option in the UI + modes = [modes[0], modes[modes.length - 1], ...modes.slice(1, modes.length - 1)]; + return modes; } ); @@ -1199,5 +1202,6 @@ export const getSelectedConversationExpirationSettings = createSelector( (convo: ReduxConversationType | undefined) => ({ expirationType: convo?.expirationType, expireTimer: convo?.expireTimer, + isGroup: convo?.isGroup, }) ); diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index dbf9a699c..f84fc5600 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -217,6 +217,8 @@ export type LocalizerKeys = | 'disappearingMessagesModeAfterReadSubtitle' | 'disappearingMessagesModeAfterSend' | 'disappearingMessagesModeAfterSendSubtitle' + | 'disappearingMessagesModeLegacy' + | 'disappearingMessagesModeLegacySubtitle' | 'disappearingMessagesDisabled' | 'disabledDisappearingMessages' | 'youDisabledDisappearingMessages' diff --git a/ts/util/expiringMessages.ts b/ts/util/expiringMessages.ts index 71c93a33e..8dbe9744f 100644 --- a/ts/util/expiringMessages.ts +++ b/ts/util/expiringMessages.ts @@ -13,9 +13,17 @@ import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; export const DisappearingMessageMode = ['deleteAfterRead', 'deleteAfterSend']; export type DisappearingMessageType = typeof DisappearingMessageMode[number] | null; -export const DisappearingMessageConversationSetting = ['off', ...DisappearingMessageMode]; +export const DisappearingMessageConversationSetting = ['off', ...DisappearingMessageMode, 'legacy']; export type DisappearingMessageConversationType = typeof DisappearingMessageConversationSetting[number]; +export type DisappearingMessageUpdate = { + expirationType: DisappearingMessageType; + // TODO rename to expirationTimer? + expireTimer: number; + // This is used for the expirationTimerUpdate + lastDisappearingMessageChangeTimestamp?: number; +}; + export async function destroyMessagesAndUpdateRedux( messages: Array<{ conversationKey: string;