From 7d35216fda80f6a94a2a847ffffaf9cba8fc9db8 Mon Sep 17 00:00:00 2001 From: Josh Perez Date: Tue, 3 Oct 2023 20:12:57 -0400 Subject: [PATCH] Replace MessageController with MessageCache --- .eslintrc.js | 10 + app/main.ts | 7 +- test/modules/.eslintrc.js | 33 - test/test.js | 13 +- ts/CI.ts | 7 +- ts/ConversationController.ts | 11 +- ts/background.ts | 83 +-- ts/groups.ts | 18 +- ts/jobs/helpers/sendDeleteForEveryone.ts | 4 +- ts/jobs/helpers/sendDeleteStoryForEveryone.ts | 4 +- ts/jobs/helpers/sendNormalMessage.ts | 6 +- ts/jobs/helpers/sendReaction.ts | 10 +- ts/messageModifiers/AttachmentDownloads.ts | 8 +- ts/messageModifiers/Deletes.ts | 5 +- ts/messageModifiers/Edits.ts | 5 +- ts/messageModifiers/MessageReceipts.ts | 18 +- ts/messageModifiers/Reactions.ts | 5 +- ts/messageModifiers/ReadSyncs.ts | 6 +- ts/messageModifiers/ViewOnceOpenSyncs.ts | 6 +- ts/messageModifiers/ViewSyncs.ts | 6 +- ts/messages/getMessageById.ts | 12 +- ts/messages/getMessagesById.ts | 8 +- ts/model-types.d.ts | 5 - ts/models/conversations.ts | 83 ++- ts/models/messages.ts | 693 ++---------------- ts/reactions/enqueueReactionForSend.ts | 4 +- ts/scripts/test-electron.ts | 10 + ts/services/MessageCache.ts | 410 +++++++++++ ts/services/expiringMessagesDeletion.ts | 5 +- ts/services/messageStateCleanup.ts | 17 + .../tapToViewMessagesDeletionService.ts | 6 +- ts/signal.ts | 3 - ts/state/ducks/composer.ts | 6 +- ts/state/ducks/conversations.ts | 44 +- ts/state/ducks/globalModals.ts | 28 +- ts/state/ducks/lightbox.ts | 8 +- ts/state/ducks/mediaGallery.ts | 6 +- ts/state/ducks/stories.ts | 8 +- ts/state/initializeRedux.ts | 98 +++ ts/state/smart/ForwardMessagesModal.tsx | 11 +- ts/test-electron/models/conversations_test.ts | 6 +- ts/test-electron/models/messages_test.ts | 123 ++-- .../services/MessageCache_test.ts | 382 ++++++++++ ts/test-electron/state/ducks/stories_test.ts | 30 +- .../util/MessageController_test.ts | 56 -- ts/test-mock/messaging/stories_test.ts | 7 +- ts/util/MessageController.ts | 143 ---- ts/util/MessageModelLogger.ts | 55 ++ ts/util/callDisposition.ts | 9 +- ts/util/cleanup.ts | 13 +- ts/util/deleteGroupStoryReplyForEveryone.ts | 4 +- ts/util/deleteStoryForEveryone.ts | 4 +- .../findAndDeleteOnboardingStoryIfExists.ts | 19 +- ts/util/findStoryMessage.ts | 9 +- ts/util/getMessageAuthorText.ts | 50 ++ ts/util/getMessageConversation.ts | 13 + ts/util/getNotificationDataForMessage.ts | 452 ++++++++++++ ts/util/getNotificationTextForMessage.ts | 88 +++ ts/util/getSenderIdentifier.ts | 23 + ts/util/handleEditMessage.ts | 5 +- ts/util/hydrateStoryContext.ts | 89 +++ ts/util/lint/exceptions.json | 42 +- ts/util/markConversationRead.ts | 4 +- ts/util/markOnboardingStoryAsRead.ts | 4 +- ts/util/onStoryRecipientUpdate.ts | 6 +- ts/util/queueAttachmentDownloads.ts | 4 +- ts/util/sendDeleteForEveryoneMessage.ts | 4 +- ts/util/sendEditedMessage.ts | 4 +- ts/util/sendStoryMessage.ts | 12 +- ts/util/shouldReplyNotifyUser.ts | 10 +- ts/window.d.ts | 16 +- ts/windows/main/preload_test.ts | 41 +- ts/windows/main/start.ts | 9 +- 73 files changed, 2237 insertions(+), 1229 deletions(-) delete mode 100644 test/modules/.eslintrc.js create mode 100644 ts/services/MessageCache.ts create mode 100644 ts/services/messageStateCleanup.ts create mode 100644 ts/state/initializeRedux.ts create mode 100644 ts/test-electron/services/MessageCache_test.ts delete mode 100644 ts/test-electron/util/MessageController_test.ts delete mode 100644 ts/util/MessageController.ts create mode 100644 ts/util/MessageModelLogger.ts create mode 100644 ts/util/getMessageAuthorText.ts create mode 100644 ts/util/getMessageConversation.ts create mode 100644 ts/util/getNotificationDataForMessage.ts create mode 100644 ts/util/getNotificationTextForMessage.ts create mode 100644 ts/util/getSenderIdentifier.ts create mode 100644 ts/util/hydrateStoryContext.ts diff --git a/.eslintrc.js b/.eslintrc.js index b9d4c7e78e..46a94e7740 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -20,6 +20,16 @@ const rules = { 'brace-style': ['error', '1tbs', { allowSingleLine: false }], curly: ['error', 'all'], + // Immer support + 'no-param-reassign': [ + 'error', + { + props: true, + ignorePropertyModificationsForRegex: ['^draft'], + ignorePropertyModificationsFor: ['acc', 'ctx', 'context'], + }, + ], + // Always use === and !== except when directly comparing to null // (which only will equal null or undefined) eqeqeq: ['error', 'always', { null: 'never' }], diff --git a/app/main.ts b/app/main.ts index 452f6c13d1..44ef45ffff 100644 --- a/app/main.ts +++ b/app/main.ts @@ -776,8 +776,9 @@ async function createWindow() { } const startInTray = + isTestEnvironment(getEnvironment()) || (await systemTraySettingCache.get()) === - SystemTraySetting.MinimizeToAndStartInSystemTray; + SystemTraySetting.MinimizeToAndStartInSystemTray; const visibleOnAnyScreen = some(screen.getAllDisplays(), display => { if ( @@ -2882,6 +2883,10 @@ async function showStickerCreatorWindow() { } if (isTestEnvironment(getEnvironment())) { + ipc.handle('ci:test-electron:debug', async (_event, info) => { + process.stdout.write(`ci:test-electron:debug=${JSON.stringify(info)}\n`); + }); + ipc.handle('ci:test-electron:done', async (_event, info) => { if (!process.env.TEST_QUIT_ON_COMPLETE) { return; diff --git a/test/modules/.eslintrc.js b/test/modules/.eslintrc.js deleted file mode 100644 index 1db728015b..0000000000 --- a/test/modules/.eslintrc.js +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2018 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -// For reference: https://github.com/airbnb/javascript - -module.exports = { - env: { - mocha: true, - browser: true, - }, - - globals: { - check: true, - gen: true, - }, - - parserOptions: { - sourceType: 'module', - }, - - rules: { - // We still get the value of this rule, it just allows for dev deps - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - }, - ], - - // We want to keep each test structured the same, even if its contents are tiny - 'arrow-body-style': 'off', - }, -}; diff --git a/test/test.js b/test/test.js index f4f73f84d6..3edda1b68c 100644 --- a/test/test.js +++ b/test/test.js @@ -1,8 +1,6 @@ // Copyright 2014 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* global Whisper, _, Backbone */ - /* * global helpers for tests */ @@ -18,20 +16,21 @@ function deleteIndexedDB() { }); } +window.Events = { + getThemeSetting: () => 'light', +}; + /* Delete the database before running any tests */ before(async () => { - window.testUtilities.installMessageController(); - + await window.testUtilities.initialize(); await deleteIndexedDB(); - await window.testUtilities.initializeMessageCounter(); await window.Signal.Data.removeAll(); await window.storage.fetch(); }); -window.textsecure.storage.protocol = window.getSignalProtocolStore(); - window.testUtilities.prepareTests(); delete window.testUtilities.prepareTests; +window.textsecure.storage.protocol = window.getSignalProtocolStore(); !(function () { const passed = []; diff --git a/ts/CI.ts b/ts/CI.ts index 0a30ad7371..dfa7506fd9 100644 --- a/ts/CI.ts +++ b/ts/CI.ts @@ -120,7 +120,12 @@ export function getCI(deviceName: string): CIType { [sentAt] ); return messages.map( - m => window.MessageController.register(m.id, m).attributes + m => + window.MessageCache.__DEPRECATED$register( + m.id, + m, + 'CI.getMessagesBySentAt' + ).attributes ); } diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 276ec4f4b2..85b95a3522 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -12,7 +12,6 @@ import type { ConversationRenderInfoType, } from './model-types.d'; import type { ConversationModel } from './models/conversations'; -import type { MessageModel } from './models/messages'; import dataInterface from './sql/Client'; import * as log from './logging/log'; @@ -1127,13 +1126,11 @@ export class ConversationController { }); } - log.warn(`${logId}: Update cached messages in MessageController`); - window.MessageController.update((message: MessageModel) => { - if (message.get('conversationId') === obsoleteId) { - message.set({ conversationId: currentId }); - } + log.warn(`${logId}: Update cached messages in MessageCache`); + window.MessageCache.replaceAllObsoleteConversationIds({ + conversationId: currentId, + obsoleteId, }); - log.warn(`${logId}: Update messages table`); await migrateConversationMessages(obsoleteId, currentId); diff --git a/ts/background.ts b/ts/background.ts index bfd5c5e5e5..7c9295ee8f 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -3,7 +3,6 @@ import { webFrame } from 'electron'; import { isNumber, throttle, groupBy } from 'lodash'; -import { bindActionCreators } from 'redux'; import { render } from 'react-dom'; import { batch as batchDispatch } from 'react-redux'; import PQueue from 'p-queue'; @@ -108,7 +107,6 @@ import { AppViewType } from './state/ducks/app'; import type { BadgesStateType } from './state/ducks/badges'; import { areAnyCallsActiveOrRinging } from './state/selectors/calling'; import { badgeImageFileDownloader } from './badges/badgeImageFileDownloader'; -import { actionCreators } from './state/actions'; import * as Deletes from './messageModifiers/Deletes'; import type { EditAttributesType } from './messageModifiers/Edits'; import * as Edits from './messageModifiers/Edits'; @@ -154,7 +152,6 @@ import { startInteractionMode } from './services/InteractionMode'; import type { MainWindowStatsType } from './windows/context'; import { ReactionSource } from './reactions/ReactionSource'; import { singleProtoJobQueue } from './jobs/singleProtoJobQueue'; -import { getInitialState } from './state/getInitialState'; import { conversationJobQueue, conversationQueueJobEnum, @@ -164,6 +161,7 @@ import MessageSender from './textsecure/SendMessage'; import type AccountManager from './textsecure/AccountManager'; import { onStoryRecipientUpdate } from './util/onStoryRecipientUpdate'; import { flushAttachmentDownloadQueue } from './util/attachmentDownloadQueue'; +import { initializeRedux } from './state/initializeRedux'; import { StartupQueue } from './util/StartupQueue'; import { showConfirmationDialog } from './util/showConfirmationDialog'; import { onCallEventSync } from './util/onCallEventSync'; @@ -1151,7 +1149,7 @@ export async function startApp(): Promise { Errors.toLogFormat(error) ); } finally { - initializeRedux({ mainWindowStats, menuOptions }); + setupAppState({ mainWindowStats, menuOptions }); drop(start()); window.Signal.Services.initializeNetworkObserver( window.reduxActions.network @@ -1173,89 +1171,24 @@ export async function startApp(): Promise { } }); - function initializeRedux({ + function setupAppState({ mainWindowStats, menuOptions, }: { mainWindowStats: MainWindowStatsType; menuOptions: MenuOptionsType; }) { - // Here we set up a full redux store with initial state for our LeftPane Root - const convoCollection = window.getConversations(); - const initialState = getInitialState({ - badges: initialBadgesState, + initializeRedux({ + callsHistory: getCallsHistoryForRedux(), + initialBadgesState, mainWindowStats, menuOptions, stories: getStoriesForRedux(), storyDistributionLists: getDistributionListsForRedux(), - callsHistory: getCallsHistoryForRedux(), }); - const store = window.Signal.State.createStore(initialState); - window.reduxStore = store; - - // Binding these actions to our redux store and exposing them allows us to update - // redux when things change in the backbone world. - window.reduxActions = { - accounts: bindActionCreators(actionCreators.accounts, store.dispatch), - app: bindActionCreators(actionCreators.app, store.dispatch), - audioPlayer: bindActionCreators( - actionCreators.audioPlayer, - store.dispatch - ), - audioRecorder: bindActionCreators( - actionCreators.audioRecorder, - store.dispatch - ), - badges: bindActionCreators(actionCreators.badges, store.dispatch), - callHistory: bindActionCreators( - actionCreators.callHistory, - store.dispatch - ), - calling: bindActionCreators(actionCreators.calling, store.dispatch), - composer: bindActionCreators(actionCreators.composer, store.dispatch), - conversations: bindActionCreators( - actionCreators.conversations, - store.dispatch - ), - crashReports: bindActionCreators( - actionCreators.crashReports, - store.dispatch - ), - inbox: bindActionCreators(actionCreators.inbox, store.dispatch), - emojis: bindActionCreators(actionCreators.emojis, store.dispatch), - expiration: bindActionCreators(actionCreators.expiration, store.dispatch), - globalModals: bindActionCreators( - actionCreators.globalModals, - store.dispatch - ), - items: bindActionCreators(actionCreators.items, store.dispatch), - lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch), - linkPreviews: bindActionCreators( - actionCreators.linkPreviews, - store.dispatch - ), - mediaGallery: bindActionCreators( - actionCreators.mediaGallery, - store.dispatch - ), - network: bindActionCreators(actionCreators.network, store.dispatch), - safetyNumber: bindActionCreators( - actionCreators.safetyNumber, - store.dispatch - ), - search: bindActionCreators(actionCreators.search, store.dispatch), - stickers: bindActionCreators(actionCreators.stickers, store.dispatch), - stories: bindActionCreators(actionCreators.stories, store.dispatch), - storyDistributionLists: bindActionCreators( - actionCreators.storyDistributionLists, - store.dispatch - ), - toast: bindActionCreators(actionCreators.toast, store.dispatch), - updates: bindActionCreators(actionCreators.updates, store.dispatch), - user: bindActionCreators(actionCreators.user, store.dispatch), - username: bindActionCreators(actionCreators.username, store.dispatch), - }; + // Here we set up a full redux store with initial state for our LeftPane Root + const convoCollection = window.getConversations(); const { conversationAdded, diff --git a/ts/groups.ts b/ts/groups.ts index 9c2ae10cfc..63c7e8170e 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -2008,8 +2008,12 @@ export async function createGroupV2( forceSave: true, ourAci, }); - const model = new window.Whisper.Message(createdTheGroupMessage); - window.MessageController.register(model.id, model); + let model = new window.Whisper.Message(createdTheGroupMessage); + model = window.MessageCache.__DEPRECATED$register( + model.id, + model, + 'createGroupV2' + ); conversation.trigger('newmessage', model); if (expireTimer) { @@ -3371,7 +3375,7 @@ async function appendChangeMessages( let newMessages = 0; for (const changeMessage of mergedMessages) { - const existing = window.MessageController.getById(changeMessage.id); + const existing = window.MessageCache.__DEPRECATED$getById(changeMessage.id); // Update existing message if (existing) { @@ -3383,8 +3387,12 @@ async function appendChangeMessages( continue; } - const model = new window.Whisper.Message(changeMessage); - window.MessageController.register(model.id, model); + let model = new window.Whisper.Message(changeMessage); + model = window.MessageCache.__DEPRECATED$register( + model.id, + model, + 'appendChangeMessages' + ); conversation.trigger('newmessage', model); newMessages += 1; } diff --git a/ts/jobs/helpers/sendDeleteForEveryone.ts b/ts/jobs/helpers/sendDeleteForEveryone.ts index 73405e3339..427d256271 100644 --- a/ts/jobs/helpers/sendDeleteForEveryone.ts +++ b/ts/jobs/helpers/sendDeleteForEveryone.ts @@ -27,7 +27,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe import { handleMessageSend } from '../../util/handleMessageSend'; import { isConversationAccepted } from '../../util/isConversationAccepted'; import { isConversationUnregistered } from '../../util/isConversationUnregistered'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { isNotNil } from '../../util/isNotNil'; import type { CallbackResultType } from '../../textsecure/Types.d'; import type { MessageModel } from '../../models/messages'; @@ -59,7 +59,7 @@ export async function sendDeleteForEveryone( const logId = `sendDeleteForEveryone(${conversation.idForLogging()}, ${messageId})`; - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.error(`${logId}: Failed to fetch message. Failing job.`); return; diff --git a/ts/jobs/helpers/sendDeleteStoryForEveryone.ts b/ts/jobs/helpers/sendDeleteStoryForEveryone.ts index fed9ac955a..b895e0bb94 100644 --- a/ts/jobs/helpers/sendDeleteStoryForEveryone.ts +++ b/ts/jobs/helpers/sendDeleteStoryForEveryone.ts @@ -20,7 +20,7 @@ import { getUntrustedConversationServiceIds } from './getUntrustedConversationSe import { handleMessageSend } from '../../util/handleMessageSend'; import { isConversationAccepted } from '../../util/isConversationAccepted'; import { isConversationUnregistered } from '../../util/isConversationUnregistered'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { isNotNil } from '../../util/isNotNil'; import type { CallbackResultType } from '../../textsecure/Types.d'; import type { MessageModel } from '../../models/messages'; @@ -45,7 +45,7 @@ export async function sendDeleteStoryForEveryone( const logId = `sendDeleteStoryForEveryone(${storyId})`; - const message = await getMessageById(storyId); + const message = await __DEPRECATED$getMessageById(storyId); if (!message) { log.error(`${logId}: Failed to fetch message. Failing job.`); return; diff --git a/ts/jobs/helpers/sendNormalMessage.ts b/ts/jobs/helpers/sendNormalMessage.ts index 1d3a85c30a..62cedf8ae9 100644 --- a/ts/jobs/helpers/sendNormalMessage.ts +++ b/ts/jobs/helpers/sendNormalMessage.ts @@ -7,7 +7,7 @@ import PQueue from 'p-queue'; import * as Errors from '../../types/errors'; import { strictAssert } from '../../util/assert'; import type { MessageModel } from '../../models/messages'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import type { ConversationModel } from '../../models/conversations'; import { isGroup, isGroupV2, isMe } from '../../util/whatTypeOfConversation'; import { getSendOptions } from '../../util/getSendOptions'; @@ -72,7 +72,7 @@ export async function sendNormalMessage( const { Message } = window.Signal.Types; const { messageId, revision, editedMessageTimestamp } = data; - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.info( `message ${messageId} was not found, maybe because it was deleted. Giving up on sending it` @@ -551,7 +551,7 @@ async function getMessageSendData({ uploadMessagePreviews(message, uploadQueue), uploadMessageQuote(message, uploadQueue), uploadMessageSticker(message, uploadQueue), - storyId ? getMessageById(storyId) : undefined, + storyId ? __DEPRECATED$getMessageById(storyId) : undefined, ]); // Save message after uploading attachments diff --git a/ts/jobs/helpers/sendReaction.ts b/ts/jobs/helpers/sendReaction.ts index 3bc8ec257b..7a21d185f7 100644 --- a/ts/jobs/helpers/sendReaction.ts +++ b/ts/jobs/helpers/sendReaction.ts @@ -14,7 +14,7 @@ import type { ConversationModel } from '../../models/conversations'; import * as reactionUtil from '../../reactions/util'; import { isSent, SendStatus } from '../../messages/MessageSendState'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { isIncoming } from '../../messages/helpers'; import { isMe, @@ -60,7 +60,7 @@ export async function sendReaction( const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.info( `message ${messageId} was not found, maybe because it was deleted. Giving up on sending its reactions` @@ -334,7 +334,11 @@ export async function sendReaction( }); void conversation.addSingleMessage( - window.MessageController.register(reactionMessage.id, reactionMessage) + window.MessageCache.__DEPRECATED$register( + reactionMessage.id, + reactionMessage, + 'sendReaction' + ) ); } } diff --git a/ts/messageModifiers/AttachmentDownloads.ts b/ts/messageModifiers/AttachmentDownloads.ts index 98bc25c116..3439ce1896 100644 --- a/ts/messageModifiers/AttachmentDownloads.ts +++ b/ts/messageModifiers/AttachmentDownloads.ts @@ -391,7 +391,7 @@ async function _getMessageById( id: string, messageId: string ): Promise { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (message) { return message; @@ -408,7 +408,11 @@ async function _getMessageById( } strictAssert(messageId === messageAttributes.id, 'message id mismatch'); - return window.MessageController.register(messageId, messageAttributes); + return window.MessageCache.__DEPRECATED$register( + messageId, + messageAttributes, + 'AttachmentDownloads._getMessageById' + ); } async function _finishJob( diff --git a/ts/messageModifiers/Deletes.ts b/ts/messageModifiers/Deletes.ts index 71c0890080..f14a3c8e40 100644 --- a/ts/messageModifiers/Deletes.ts +++ b/ts/messageModifiers/Deletes.ts @@ -85,9 +85,10 @@ export async function onDelete(del: DeleteAttributesType): Promise { return; } - const message = window.MessageController.register( + const message = window.MessageCache.__DEPRECATED$register( targetMessage.id, - targetMessage + targetMessage, + 'Deletes.onDelete' ); await deleteForEveryone(message, del); diff --git a/ts/messageModifiers/Edits.ts b/ts/messageModifiers/Edits.ts index d8fde3fa97..8b4f5ecb4f 100644 --- a/ts/messageModifiers/Edits.ts +++ b/ts/messageModifiers/Edits.ts @@ -106,9 +106,10 @@ export async function onEdit(edit: EditAttributesType): Promise { return; } - const message = window.MessageController.register( + const message = window.MessageCache.__DEPRECATED$register( targetMessage.id, - targetMessage + targetMessage, + 'Edits.onEdit' ); await handleEditMessage(message.attributes, edit); diff --git a/ts/messageModifiers/MessageReceipts.ts b/ts/messageModifiers/MessageReceipts.ts index 2b13962e4c..24ae3af4d3 100644 --- a/ts/messageModifiers/MessageReceipts.ts +++ b/ts/messageModifiers/MessageReceipts.ts @@ -95,7 +95,11 @@ async function getTargetMessage( (isOutgoing(item) || isStory(item)) && sourceId === item.conversationId ); if (message) { - return window.MessageController.register(message.id, message); + return window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'MessageReceipts.getTargetMessage 1' + ); } const groups = await window.Signal.Data.getAllGroupsInvolvingServiceId( @@ -113,7 +117,11 @@ async function getTargetMessage( return null; } - return window.MessageController.register(target.id, target); + return window.MessageCache.__DEPRECATED$register( + target.id, + target, + 'MessageReceipts.getTargetMessage 2' + ); } const wasDeliveredWithSealedSender = ( @@ -376,7 +384,11 @@ export async function onReceipt( await Promise.all( targetMessages.map(msg => { - const model = window.MessageController.register(msg.id, msg); + const model = window.MessageCache.__DEPRECATED$register( + msg.id, + msg, + 'MessageReceipts.onReceipt' + ); return updateMessageSendState(receipt, model); }) ); diff --git a/ts/messageModifiers/Reactions.ts b/ts/messageModifiers/Reactions.ts index 05f3002a99..4918e50388 100644 --- a/ts/messageModifiers/Reactions.ts +++ b/ts/messageModifiers/Reactions.ts @@ -196,9 +196,10 @@ export async function onReaction( return; } - const message = window.MessageController.register( + const message = window.MessageCache.__DEPRECATED$register( targetMessage.id, - targetMessage + targetMessage, + 'Reactions.onReaction' ); // Use the generated message in ts/background.ts to create a message diff --git a/ts/messageModifiers/ReadSyncs.ts b/ts/messageModifiers/ReadSyncs.ts index 549233bc8d..17f8175678 100644 --- a/ts/messageModifiers/ReadSyncs.ts +++ b/ts/messageModifiers/ReadSyncs.ts @@ -125,7 +125,11 @@ export async function onSync(sync: ReadSyncAttributesType): Promise { notificationService.removeBy({ messageId: found.id }); - const message = window.MessageController.register(found.id, found); + const message = window.MessageCache.__DEPRECATED$register( + found.id, + found, + 'ReadSyncs.onSync' + ); const readAt = Math.min(sync.readAt, Date.now()); const newestSentAt = sync.timestamp; diff --git a/ts/messageModifiers/ViewOnceOpenSyncs.ts b/ts/messageModifiers/ViewOnceOpenSyncs.ts index 0b8f20b7ef..43d206ec7d 100644 --- a/ts/messageModifiers/ViewOnceOpenSyncs.ts +++ b/ts/messageModifiers/ViewOnceOpenSyncs.ts @@ -97,7 +97,11 @@ export async function onSync( return; } - const message = window.MessageController.register(found.id, found); + const message = window.MessageCache.__DEPRECATED$register( + found.id, + found, + 'ViewOnceOpenSyncs.onSync' + ); await message.markViewOnceMessageViewed({ fromSync: true }); viewOnceSyncs.delete(sync.timestamp); diff --git a/ts/messageModifiers/ViewSyncs.ts b/ts/messageModifiers/ViewSyncs.ts index aaf431e545..759706459f 100644 --- a/ts/messageModifiers/ViewSyncs.ts +++ b/ts/messageModifiers/ViewSyncs.ts @@ -99,7 +99,11 @@ export async function onSync(sync: ViewSyncAttributesType): Promise { notificationService.removeBy({ messageId: found.id }); - const message = window.MessageController.register(found.id, found); + const message = window.MessageCache.__DEPRECATED$register( + found.id, + found, + 'ViewSyncs.onSync' + ); let didChangeMessage = false; if (message.get('readStatus') !== ReadStatus.Viewed) { diff --git a/ts/messages/getMessageById.ts b/ts/messages/getMessageById.ts index 3421641a50..20ab189299 100644 --- a/ts/messages/getMessageById.ts +++ b/ts/messages/getMessageById.ts @@ -3,13 +3,13 @@ import * as log from '../logging/log'; import type { MessageAttributesType } from '../model-types.d'; -import type { MessageModel } from '../models/messages'; import * as Errors from '../types/errors'; +import type { MessageModel } from '../models/messages'; -export async function getMessageById( +export async function __DEPRECATED$getMessageById( messageId: string ): Promise { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (message) { return message; } @@ -28,5 +28,9 @@ export async function getMessageById( return undefined; } - return window.MessageController.register(found.id, found); + return window.MessageCache.__DEPRECATED$register( + found.id, + found, + '__DEPRECATED$getMessageById' + ); } diff --git a/ts/messages/getMessagesById.ts b/ts/messages/getMessagesById.ts index 5038df69de..8912a214e8 100644 --- a/ts/messages/getMessagesById.ts +++ b/ts/messages/getMessagesById.ts @@ -13,7 +13,7 @@ export async function getMessagesById( const messageIdsToLookUpInDatabase: Array = []; for (const messageId of messageIds) { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (message) { messagesFromMemory.push(message); } else { @@ -39,7 +39,11 @@ export async function getMessagesById( // We use `window.Whisper.Message` instead of `MessageModel` here to avoid a circular // import. const message = new window.Whisper.Message(rawMessage); - return window.MessageController.register(message.id, message); + return window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'getMessagesById' + ); }); return [...messagesFromMemory, ...messagesFromDatabase]; diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index fc3c34117d..258552abc3 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -1,15 +1,12 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -/* eslint-disable max-classes-per-file */ - import * as Backbone from 'backbone'; import type { GroupV2ChangeType } from './groups'; import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange'; import type { CustomColorType, ConversationColorType } from './types/Colors'; import type { SendMessageChallengeData } from './textsecure/Errors'; -import type { MessageModel } from './models/messages'; import type { ConversationModel } from './models/conversations'; import type { ProfileNameChangeType } from './util/getStringForProfileChange'; import type { CapabilitiesType } from './textsecure/WebAPI'; @@ -503,5 +500,3 @@ export type ShallowChallengeError = CustomError & { export declare class ConversationModelCollectionType extends Backbone.Collection { resetLookups(): void; } - -export declare class MessageModelCollectionType extends Backbone.Collection {} diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index 282f4ad972..0cff6f36c5 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -160,6 +160,7 @@ import { getQuoteAttachment } from '../util/makeQuote'; import { deriveProfileKeyVersion } from '../util/zkgroup'; import { incrementMessageCounter } from '../util/incrementMessageCounter'; import OS from '../util/os/osMain'; +import { getMessageAuthorText } from '../util/getMessageAuthorText'; /* eslint-disable more/no-then */ window.Whisper = window.Whisper || {}; @@ -1765,7 +1766,13 @@ export class ConversationModel extends window.Backbone ): Promise> { const result = messages .filter(message => Boolean(message.id)) - .map(message => window.MessageController.register(message.id, message)); + .map(message => + window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'cleanModels' + ) + ); const eliminated = messages.length - result.length; if (eliminated > 0) { @@ -2078,7 +2085,11 @@ export class ConversationModel extends window.Backbone // eslint-disable-next-line no-await-in-loop await Promise.all( readMessages.map(async m => { - const registered = window.MessageController.register(m.id, m); + const registered = window.MessageCache.__DEPRECATED$register( + m.id, + m, + 'handleReadAndDownloadAttachments' + ); const shouldSave = await registered.queueAttachmentDownloads(); if (shouldSave) { await window.Signal.Data.saveMessage(registered.attributes, { @@ -2824,12 +2835,13 @@ export class ConversationModel extends window.Backbone const id = await window.Signal.Data.saveMessage(message, { ourAci: window.textsecure.storage.user.getCheckedAci(), }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...message, id, - }) + }), + 'addChatSessionRefreshed' ); this.trigger('newmessage', model); @@ -2868,12 +2880,13 @@ export class ConversationModel extends window.Backbone const id = await window.Signal.Data.saveMessage(message, { ourAci: window.textsecure.storage.user.getCheckedAci(), }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...message, id, - }) + }), + 'addDeliveryIssue' ); this.trigger('newmessage', model); @@ -2922,9 +2935,10 @@ export class ConversationModel extends window.Backbone ourAci: window.textsecure.storage.user.getCheckedAci(), forceSave: true, }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( message.id, - new window.Whisper.Message(message) + new window.Whisper.Message(message), + 'addKeyChange' ); const isUntrusted = await this.isUntrusted(); @@ -3001,12 +3015,13 @@ export class ConversationModel extends window.Backbone ourAci: window.textsecure.storage.user.getCheckedAci(), forceSave: true, }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...message, id, - }) + }), + 'addConversationMerge' ); this.trigger('newmessage', model); @@ -3052,9 +3067,10 @@ export class ConversationModel extends window.Backbone ourAci: window.textsecure.storage.user.getCheckedAci(), forceSave: true, }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( message.id, - new window.Whisper.Message(message) + new window.Whisper.Message(message), + 'addVerifiedChange' ); this.trigger('newmessage', model); @@ -3093,12 +3109,13 @@ export class ConversationModel extends window.Backbone const id = await window.Signal.Data.saveMessage(message, { ourAci: window.textsecure.storage.user.getCheckedAci(), }); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...message, id, - }) + }), + 'addProfileChange' ); this.trigger('newmessage', model); @@ -3139,12 +3156,13 @@ export class ConversationModel extends window.Backbone ourAci: window.textsecure.storage.user.getCheckedAci(), } ); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...(message as MessageAttributesType), id, - }) + }), + 'addNotification' ); this.trigger('newmessage', model); @@ -3224,7 +3242,7 @@ export class ConversationModel extends window.Backbone `maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification` ); - const message = window.MessageController.getById(notificationId); + const message = window.MessageCache.__DEPRECATED$getById(notificationId); if (message) { await window.Signal.Data.removeMessage(message.id); } @@ -3261,7 +3279,7 @@ export class ConversationModel extends window.Backbone `maybeClearContactRemoved(${this.idForLogging()}): removed notification` ); - const message = window.MessageController.getById(notificationId); + const message = window.MessageCache.__DEPRECATED$getById(notificationId); if (message) { await window.Signal.Data.removeMessage(message.id); } @@ -3643,7 +3661,7 @@ export class ConversationModel extends window.Backbone draftBodyRanges: [], draftTimestamp: null, quotedMessageId: undefined, - lastMessageAuthor: message.getAuthorText(), + lastMessageAuthor: getMessageAuthorText(message.attributes), lastMessageBodyRanges: message.get('bodyRanges'), lastMessage: notificationData?.text || message.getNotificationText() || '', @@ -3795,7 +3813,11 @@ export class ConversationModel extends window.Backbone }); const model = new window.Whisper.Message(attributes); - const message = window.MessageController.register(model.id, model); + const message = window.MessageCache.__DEPRECATED$register( + model.id, + model, + 'enqueueMessageForSend' + ); message.cachedOutgoingContactData = contact; // Attach path to preview images so that sendNormalMessage can use them to @@ -3985,17 +4007,22 @@ export class ConversationModel extends window.Backbone let previewMessage: MessageModel | undefined; let activityMessage: MessageModel | undefined; - // Register the message with MessageController so that if it already exists + // Register the message with MessageCache so that if it already exists // in memory we use that data instead of the data from the db which may // be out of date. if (preview) { - previewMessage = window.MessageController.register(preview.id, preview); + previewMessage = window.MessageCache.__DEPRECATED$register( + preview.id, + preview, + 'previewMessage' + ); } if (activity) { - activityMessage = window.MessageController.register( + activityMessage = window.MessageCache.__DEPRECATED$register( activity.id, - activity + activity, + 'activityMessage' ); } @@ -4027,7 +4054,7 @@ export class ConversationModel extends window.Backbone notificationData?.text || previewMessage?.getNotificationText() || '', lastMessageBodyRanges: notificationData?.bodyRanges, lastMessagePrefix: notificationData?.emoji, - lastMessageAuthor: previewMessage?.getAuthorText(), + lastMessageAuthor: getMessageAuthorText(previewMessage?.attributes), lastMessageStatus: (previewMessage ? getMessagePropStatus(previewMessage.attributes, ourConversationId) @@ -4394,7 +4421,11 @@ export class ConversationModel extends window.Backbone model.set({ id }); - const message = window.MessageController.register(id, model); + const message = window.MessageCache.__DEPRECATED$register( + id, + model, + 'updateExpirationTimer' + ); void this.addSingleMessage(message); void this.updateUnread(); diff --git a/ts/models/messages.ts b/ts/models/messages.ts index c55a2f7d9f..8f992be5c3 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -27,11 +27,10 @@ import type { DeleteAttributesType } from '../messageModifiers/Deletes'; import type { SentEventData } from '../textsecure/messageReceiverEvents'; import { isNotNil } from '../util/isNotNil'; import { isNormalNumber } from '../util/isNormalNumber'; -import { softAssert, strictAssert } from '../util/assert'; +import { strictAssert } from '../util/assert'; +import { hydrateStoryContext } from '../util/hydrateStoryContext'; import { drop } from '../util/drop'; -import { dropNull } from '../util/dropNull'; import type { ConversationModel } from './conversations'; -import { getCallingNotificationText } from '../util/callingNotification'; import type { ProcessedDataMessage, ProcessedQuote, @@ -39,7 +38,6 @@ import type { CallbackResultType, } from '../textsecure/Types.d'; import { SendMessageProtoError } from '../textsecure/Errors'; -import * as expirationTimer from '../util/expirationTimer'; import { getUserLanguages } from '../util/userLanguages'; import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp'; import { copyCdnFields } from '../util/attachments'; @@ -49,15 +47,11 @@ import type { ServiceIdString } from '../types/ServiceId'; import { normalizeServiceId } from '../types/ServiceId'; import { isAciString } from '../util/isAciString'; import * as reactionUtil from '../reactions/util'; -import * as Stickers from '../types/Stickers'; import * as Errors from '../types/errors'; -import * as EmbeddedContact from '../types/EmbeddedContact'; import type { AttachmentType } from '../types/Attachment'; import { isImage, isVideo } from '../types/Attachment'; -import * as Attachment from '../types/Attachment'; import { stringToMIMEType } from '../types/MIME'; import * as MIME from '../types/MIME'; -import * as GroupChange from '../groupChange'; import { ReadStatus } from '../messages/MessageReadStatus'; import type { SendStateByConversationId } from '../messages/MessageSendState'; import { @@ -79,12 +73,9 @@ import { } from '../util/whatTypeOfConversation'; import { handleMessageSend } from '../util/handleMessageSend'; import { getSendOptions } from '../util/getSendOptions'; -import { findAndFormatContact } from '../util/findAndFormatContact'; import { modifyTargetMessage } from '../util/modifyTargetMessage'; import { - getAttachmentsForMessage, getMessagePropStatus, - getPropsForCallHistory, hasErrors, isCallHistory, isChatSessionRefreshed, @@ -106,14 +97,9 @@ import { isUnsupportedMessage, isVerifiedChange, isConversationMerge, - extractHydratedMentions, } from '../state/selectors/message'; -import { - isInCall, - getCallSelector, - getActiveCall, -} from '../state/selectors/calling'; import type { ReactionAttributesType } from '../messageModifiers/Reactions'; +import { isInCall } from '../state/selectors/calling'; import { ReactionSource } from '../reactions/ReactionSource'; import * as LinkPreview from '../types/LinkPreview'; import { SignalService as Proto } from '../protobuf'; @@ -138,9 +124,7 @@ import { isCustomError, messageHasPaymentEvent, isQuoteAMatch, - getPaymentEventNotificationText, } from '../messages/helpers'; -import type { ReplacementValuesType } from '../types/I18N'; import { viewOnceOpenJobQueue } from '../jobs/viewOnceOpenJobQueue'; import { getMessageIdForLogging } from '../util/idForLogging'; import { hasAttachmentDownloads } from '../util/hasAttachmentDownloads'; @@ -148,33 +132,29 @@ import { queueAttachmentDownloads } from '../util/queueAttachmentDownloads'; import { findStoryMessages } from '../util/findStoryMessage'; import { getStoryDataFromMessageAttributes } from '../services/storyLoader'; import type { ConversationQueueJobData } from '../jobs/conversationJobQueue'; -import { getMessageById } from '../messages/getMessageById'; import { shouldDownloadStory } from '../util/shouldDownloadStory'; import type { EmbeddedContactWithHydratedAvatar } from '../types/EmbeddedContact'; import { SeenStatus } from '../MessageSeenStatus'; import { isNewReactionReplacingPrevious } from '../reactions/util'; import { parseBoostBadgeListFromServer } from '../badges/parseBadgesFromServer'; -import { GiftBadgeStates } from '../components/conversation/Message'; import type { StickerWithHydratedData } from '../types/Stickers'; -import { getStringForConversationMerge } from '../util/getStringForConversationMerge'; import { addToAttachmentDownloadQueue, shouldUseAttachmentDownloadQueue, } from '../util/attachmentDownloadQueue'; -import { getTitleNoDefault, getNumber } from '../util/getTitle'; import dataInterface from '../sql/Client'; import { getQuoteBodyText } from '../util/getQuoteBodyText'; import { shouldReplyNotifyUser } from '../util/shouldReplyNotifyUser'; -import { isConversationAccepted } from '../util/isConversationAccepted'; import type { RawBodyRange } from '../types/BodyRange'; -import { BodyRange, applyRangesForText } from '../types/BodyRange'; -import { getStringForProfileChange } from '../util/getStringForProfileChange'; +import { BodyRange } from '../types/BodyRange'; import { queueUpdateMessage, saveNewMessageBatcher, } from '../util/messageBatcher'; -import { getCallHistorySelector } from '../state/selectors/callHistory'; -import { getConversationSelector } from '../state/selectors/conversations'; +import { getSenderIdentifier } from '../util/getSenderIdentifier'; +import { getNotificationDataForMessage } from '../util/getNotificationDataForMessage'; +import { getNotificationTextForMessage } from '../util/getNotificationTextForMessage'; +import { getMessageAuthorText } from '../util/getMessageAuthorText'; /* eslint-disable more/no-then */ @@ -212,9 +192,17 @@ export class MessageModel extends window.Backbone.Model { cachedOutgoingStickerData?: StickerWithHydratedData; + public registerLocations: Set; + constructor(attributes: MessageAttributesType) { super(attributes); + if (!this.id && attributes.id) { + this.id = attributes.id; + } + + this.registerLocations = new Set(); + // Note that we intentionally don't use `initialize()` method because it // isn't compatible with esnext output of esbuild. if (isObject(attributes)) { @@ -266,6 +254,12 @@ export class MessageModel extends window.Backbone.Model { return; } + window.MessageCache.setAttributes({ + messageId: this.id, + messageAttributes: this.attributes, + skipSaveToDatabase: true, + }); + const { storyChanged } = window.reduxActions.stories; if (isStory(this.attributes)) { @@ -293,19 +287,7 @@ export class MessageModel extends window.Backbone.Model { } getSenderIdentifier(): string { - const sentAt = this.get('sent_at'); - const source = this.get('source'); - const sourceServiceId = this.get('sourceServiceId'); - const sourceDevice = this.get('sourceDevice'); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const conversation = window.ConversationController.lookupOrCreate({ - e164: source, - serviceId: sourceServiceId, - reason: 'MessageModel.getSenderIdentifier', - })!; - - return `${conversation?.id}.${sourceDevice}-${sentAt}`; + return getSenderIdentifier(this.attributes); } getReceivedAt(): number { @@ -345,63 +327,7 @@ export class MessageModel extends window.Backbone.Model { shouldSave?: boolean; } = {} ): Promise { - const ourAci = window.textsecure.storage.user.getCheckedAci(); - const storyId = this.get('storyId'); - if (!storyId) { - return; - } - - const context = this.get('storyReplyContext'); - // We'll continue trying to get the attachment as long as the message still exists - if (context && (context.attachment?.url || !context.messageId)) { - return; - } - - const message = - inMemoryMessage === undefined - ? (await getMessageById(storyId))?.attributes - : inMemoryMessage; - - if (!message) { - const conversation = this.getConversation(); - softAssert( - conversation && isDirectConversation(conversation.attributes), - 'hydrateStoryContext: Not a type=direct conversation' - ); - this.set({ - storyReplyContext: { - attachment: undefined, - // This is ok to do because story replies only show in 1:1 conversations - // so the story that was quoted should be from the same conversation. - authorAci: conversation?.getAci(), - // No messageId, referenced story not found! - messageId: '', - }, - }); - if (shouldSave) { - await window.Signal.Data.saveMessage(this.attributes, { ourAci }); - } - return; - } - - const attachments = getAttachmentsForMessage({ ...message }); - let attachment: AttachmentType | undefined = attachments?.[0]; - if (attachment && !attachment.url && !attachment.textAttachment) { - attachment = undefined; - } - - const { sourceServiceId: authorAci } = message; - strictAssert(isAciString(authorAci), 'Story message from pni'); - this.set({ - storyReplyContext: { - attachment: omit(attachment, 'screenshotData'), - authorAci, - messageId: message.id, - }, - }); - if (shouldSave) { - await window.Signal.Data.saveMessage(this.attributes, { ourAci }); - } + await hydrateStoryContext(this.id, inMemoryMessage, { shouldSave }); } // Dependencies of prop-generation functions @@ -414,486 +340,11 @@ export class MessageModel extends window.Backbone.Model { text: string; bodyRanges?: ReadonlyArray; } { - // eslint-disable-next-line prefer-destructuring - const attributes: MessageAttributesType = this.attributes; - - if (isDeliveryIssue(attributes)) { - return { - emoji: '⚠️', - text: window.i18n('icu:DeliveryIssue--preview'), - }; - } - - if (isConversationMerge(attributes)) { - const conversation = this.getConversation(); - strictAssert( - conversation, - 'getNotificationData/isConversationMerge/conversation' - ); - strictAssert( - attributes.conversationMerge, - 'getNotificationData/isConversationMerge/conversationMerge' - ); - - return { - text: getStringForConversationMerge({ - obsoleteConversationTitle: getTitleNoDefault( - attributes.conversationMerge.renderInfo - ), - obsoleteConversationNumber: getNumber( - attributes.conversationMerge.renderInfo - ), - conversationTitle: conversation.getTitle(), - i18n: window.i18n, - }), - }; - } - - if (isChatSessionRefreshed(attributes)) { - return { - emoji: '🔁', - text: window.i18n('icu:ChatRefresh--notification'), - }; - } - - if (isUnsupportedMessage(attributes)) { - return { - text: window.i18n('icu:message--getDescription--unsupported-message'), - }; - } - - if (isGroupV1Migration(attributes)) { - return { - text: window.i18n('icu:GroupV1--Migration--was-upgraded'), - }; - } - - if (isProfileChange(attributes)) { - const change = this.get('profileChange'); - const changedId = this.get('changedId'); - const changedContact = findAndFormatContact(changedId); - if (!change) { - throw new Error('getNotificationData: profileChange was missing!'); - } - - return { - text: getStringForProfileChange(change, changedContact, window.i18n), - }; - } - - if (isGroupV2Change(attributes)) { - const change = this.get('groupV2Change'); - strictAssert( - change, - 'getNotificationData: isGroupV2Change true, but no groupV2Change!' - ); - - const changes = GroupChange.renderChange(change, { - i18n: window.i18n, - ourAci: window.textsecure.storage.user.getCheckedAci(), - ourPni: window.textsecure.storage.user.getCheckedPni(), - renderContact: (conversationId: string) => { - const conversation = - window.ConversationController.get(conversationId); - return conversation - ? conversation.getTitle() - : window.i18n('icu:unknownContact'); - }, - renderString: ( - key: string, - _i18n: unknown, - components: ReplacementValuesType | undefined - ) => { - // eslint-disable-next-line local-rules/valid-i18n-keys - return window.i18n(key, components); - }, - }); - - return { text: changes.map(({ text }) => text).join(' ') }; - } - - if (messageHasPaymentEvent(attributes)) { - const sender = findAndFormatContact(attributes.sourceServiceId); - const conversation = findAndFormatContact(attributes.conversationId); - return { - text: getPaymentEventNotificationText( - attributes.payment, - sender.title, - conversation.title, - sender.isMe, - window.i18n - ), - emoji: '💳', - }; - } - - const attachments = this.get('attachments') || []; - - if (isTapToView(attributes)) { - if (this.isErased()) { - return { - text: window.i18n('icu:message--getDescription--disappearing-media'), - }; - } - - if (Attachment.isImage(attachments)) { - return { - text: window.i18n('icu:message--getDescription--disappearing-photo'), - emoji: '📷', - }; - } - if (Attachment.isVideo(attachments)) { - return { - text: window.i18n('icu:message--getDescription--disappearing-video'), - emoji: '🎥', - }; - } - // There should be an image or video attachment, but we have a fallback just in - // case. - return { text: window.i18n('icu:mediaMessage'), emoji: '📎' }; - } - - if (isGroupUpdate(attributes)) { - const groupUpdate = this.get('group_update'); - const fromContact = getContact(this.attributes); - const messages = []; - if (!groupUpdate) { - throw new Error('getNotificationData: Missing group_update'); - } - - if (groupUpdate.left === 'You') { - return { text: window.i18n('icu:youLeftTheGroup') }; - } - if (groupUpdate.left) { - return { - text: window.i18n('icu:leftTheGroup', { - name: this.getNameForNumber(groupUpdate.left), - }), - }; - } - - if (!fromContact) { - return { text: '' }; - } - - if (isMe(fromContact.attributes)) { - messages.push(window.i18n('icu:youUpdatedTheGroup')); - } else { - messages.push( - window.i18n('icu:updatedTheGroup', { - name: fromContact.getTitle(), - }) - ); - } - - if (groupUpdate.joined && groupUpdate.joined.length) { - const joinedContacts = groupUpdate.joined.map(item => - window.ConversationController.getOrCreate(item, 'private') - ); - const joinedWithoutMe = joinedContacts.filter( - contact => !isMe(contact.attributes) - ); - - if (joinedContacts.length > 1) { - messages.push( - window.i18n('icu:multipleJoinedTheGroup', { - names: joinedWithoutMe - .map(contact => contact.getTitle()) - .join(', '), - }) - ); - - if (joinedWithoutMe.length < joinedContacts.length) { - messages.push(window.i18n('icu:youJoinedTheGroup')); - } - } else { - const joinedContact = window.ConversationController.getOrCreate( - groupUpdate.joined[0], - 'private' - ); - if (isMe(joinedContact.attributes)) { - messages.push(window.i18n('icu:youJoinedTheGroup')); - } else { - messages.push( - window.i18n('icu:joinedTheGroup', { - name: joinedContacts[0].getTitle(), - }) - ); - } - } - } - - if (groupUpdate.name) { - messages.push( - window.i18n('icu:titleIsNow', { - name: groupUpdate.name, - }) - ); - } - if (groupUpdate.avatarUpdated) { - messages.push(window.i18n('icu:updatedGroupAvatar')); - } - - return { text: messages.join(' ') }; - } - if (isEndSession(attributes)) { - return { text: window.i18n('icu:sessionEnded') }; - } - if (isIncoming(attributes) && hasErrors(attributes)) { - return { text: window.i18n('icu:incomingError') }; - } - - const body = (this.get('body') || '').trim(); - const bodyRanges = this.get('bodyRanges') || []; - - if (attachments.length) { - // This should never happen but we want to be extra-careful. - const attachment = attachments[0] || {}; - const { contentType } = attachment; - - if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) { - return { - bodyRanges, - emoji: '🎡', - text: body || window.i18n('icu:message--getNotificationText--gif'), - }; - } - if (Attachment.isImage(attachments)) { - return { - bodyRanges, - emoji: '📷', - text: body || window.i18n('icu:message--getNotificationText--photo'), - }; - } - if (Attachment.isVideo(attachments)) { - return { - bodyRanges, - emoji: '🎥', - text: body || window.i18n('icu:message--getNotificationText--video'), - }; - } - if (Attachment.isVoiceMessage(attachment)) { - return { - bodyRanges, - emoji: '🎤', - text: - body || - window.i18n('icu:message--getNotificationText--voice-message'), - }; - } - if (Attachment.isAudio(attachments)) { - return { - bodyRanges, - emoji: '🔈', - text: - body || - window.i18n('icu:message--getNotificationText--audio-message'), - }; - } - - return { - bodyRanges, - text: body || window.i18n('icu:message--getNotificationText--file'), - emoji: '📎', - }; - } - - const stickerData = this.get('sticker'); - if (stickerData) { - const emoji = - Stickers.getSticker(stickerData.packId, stickerData.stickerId)?.emoji || - stickerData?.emoji; - - if (!emoji) { - log.warn('Unable to get emoji for sticker'); - } - return { - text: window.i18n('icu:message--getNotificationText--stickers'), - emoji: dropNull(emoji), - }; - } - - if (isCallHistory(attributes)) { - const state = window.reduxStore.getState(); - const callingNotification = getPropsForCallHistory(attributes, { - callSelector: getCallSelector(state), - activeCall: getActiveCall(state), - callHistorySelector: getCallHistorySelector(state), - conversationSelector: getConversationSelector(state), - }); - if (callingNotification) { - const text = getCallingNotificationText( - callingNotification, - window.i18n - ); - if (text != null) { - return { - text, - }; - } - } - - log.error("This call history message doesn't have valid call history"); - } - if (isExpirationTimerUpdate(attributes)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { expireTimer } = this.get('expirationTimerUpdate')!; - if (!expireTimer) { - return { text: window.i18n('icu:disappearingMessagesDisabled') }; - } - - return { - text: window.i18n('icu:timerSetTo', { - time: expirationTimer.format(window.i18n, expireTimer), - }), - }; - } - - if (isKeyChange(attributes)) { - const identifier = this.get('key_changed'); - const conversation = window.ConversationController.get(identifier); - return { - text: window.i18n('icu:safetyNumberChangedGroup', { - name: conversation ? conversation.getTitle() : '', - }), - }; - } - const contacts = this.get('contact'); - if (contacts && contacts.length) { - return { - text: - EmbeddedContact.getName(contacts[0]) || - window.i18n('icu:unknownContact'), - emoji: '👤', - }; - } - - const giftBadge = this.get('giftBadge'); - if (giftBadge) { - const emoji = '✨'; - - if (isOutgoing(this.attributes)) { - const toContact = window.ConversationController.get( - this.attributes.conversationId - ); - const recipient = - toContact?.getTitle() ?? window.i18n('icu:unknownContact'); - return { - emoji, - text: window.i18n('icu:message--donation--preview--sent', { - recipient, - }), - }; - } - - const fromContact = getContact(this.attributes); - const sender = - fromContact?.getTitle() ?? window.i18n('icu:unknownContact'); - return { - emoji, - text: - giftBadge.state === GiftBadgeStates.Unopened - ? window.i18n('icu:message--donation--preview--unopened', { - sender, - }) - : window.i18n('icu:message--donation--preview--redeemed'), - }; - } - - if (body) { - return { - text: body, - bodyRanges, - }; - } - - return { text: '' }; - } - - getAuthorText(): string | undefined { - // if it's outgoing, it must be self-authored - const selfAuthor = isOutgoing(this.attributes) - ? window.i18n('icu:you') - : undefined; - - // if it's not selfAuthor and there's no incoming contact, - // it might be a group notification, so we return undefined - return selfAuthor ?? this.getIncomingContact()?.getTitle({ isShort: true }); + return getNotificationDataForMessage(this.attributes); } getNotificationText(): string { - const { text, emoji } = this.getNotificationData(); - const { attributes } = this; - - const conversation = this.getConversation(); - - strictAssert( - conversation != null, - 'Conversation not found in ConversationController' - ); - - if (!isConversationAccepted(conversation.attributes)) { - return window.i18n('icu:message--getNotificationText--messageRequest'); - } - - if (attributes.storyReaction) { - if (attributes.type === 'outgoing') { - const name = this.getConversation()?.get('profileName'); - - if (!name) { - return window.i18n( - 'icu:Quote__story-reaction-notification--outgoing--nameless', - { - emoji: attributes.storyReaction.emoji, - } - ); - } - - return window.i18n('icu:Quote__story-reaction-notification--outgoing', { - emoji: attributes.storyReaction.emoji, - name, - }); - } - - const ourAci = window.textsecure.storage.user.getCheckedAci(); - - if ( - attributes.type === 'incoming' && - attributes.storyReaction.targetAuthorAci === ourAci - ) { - return window.i18n('icu:Quote__story-reaction-notification--incoming', { - emoji: attributes.storyReaction.emoji, - }); - } - - if (!window.Signal.OS.isLinux()) { - return attributes.storyReaction.emoji; - } - - return window.i18n('icu:Quote__story-reaction--single'); - } - - const mentions = - extractHydratedMentions(attributes, { - conversationSelector: findAndFormatContact, - }) || []; - const spoilers = (attributes.bodyRanges || []).filter( - range => - BodyRange.isFormatting(range) && range.style === BodyRange.Style.SPOILER - ) as Array>; - const modifiedText = applyRangesForText({ text, mentions, spoilers }); - - // Linux emoji support is mixed, so we disable it. (Note that this doesn't touch - // the `text`, which can contain emoji.) - const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux(); - if (shouldIncludeEmoji) { - return window.i18n('icu:message--getNotificationText--text-with-emoji', { - text: modifiedText, - emoji, - }); - } - - return modifiedText || ''; + return getNotificationTextForMessage(this.attributes); } // General @@ -921,14 +372,6 @@ export class MessageModel extends window.Backbone.Model { this.set(attributes); } - getNameForNumber(number: string): string { - const conversation = window.ConversationController.get(number); - if (!conversation) { - return number; - } - return conversation.getTitle(); - } - async cleanup(): Promise { await cleanupMessage(this.attributes); } @@ -1041,7 +484,7 @@ export class MessageModel extends window.Backbone.Model { `doubleCheckMissingQuoteReference/${logId}: missing story reference` ); - const message = window.MessageController.getById(storyId); + const message = window.MessageCache.__DEPRECATED$getById(storyId); if (!message) { return; } @@ -1068,7 +511,7 @@ export class MessageModel extends window.Backbone.Model { log.info( `doubleCheckMissingQuoteReference/${logId}: Verifying reference to ${sentAt}` ); - const inMemoryMessages = window.MessageController.filterBySentAt( + const inMemoryMessages = window.MessageCache.__DEPRECATED$filterBySentAt( Number(sentAt) ); let matchingMessage = find(inMemoryMessages, message => @@ -1082,7 +525,11 @@ export class MessageModel extends window.Backbone.Model { isQuoteAMatch(item, this.get('conversationId'), quote) ); if (found) { - matchingMessage = window.MessageController.register(found.id, found); + matchingMessage = window.MessageCache.__DEPRECATED$register( + found.id, + found, + 'doubleCheckMissingQuoteReference' + ); } } @@ -1292,21 +739,6 @@ export class MessageModel extends window.Backbone.Model { this.set(markRead(this.attributes, readAt, options)); } - getIncomingContact(): ConversationModel | undefined | null { - if (!isIncoming(this.attributes)) { - return null; - } - const sourceServiceId = this.get('sourceServiceId'); - if (!sourceServiceId) { - return null; - } - - return window.ConversationController.getOrCreate( - sourceServiceId, - 'private' - ); - } - async retrySend(): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const conversation = this.getConversation()!; @@ -1971,7 +1403,8 @@ export class MessageModel extends window.Backbone.Model { messageId: '', }; - const inMemoryMessages = window.MessageController.filterBySentAt(id); + const inMemoryMessages = + window.MessageCache.__DEPRECATED$filterBySentAt(id); const matchingMessage = find(inMemoryMessages, item => isQuoteAMatch(item.attributes, conversationId, result) ); @@ -1992,7 +1425,11 @@ export class MessageModel extends window.Backbone.Model { return result; } - queryMessage = window.MessageController.register(found.id, found); + queryMessage = window.MessageCache.__DEPRECATED$register( + found.id, + found, + 'copyFromQuotedMessage' + ); } if (queryMessage) { @@ -2146,7 +1583,7 @@ export class MessageModel extends window.Backbone.Model { // 3. in rare cases, an incoming message can be retried, though it will // still go through one of the previous two codepaths // eslint-disable-next-line @typescript-eslint/no-this-alias - const message = this; + let message: MessageModel = this; const source = message.get('source'); const sourceServiceId = message.get('sourceServiceId'); const type = message.get('type'); @@ -2164,9 +1601,9 @@ export class MessageModel extends window.Backbone.Model { log.info(`${idLog}: starting processing in queue`); // First, check for duplicates. If we find one, stop processing here. - const inMemoryMessage = window.MessageController.findBySender( + const inMemoryMessage = window.MessageCache.findBySender( this.getSenderIdentifier() - )?.attributes; + ); if (inMemoryMessage) { log.info(`${idLog}: cache hit`, this.getSenderIdentifier()); } else { @@ -2197,9 +1634,10 @@ export class MessageModel extends window.Backbone.Model { `${idLog}: Updating message ${message.idForLogging()} with received transcript` ); - const toUpdate = window.MessageController.register( + const toUpdate = window.MessageCache.__DEPRECATED$register( existingMessage.id, - existingMessage + existingMessage, + 'handleDataMessage/outgoing/toUpdate' ); const unidentifiedDeliveriesSet = new Set( @@ -2594,6 +2032,7 @@ export class MessageModel extends window.Backbone.Model { const ourPni = window.textsecure.storage.user.getCheckedPni(); const ourServiceIds: Set = new Set([ourAci, ourPni]); + window.MessageCache.toMessageAttributes(this.attributes); message.set({ id: messageId, attachments: dataMessage.attachments, @@ -2786,12 +2225,16 @@ export class MessageModel extends window.Backbone.Model { ) { conversation.set({ lastMessage: message.getNotificationText(), - lastMessageAuthor: message.getAuthorText(), + lastMessageAuthor: getMessageAuthorText(message.attributes), timestamp: message.get('sent_at'), }); } - window.MessageController.register(message.id, message); + message = window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'handleDataMessage/message' + ); conversation.incrementMessageCount(); // If we sent a message in a given conversation, unarchive it! @@ -2892,7 +2335,7 @@ export class MessageModel extends window.Backbone.Model { const isFirstRun = false; await this.modifyTargetMessage(conversation, isFirstRun); - if (await shouldReplyNotifyUser(this, conversation)) { + if (await shouldReplyNotifyUser(this.attributes, conversation)) { await conversation.notify(this); } @@ -3042,9 +2485,10 @@ export class MessageModel extends window.Backbone.Model { timestamp: reaction.timestamp, }); - const messageToAdd = window.MessageController.register( + const messageToAdd = window.MessageCache.__DEPRECATED$register( generatedMessage.id, - generatedMessage + generatedMessage, + 'generatedMessage' ); if (isDirectConversation(targetConversation.attributes)) { await targetConversation.addSingleMessage(messageToAdd); @@ -3063,7 +2507,12 @@ export class MessageModel extends window.Backbone.Model { 'handleReaction: notifying for story reaction to ' + `${getMessageIdForLogging(storyMessage)} from someone else` ); - if (await shouldReplyNotifyUser(messageToAdd, targetConversation)) { + if ( + await shouldReplyNotifyUser( + messageToAdd.attributes, + targetConversation + ) + ) { drop(targetConversation.notify(messageToAdd)); } } @@ -3188,9 +2637,10 @@ export class MessageModel extends window.Backbone.Model { }); void conversation.addSingleMessage( - window.MessageController.register( + window.MessageCache.__DEPRECATED$register( generatedMessage.id, - generatedMessage + generatedMessage, + 'generatedMessage2' ) ); @@ -3285,14 +2735,3 @@ export class MessageModel extends window.Backbone.Model { } window.Whisper.Message = MessageModel; - -window.Whisper.MessageCollection = window.Backbone.Collection.extend({ - model: window.Whisper.Message, - comparator(left: Readonly, right: Readonly) { - if (left.get('received_at') === right.get('received_at')) { - return (left.get('sent_at') || 0) - (right.get('sent_at') || 0); - } - - return (left.get('received_at') || 0) - (right.get('received_at') || 0); - }, -}); diff --git a/ts/reactions/enqueueReactionForSend.ts b/ts/reactions/enqueueReactionForSend.ts index f88f0d9ecd..2dbfffaf7a 100644 --- a/ts/reactions/enqueueReactionForSend.ts +++ b/ts/reactions/enqueueReactionForSend.ts @@ -6,7 +6,7 @@ import { v4 as generateUuid } from 'uuid'; import type { ReactionAttributesType } from '../messageModifiers/Reactions'; import { ReactionSource } from './ReactionSource'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getSourceServiceId, isStory } from '../messages/helpers'; import { strictAssert } from '../util/assert'; import { isDirectConversation } from '../util/whatTypeOfConversation'; @@ -26,7 +26,7 @@ export async function enqueueReactionForSend({ messageId: string; remove: boolean; }>): Promise { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); strictAssert(message, 'enqueueReactionForSend: no message found'); const targetAuthorAci = getSourceServiceId(message.attributes); diff --git a/ts/scripts/test-electron.ts b/ts/scripts/test-electron.ts index 1b6638949c..a2bf7aef46 100644 --- a/ts/scripts/test-electron.ts +++ b/ts/scripts/test-electron.ts @@ -31,6 +31,16 @@ try { process.exit(1); } +const debugMatch = stdout.matchAll(/ci:test-electron:debug=(.*)?\n/g); +Array.from(debugMatch).forEach(info => { + try { + const args = JSON.parse(info[1]); + console.log('DEBUG:', args); + } catch { + // this section intentionally left blank + } +}); + const match = stdout.match(/ci:test-electron:done=(.*)?\n/); if (!match) { diff --git a/ts/services/MessageCache.ts b/ts/services/MessageCache.ts new file mode 100644 index 0000000000..77c3249ecc --- /dev/null +++ b/ts/services/MessageCache.ts @@ -0,0 +1,410 @@ +// Copyright 2019 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import cloneDeep from 'lodash/cloneDeep'; +import type { MessageAttributesType } from '../model-types.d'; +import type { MessageModel } from '../models/messages'; +import * as Errors from '../types/errors'; +import * as log from '../logging/log'; +import { drop } from '../util/drop'; +import { getEnvironment, Environment } from '../environment'; +import { getMessageConversation } from '../util/getMessageConversation'; +import { getMessageModelLogger } from '../util/MessageModelLogger'; +import { getSenderIdentifier } from '../util/getSenderIdentifier'; +import { isNotNil } from '../util/isNotNil'; +import { map } from '../util/iterables'; +import { softAssert, strictAssert } from '../util/assert'; + +export class MessageCache { + private state = { + messages: new Map(), + messageIdsBySender: new Map(), + messageIdsBySentAt: new Map>(), + lastAccessedAt: new Map(), + }; + + // Stores the models so that __DEPRECATED$register always returns the existing + // copy instead of a new model. + private modelCache = new Map(); + + // Synchronously access a message's attributes from internal cache. Will + // return undefined if the message does not exist in memory. + public accessAttributes( + messageId: string + ): Readonly | undefined { + const messageAttributes = this.state.messages.get(messageId); + return messageAttributes + ? this.freezeAttributes(messageAttributes) + : undefined; + } + + // Synchronously access a message's attributes from internal cache. Throws + // if the message does not exist in memory. + public accessAttributesOrThrow( + source: string, + messageId: string + ): Readonly { + const messageAttributes = this.accessAttributes(messageId); + strictAssert( + messageAttributes, + `MessageCache.accessAttributesOrThrow/${source}: no message` + ); + return messageAttributes; + } + + // Evicts messages from the message cache if they have not been accessed past + // the expiry time. + public deleteExpiredMessages(expiryTime: number): void { + const now = Date.now(); + + for (const [messageId, messageAttributes] of this.state.messages) { + const timeLastAccessed = this.state.lastAccessedAt.get(messageId) ?? 0; + const conversation = getMessageConversation(messageAttributes); + + const state = window.reduxStore.getState(); + const selectedId = state?.conversations?.selectedConversationId; + const inActiveConversation = + conversation && selectedId && conversation.id === selectedId; + + if (now - timeLastAccessed > expiryTime && !inActiveConversation) { + this.__DEPRECATED$unregister(messageId); + } + } + } + + // Finds a message in the cache by sender identifier + public findBySender( + senderIdentifier: string + ): Readonly | undefined { + const id = this.state.messageIdsBySender.get(senderIdentifier); + if (!id) { + return undefined; + } + + return this.accessAttributes(id); + } + + public replaceAllObsoleteConversationIds({ + conversationId, + obsoleteId, + }: { + conversationId: string; + obsoleteId: string; + }): void { + for (const [messageId, messageAttributes] of this.state.messages) { + if (messageAttributes.conversationId !== obsoleteId) { + continue; + } + this.setAttributes({ + messageId, + messageAttributes: { conversationId }, + skipSaveToDatabase: true, + }); + } + } + + // Find the message's attributes whether in memory or in the database. + // Refresh the attributes in the cache if they exist. Throw if we cannot find + // a matching message. + public async resolveAttributes( + source: string, + messageId: string + ): Promise> { + const inMemoryMessageAttributes = this.accessAttributes(messageId); + + if (inMemoryMessageAttributes) { + return inMemoryMessageAttributes; + } + + let messageAttributesFromDatabase: MessageAttributesType | undefined; + try { + messageAttributesFromDatabase = await window.Signal.Data.getMessageById( + messageId + ); + } catch (err: unknown) { + log.error( + `MessageCache.resolveAttributes(${messageId}): db error ${Errors.toLogFormat( + err + )}` + ); + } + + strictAssert( + messageAttributesFromDatabase, + `MessageCache.resolveAttributes/${source}: no message` + ); + + return this.freezeAttributes(messageAttributesFromDatabase); + } + + // Updates a message's attributes and saves the message to cache and to the + // database. Option to skip the save to the database. + public setAttributes({ + messageId, + messageAttributes: partialMessageAttributes, + skipSaveToDatabase = false, + }: { + messageId: string; + messageAttributes: Partial; + skipSaveToDatabase: boolean; + }): void { + let messageAttributes = this.accessAttributes(messageId); + + softAssert(messageAttributes, 'could not find message attributes'); + if (!messageAttributes) { + // We expect message attributes to be defined in cache if one is trying to + // set new attributes. In the case that the attributes are missing in cache + // we'll add whatever we currently have to cache as a defensive measure so + // that the code continues to work properly downstream. The softAssert above + // that logs/debugger should be addressed upstream immediately by ensuring + // that message is in cache. + const partiallyCachedMessage = { + id: messageId, + ...partialMessageAttributes, + } as MessageAttributesType; + + this.addMessageToCache(partiallyCachedMessage); + messageAttributes = partiallyCachedMessage; + } + + this.state.messageIdsBySender.delete( + getSenderIdentifier(messageAttributes) + ); + + const nextMessageAttributes = { + ...messageAttributes, + ...partialMessageAttributes, + }; + + const { id, sent_at: sentAt } = nextMessageAttributes; + const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt); + + let nextIdsBySentAtSet: Set; + if (previousIdsBySentAt) { + nextIdsBySentAtSet = new Set(previousIdsBySentAt); + nextIdsBySentAtSet.add(id); + } else { + nextIdsBySentAtSet = new Set([id]); + } + + this.state.messages.set(id, nextMessageAttributes); + this.state.lastAccessedAt.set(id, Date.now()); + this.state.messageIdsBySender.set( + getSenderIdentifier(messageAttributes), + id + ); + + this.markModelStale(nextMessageAttributes); + + if (window.reduxActions) { + window.reduxActions.conversations.messageChanged( + messageId, + nextMessageAttributes.conversationId, + nextMessageAttributes + ); + } + + if (skipSaveToDatabase) { + return; + } + drop( + window.Signal.Data.saveMessage(messageAttributes, { + ourAci: window.textsecure.storage.user.getCheckedAci(), + }) + ); + } + + // When you already have the message attributes from the db and want to + // ensure that they're added to the cache. The latest attributes from cache + // are returned if they exist, if not the attributes passed in are returned. + public toMessageAttributes( + messageAttributes: MessageAttributesType + ): Readonly { + this.addMessageToCache(messageAttributes); + + const nextMessageAttributes = this.state.messages.get(messageAttributes.id); + strictAssert( + nextMessageAttributes, + `MessageCache.toMessageAttributes: no message for id ${messageAttributes.id}` + ); + + if (getEnvironment() === Environment.Development) { + return Object.freeze(cloneDeep(nextMessageAttributes)); + } + return nextMessageAttributes; + } + + static install(): MessageCache { + const instance = new MessageCache(); + window.MessageCache = instance; + return instance; + } + + private addMessageToCache(messageAttributes: MessageAttributesType): void { + if (!messageAttributes.id) { + return; + } + + if (this.state.messages.has(messageAttributes.id)) { + this.state.lastAccessedAt.set(messageAttributes.id, Date.now()); + return; + } + + const { id, sent_at: sentAt } = messageAttributes; + const previousIdsBySentAt = this.state.messageIdsBySentAt.get(sentAt); + + let nextIdsBySentAtSet: Set; + if (previousIdsBySentAt) { + nextIdsBySentAtSet = new Set(previousIdsBySentAt); + nextIdsBySentAtSet.add(id); + } else { + nextIdsBySentAtSet = new Set([id]); + } + + this.state.messages.set(messageAttributes.id, { ...messageAttributes }); + this.state.lastAccessedAt.set(messageAttributes.id, Date.now()); + this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet)); + this.state.messageIdsBySender.set( + getSenderIdentifier(messageAttributes), + id + ); + } + + private freezeAttributes( + messageAttributes: MessageAttributesType + ): Readonly { + this.addMessageToCache(messageAttributes); + + if (getEnvironment() === Environment.Development) { + return Object.freeze(cloneDeep(messageAttributes)); + } + return messageAttributes; + } + + private removeMessage(messageId: string): void { + const messageAttributes = this.state.messages.get(messageId); + if (!messageAttributes) { + return; + } + + const { id, sent_at: sentAt } = messageAttributes; + const nextIdsBySentAtSet = + new Set(this.state.messageIdsBySentAt.get(sentAt)) || new Set(); + + nextIdsBySentAtSet.delete(id); + + if (nextIdsBySentAtSet.size) { + this.state.messageIdsBySentAt.set(sentAt, Array.from(nextIdsBySentAtSet)); + } else { + this.state.messageIdsBySentAt.delete(sentAt); + } + + this.state.messages.delete(messageId); + this.state.lastAccessedAt.delete(messageId); + this.state.messageIdsBySender.delete( + getSenderIdentifier(messageAttributes) + ); + } + + // Deprecated methods below + + // Adds the message into the cache and eturns a Proxy that resembles + // a MessageModel + public __DEPRECATED$register( + id: string, + data: MessageModel | MessageAttributesType, + location: string + ): MessageModel { + if (!id || !data) { + throw new Error( + 'MessageCache.__DEPRECATED$register: Got falsey id or message' + ); + } + + const existing = this.__DEPRECATED$getById(id); + + if (existing) { + this.addMessageToCache(existing.attributes); + return existing; + } + + const modelProxy = this.toModel(data); + const messageAttributes = 'attributes' in data ? data.attributes : data; + this.addMessageToCache(messageAttributes); + modelProxy.registerLocations.add(location); + + return modelProxy; + } + + // Deletes the message from our cache + public __DEPRECATED$unregister(id: string): void { + const model = this.modelCache.get(id); + if (!model) { + return; + } + + this.removeMessage(id); + this.modelCache.delete(id); + } + + // Finds a message in the cache by Id + public __DEPRECATED$getById(id: string): MessageModel | undefined { + const data = this.state.messages.get(id); + if (!data) { + return undefined; + } + + return this.toModel(data); + } + + // Finds a message in the cache by sentAt/timestamp + public __DEPRECATED$filterBySentAt(sentAt: number): Iterable { + const items = this.state.messageIdsBySentAt.get(sentAt) ?? []; + const attrs = items.map(id => this.accessAttributes(id)).filter(isNotNil); + return map(attrs, data => this.toModel(data)); + } + + // Marks cached model as "should be stale" to discourage continued use. + // The model's attributes are directly updated so that the model is in sync + // with the in-memory attributes. + private markModelStale(messageAttributes: MessageAttributesType): void { + const { id } = messageAttributes; + const model = this.modelCache.get(id); + + if (!model) { + return; + } + + model.attributes = { ...messageAttributes }; + + if (getEnvironment() === Environment.Development) { + log.warn('MessageCache: stale model', { + cid: model.cid, + locations: Array.from(model.registerLocations).join('+'), + }); + } + } + + // Creates a proxy object for MessageModel which logs usage in development + // so that we're able to migrate off of models + private toModel( + messageAttributes: MessageAttributesType | MessageModel + ): MessageModel { + const existingModel = this.modelCache.get(messageAttributes.id); + + if (existingModel) { + return existingModel; + } + + const model = + 'attributes' in messageAttributes + ? messageAttributes + : new window.Whisper.Message(messageAttributes); + + const proxy = getMessageModelLogger(model); + + this.modelCache.set(messageAttributes.id, proxy); + + return proxy; + } +} diff --git a/ts/services/expiringMessagesDeletion.ts b/ts/services/expiringMessagesDeletion.ts index 67ada86a00..dcb943041a 100644 --- a/ts/services/expiringMessagesDeletion.ts +++ b/ts/services/expiringMessagesDeletion.ts @@ -33,9 +33,10 @@ class ExpiringMessagesDeletionService { const inMemoryMessages: Array = []; messages.forEach(dbMessage => { - const message = window.MessageController.register( + const message = window.MessageCache.__DEPRECATED$register( dbMessage.id, - dbMessage + dbMessage, + 'destroyExpiredMessages' ); messageIds.push(message.id); inMemoryMessages.push(message); diff --git a/ts/services/messageStateCleanup.ts b/ts/services/messageStateCleanup.ts new file mode 100644 index 0000000000..28b595cd03 --- /dev/null +++ b/ts/services/messageStateCleanup.ts @@ -0,0 +1,17 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import * as durations from '../util/durations'; +import { isEnabled } from '../RemoteConfig'; +import { MessageCache } from './MessageCache'; + +const TEN_MINUTES = 10 * durations.MINUTE; + +export function initMessageCleanup(): void { + setInterval( + () => window.MessageCache.deleteExpiredMessages(TEN_MINUTES), + isEnabled('desktop.messageCleanup') ? TEN_MINUTES : durations.HOUR + ); + + MessageCache.install(); +} diff --git a/ts/services/tapToViewMessagesDeletionService.ts b/ts/services/tapToViewMessagesDeletionService.ts index 902e99baa8..11fd773dff 100644 --- a/ts/services/tapToViewMessagesDeletionService.ts +++ b/ts/services/tapToViewMessagesDeletionService.ts @@ -15,7 +15,11 @@ async function eraseTapToViewMessages() { await window.Signal.Data.getTapToViewMessagesNeedingErase(); await Promise.all( messages.map(async fromDB => { - const message = window.MessageController.register(fromDB.id, fromDB); + const message = window.MessageCache.__DEPRECATED$register( + fromDB.id, + fromDB, + 'eraseTapToViewMessages' + ); window.SignalContext.log.info( 'eraseTapToViewMessages: erasing message contents', diff --git a/ts/signal.ts b/ts/signal.ts index 9401fae72c..e1f5890854 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -18,8 +18,6 @@ import { ConfirmationDialog } from './components/ConfirmationDialog'; import { createApp } from './state/roots/createApp'; import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer'; -import { createStore } from './state/createStore'; - // Types import * as TypesAttachment from './types/Attachment'; import * as VisualAttachment from './types/VisualAttachment'; @@ -379,7 +377,6 @@ export const setup = (options: { }; const State = { - createStore, Roots, }; diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 4f878aa23e..d6fe2c8b5d 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -67,7 +67,7 @@ import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftDat import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast'; import { writeDraftAttachment } from '../../util/writeDraftAttachment'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { canReply } from '../selectors/message'; import { getContactId } from '../../messages/helpers'; import { getConversationSelector } from '../selectors/conversations'; @@ -747,7 +747,9 @@ export function setQuoteByMessageId( return; } - const message = messageId ? await getMessageById(messageId) : undefined; + const message = messageId + ? await __DEPRECATED$getMessageById(messageId) + : undefined; const state = getState(); if ( diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index ec423b7ee3..f9c551f639 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -137,7 +137,7 @@ import { buildUpdateAttributesChange, initiateMigrationToGroupV2 as doInitiateMigrationToGroupV2, } from '../../groups'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import type { PanelRenderType, PanelRequestType } from '../../types/Panels'; import type { ConversationQueueJobData } from '../../jobs/conversationJobQueue'; import { isOlderThan } from '../../util/timestamp'; @@ -1329,7 +1329,7 @@ function markMessageRead( return; } - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`markMessageRead: failed to load message ${messageId}`); } @@ -1674,7 +1674,7 @@ function deleteMessages({ await Promise.all( messageIds.map(async messageId => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`deleteMessages: Message ${messageId} missing!`); } @@ -1778,7 +1778,7 @@ function setMessageToEdit( return; } - const message = (await getMessageById(messageId))?.attributes; + const message = (await __DEPRECATED$getMessageById(messageId))?.attributes; if (!message) { return; } @@ -1855,7 +1855,7 @@ function generateNewGroupLink( * replace it with an actual action that fits in with the redux approach. */ export const markViewed = (messageId: string): void => { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (!message) { throw new Error(`markViewed: Message ${messageId} missing!`); } @@ -2126,7 +2126,7 @@ function kickOffAttachmentDownload( options: Readonly<{ messageId: string }> ): ThunkAction { return async dispatch => { - const message = await getMessageById(options.messageId); + const message = await __DEPRECATED$getMessageById(options.messageId); if (!message) { throw new Error( `kickOffAttachmentDownload: Message ${options.messageId} missing!` @@ -2158,7 +2158,7 @@ function markAttachmentAsCorrupted( options: AttachmentOptions ): ThunkAction { return async dispatch => { - const message = await getMessageById(options.messageId); + const message = await __DEPRECATED$getMessageById(options.messageId); if (!message) { throw new Error( `markAttachmentAsCorrupted: Message ${options.messageId} missing!` @@ -2177,7 +2177,7 @@ function openGiftBadge( messageId: string ): ThunkAction { return async dispatch => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`openGiftBadge: Message ${messageId} missing!`); } @@ -2197,7 +2197,7 @@ function retryMessageSend( messageId: string ): ThunkAction { return async dispatch => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`retryMessageSend: Message ${messageId} missing!`); } @@ -2214,7 +2214,7 @@ export function copyMessageText( messageId: string ): ThunkAction { return async dispatch => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`copy: Message ${messageId} missing!`); } @@ -2233,7 +2233,7 @@ export function retryDeleteForEveryone( messageId: string ): ThunkAction { return async dispatch => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`retryDeleteForEveryone: Message ${messageId} missing!`); } @@ -2737,7 +2737,7 @@ function conversationStoppedByMissingVerification(payload: { }; } -function messageChanged( +export function messageChanged( id: string, conversationId: string, data: MessageAttributesType @@ -2962,7 +2962,7 @@ function pushPanelForConversation( const message = state.conversations.messagesLookup[messageId] || - (await getMessageById(messageId))?.attributes; + (await __DEPRECATED$getMessageById(messageId))?.attributes; if (!message) { throw new Error( 'pushPanelForConversation: could not find message for MessageDetails' @@ -3038,7 +3038,7 @@ function deleteMessagesForEveryone( await Promise.all( messageIds.map(async messageId => { try { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (!message) { throw new Error( `deleteMessageForEveryone: Message ${messageId} missing!` @@ -3394,7 +3394,11 @@ function loadRecentMediaItems( // Cache these messages in memory to ensure Lightbox can find them messages.forEach(message => { - window.MessageController.register(message.id, message); + window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'loadRecentMediaItems' + ); }); const recentMediaItems = messages @@ -3492,7 +3496,7 @@ export function saveAttachmentFromMessage( providedAttachment?: AttachmentType ): ThunkAction { return async (dispatch, getState) => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error( `saveAttachmentFromMessage: Message ${messageId} missing!` @@ -3585,7 +3589,7 @@ export function scrollToMessage( throw new Error('scrollToMessage: No conversation found'); } - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`scrollToMessage: failed to load message ${messageId}`); } @@ -3599,7 +3603,7 @@ export function scrollToMessage( let isInMemory = true; - if (!window.MessageController.getById(messageId)) { + if (!window.MessageCache.__DEPRECATED$getById(messageId)) { isInMemory = false; } @@ -4009,7 +4013,7 @@ function onConversationOpened( conversation.onOpenStart(); if (messageId) { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (message) { drop(conversation.loadAndScroll(messageId)); @@ -4138,7 +4142,7 @@ function showArchivedConversations(): ShowArchivedConversationsActionType { } function doubleCheckMissingQuoteReference(messageId: string): NoopActionType { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); if (message) { void message.doubleCheckMissingQuoteReference(); } diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 33dc6f1387..79e1f043df 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -21,7 +21,6 @@ import * as Errors from '../../types/errors'; import * as SingleServePromise from '../../services/singleServePromise'; import * as Stickers from '../../types/Stickers'; import * as log from '../../logging/log'; -import { getMessageById } from '../../messages/getMessageById'; import { getMessagePropsSelector } from '../selectors/message'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { longRunningTaskWrapper } from '../../util/longRunningTaskWrapper'; @@ -559,15 +558,12 @@ function toggleForwardMessagesModal( const messagesProps = await Promise.all( messageIds.map(async messageId => { - const message = await getMessageById(messageId); + const messageAttributes = await window.MessageCache.resolveAttributes( + 'toggleForwardMessagesModal', + messageId + ); - if (!message) { - throw new Error( - `toggleForwardMessagesModal: no message found for ${messageId}` - ); - } - - const attachments = message.get('attachments') ?? []; + const { attachments = [] } = messageAttributes; if (!attachments.every(isDownloaded)) { dispatch( @@ -576,7 +572,7 @@ function toggleForwardMessagesModal( } const messagePropsSelector = getMessagePropsSelector(getState()); - const messageProps = messagePropsSelector(message.attributes); + const messageProps = messagePropsSelector(messageAttributes); return messageProps; }) @@ -765,14 +761,10 @@ function showEditHistoryModal( messageId: string ): ThunkAction { return async dispatch => { - const message = await getMessageById(messageId); - - if (!message) { - log.warn('showEditHistoryModal: no message found'); - return; - } - - const messageAttributes = message.attributes; + const messageAttributes = await window.MessageCache.resolveAttributes( + 'showEditHistoryModal', + messageId + ); const nextEditHistoryMessages = copyOverMessageAttributesIntoEditHistory(messageAttributes); diff --git a/ts/state/ducks/lightbox.ts b/ts/state/ducks/lightbox.ts index 719db35751..b96164fab7 100644 --- a/ts/state/ducks/lightbox.ts +++ b/ts/state/ducks/lightbox.ts @@ -17,7 +17,7 @@ import type { ShowToastActionType } from './toast'; import type { StateType as RootStateType } from '../reducer'; import * as log from '../../logging/log'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import type { MessageAttributesType } from '../../model-types.d'; import { isGIF } from '../../types/Attachment'; import { @@ -137,7 +137,7 @@ function showLightboxForViewOnceMedia( return async dispatch => { log.info('showLightboxForViewOnceMedia: attempting to display message'); - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error( `showLightboxForViewOnceMedia: Message ${messageId} missing!` @@ -232,7 +232,7 @@ function showLightbox(opts: { return async (dispatch, getState) => { const { attachment, messageId } = opts; - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error(`showLightbox: Message ${messageId} missing!`); } @@ -373,7 +373,7 @@ function showLightboxForAdjacentMessage( sent_at: sentAt, } = media.message; - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.warn('showLightboxForAdjacentMessage: original message is gone'); dispatch({ diff --git a/ts/state/ducks/mediaGallery.ts b/ts/state/ducks/mediaGallery.ts index d5e515f3ca..9a4b881912 100644 --- a/ts/state/ducks/mediaGallery.ts +++ b/ts/state/ducks/mediaGallery.ts @@ -101,7 +101,11 @@ function loadMediaItems( await Promise.all( rawMedia.map(async message => { const { schemaVersion } = message; - const model = window.MessageController.register(message.id, message); + const model = window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'loadMediaItems' + ); if (schemaVersion && schemaVersion < VERSION_NEEDED_FOR_DISPLAY) { const upgradedMsgAttributes = await upgradeMessageSchema(message); diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index fc5ed78fca..5a43b02f4e 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -36,7 +36,7 @@ import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUnti import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone'; import { deleteGroupStoryReplyForEveryone as doDeleteGroupStoryReplyForEveryone } from '../../util/deleteGroupStoryReplyForEveryone'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; -import { getMessageById } from '../../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; import { markOnboardingStoryAsRead } from '../../util/markOnboardingStoryAsRead'; import { markViewed } from '../../services/MessageUpdater'; import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads'; @@ -387,7 +387,7 @@ function markStoryRead( return; } - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.warn(`markStoryRead: no message found ${messageId}`); @@ -520,7 +520,7 @@ function queueStoryDownload( return; } - const message = await getMessageById(storyId); + const message = await __DEPRECATED$getMessageById(storyId); if (message) { // We want to ensure that we re-hydrate the story reply context with the @@ -1395,7 +1395,7 @@ function removeAllContactStories( const messages = ( await Promise.all( messageIds.map(async messageId => { - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { log.warn(`${logId}: no message found ${messageId}`); diff --git a/ts/state/initializeRedux.ts b/ts/state/initializeRedux.ts new file mode 100644 index 0000000000..9a86d7b5f4 --- /dev/null +++ b/ts/state/initializeRedux.ts @@ -0,0 +1,98 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { bindActionCreators } from 'redux'; +import type { BadgesStateType } from './ducks/badges'; +import type { CallHistoryDetails } from '../types/CallDisposition'; +import type { MainWindowStatsType } from '../windows/context'; +import type { MenuOptionsType } from '../types/menu'; +import type { StoryDataType } from './ducks/stories'; +import type { StoryDistributionListDataType } from './ducks/storyDistributionLists'; +import { actionCreators } from './actions'; +import { createStore } from './createStore'; +import { getInitialState } from './getInitialState'; + +export function initializeRedux({ + callsHistory, + initialBadgesState, + mainWindowStats, + menuOptions, + stories, + storyDistributionLists, +}: { + callsHistory: ReadonlyArray; + initialBadgesState: BadgesStateType; + mainWindowStats: MainWindowStatsType; + menuOptions: MenuOptionsType; + stories: Array; + storyDistributionLists: Array; +}): void { + const initialState = getInitialState({ + badges: initialBadgesState, + callsHistory, + mainWindowStats, + menuOptions, + stories, + storyDistributionLists, + }); + + const store = createStore(initialState); + window.reduxStore = store; + + // Binding these actions to our redux store and exposing them allows us to update + // redux when things change in the backbone world. + window.reduxActions = { + accounts: bindActionCreators(actionCreators.accounts, store.dispatch), + app: bindActionCreators(actionCreators.app, store.dispatch), + audioPlayer: bindActionCreators(actionCreators.audioPlayer, store.dispatch), + audioRecorder: bindActionCreators( + actionCreators.audioRecorder, + store.dispatch + ), + badges: bindActionCreators(actionCreators.badges, store.dispatch), + callHistory: bindActionCreators(actionCreators.callHistory, store.dispatch), + calling: bindActionCreators(actionCreators.calling, store.dispatch), + composer: bindActionCreators(actionCreators.composer, store.dispatch), + conversations: bindActionCreators( + actionCreators.conversations, + store.dispatch + ), + crashReports: bindActionCreators( + actionCreators.crashReports, + store.dispatch + ), + inbox: bindActionCreators(actionCreators.inbox, store.dispatch), + emojis: bindActionCreators(actionCreators.emojis, store.dispatch), + expiration: bindActionCreators(actionCreators.expiration, store.dispatch), + globalModals: bindActionCreators( + actionCreators.globalModals, + store.dispatch + ), + items: bindActionCreators(actionCreators.items, store.dispatch), + lightbox: bindActionCreators(actionCreators.lightbox, store.dispatch), + linkPreviews: bindActionCreators( + actionCreators.linkPreviews, + store.dispatch + ), + mediaGallery: bindActionCreators( + actionCreators.mediaGallery, + store.dispatch + ), + network: bindActionCreators(actionCreators.network, store.dispatch), + safetyNumber: bindActionCreators( + actionCreators.safetyNumber, + store.dispatch + ), + search: bindActionCreators(actionCreators.search, store.dispatch), + stickers: bindActionCreators(actionCreators.stickers, store.dispatch), + stories: bindActionCreators(actionCreators.stories, store.dispatch), + storyDistributionLists: bindActionCreators( + actionCreators.storyDistributionLists, + store.dispatch + ), + toast: bindActionCreators(actionCreators.toast, store.dispatch), + updates: bindActionCreators(actionCreators.updates, store.dispatch), + user: bindActionCreators(actionCreators.user, store.dispatch), + username: bindActionCreators(actionCreators.username, store.dispatch), + }; +} diff --git a/ts/state/smart/ForwardMessagesModal.tsx b/ts/state/smart/ForwardMessagesModal.tsx index a3e10352d4..1f702d86d8 100644 --- a/ts/state/smart/ForwardMessagesModal.tsx +++ b/ts/state/smart/ForwardMessagesModal.tsx @@ -19,7 +19,6 @@ import { } from '../selectors/conversations'; import { getIntl, getTheme, getRegionCode } from '../selectors/user'; import { getLinkPreview } from '../selectors/linkPreviews'; -import { getMessageById } from '../../messages/getMessageById'; import { getPreferredBadgeSelector } from '../selectors/badges'; import type { ForwardMessageData, @@ -36,6 +35,8 @@ import { SmartCompositionTextArea } from './CompositionTextArea'; import { useToastActions } from '../ducks/toast'; import { hydrateRanges } from '../../types/BodyRange'; import { isDownloaded } from '../../types/Attachment'; +import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; +import { strictAssert } from '../../util/assert'; function toMessageForwardDraft( props: ForwardMessagePropsType, @@ -119,10 +120,10 @@ function SmartForwardMessagesModalInner({ try { const messages = await Promise.all( finalDrafts.map(async (draft): Promise => { - const message = await getMessageById(draft.originalMessageId); - if (message == null) { - throw new Error('No message found'); - } + const message = await __DEPRECATED$getMessageById( + draft.originalMessageId + ); + strictAssert(message, 'no message found'); return { draft, originalMessage: message.attributes, diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index b6673d3a70..5fa3741ee2 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -83,7 +83,11 @@ describe('Conversations', () => { forceSave: true, ourAci, }); - message = window.MessageController.register(message.id, message); + message = window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'test' + ); await window.Signal.Data.updateConversation(conversation.attributes); await conversation.updateLastMessage(); diff --git a/ts/test-electron/models/messages_test.ts b/ts/test-electron/models/messages_test.ts index af29d87eec..16e990152c 100644 --- a/ts/test-electron/models/messages_test.ts +++ b/ts/test-electron/models/messages_test.ts @@ -5,17 +5,30 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { v4 as generateUuid } from 'uuid'; -import { setupI18n } from '../../util/setupI18n'; +import type { AttachmentType } from '../../types/Attachment'; +import type { CallbackResultType } from '../../textsecure/Types.d'; +import type { ConversationModel } from '../../models/conversations'; +import type { MessageAttributesType } from '../../model-types.d'; +import type { MessageModel } from '../../models/messages'; +import type { RawBodyRange } from '../../types/BodyRange'; +import type { StorageAccessType } from '../../types/Storage.d'; +import type { WebAPIType } from '../../textsecure/WebAPI'; +import MessageSender from '../../textsecure/SendMessage'; import enMessages from '../../../_locales/en/messages.json'; import { SendStatus } from '../../messages/MessageSendState'; -import MessageSender from '../../textsecure/SendMessage'; -import type { WebAPIType } from '../../textsecure/WebAPI'; -import type { CallbackResultType } from '../../textsecure/Types.d'; -import type { StorageAccessType } from '../../types/Storage.d'; -import { generateAci } from '../../types/ServiceId'; import { SignalService as Proto } from '../../protobuf'; +import { generateAci } from '../../types/ServiceId'; import { getContact } from '../../messages/helpers'; -import type { ConversationModel } from '../../models/conversations'; +import { setupI18n } from '../../util/setupI18n'; +import { + APPLICATION_JSON, + AUDIO_MP3, + IMAGE_GIF, + IMAGE_PNG, + LONG_MESSAGE, + TEXT_ATTACHMENT, + VIDEO_MP4, +} from '../../types/MIME'; describe('Message', () => { const STORAGE_KEYS_TO_RESTORE: Array = [ @@ -28,7 +41,7 @@ describe('Message', () => { const i18n = setupI18n('en', enMessages); const attributes = { - type: 'outgoing', + type: 'outgoing' as const, body: 'hi', conversationId: 'foo', attachments: [], @@ -39,12 +52,12 @@ describe('Message', () => { const me = '+14155555556'; const ourServiceId = generateAci(); - function createMessage(attrs: { [key: string]: unknown }) { - const messages = new window.Whisper.MessageCollection(); - return messages.add({ - received_at: Date.now(), + function createMessage(attrs: Partial): MessageModel { + return new window.Whisper.Message({ + id: generateUuid(), ...attrs, - }); + received_at: Date.now(), + } as MessageAttributesType); } function createMessageAndGetNotificationData(attrs: { @@ -214,14 +227,12 @@ describe('Message', () => { describe('getContact', () => { it('gets outgoing contact', () => { - const messages = new window.Whisper.MessageCollection(); - const message = messages.add(attributes); + const message = createMessage(attributes); assert.exists(getContact(message.attributes)); }); it('gets incoming contact', () => { - const messages = new window.Whisper.MessageCollection(); - const message = messages.add({ + const message = createMessage({ type: 'incoming', source, }); @@ -287,7 +298,8 @@ describe('Message', () => { isErased: false, attachments: [ { - contentType: 'image/png', + contentType: IMAGE_PNG, + size: 0, }, ], }), @@ -302,7 +314,8 @@ describe('Message', () => { isErased: false, attachments: [ { - contentType: 'video/mp4', + contentType: VIDEO_MP4, + size: 0, }, ], }), @@ -317,7 +330,8 @@ describe('Message', () => { isErased: false, attachments: [ { - contentType: 'text/plain', + contentType: LONG_MESSAGE, + size: 0, }, ], }), @@ -482,7 +496,7 @@ describe('Message', () => { createMessageAndGetNotificationData({ type: 'incoming', source, - flags: true, + flags: 1, }), { text: i18n('icu:sessionEnded') } ); @@ -493,17 +507,26 @@ describe('Message', () => { createMessageAndGetNotificationData({ type: 'incoming', source, - errors: [{}], + errors: [new Error()], }), { text: i18n('icu:incomingError') } ); }); - const attachmentTestCases = [ + const attachmentTestCases: Array<{ + title: string; + attachment: AttachmentType; + expectedResult: { + text: string; + emoji: string; + bodyRanges?: Array; + }; + }> = [ { title: 'GIF', attachment: { - contentType: 'image/gif', + contentType: IMAGE_GIF, + size: 0, }, expectedResult: { text: 'GIF', @@ -514,7 +537,8 @@ describe('Message', () => { { title: 'photo', attachment: { - contentType: 'image/png', + contentType: IMAGE_PNG, + size: 0, }, expectedResult: { text: 'Photo', @@ -525,7 +549,8 @@ describe('Message', () => { { title: 'video', attachment: { - contentType: 'video/mp4', + contentType: VIDEO_MP4, + size: 0, }, expectedResult: { text: 'Video', @@ -536,8 +561,9 @@ describe('Message', () => { { title: 'voice message', attachment: { - contentType: 'audio/ogg', + contentType: AUDIO_MP3, flags: Proto.AttachmentPointer.Flags.VOICE_MESSAGE, + size: 0, }, expectedResult: { text: 'Voice Message', @@ -548,8 +574,9 @@ describe('Message', () => { { title: 'audio message', attachment: { - contentType: 'audio/ogg', - fileName: 'audio.ogg', + contentType: AUDIO_MP3, + fileName: 'audio.mp3', + size: 0, }, expectedResult: { text: 'Audio Message', @@ -560,7 +587,8 @@ describe('Message', () => { { title: 'plain text', attachment: { - contentType: 'text/plain', + contentType: LONG_MESSAGE, + size: 0, }, expectedResult: { text: 'File', @@ -571,7 +599,8 @@ describe('Message', () => { { title: 'unspecified-type', attachment: { - contentType: null, + contentType: APPLICATION_JSON, + size: 0, }, expectedResult: { text: 'File', @@ -600,7 +629,8 @@ describe('Message', () => { attachments: [ attachment, { - contentType: 'text/html', + contentType: TEXT_ATTACHMENT, + size: 0, }, ], }), @@ -671,7 +701,8 @@ describe('Message', () => { source, attachments: [ { - contentType: 'image/png', + contentType: IMAGE_PNG, + size: 0, }, ], }).getNotificationText(), @@ -699,7 +730,8 @@ describe('Message', () => { source, attachments: [ { - contentType: 'image/png', + contentType: IMAGE_PNG, + size: 0, }, ], }).getNotificationText(), @@ -708,26 +740,3 @@ describe('Message', () => { }); }); }); - -describe('MessageCollection', () => { - it('should be ordered oldest to newest', () => { - const messages = new window.Whisper.MessageCollection(); - // Timestamps - const today = Date.now(); - const tomorrow = today + 12345; - - // Add threads - messages.add({ received_at: today }); - messages.add({ received_at: tomorrow }); - - const { models } = messages; - const firstTimestamp = models[0].get('received_at'); - const secondTimestamp = models[1].get('received_at'); - - // Compare timestamps - assert(typeof firstTimestamp === 'number'); - assert(typeof secondTimestamp === 'number'); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - assert(firstTimestamp! < secondTimestamp!); - }); -}); diff --git a/ts/test-electron/services/MessageCache_test.ts b/ts/test-electron/services/MessageCache_test.ts new file mode 100644 index 0000000000..7e313dd7a8 --- /dev/null +++ b/ts/test-electron/services/MessageCache_test.ts @@ -0,0 +1,382 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import { v4 as uuid } from 'uuid'; +import type { MessageAttributesType } from '../../model-types.d'; +import { MessageModel } from '../../models/messages'; +import { strictAssert } from '../../util/assert'; + +import { MessageCache } from '../../services/MessageCache'; + +describe('MessageCache', () => { + describe('filterBySentAt', () => { + it('returns an empty iterable if no messages match', () => { + const mc = new MessageCache(); + + assert.isEmpty([...mc.__DEPRECATED$filterBySentAt(123)]); + }); + + it('returns all messages that match the timestamp', () => { + const mc = new MessageCache(); + + let message1 = new MessageModel({ + conversationId: 'xyz', + body: 'message1', + id: uuid(), + received_at: 1, + sent_at: 1234, + timestamp: 9999, + type: 'incoming', + }); + let message2 = new MessageModel({ + conversationId: 'xyz', + body: 'message2', + id: uuid(), + received_at: 2, + sent_at: 1234, + timestamp: 9999, + type: 'outgoing', + }); + const message3 = new MessageModel({ + conversationId: 'xyz', + body: 'message3', + id: uuid(), + received_at: 3, + sent_at: 5678, + timestamp: 9999, + type: 'outgoing', + }); + + message1 = mc.__DEPRECATED$register(message1.id, message1, 'test'); + message2 = mc.__DEPRECATED$register(message2.id, message2, 'test'); + // We deliberately register this message twice for testing. + message2 = mc.__DEPRECATED$register(message2.id, message2, 'test'); + mc.__DEPRECATED$register(message3.id, message3, 'test'); + + const filteredMessages = Array.from( + mc.__DEPRECATED$filterBySentAt(1234) + ).map(x => x.attributes); + + assert.deepEqual( + filteredMessages, + [message1.attributes, message2.attributes], + 'first' + ); + + mc.__DEPRECATED$unregister(message2.id); + + const filteredMessages2 = Array.from( + mc.__DEPRECATED$filterBySentAt(1234) + ).map(x => x.attributes); + + assert.deepEqual(filteredMessages2, [message1.attributes], 'second'); + }); + }); + + describe('__DEPRECATED$register: syncing with backbone', () => { + it('backbone to redux', () => { + const message1 = new MessageModel({ + conversationId: 'xyz', + id: uuid(), + body: 'test1', + received_at: 1, + sent_at: Date.now(), + timestamp: Date.now(), + type: 'outgoing', + }); + const messageFromController = window.MessageCache.__DEPRECATED$register( + message1.id, + message1, + 'test' + ); + + assert.strictEqual( + message1, + messageFromController, + 'same objects from mc.__DEPRECATED$register' + ); + + const messageById = window.MessageCache.__DEPRECATED$getById(message1.id); + + assert.strictEqual(message1, messageById, 'same objects from mc.getById'); + + const messageInCache = window.MessageCache.accessAttributes(message1.id); + strictAssert(messageInCache, 'no message found'); + assert.deepEqual( + message1.attributes, + messageInCache, + 'same attributes as in cache' + ); + + message1.set({ body: 'test2' }); + assert.equal(message1.attributes.body, 'test2', 'message model updated'); + assert.equal( + messageById?.attributes.body, + 'test2', + 'old reference from messageById was updated' + ); + assert.equal( + messageInCache.body, + 'test1', + 'old cache reference not updated' + ); + + const newMessageById = window.MessageCache.__DEPRECATED$getById( + message1.id + ); + assert.deepEqual( + message1.attributes, + newMessageById?.attributes, + 'same attributes from mc.getById (2)' + ); + + const newMessageInCache = window.MessageCache.accessAttributes( + message1.id + ); + strictAssert(newMessageInCache, 'no message found'); + assert.deepEqual( + message1.attributes, + newMessageInCache, + 'same attributes as in cache (2)' + ); + }); + + it('redux to backbone (working with models)', () => { + const message = new MessageModel({ + conversationId: 'xyz', + id: uuid(), + body: 'test1', + received_at: 1, + sent_at: Date.now(), + timestamp: Date.now(), + type: 'outgoing', + }); + + window.MessageCache.toMessageAttributes(message.attributes); + + const messageFromController = window.MessageCache.__DEPRECATED$register( + message.id, + message, + 'test' + ); + + assert.notStrictEqual( + message, + messageFromController, + 'mc.__DEPRECATED$register returns existing but it is not the same reference' + ); + assert.deepEqual( + message.attributes, + messageFromController.attributes, + 'mc.__DEPRECATED$register returns existing and is the same attributes' + ); + + messageFromController.set({ body: 'test2' }); + + assert.notEqual( + message.get('body'), + messageFromController.get('body'), + 'new model is not equal to old model' + ); + + const messageInCache = window.MessageCache.accessAttributes(message.id); + strictAssert(messageInCache, 'no message found'); + assert.equal( + messageFromController.get('body'), + messageInCache.body, + 'new update is in cache' + ); + + assert.isUndefined( + messageFromController.get('storyReplyContext'), + 'storyReplyContext is undefined' + ); + + window.MessageCache.setAttributes({ + messageId: message.id, + messageAttributes: { + storyReplyContext: { + attachment: undefined, + authorAci: undefined, + messageId: 'test123', + }, + }, + skipSaveToDatabase: true, + }); + + // This works because we refresh the model whenever an attribute changes + // but this should log a warning. + assert.equal( + messageFromController.get('storyReplyContext')?.messageId, + 'test123', + 'storyReplyContext was updated (stale model)' + ); + + const newMessageFromController = + window.MessageCache.__DEPRECATED$register(message.id, message, 'test'); + + assert.equal( + newMessageFromController.get('storyReplyContext')?.messageId, + 'test123', + 'storyReplyContext was updated (not stale)' + ); + }); + + it('redux to backbone (working with attributes)', () => { + it('sets the attributes and returns a fresh copy', () => { + const mc = new MessageCache(); + + const messageAttributes: MessageAttributesType = { + conversationId: uuid(), + id: uuid(), + received_at: 1, + sent_at: Date.now(), + timestamp: Date.now(), + type: 'incoming', + }; + + const messageModel = mc.__DEPRECATED$register( + messageAttributes.id, + messageAttributes, + 'test/updateAttributes' + ); + + assert.deepEqual( + messageAttributes, + messageModel.attributes, + 'initial attributes matches message model' + ); + + const proposedStoryReplyContext = { + attachment: undefined, + authorAci: undefined, + messageId: 'test123', + }; + + assert.notDeepEqual( + messageModel.attributes.storyReplyContext, + proposedStoryReplyContext, + 'attributes were changed outside of the message model' + ); + + mc.setAttributes({ + messageId: messageAttributes.id, + messageAttributes: { + storyReplyContext: proposedStoryReplyContext, + }, + skipSaveToDatabase: true, + }); + + const nextMessageAttributes = mc.accessAttributesOrThrow( + 'test', + messageAttributes.id + ); + + assert.notDeepEqual( + messageAttributes, + nextMessageAttributes, + 'initial attributes are stale' + ); + assert.notDeepEqual( + messageAttributes.storyReplyContext, + proposedStoryReplyContext, + 'initial attributes are stale 2' + ); + + assert.deepEqual( + nextMessageAttributes.storyReplyContext, + proposedStoryReplyContext, + 'fresh attributes match what was proposed' + ); + assert.notStrictEqual( + nextMessageAttributes.storyReplyContext, + proposedStoryReplyContext, + 'fresh attributes are not the same reference as proposed attributes' + ); + + assert.deepEqual( + messageModel.attributes, + nextMessageAttributes, + 'model was updated' + ); + + assert.equal( + messageModel.get('storyReplyContext')?.messageId, + 'test123', + 'storyReplyContext in model is set correctly' + ); + }); + }); + }); + + describe('accessAttributes', () => { + it('gets the attributes if they exist', () => { + const mc = new MessageCache(); + + const messageAttributes: MessageAttributesType = { + conversationId: uuid(), + id: uuid(), + received_at: 1, + sent_at: Date.now(), + timestamp: Date.now(), + type: 'incoming', + }; + + mc.toMessageAttributes(messageAttributes); + + const accessAttributes = mc.accessAttributes(messageAttributes.id); + + assert.deepEqual( + accessAttributes, + messageAttributes, + 'attributes returned have the same values' + ); + assert.notStrictEqual( + accessAttributes, + messageAttributes, + 'attributes returned are not the same references' + ); + + const undefinedMessage = mc.accessAttributes(uuid()); + assert.isUndefined(undefinedMessage, 'access did not find message'); + }); + }); + + describe('accessAttributesOrThrow', () => { + it('accesses the attributes or throws if they do not exist', () => { + const mc = new MessageCache(); + + const messageAttributes: MessageAttributesType = { + conversationId: uuid(), + id: uuid(), + received_at: 1, + sent_at: Date.now(), + timestamp: Date.now(), + type: 'incoming', + }; + + mc.toMessageAttributes(messageAttributes); + + const accessAttributes = mc.accessAttributesOrThrow( + 'tests.1', + messageAttributes.id + ); + + assert.deepEqual( + accessAttributes, + messageAttributes, + 'attributes returned have the same values' + ); + assert.notStrictEqual( + accessAttributes, + messageAttributes, + 'attributes returned are not the same references' + ); + + assert.throws(() => { + mc.accessAttributesOrThrow('tests.2', uuid()); + }); + }); + }); +}); diff --git a/ts/test-electron/state/ducks/stories_test.ts b/ts/test-electron/state/ducks/stories_test.ts index 132c5e9dd2..916382474a 100644 --- a/ts/test-electron/state/ducks/stories_test.ts +++ b/ts/test-electron/state/ducks/stories_test.ts @@ -861,7 +861,11 @@ describe('both/state/ducks/stories', () => { const storyId = generateUuid(); const messageAttributes = getStoryMessage(storyId); - window.MessageController.register(storyId, messageAttributes); + window.MessageCache.__DEPRECATED$register( + storyId, + messageAttributes, + 'test' + ); const dispatch = sinon.spy(); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); @@ -883,7 +887,11 @@ describe('both/state/ducks/stories', () => { ], }; - window.MessageController.register(storyId, messageAttributes); + window.MessageCache.__DEPRECATED$register( + storyId, + messageAttributes, + 'test' + ); const dispatch = sinon.spy(); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); @@ -905,7 +913,11 @@ describe('both/state/ducks/stories', () => { ], }; - window.MessageController.register(storyId, messageAttributes); + window.MessageCache.__DEPRECATED$register( + storyId, + messageAttributes, + 'test' + ); const dispatch = sinon.spy(); await queueStoryDownload(storyId)(dispatch, getEmptyRootState, null); @@ -947,7 +959,11 @@ describe('both/state/ducks/stories', () => { }, }); - window.MessageController.register(storyId, messageAttributes); + window.MessageCache.__DEPRECATED$register( + storyId, + messageAttributes, + 'test' + ); const dispatch = sinon.spy(); await queueStoryDownload(storyId)(dispatch, getState, null); @@ -1004,7 +1020,11 @@ describe('both/state/ducks/stories', () => { }, }); - window.MessageController.register(storyId, messageAttributes); + window.MessageCache.__DEPRECATED$register( + storyId, + messageAttributes, + 'test' + ); const dispatch = sinon.spy(); await queueStoryDownload(storyId)(dispatch, getState, null); diff --git a/ts/test-electron/util/MessageController_test.ts b/ts/test-electron/util/MessageController_test.ts deleted file mode 100644 index 4b6c583c32..0000000000 --- a/ts/test-electron/util/MessageController_test.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { assert } from 'chai'; -import { MessageModel } from '../../models/messages'; - -import { MessageController } from '../../util/MessageController'; - -describe('MessageController', () => { - describe('filterBySentAt', () => { - it('returns an empty iterable if no messages match', () => { - const mc = new MessageController(); - - assert.isEmpty([...mc.filterBySentAt(123)]); - }); - - it('returns all messages that match the timestamp', () => { - const mc = new MessageController(); - const message1 = new MessageModel({ - conversationId: 'xyz', - id: 'abc', - received_at: 1, - sent_at: 1234, - timestamp: 9999, - type: 'incoming', - }); - const message2 = new MessageModel({ - conversationId: 'xyz', - id: 'def', - received_at: 2, - sent_at: 1234, - timestamp: 9999, - type: 'outgoing', - }); - const message3 = new MessageModel({ - conversationId: 'xyz', - id: 'ignored', - received_at: 3, - sent_at: 5678, - timestamp: 9999, - type: 'outgoing', - }); - mc.register(message1.id, message1); - mc.register(message2.id, message2); - // We deliberately register this message twice for testing. - mc.register(message2.id, message2); - mc.register(message3.id, message3); - - assert.sameMembers([...mc.filterBySentAt(1234)], [message1, message2]); - - mc.unregister(message2.id); - - assert.sameMembers([...mc.filterBySentAt(1234)], [message1]); - }); - }); -}); diff --git a/ts/test-mock/messaging/stories_test.ts b/ts/test-mock/messaging/stories_test.ts index 56f41e1b2f..926b0b4166 100644 --- a/ts/test-mock/messaging/stories_test.ts +++ b/ts/test-mock/messaging/stories_test.ts @@ -13,7 +13,7 @@ import { generateStoryDistributionId } from '../../types/StoryDistributionId'; import type { App } from '../playwright'; import { Bootstrap } from '../bootstrap'; -export const debug = createDebug('mock:test:edit'); +export const debug = createDebug('mock:test:stories'); const IdentifierType = Proto.ManifestRecord.Identifier.Type; @@ -236,7 +236,10 @@ describe('story/messaging', function unknownContacts() { } const sentAt = new Date(time).valueOf(); - debug('Contact sends reply to group story'); + debug('Contact sends reply to group story', { + story: sentAt, + reply: sentAt + 1, + }); await contacts[0].sendRaw( desktop, { diff --git a/ts/util/MessageController.ts b/ts/util/MessageController.ts deleted file mode 100644 index 92e581b050..0000000000 --- a/ts/util/MessageController.ts +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2019 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { MessageModel } from '../models/messages'; -import * as durations from './durations'; -import * as log from '../logging/log'; -import { map, filter } from './iterables'; -import { isNotNil } from './isNotNil'; -import type { MessageAttributesType } from '../model-types.d'; -import { isEnabled } from '../RemoteConfig'; - -const FIVE_MINUTES = 5 * durations.MINUTE; - -type LookupItemType = { - timestamp: number; - message: MessageModel; -}; -type LookupType = Record; - -export class MessageController { - private messageLookup: LookupType = Object.create(null); - - private msgIDsBySender = new Map(); - - private msgIDsBySentAt = new Map>(); - - static install(): MessageController { - const instance = new MessageController(); - window.MessageController = instance; - - instance.startCleanupInterval(); - return instance; - } - - register( - id: string, - data: MessageModel | MessageAttributesType - ): MessageModel { - if (!id || !data) { - throw new Error('MessageController.register: Got falsey id or message'); - } - - const existing = this.messageLookup[id]; - if (existing) { - this.messageLookup[id] = { - message: existing.message, - timestamp: Date.now(), - }; - return existing.message; - } - - const message = - 'attributes' in data ? data : new window.Whisper.Message(data); - this.messageLookup[id] = { - message, - timestamp: Date.now(), - }; - - const sentAt = message.get('sent_at'); - const previousIdsBySentAt = this.msgIDsBySentAt.get(sentAt); - if (previousIdsBySentAt) { - previousIdsBySentAt.add(id); - } else { - this.msgIDsBySentAt.set(sentAt, new Set([id])); - } - - this.msgIDsBySender.set(message.getSenderIdentifier(), id); - - return message; - } - - unregister(id: string): void { - const { message } = this.messageLookup[id] || {}; - if (message) { - this.msgIDsBySender.delete(message.getSenderIdentifier()); - - const sentAt = message.get('sent_at'); - const idsBySentAt = this.msgIDsBySentAt.get(sentAt) || new Set(); - idsBySentAt.delete(id); - if (!idsBySentAt.size) { - this.msgIDsBySentAt.delete(sentAt); - } - } - delete this.messageLookup[id]; - } - - cleanup(): void { - const messages = Object.values(this.messageLookup); - const now = Date.now(); - - for (let i = 0, max = messages.length; i < max; i += 1) { - const { message, timestamp } = messages[i]; - const conversation = message.getConversation(); - - const state = window.reduxStore.getState(); - const selectedId = state?.conversations?.selectedConversationId; - const inActiveConversation = - conversation && selectedId && conversation.id === selectedId; - - if (now - timestamp > FIVE_MINUTES && !inActiveConversation) { - this.unregister(message.id); - } - } - } - - getById(id: string): MessageModel | undefined { - const existing = this.messageLookup[id]; - return existing && existing.message ? existing.message : undefined; - } - - filterBySentAt(sentAt: number): Iterable { - const ids = this.msgIDsBySentAt.get(sentAt) || []; - const maybeMessages = map(ids, id => this.getById(id)); - return filter(maybeMessages, isNotNil); - } - - findBySender(sender: string): MessageModel | undefined { - const id = this.msgIDsBySender.get(sender); - if (!id) { - return undefined; - } - return this.getById(id); - } - - update(predicate: (message: MessageModel) => void): void { - const values = Object.values(this.messageLookup); - log.info( - `MessageController.update: About to process ${values.length} messages` - ); - values.forEach(({ message }) => predicate(message)); - } - - _get(): LookupType { - return this.messageLookup; - } - - startCleanupInterval(): NodeJS.Timeout | number { - return setInterval( - this.cleanup.bind(this), - isEnabled('desktop.messageCleanup') ? FIVE_MINUTES : durations.HOUR - ); - } -} diff --git a/ts/util/MessageModelLogger.ts b/ts/util/MessageModelLogger.ts new file mode 100644 index 0000000000..e422ea21e4 --- /dev/null +++ b/ts/util/MessageModelLogger.ts @@ -0,0 +1,55 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { MessageModel } from '../models/messages'; +import * as log from '../logging/log'; +import { getEnvironment, Environment } from '../environment'; + +export function getMessageModelLogger(model: MessageModel): MessageModel { + const { id } = model; + + if (getEnvironment() !== Environment.Development) { + return model; + } + + const proxyHandler: ProxyHandler = { + get(target: MessageModel, property: keyof MessageModel) { + // Allowed set of attributes & methods + if (property === 'attributes') { + return model.attributes; + } + + if (property === 'id') { + return id; + } + + if (property === 'get') { + return model.get.bind(model); + } + + if (property === 'set') { + return model.set.bind(model); + } + + if (property === 'registerLocations') { + return target.registerLocations; + } + + // Disallowed set of methods & attributes + + log.warn(`MessageModelLogger: model.${property}`, new Error().stack); + + if (typeof target[property] === 'function') { + return target[property].bind(target); + } + + if (typeof target[property] !== 'undefined') { + return target[property]; + } + + return undefined; + }, + }; + + return new Proxy(model, proxyHandler); +} diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index c52ffff66d..281a417605 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -883,12 +883,13 @@ async function saveCallHistory( }); log.info('saveCallHistory: Saved call history message:', id); - const model = window.MessageController.register( + const model = window.MessageCache.__DEPRECATED$register( id, new window.Whisper.Message({ ...message, id, - }) + }), + 'callDisposition' ); if (callHistory.direction === CallDirection.Outgoing) { @@ -986,7 +987,7 @@ export async function clearCallHistoryDataAndSync(): Promise { const messageIds = await window.Signal.Data.clearCallHistory(timestamp); messageIds.forEach(messageId => { - const message = window.MessageController.getById(messageId); + const message = window.MessageCache.__DEPRECATED$getById(messageId); const conversation = message?.getConversation(); if (message == null || conversation == null) { return; @@ -996,7 +997,7 @@ export async function clearCallHistoryDataAndSync(): Promise { message.get('conversationId') ); conversation.debouncedUpdateLastMessage(); - window.MessageController.unregister(messageId); + window.MessageCache.__DEPRECATED$unregister(messageId); }); const ourAci = window.textsecure.storage.user.getCheckedAci(); diff --git a/ts/util/cleanup.ts b/ts/util/cleanup.ts index b45f7b2573..ef4f5d188c 100644 --- a/ts/util/cleanup.ts +++ b/ts/util/cleanup.ts @@ -25,7 +25,7 @@ export function cleanupMessageFromMemory(message: MessageAttributesType): void { const parentConversation = window.ConversationController.get(conversationId); parentConversation?.debouncedUpdateLastMessage(); - window.MessageController.unregister(id); + window.MessageCache.__DEPRECATED$unregister(id); } async function cleanupStoryReplies( @@ -72,9 +72,10 @@ async function cleanupStoryReplies( // Cleanup all group replies await Promise.all( replies.map(reply => { - const replyMessageModel = window.MessageController.register( + const replyMessageModel = window.MessageCache.__DEPRECATED$register( reply.id, - reply + reply, + 'cleanupStoryReplies/group' ); return replyMessageModel.eraseContents(); }) @@ -83,7 +84,11 @@ async function cleanupStoryReplies( // Refresh the storyReplyContext data for 1:1 conversations await Promise.all( replies.map(async reply => { - const model = window.MessageController.register(reply.id, reply); + const model = window.MessageCache.__DEPRECATED$register( + reply.id, + reply, + 'cleanupStoryReplies/1:1' + ); model.unset('storyReplyContext'); await model.hydrateStoryContext(story, { shouldSave: true }); }) diff --git a/ts/util/deleteGroupStoryReplyForEveryone.ts b/ts/util/deleteGroupStoryReplyForEveryone.ts index c7a1ee1622..f6a7cec267 100644 --- a/ts/util/deleteGroupStoryReplyForEveryone.ts +++ b/ts/util/deleteGroupStoryReplyForEveryone.ts @@ -3,13 +3,13 @@ import { DAY } from './durations'; import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import * as log from '../logging/log'; export async function deleteGroupStoryReplyForEveryone( replyMessageId: string ): Promise { - const messageModel = await getMessageById(replyMessageId); + const messageModel = await __DEPRECATED$getMessageById(replyMessageId); if (!messageModel) { log.warn( diff --git a/ts/util/deleteStoryForEveryone.ts b/ts/util/deleteStoryForEveryone.ts index 8367749d43..27d4e0b436 100644 --- a/ts/util/deleteStoryForEveryone.ts +++ b/ts/util/deleteStoryForEveryone.ts @@ -19,7 +19,7 @@ import { import { onStoryRecipientUpdate } from './onStoryRecipientUpdate'; import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage'; import { isGroupV2 } from './whatTypeOfConversation'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { strictAssert } from './assert'; import { repeat, zipObject } from './iterables'; import { isOlderThan } from './timestamp'; @@ -46,7 +46,7 @@ export async function deleteStoryForEveryone( } const logId = `deleteStoryForEveryone(${story.messageId})`; - const message = await getMessageById(story.messageId); + const message = await __DEPRECATED$getMessageById(story.messageId); if (!message) { throw new Error('Story not found'); } diff --git a/ts/util/findAndDeleteOnboardingStoryIfExists.ts b/ts/util/findAndDeleteOnboardingStoryIfExists.ts index abcbb8d66e..79f621544c 100644 --- a/ts/util/findAndDeleteOnboardingStoryIfExists.ts +++ b/ts/util/findAndDeleteOnboardingStoryIfExists.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as log from '../logging/log'; -import { getMessageById } from '../messages/getMessageById'; import { calculateExpirationTimestamp } from './expirationTimer'; import { DAY } from './durations'; @@ -16,23 +15,23 @@ export async function findAndDeleteOnboardingStoryIfExists(): Promise { } const hasExpired = await (async () => { - for (const id of existingOnboardingStoryMessageIds) { - // eslint-disable-next-line no-await-in-loop - const message = await getMessageById(id); - if (!message) { - continue; - } + const [storyId] = existingOnboardingStoryMessageIds; + try { + const messageAttributes = await window.MessageCache.resolveAttributes( + 'findAndDeleteOnboardingStoryIfExists', + storyId + ); - const expires = calculateExpirationTimestamp(message.attributes) ?? 0; + const expires = calculateExpirationTimestamp(messageAttributes) ?? 0; const now = Date.now(); const isExpired = expires < now; const needsRepair = expires > now + 2 * DAY; return isExpired || needsRepair; + } catch { + return true; } - - return true; })(); if (!hasExpired) { diff --git a/ts/util/findStoryMessage.ts b/ts/util/findStoryMessage.ts index b7485b09af..40191bdd24 100644 --- a/ts/util/findStoryMessage.ts +++ b/ts/util/findStoryMessage.ts @@ -31,7 +31,8 @@ export async function findStoryMessages( const ourConversationId = window.ConversationController.getOurConversationIdOrThrow(); - const inMemoryMessages = window.MessageController.filterBySentAt(sentAt); + const inMemoryMessages = + window.MessageCache.__DEPRECATED$filterBySentAt(sentAt); const matchingMessages = [ ...filter(inMemoryMessages, item => isStoryAMatch( @@ -60,7 +61,11 @@ export async function findStoryMessages( } const result = found.map(attributes => - window.MessageController.register(attributes.id, attributes) + window.MessageCache.__DEPRECATED$register( + attributes.id, + attributes, + 'findStoryMessages' + ) ); return result; } diff --git a/ts/util/getMessageAuthorText.ts b/ts/util/getMessageAuthorText.ts new file mode 100644 index 0000000000..7e380c76d1 --- /dev/null +++ b/ts/util/getMessageAuthorText.ts @@ -0,0 +1,50 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { + ConversationAttributesType, + MessageAttributesType, +} from '../model-types.d'; +import { isIncoming, isOutgoing } from '../state/selectors/message'; +import { getTitle } from './getTitle'; + +function getIncomingContact( + messageAttributes: MessageAttributesType +): ConversationAttributesType | undefined { + if (!isIncoming(messageAttributes)) { + return undefined; + } + const { sourceServiceId } = messageAttributes; + if (!sourceServiceId) { + return undefined; + } + + return window.ConversationController.getOrCreate(sourceServiceId, 'private') + .attributes; +} + +export function getMessageAuthorText( + messageAttributes?: MessageAttributesType +): string | undefined { + if (!messageAttributes) { + return undefined; + } + + // if it's outgoing, it must be self-authored + const selfAuthor = isOutgoing(messageAttributes) + ? window.i18n('icu:you') + : undefined; + + if (selfAuthor) { + return selfAuthor; + } + + const incomingContact = getIncomingContact(messageAttributes); + if (incomingContact) { + return getTitle(incomingContact, { isShort: true }); + } + + // if it's not selfAuthor and there's no incoming contact, + // it might be a group notification, so we return undefined + return undefined; +} diff --git a/ts/util/getMessageConversation.ts b/ts/util/getMessageConversation.ts new file mode 100644 index 0000000000..5693d440b4 --- /dev/null +++ b/ts/util/getMessageConversation.ts @@ -0,0 +1,13 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { MessageAttributesType } from '../model-types.d'; +import type { ConversationModel } from '../models/conversations'; + +export function getMessageConversation({ + conversationId, +}: Pick): + | ConversationModel + | undefined { + return window.ConversationController.get(conversationId); +} diff --git a/ts/util/getNotificationDataForMessage.ts b/ts/util/getNotificationDataForMessage.ts new file mode 100644 index 0000000000..c82c023640 --- /dev/null +++ b/ts/util/getNotificationDataForMessage.ts @@ -0,0 +1,452 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { RawBodyRange } from '../types/BodyRange'; +import type { MessageAttributesType } from '../model-types.d'; +import type { ReplacementValuesType } from '../types/I18N'; +import * as Attachment from '../types/Attachment'; +import * as EmbeddedContact from '../types/EmbeddedContact'; +import * as GroupChange from '../groupChange'; +import * as MIME from '../types/MIME'; +import * as Stickers from '../types/Stickers'; +import * as expirationTimer from './expirationTimer'; +import * as log from '../logging/log'; +import { GiftBadgeStates } from '../components/conversation/Message'; +import { dropNull } from './dropNull'; +import { getCallHistorySelector } from '../state/selectors/callHistory'; +import { getCallSelector, getActiveCall } from '../state/selectors/calling'; +import { getCallingNotificationText } from './callingNotification'; +import { getConversationSelector } from '../state/selectors/conversations'; +import { getStringForConversationMerge } from './getStringForConversationMerge'; +import { getStringForProfileChange } from './getStringForProfileChange'; +import { getTitleNoDefault, getNumber } from './getTitle'; +import { findAndFormatContact } from './findAndFormatContact'; +import { isMe } from './whatTypeOfConversation'; +import { strictAssert } from './assert'; +import { + getPropsForCallHistory, + hasErrors, + isCallHistory, + isChatSessionRefreshed, + isDeliveryIssue, + isEndSession, + isExpirationTimerUpdate, + isGroupUpdate, + isGroupV1Migration, + isGroupV2Change, + isIncoming, + isKeyChange, + isOutgoing, + isProfileChange, + isTapToView, + isUnsupportedMessage, + isConversationMerge, +} from '../state/selectors/message'; +import { + getContact, + messageHasPaymentEvent, + getPaymentEventNotificationText, +} from '../messages/helpers'; + +function getNameForNumber(e164: string): string { + const conversation = window.ConversationController.get(e164); + if (!conversation) { + return e164; + } + return conversation.getTitle(); +} + +export function getNotificationDataForMessage( + attributes: MessageAttributesType +): { + bodyRanges?: ReadonlyArray; + emoji?: string; + text: string; +} { + if (isDeliveryIssue(attributes)) { + return { + emoji: '⚠️', + text: window.i18n('icu:DeliveryIssue--preview'), + }; + } + + if (isConversationMerge(attributes)) { + const conversation = window.ConversationController.get( + attributes.conversationId + ); + strictAssert( + conversation, + 'getNotificationData/isConversationMerge/conversation' + ); + strictAssert( + attributes.conversationMerge, + 'getNotificationData/isConversationMerge/conversationMerge' + ); + + return { + text: getStringForConversationMerge({ + obsoleteConversationTitle: getTitleNoDefault( + attributes.conversationMerge.renderInfo + ), + obsoleteConversationNumber: getNumber( + attributes.conversationMerge.renderInfo + ), + conversationTitle: conversation.getTitle(), + i18n: window.i18n, + }), + }; + } + + if (isChatSessionRefreshed(attributes)) { + return { + emoji: '🔁', + text: window.i18n('icu:ChatRefresh--notification'), + }; + } + + if (isUnsupportedMessage(attributes)) { + return { + text: window.i18n('icu:message--getDescription--unsupported-message'), + }; + } + + if (isGroupV1Migration(attributes)) { + return { + text: window.i18n('icu:GroupV1--Migration--was-upgraded'), + }; + } + + if (isProfileChange(attributes)) { + const { profileChange: change, changedId } = attributes; + const changedContact = findAndFormatContact(changedId); + if (!change) { + throw new Error('getNotificationData: profileChange was missing!'); + } + + return { + text: getStringForProfileChange(change, changedContact, window.i18n), + }; + } + + if (isGroupV2Change(attributes)) { + const { groupV2Change: change } = attributes; + strictAssert( + change, + 'getNotificationData: isGroupV2Change true, but no groupV2Change!' + ); + + const changes = GroupChange.renderChange(change, { + i18n: window.i18n, + ourAci: window.textsecure.storage.user.getCheckedAci(), + ourPni: window.textsecure.storage.user.getCheckedPni(), + renderContact: (conversationId: string) => { + const conversation = window.ConversationController.get(conversationId); + return conversation + ? conversation.getTitle() + : window.i18n('icu:unknownContact'); + }, + renderString: ( + key: string, + _i18n: unknown, + components: ReplacementValuesType | undefined + ) => { + // eslint-disable-next-line local-rules/valid-i18n-keys + return window.i18n(key, components); + }, + }); + + return { text: changes.map(({ text }) => text).join(' ') }; + } + + if (messageHasPaymentEvent(attributes)) { + const sender = findAndFormatContact(attributes.sourceServiceId); + const conversation = findAndFormatContact(attributes.conversationId); + return { + text: getPaymentEventNotificationText( + attributes.payment, + sender.title, + conversation.title, + sender.isMe, + window.i18n + ), + emoji: '💳', + }; + } + + const { attachments = [] } = attributes; + + if (isTapToView(attributes)) { + if (attributes.isErased) { + return { + text: window.i18n('icu:message--getDescription--disappearing-media'), + }; + } + + if (Attachment.isImage(attachments)) { + return { + text: window.i18n('icu:message--getDescription--disappearing-photo'), + emoji: '📷', + }; + } + if (Attachment.isVideo(attachments)) { + return { + text: window.i18n('icu:message--getDescription--disappearing-video'), + emoji: '🎥', + }; + } + // There should be an image or video attachment, but we have a fallback just in + // case. + return { text: window.i18n('icu:mediaMessage'), emoji: '📎' }; + } + + if (isGroupUpdate(attributes)) { + const { group_update: groupUpdate } = attributes; + const fromContact = getContact(attributes); + const messages = []; + if (!groupUpdate) { + throw new Error('getNotificationData: Missing group_update'); + } + + if (groupUpdate.left === 'You') { + return { text: window.i18n('icu:youLeftTheGroup') }; + } + if (groupUpdate.left) { + return { + text: window.i18n('icu:leftTheGroup', { + name: getNameForNumber(groupUpdate.left), + }), + }; + } + + if (!fromContact) { + return { text: '' }; + } + + if (isMe(fromContact.attributes)) { + messages.push(window.i18n('icu:youUpdatedTheGroup')); + } else { + messages.push( + window.i18n('icu:updatedTheGroup', { + name: fromContact.getTitle(), + }) + ); + } + + if (groupUpdate.joined && groupUpdate.joined.length) { + const joinedContacts = groupUpdate.joined.map(item => + window.ConversationController.getOrCreate(item, 'private') + ); + const joinedWithoutMe = joinedContacts.filter( + contact => !isMe(contact.attributes) + ); + + if (joinedContacts.length > 1) { + messages.push( + window.i18n('icu:multipleJoinedTheGroup', { + names: joinedWithoutMe + .map(contact => contact.getTitle()) + .join(', '), + }) + ); + + if (joinedWithoutMe.length < joinedContacts.length) { + messages.push(window.i18n('icu:youJoinedTheGroup')); + } + } else { + const joinedContact = window.ConversationController.getOrCreate( + groupUpdate.joined[0], + 'private' + ); + if (isMe(joinedContact.attributes)) { + messages.push(window.i18n('icu:youJoinedTheGroup')); + } else { + messages.push( + window.i18n('icu:joinedTheGroup', { + name: joinedContacts[0].getTitle(), + }) + ); + } + } + } + + if (groupUpdate.name) { + messages.push( + window.i18n('icu:titleIsNow', { + name: groupUpdate.name, + }) + ); + } + if (groupUpdate.avatarUpdated) { + messages.push(window.i18n('icu:updatedGroupAvatar')); + } + + return { text: messages.join(' ') }; + } + if (isEndSession(attributes)) { + return { text: window.i18n('icu:sessionEnded') }; + } + if (isIncoming(attributes) && hasErrors(attributes)) { + return { text: window.i18n('icu:incomingError') }; + } + + const { body: untrimmedBody = '', bodyRanges = [] } = attributes; + const body = untrimmedBody.trim(); + + if (attachments.length) { + // This should never happen but we want to be extra-careful. + const attachment = attachments[0] || {}; + const { contentType } = attachment; + + if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) { + return { + bodyRanges, + emoji: '🎡', + text: body || window.i18n('icu:message--getNotificationText--gif'), + }; + } + if (Attachment.isImage(attachments)) { + return { + bodyRanges, + emoji: '📷', + text: body || window.i18n('icu:message--getNotificationText--photo'), + }; + } + if (Attachment.isVideo(attachments)) { + return { + bodyRanges, + emoji: '🎥', + text: body || window.i18n('icu:message--getNotificationText--video'), + }; + } + if (Attachment.isVoiceMessage(attachment)) { + return { + bodyRanges, + emoji: '🎤', + text: + body || + window.i18n('icu:message--getNotificationText--voice-message'), + }; + } + if (Attachment.isAudio(attachments)) { + return { + bodyRanges, + emoji: '🔈', + text: + body || + window.i18n('icu:message--getNotificationText--audio-message'), + }; + } + + return { + bodyRanges, + text: body || window.i18n('icu:message--getNotificationText--file'), + emoji: '📎', + }; + } + + const { sticker: stickerData } = attributes; + if (stickerData) { + const emoji = + Stickers.getSticker(stickerData.packId, stickerData.stickerId)?.emoji || + stickerData?.emoji; + + if (!emoji) { + log.warn('Unable to get emoji for sticker'); + } + return { + text: window.i18n('icu:message--getNotificationText--stickers'), + emoji: dropNull(emoji), + }; + } + + if (isCallHistory(attributes)) { + const state = window.reduxStore.getState(); + const callingNotification = getPropsForCallHistory(attributes, { + callSelector: getCallSelector(state), + activeCall: getActiveCall(state), + callHistorySelector: getCallHistorySelector(state), + conversationSelector: getConversationSelector(state), + }); + if (callingNotification) { + const text = getCallingNotificationText(callingNotification, window.i18n); + if (text != null) { + return { + text, + }; + } + } + + log.error("This call history message doesn't have valid call history"); + } + if (isExpirationTimerUpdate(attributes)) { + const { expireTimer } = attributes.expirationTimerUpdate ?? {}; + if (!expireTimer) { + return { text: window.i18n('icu:disappearingMessagesDisabled') }; + } + + return { + text: window.i18n('icu:timerSetTo', { + time: expirationTimer.format(window.i18n, expireTimer), + }), + }; + } + + if (isKeyChange(attributes)) { + const { key_changed: identifier } = attributes; + const conversation = window.ConversationController.get(identifier); + return { + text: window.i18n('icu:safetyNumberChangedGroup', { + name: conversation ? conversation.getTitle() : '', + }), + }; + } + const { contact: contacts } = attributes; + if (contacts && contacts.length) { + return { + text: + EmbeddedContact.getName(contacts[0]) || + window.i18n('icu:unknownContact'), + emoji: '👤', + }; + } + + const { giftBadge } = attributes; + if (giftBadge) { + const emoji = '✨'; + + if (isOutgoing(attributes)) { + const toContact = window.ConversationController.get( + attributes.conversationId + ); + const recipient = + toContact?.getTitle() ?? window.i18n('icu:unknownContact'); + return { + emoji, + text: window.i18n('icu:message--donation--preview--sent', { + recipient, + }), + }; + } + + const fromContact = getContact(attributes); + const sender = fromContact?.getTitle() ?? window.i18n('icu:unknownContact'); + return { + emoji, + text: + giftBadge.state === GiftBadgeStates.Unopened + ? window.i18n('icu:message--donation--preview--unopened', { + sender, + }) + : window.i18n('icu:message--donation--preview--redeemed'), + }; + } + + if (body) { + return { + text: body, + bodyRanges, + }; + } + + return { text: '' }; +} diff --git a/ts/util/getNotificationTextForMessage.ts b/ts/util/getNotificationTextForMessage.ts new file mode 100644 index 0000000000..a48fcf66b3 --- /dev/null +++ b/ts/util/getNotificationTextForMessage.ts @@ -0,0 +1,88 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { MessageAttributesType } from '../model-types.d'; +import { BodyRange, applyRangesForText } from '../types/BodyRange'; +import { extractHydratedMentions } from '../state/selectors/message'; +import { findAndFormatContact } from './findAndFormatContact'; +import { getNotificationDataForMessage } from './getNotificationDataForMessage'; +import { isConversationAccepted } from './isConversationAccepted'; +import { strictAssert } from './assert'; + +export function getNotificationTextForMessage( + attributes: MessageAttributesType +): string { + const { text, emoji } = getNotificationDataForMessage(attributes); + + const conversation = window.ConversationController.get( + attributes.conversationId + ); + + strictAssert( + conversation != null, + 'Conversation not found in ConversationController' + ); + + if (!isConversationAccepted(conversation.attributes)) { + return window.i18n('icu:message--getNotificationText--messageRequest'); + } + + if (attributes.storyReaction) { + if (attributes.type === 'outgoing') { + const { profileName: name } = conversation.attributes; + + if (!name) { + return window.i18n( + 'icu:Quote__story-reaction-notification--outgoing--nameless', + { + emoji: attributes.storyReaction.emoji, + } + ); + } + + return window.i18n('icu:Quote__story-reaction-notification--outgoing', { + emoji: attributes.storyReaction.emoji, + name, + }); + } + + const ourAci = window.textsecure.storage.user.getCheckedAci(); + + if ( + attributes.type === 'incoming' && + attributes.storyReaction.targetAuthorAci === ourAci + ) { + return window.i18n('icu:Quote__story-reaction-notification--incoming', { + emoji: attributes.storyReaction.emoji, + }); + } + + if (!window.Signal.OS.isLinux()) { + return attributes.storyReaction.emoji; + } + + return window.i18n('icu:Quote__story-reaction--single'); + } + + const mentions = + extractHydratedMentions(attributes, { + conversationSelector: findAndFormatContact, + }) || []; + const spoilers = (attributes.bodyRanges || []).filter( + range => + BodyRange.isFormatting(range) && range.style === BodyRange.Style.SPOILER + ) as Array>; + const modifiedText = applyRangesForText({ text, mentions, spoilers }); + + // Linux emoji support is mixed, so we disable it. (Note that this doesn't touch + // the `text`, which can contain emoji.) + const shouldIncludeEmoji = Boolean(emoji) && !window.Signal.OS.isLinux(); + if (shouldIncludeEmoji) { + return window.i18n('icu:message--getNotificationText--text-with-emoji', { + text: modifiedText, + emoji, + }); + } + + return modifiedText || ''; +} diff --git a/ts/util/getSenderIdentifier.ts b/ts/util/getSenderIdentifier.ts new file mode 100644 index 0000000000..8cc9abefb6 --- /dev/null +++ b/ts/util/getSenderIdentifier.ts @@ -0,0 +1,23 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { MessageAttributesType } from '../model-types.d'; + +export function getSenderIdentifier({ + sent_at: sentAt, + source, + sourceServiceId, + sourceDevice, +}: Pick< + MessageAttributesType, + 'sent_at' | 'source' | 'sourceServiceId' | 'sourceDevice' +>): string { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const conversation = window.ConversationController.lookupOrCreate({ + e164: source, + serviceId: sourceServiceId, + reason: 'MessageModel.getSenderIdentifier', + })!; + + return `${conversation?.id}.${sourceDevice}-${sentAt}`; +} diff --git a/ts/util/handleEditMessage.ts b/ts/util/handleEditMessage.ts index d1cde5b854..84ac2cbb6d 100644 --- a/ts/util/handleEditMessage.ts +++ b/ts/util/handleEditMessage.ts @@ -89,9 +89,10 @@ export async function handleEditMessage( return; } - const mainMessageModel = window.MessageController.register( + const mainMessageModel = window.MessageCache.__DEPRECATED$register( mainMessage.id, - mainMessage + mainMessage, + 'handleEditMessage' ); // Pull out the edit history from the main message. If this is the first edit diff --git a/ts/util/hydrateStoryContext.ts b/ts/util/hydrateStoryContext.ts new file mode 100644 index 0000000000..74d6f5161d --- /dev/null +++ b/ts/util/hydrateStoryContext.ts @@ -0,0 +1,89 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import omit from 'lodash/omit'; +import type { AttachmentType } from '../types/Attachment'; +import type { MessageAttributesType } from '../model-types.d'; +import { getAttachmentsForMessage } from '../state/selectors/message'; +import { isAciString } from './isAciString'; +import { isDirectConversation } from './whatTypeOfConversation'; +import { softAssert, strictAssert } from './assert'; + +export async function hydrateStoryContext( + messageId: string, + storyMessageParam?: MessageAttributesType, + { + shouldSave, + }: { + shouldSave?: boolean; + } = {} +): Promise { + const messageAttributes = await window.MessageCache.resolveAttributes( + 'hydrateStoryContext', + messageId + ); + + const { storyId } = messageAttributes; + if (!storyId) { + return; + } + + const { storyReplyContext: context } = messageAttributes; + // We'll continue trying to get the attachment as long as the message still exists + if (context && (context.attachment?.url || !context.messageId)) { + return; + } + + const storyMessage = + storyMessageParam === undefined + ? await window.MessageCache.resolveAttributes( + 'hydrateStoryContext/story', + storyId + ) + : window.MessageCache.toMessageAttributes(storyMessageParam); + + if (!storyMessage) { + const conversation = window.ConversationController.get( + messageAttributes.conversationId + ); + softAssert( + conversation && isDirectConversation(conversation.attributes), + 'hydrateStoryContext: Not a type=direct conversation' + ); + window.MessageCache.setAttributes({ + messageId, + messageAttributes: { + storyReplyContext: { + attachment: undefined, + // This is ok to do because story replies only show in 1:1 conversations + // so the story that was quoted should be from the same conversation. + authorAci: conversation?.getAci(), + // No messageId = referenced story not found + messageId: '', + }, + }, + skipSaveToDatabase: !shouldSave, + }); + return; + } + + const attachments = getAttachmentsForMessage({ ...storyMessage }); + let attachment: AttachmentType | undefined = attachments?.[0]; + if (attachment && !attachment.url && !attachment.textAttachment) { + attachment = undefined; + } + + const { sourceServiceId: authorAci } = storyMessage; + strictAssert(isAciString(authorAci), 'Story message from pni'); + window.MessageCache.setAttributes({ + messageId, + messageAttributes: { + storyReplyContext: { + attachment: omit(attachment, 'screenshotData'), + authorAci, + messageId: storyMessage.id, + }, + }, + skipSaveToDatabase: !shouldSave, + }); +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 0077874c4c..5b688a83e4 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -1351,27 +1351,6 @@ "updated": "2020-10-13T18:36:57.012Z", "reasonDetail": "necessary for quill" }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/quill/dist/quill.js", - "line": " // this.container.innerHTML = html.replace(/\\>\\r?\\n +\\<'); // Remove spaces between tags", - "reasonCategory": "falseMatch", - "updated": "2023-05-17T01:41:49.734Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/quill/dist/quill.js", - "line": " // this.container.innerHTML = '';", - "reasonCategory": "falseMatch", - "updated": "2023-05-17T01:41:49.734Z" - }, - { - "rule": "DOM-innerHTML", - "path": "node_modules/quill/dist/quill.js", - "line": " // this.container.innerHTML = '';", - "reasonCategory": "falseMatch", - "updated": "2023-05-17T01:41:49.734Z" - }, { "rule": "DOM-innerHTML", "path": "node_modules/quill/dist/quill.js", @@ -1468,6 +1447,27 @@ "updated": "2020-10-13T18:36:57.012Z", "reasonDetail": "necessary for quill" }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/quill/dist/quill.js", + "line": " // this.container.innerHTML = html.replace(/\\>\\r?\\n +\\<'); // Remove spaces between tags", + "reasonCategory": "usageTrusted", + "updated": "2023-09-28T00:50:24.377Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/quill/dist/quill.js", + "line": " // this.container.innerHTML = '';", + "reasonCategory": "usageTrusted", + "updated": "2023-09-28T00:50:24.377Z" + }, + { + "rule": "DOM-innerHTML", + "path": "node_modules/quill/dist/quill.js", + "line": " // this.container.innerHTML = '';", + "reasonCategory": "usageTrusted", + "updated": "2023-09-28T00:50:24.377Z" + }, { "rule": "DOM-innerHTML", "path": "node_modules/quill/dist/quill.min.js", diff --git a/ts/util/markConversationRead.ts b/ts/util/markConversationRead.ts index 4b7bcd210d..786c573019 100644 --- a/ts/util/markConversationRead.ts +++ b/ts/util/markConversationRead.ts @@ -102,7 +102,9 @@ export async function markConversationRead( const allReadMessagesSync = allUnreadMessages .map(messageSyncData => { - const message = window.MessageController.getById(messageSyncData.id); + const message = window.MessageCache.__DEPRECATED$getById( + messageSyncData.id + ); // we update the in-memory MessageModel with the fresh database call data if (message) { message.set(omit(messageSyncData, 'originalReadStatus')); diff --git a/ts/util/markOnboardingStoryAsRead.ts b/ts/util/markOnboardingStoryAsRead.ts index 15bf77adcd..86bccee8dd 100644 --- a/ts/util/markOnboardingStoryAsRead.ts +++ b/ts/util/markOnboardingStoryAsRead.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as log from '../logging/log'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { isNotNil } from './isNotNil'; import { DurationInSeconds } from './durations'; import { markViewed } from '../services/MessageUpdater'; @@ -19,7 +19,7 @@ export async function markOnboardingStoryAsRead(): Promise { } const messages = await Promise.all( - existingOnboardingStoryMessageIds.map(getMessageById) + existingOnboardingStoryMessageIds.map(__DEPRECATED$getMessageById) ); const storyReadDate = Date.now(); diff --git a/ts/util/onStoryRecipientUpdate.ts b/ts/util/onStoryRecipientUpdate.ts index 2ea1098f58..6c46fe929a 100644 --- a/ts/util/onStoryRecipientUpdate.ts +++ b/ts/util/onStoryRecipientUpdate.ts @@ -161,7 +161,11 @@ export async function onStoryRecipientUpdate( return true; } - const message = window.MessageController.register(item.id, item); + const message = window.MessageCache.__DEPRECATED$register( + item.id, + item, + 'onStoryRecipientUpdate' + ); const sendStateConversationIds = new Set( Object.keys(nextSendStateByConversationId) diff --git a/ts/util/queueAttachmentDownloads.ts b/ts/util/queueAttachmentDownloads.ts index c5ad01db54..66cd588dea 100644 --- a/ts/util/queueAttachmentDownloads.ts +++ b/ts/util/queueAttachmentDownloads.ts @@ -30,7 +30,7 @@ import type { StickerType } from '../types/Stickers'; import type { LinkPreviewType } from '../types/message/LinkPreviews'; import { isNotNil } from './isNotNil'; -type ReturnType = { +export type MessageAttachmentsDownloadedType = { bodyAttachment?: AttachmentType; attachments: Array; editHistory?: Array; @@ -45,7 +45,7 @@ type ReturnType = { // count then you'll also have to modify ./hasAttachmentsDownloads export async function queueAttachmentDownloads( message: MessageAttributesType -): Promise { +): Promise { const attachmentsToQueue = message.attachments || []; const messageId = message.id; const idForLogging = getMessageIdForLogging(message); diff --git a/ts/util/sendDeleteForEveryoneMessage.ts b/ts/util/sendDeleteForEveryoneMessage.ts index 309db8903a..73be82b544 100644 --- a/ts/util/sendDeleteForEveryoneMessage.ts +++ b/ts/util/sendDeleteForEveryoneMessage.ts @@ -15,7 +15,7 @@ import { getConversationIdForLogging, getMessageIdForLogging, } from './idForLogging'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { getRecipientConversationIds } from './getRecipientConversationIds'; import { getRecipients } from './getRecipients'; import { repeat, zipObject } from './iterables'; @@ -35,7 +35,7 @@ export async function sendDeleteForEveryoneMessage( timestamp: targetTimestamp, id: messageId, } = options; - const message = await getMessageById(messageId); + const message = await __DEPRECATED$getMessageById(messageId); if (!message) { throw new Error('sendDeleteForEveryoneMessage: Cannot find message!'); } diff --git a/ts/util/sendEditedMessage.ts b/ts/util/sendEditedMessage.ts index e0af1ea5fc..d4161bd9bd 100644 --- a/ts/util/sendEditedMessage.ts +++ b/ts/util/sendEditedMessage.ts @@ -23,7 +23,7 @@ import { import { concat, filter, map, repeat, zipObject, find } from './iterables'; import { getConversationIdForLogging } from './idForLogging'; import { isQuoteAMatch } from '../messages/helpers'; -import { getMessageById } from '../messages/getMessageById'; +import { __DEPRECATED$getMessageById } from '../messages/getMessageById'; import { handleEditMessage } from './handleEditMessage'; import { incrementMessageCounter } from './incrementMessageCounter'; import { isGroupV1 } from './whatTypeOfConversation'; @@ -64,7 +64,7 @@ export async function sendEditedMessage( conversation.attributes )})`; - const targetMessage = await getMessageById(targetMessageId); + const targetMessage = await __DEPRECATED$getMessageById(targetMessageId); strictAssert(targetMessage, 'could not find message to edit'); if (isGroupV1(conversation.attributes)) { diff --git a/ts/util/sendStoryMessage.ts b/ts/util/sendStoryMessage.ts index e79f114130..b9ad696505 100644 --- a/ts/util/sendStoryMessage.ts +++ b/ts/util/sendStoryMessage.ts @@ -311,7 +311,11 @@ export async function sendStoryMessage( await Promise.all( distributionListMessages.map(messageAttributes => { const model = new window.Whisper.Message(messageAttributes); - const message = window.MessageController.register(model.id, model); + const message = window.MessageCache.__DEPRECATED$register( + model.id, + model, + 'sendStoryMessage' + ); void ourConversation.addSingleMessage(model, { isJustSent: true }); @@ -361,7 +365,11 @@ export async function sendStoryMessage( }, async jobToInsert => { const model = new window.Whisper.Message(messageAttributes); - const message = window.MessageController.register(model.id, model); + const message = window.MessageCache.__DEPRECATED$register( + model.id, + model, + 'sendStoryMessage' + ); const conversation = message.getConversation(); void conversation?.addSingleMessage(model, { isJustSent: true }); diff --git a/ts/util/shouldReplyNotifyUser.ts b/ts/util/shouldReplyNotifyUser.ts index e216181821..ad97cedb82 100644 --- a/ts/util/shouldReplyNotifyUser.ts +++ b/ts/util/shouldReplyNotifyUser.ts @@ -2,22 +2,24 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationModel } from '../models/conversations'; -import type { MessageModel } from '../models/messages'; +import type { MessageAttributesType } from '../model-types.d'; import * as log from '../logging/log'; import dataInterface from '../sql/Client'; import { isGroup } from './whatTypeOfConversation'; import { isMessageUnread } from './isMessageUnread'; export async function shouldReplyNotifyUser( - message: MessageModel, + messageAttributes: Readonly< + Pick + >, conversation: ConversationModel ): Promise { // Don't notify if the message has already been read - if (!isMessageUnread(message.attributes)) { + if (!isMessageUnread(messageAttributes)) { return false; } - const storyId = message.get('storyId'); + const { storyId } = messageAttributes; // If this is not a reply to a story, always notify. if (storyId == null) { diff --git a/ts/window.d.ts b/ts/window.d.ts index 1134a73425..7d10164c36 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -9,10 +9,7 @@ import type PQueue from 'p-queue/dist'; import type { assert } from 'chai'; import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber'; -import type { - ConversationModelCollectionType, - MessageModelCollectionType, -} from './model-types.d'; +import type { ConversationModelCollectionType } from './model-types.d'; import type { textsecure } from './textsecure'; import type { Storage } from './textsecure/Storage'; import type { @@ -33,7 +30,6 @@ import type { LocalizerType, ThemeType } from './types/Util'; import type { Receipt } from './types/Receipt'; import type { ConversationController } from './ConversationController'; import type { ReduxActions } from './state/types'; -import type { createStore } from './state/createStore'; import type { createApp } from './state/roots/createApp'; import type Data from './sql/Client'; import type { MessageModel } from './models/messages'; @@ -43,7 +39,7 @@ import type { ConfirmationDialog } from './components/ConfirmationDialog'; import type { SignalProtocolStore } from './SignalProtocolStore'; import type { SocketStatus } from './types/SocketStatus'; import type SyncRequest from './textsecure/SyncRequest'; -import type { MessageController } from './util/MessageController'; +import type { MessageCache } from './services/MessageCache'; import type { StateType } from './state/reducer'; import type { SystemTraySetting } from './types/SystemTraySetting'; import type { Address } from './types/Address'; @@ -164,7 +160,6 @@ export type SignalCoreType = { }; OS: OSType; State: { - createStore: typeof createStore; Roots: { createApp: typeof createApp; }; @@ -235,7 +230,7 @@ declare global { ConversationController: ConversationController; Events: IPCEventsType; FontFace: typeof FontFace; - MessageController: MessageController; + MessageCache: MessageCache; SignalProtocolStore: typeof SignalProtocolStore; WebAPI: WebAPIConnectType; Whisper: WhisperType; @@ -277,10 +272,10 @@ declare global { RETRY_DELAY: boolean; assert: typeof assert; testUtilities: { + debug: (info: unknown) => void; + initialize: () => Promise; onComplete: (info: unknown) => void; prepareTests: () => void; - installMessageController: () => void; - initializeMessageCounter: () => Promise; }; } @@ -308,7 +303,6 @@ export type WhisperType = { Conversation: typeof ConversationModel; ConversationCollection: typeof ConversationModelCollectionType; Message: typeof MessageModel; - MessageCollection: typeof MessageModelCollectionType; deliveryReceiptQueue: PQueue; deliveryReceiptBatcher: BatcherType; diff --git a/ts/windows/main/preload_test.ts b/ts/windows/main/preload_test.ts index a8299f6c56..6d40ffd8e5 100644 --- a/ts/windows/main/preload_test.ts +++ b/ts/windows/main/preload_test.ts @@ -10,18 +10,49 @@ import { sync } from 'fast-glob'; import { assert } from 'chai'; import { getSignalProtocolStore } from '../../SignalProtocolStore'; -import { MessageController } from '../../util/MessageController'; +import { initMessageCleanup } from '../../services/messageStateCleanup'; import { initializeMessageCounter } from '../../util/incrementMessageCounter'; +import { initializeRedux } from '../../state/initializeRedux'; +import * as Stickers from '../../types/Stickers'; window.assert = assert; // This is a hack to let us run TypeScript tests in the renderer process. See the -// code in `test/index.html`. +// code in `test/test.js`. window.testUtilities = { + debug(info) { + return ipc.invoke('ci:test-electron:debug', info); + }, + onComplete(info) { return ipc.invoke('ci:test-electron:done', info); }, + + async initialize() { + initMessageCleanup(); + await initializeMessageCounter(); + await Stickers.load(); + + initializeRedux({ + callsHistory: [], + initialBadgesState: { byId: {} }, + mainWindowStats: { + isFullScreen: false, + isMaximized: false, + }, + menuOptions: { + development: false, + devTools: false, + includeSetup: false, + isProduction: false, + platform: 'test', + }, + stories: [], + storyDistributionLists: [], + }); + }, + prepareTests() { console.log('Preparing tests...'); sync('../../test-{both,electron}/**/*_test.js', { @@ -29,12 +60,6 @@ window.testUtilities = { cwd: __dirname, }).forEach(require); }, - installMessageController() { - MessageController.install(); - }, - initializeMessageCounter() { - return initializeMessageCounter(); - }, }; window.getSignalProtocolStore = getSignalProtocolStore; diff --git a/ts/windows/main/start.ts b/ts/windows/main/start.ts index 3fbb8a2b63..33b6ec35f0 100644 --- a/ts/windows/main/start.ts +++ b/ts/windows/main/start.ts @@ -13,11 +13,11 @@ import './phase3-post-signal'; import './phase4-test'; import '../../backbone/reliable_trigger'; +import type { CdsLookupOptionsType } from '../../textsecure/WebAPI'; import type { FeatureFlagType } from '../../window.d'; import type { StorageAccessType } from '../../types/Storage.d'; -import type { CdsLookupOptionsType } from '../../textsecure/WebAPI'; import { start as startConversationController } from '../../ConversationController'; -import { MessageController } from '../../util/MessageController'; +import { initMessageCleanup } from '../../services/messageStateCleanup'; import { Environment, getEnvironment } from '../../environment'; import { isProduction } from '../../util/version'; import { ipcInvoke } from '../../sql/channels'; @@ -43,7 +43,7 @@ if (window.SignalContext.config.proxyUrl) { } window.Whisper.events = clone(window.Backbone.Events); -MessageController.install(); +initMessageCleanup(); startConversationController(); if (!isProduction(window.SignalContext.getVersion())) { @@ -51,7 +51,8 @@ if (!isProduction(window.SignalContext.getVersion())) { cdsLookup: (options: CdsLookupOptionsType) => window.textsecure.server?.cdsLookup(options), getConversation: (id: string) => window.ConversationController.get(id), - getMessageById: (id: string) => window.MessageController.getById(id), + getMessageById: (id: string) => + window.MessageCache.__DEPRECATED$getById(id), getReduxState: () => window.reduxStore.getState(), getSfuUrl: () => window.Signal.Services.calling._sfuUrl, getStorageItem: (name: keyof StorageAccessType) => window.storage.get(name),