From abdef4847adfa9c3e62b9d52671c3ef231c26614 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 27 Aug 2024 00:26:21 +1000 Subject: [PATCH] Reinitialize redux after importing a backup --- ts/background.ts | 83 +---- ts/services/allLoaders.ts | 60 ++++ ts/services/backups/export.ts | 3 +- ts/services/backups/index.ts | 15 + ts/services/badgeLoader.ts | 22 ++ ts/services/callHistoryLoader.ts | 2 +- ts/services/userLoader.ts | 39 +++ ts/state/ducks/callHistory.ts | 5 +- ts/state/ducks/emojis.ts | 2 +- ts/state/getInitialState.ts | 310 ++++++++++-------- ts/state/initializeRedux.ts | 45 +-- ts/state/reinitializeRedux.ts | 57 ++++ ts/test-electron/backup/attachments_test.ts | 4 +- .../backup_groupv2_notifications_test.ts | 4 +- ts/test-electron/backup/bubble_test.ts | 4 +- ts/test-electron/backup/calling_test.ts | 12 +- ts/test-electron/backup/non_bubble_test.ts | 4 +- ts/test-mock/bootstrap.ts | 9 +- ts/test-mock/playwright.ts | 4 + ts/util/loadRecentEmojis.ts | 2 +- ts/windows/main/preload_test.ts | 15 +- 21 files changed, 437 insertions(+), 264 deletions(-) create mode 100644 ts/services/allLoaders.ts create mode 100644 ts/services/badgeLoader.ts create mode 100644 ts/services/userLoader.ts create mode 100644 ts/state/reinitializeRedux.ts diff --git a/ts/background.ts b/ts/background.ts index 1cb523aff707..11a7cae9cda0 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -48,11 +48,6 @@ import { update as updateExpiringMessagesService, } from './services/expiringMessagesDeletion'; import { tapToViewMessagesDeletionService } from './services/tapToViewMessagesDeletionService'; -import { getStoriesForRedux, loadStories } from './services/storyLoader'; -import { - getDistributionListsForRedux, - loadDistributionLists, -} from './services/distributionListLoader'; import { senderCertificateService } from './services/senderCertificate'; import { GROUP_CREDENTIALS_KEY } from './services/groupCredentialFetcher'; import * as KeyboardLayout from './services/keyboardLayout'; @@ -112,7 +107,6 @@ import { UpdateKeysListener } from './textsecure/UpdateKeysListener'; import { isDirectConversation } from './util/whatTypeOfConversation'; import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; 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 * as Deletes from './messageModifiers/Deletes'; @@ -148,10 +142,8 @@ import { import { isAciString } from './util/isAciString'; import { normalizeAci } from './util/normalizeAci'; import * as log from './logging/log'; -import { loadRecentEmojis } from './util/loadRecentEmojis'; import { deleteAllLogs } from './util/deleteAllLogs'; import { startInteractionMode } from './services/InteractionMode'; -import type { MainWindowStatsType } from './windows/context'; import { ReactionSource } from './reactions/ReactionSource'; import { singleProtoJobQueue } from './jobs/singleProtoJobQueue'; import { @@ -178,26 +170,15 @@ import { import { RetryPlaceholders } from './util/retryPlaceholders'; import { setBatchingStrategy } from './util/messageBatcher'; import { parseRemoteClientExpiration } from './util/parseRemoteClientExpiration'; -import { makeLookup } from './util/makeLookup'; import { addGlobalKeyboardShortcuts } from './services/addGlobalKeyboardShortcuts'; import { createEventHandler } from './quill/signal-clipboard/util'; import { onCallLogEventSync } from './util/onCallLogEventSync'; -import { - getCallsHistoryForRedux, - getCallsHistoryUnreadCountForRedux, - loadCallsHistory, -} from './services/callHistoryLoader'; -import { - getCallLinksForRedux, - loadCallLinks, -} from './services/callLinksLoader'; import { backupsService } from './services/backups'; import { getCallIdFromEra, updateLocalGroupCallHistoryTimestamp, } from './util/callDisposition'; import { deriveStorageServiceKey } from './Crypto'; -import { getThemeType } from './util/getThemeType'; import { AttachmentDownloadManager } from './jobs/AttachmentDownloadManager'; import { onCallLinkUpdateSync } from './util/onCallLinkUpdateSync'; import { CallMode } from './types/CallDisposition'; @@ -211,6 +192,7 @@ import { getConversationIdForLogging } from './util/idForLogging'; import { encryptConversationAttachments } from './util/encryptConversationAttachments'; import { DataReader, DataWriter } from './sql/Client'; import { restoreRemoteConfigFromStorage } from './RemoteConfig'; +import { getParametersForRedux, loadAll } from './services/allLoaders'; export function isOverHourIntoPast(timestamp: number): boolean { return isNumber(timestamp) && isOlderThan(timestamp, HOUR); @@ -255,13 +237,6 @@ export async function startApp(): Promise { await initializeMessageCounter(); - let initialBadgesState: BadgesStateType = { byId: {} }; - async function loadInitialBadgesState(): Promise { - initialBadgesState = { - byId: makeLookup(await DataReader.getAllBadges(), 'id'), - }; - } - // Initialize WebAPI as early as possible let server: WebAPIType | undefined; let messageReceiver: MessageReceiver | undefined; @@ -1110,21 +1085,6 @@ export async function startApp(): Promise { drop(window.Events.cleanupDownloads()); }, DAY); - let mainWindowStats = { - isMaximized: false, - isFullScreen: false, - }; - - let menuOptions = { - development: false, - devTools: false, - includeSetup: false, - isProduction: true, - platform: 'unknown', - }; - - let theme: ThemeType = window.systemTheme; - try { // This needs to load before we prime the data because we expect // ConversationController to be loaded and ready to use by then. @@ -1132,23 +1092,8 @@ export async function startApp(): Promise { await Promise.all([ window.ConversationController.getOrCreateSignalConversation(), - Stickers.load(), - loadRecentEmojis(), - loadInitialBadgesState(), - loadStories(), - loadDistributionLists(), - loadCallsHistory(), - loadCallLinks(), window.textsecure.storage.protocol.hydrateCaches(), - (async () => { - mainWindowStats = await window.SignalContext.getMainWindowStats(); - })(), - (async () => { - menuOptions = await window.SignalContext.getMenuOptions(); - })(), - (async () => { - theme = await getThemeType(); - })(), + loadAll(), ]); await window.ConversationController.checkForConflicts(); } catch (error) { @@ -1157,7 +1102,7 @@ export async function startApp(): Promise { Errors.toLogFormat(error) ); } finally { - setupAppState({ mainWindowStats, menuOptions, theme }); + setupAppState(); drop(start()); window.Signal.Services.initializeNetworkObserver( window.reduxActions.network @@ -1189,26 +1134,8 @@ export async function startApp(): Promise { log.info('Storage fetch'); drop(window.storage.fetch()); - function setupAppState({ - mainWindowStats, - menuOptions, - theme, - }: { - mainWindowStats: MainWindowStatsType; - menuOptions: MenuOptionsType; - theme: ThemeType; - }) { - initializeRedux({ - callLinks: getCallLinksForRedux(), - callsHistory: getCallsHistoryForRedux(), - callsHistoryUnreadCount: getCallsHistoryUnreadCountForRedux(), - initialBadgesState, - mainWindowStats, - menuOptions, - stories: getStoriesForRedux(), - storyDistributionLists: getDistributionListsForRedux(), - theme, - }); + function setupAppState() { + initializeRedux(getParametersForRedux()); // Here we set up a full redux store with initial state for our LeftPane Root const convoCollection = window.getConversations(); diff --git a/ts/services/allLoaders.ts b/ts/services/allLoaders.ts new file mode 100644 index 000000000000..a50341a85a36 --- /dev/null +++ b/ts/services/allLoaders.ts @@ -0,0 +1,60 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +// loader services +import { getBadgesForRedux, loadBadges } from './badgeLoader'; +import { + getCallsHistoryForRedux, + getCallsHistoryUnreadCountForRedux, + loadCallHistory, +} from './callHistoryLoader'; +import { getCallLinksForRedux, loadCallLinks } from './callLinksLoader'; +import { + getDistributionListsForRedux, + loadDistributionLists, +} from './distributionListLoader'; +import { getStoriesForRedux, loadStories } from './storyLoader'; +import { getUserDataForRedux, loadUserData } from './userLoader'; + +// old-style loaders +import { + getEmojiReducerState, + loadRecentEmojis, +} from '../util/loadRecentEmojis'; +import { + load as loadStickers, + getInitialState as getStickersReduxState, +} from '../types/Stickers'; + +import type { ReduxInitData } from '../state/initializeRedux'; + +export async function loadAll(): Promise { + await Promise.all([ + loadBadges(), + loadCallHistory(), + loadCallLinks(), + loadDistributionLists(), + loadRecentEmojis(), + loadStickers(), + loadStories(), + loadUserData(), + ]); +} + +export function getParametersForRedux(): ReduxInitData { + const { mainWindowStats, menuOptions, theme } = getUserDataForRedux(); + + return { + badgesState: getBadgesForRedux(), + callHistory: getCallsHistoryForRedux(), + callHistoryUnreadCount: getCallsHistoryUnreadCountForRedux(), + callLinks: getCallLinksForRedux(), + mainWindowStats, + menuOptions, + recentEmoji: getEmojiReducerState(), + stickers: getStickersReduxState(), + stories: getStoriesForRedux(), + storyDistributionLists: getDistributionListsForRedux(), + theme, + }; +} diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts index 697fec45a15a..ca69388a7b88 100644 --- a/ts/services/backups/export.ts +++ b/ts/services/backups/export.ts @@ -88,7 +88,6 @@ import { canBeSynced as canPreferredReactionEmojiBeSynced } from '../../reaction import { SendStatus } from '../../messages/MessageSendState'; import { BACKUP_VERSION } from './constants'; import { getMessageIdForLogging } from '../../util/idForLogging'; -import { getCallsHistoryForRedux } from '../callHistoryLoader'; import { makeLookup } from '../../util/makeLookup'; import type { CallHistoryDetails, @@ -470,7 +469,7 @@ export class BackupExportStream extends Readable { let cursor: PageMessagesCursorType | undefined; - const callHistory = getCallsHistoryForRedux(); + const callHistory = await DataReader.getAllCallHistory(); const callHistoryByCallId = makeLookup(callHistory, 'callId'); const me = window.ConversationController.getOurConversationOrThrow(); diff --git a/ts/services/backups/index.ts b/ts/services/backups/index.ts index 537d467b1ca1..3c93c5a1b915 100644 --- a/ts/services/backups/index.ts +++ b/ts/services/backups/index.ts @@ -34,6 +34,8 @@ import { getKeyMaterial } from './crypto'; import { BackupCredentials } from './credentials'; import { BackupAPI } from './api'; import { validateBackup } from './validator'; +import { reinitializeRedux } from '../../state/reinitializeRedux'; +import { getParametersForRedux, loadAll } from '../allLoaders'; const IV_LENGTH = 16; @@ -192,15 +194,28 @@ export class BackupsService { 'importBackup: Bad MAC, second pass' ); + await this.resetStateAfterImport(); + log.info('importBackup: finished...'); } catch (error) { log.info(`importBackup: failed, error: ${Errors.toLogFormat(error)}`); throw error; } finally { this.isRunning = false; + + if (window.SignalCI) { + window.SignalCI.handleEvent('backupImportComplete', null); + } } } + public async resetStateAfterImport(): Promise { + window.ConversationController.reset(); + await window.ConversationController.load(); + await loadAll(); + reinitializeRedux(getParametersForRedux()); + } + public async fetchAndSaveBackupCdnObjectMetadata(): Promise { log.info('fetchAndSaveBackupCdnObjectMetadata: clearing existing metadata'); await DataWriter.clearAllBackupCdnObjectMetadata(); diff --git a/ts/services/badgeLoader.ts b/ts/services/badgeLoader.ts new file mode 100644 index 000000000000..52ff218c1177 --- /dev/null +++ b/ts/services/badgeLoader.ts @@ -0,0 +1,22 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { DataReader } from '../sql/Client'; +import { strictAssert } from '../util/assert'; +import { makeLookup } from '../util/makeLookup'; + +import type { BadgeType } from '../badges/types'; +import type { BadgesStateType } from '../state/ducks/badges'; + +let badges: Array | undefined; + +export async function loadBadges(): Promise { + badges = await DataReader.getAllBadges(); +} + +export function getBadgesForRedux(): BadgesStateType { + strictAssert(badges != null, 'badges have not been loaded'); + return { + byId: makeLookup(badges, 'id'), + }; +} diff --git a/ts/services/callHistoryLoader.ts b/ts/services/callHistoryLoader.ts index 599c8f725264..39859167f75b 100644 --- a/ts/services/callHistoryLoader.ts +++ b/ts/services/callHistoryLoader.ts @@ -8,7 +8,7 @@ import { strictAssert } from '../util/assert'; let callsHistoryData: ReadonlyArray; let callsHistoryUnreadCount: number; -export async function loadCallsHistory(): Promise { +export async function loadCallHistory(): Promise { await DataWriter.cleanupCallHistoryMessages(); callsHistoryData = await DataReader.getAllCallHistory(); callsHistoryUnreadCount = await DataReader.getCallHistoryUnreadCount(); diff --git a/ts/services/userLoader.ts b/ts/services/userLoader.ts new file mode 100644 index 000000000000..f8346f93d3d7 --- /dev/null +++ b/ts/services/userLoader.ts @@ -0,0 +1,39 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { strictAssert } from '../util/assert'; +import { getThemeType } from '../util/getThemeType'; + +import type { MenuOptionsType } from '../types/menu'; +import type { MainWindowStatsType } from '../windows/context'; +import type { ThemeType } from '../types/Util'; + +let mainWindowStats: MainWindowStatsType | undefined; +let menuOptions: MenuOptionsType | undefined; +let theme: ThemeType | undefined; + +export async function loadUserData(): Promise { + await Promise.all([ + (async () => { + mainWindowStats = await window.SignalContext.getMainWindowStats(); + })(), + (async () => { + menuOptions = await window.SignalContext.getMenuOptions(); + })(), + (async () => { + theme = await getThemeType(); + })(), + ]); +} + +export function getUserDataForRedux(): { + mainWindowStats: MainWindowStatsType; + menuOptions: MenuOptionsType; + theme: ThemeType; +} { + strictAssert( + mainWindowStats != null && menuOptions != null && theme != null, + 'user data has not been loaded' + ); + return { mainWindowStats, menuOptions, theme }; +} diff --git a/ts/state/ducks/callHistory.ts b/ts/state/ducks/callHistory.ts index c78369edf399..032d00cd5e58 100644 --- a/ts/state/ducks/callHistory.ts +++ b/ts/state/ducks/callHistory.ts @@ -26,7 +26,7 @@ import { import { getCallsHistoryForRedux, getCallsHistoryUnreadCountForRedux, - loadCallsHistory, + loadCallHistory, } from '../../services/callHistoryLoader'; import { makeLookup } from '../../util/makeLookup'; @@ -217,7 +217,7 @@ export function reloadCallHistory(): ThunkAction< > { return async dispatch => { try { - await loadCallsHistory(); + await loadCallHistory(); const callsHistory = getCallsHistoryForRedux(); const callsHistoryUnreadCount = getCallsHistoryUnreadCountForRedux(); dispatch({ @@ -234,6 +234,7 @@ export const actions = { addCallHistory, removeCallHistory, resetCallHistory, + reloadCallHistory, clearAllCallHistory, updateCallHistoryUnreadCount, markCallHistoryRead, diff --git a/ts/state/ducks/emojis.ts b/ts/state/ducks/emojis.ts index 18e75043b73d..526076d2fa24 100644 --- a/ts/state/ducks/emojis.ts +++ b/ts/state/ducks/emojis.ts @@ -59,7 +59,7 @@ function useEmoji(payload: string): UseEmojiAction { // Reducer -function getEmptyState(): EmojisStateType { +export function getEmptyState(): EmojisStateType { return { recents: [], }; diff --git a/ts/state/getInitialState.ts b/ts/state/getInitialState.ts index fbf04905a1e3..0d3189392dca 100644 --- a/ts/state/getInitialState.ts +++ b/ts/state/getInitialState.ts @@ -1,75 +1,175 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { getEmptyState as accounts } from './ducks/accounts'; -import { getEmptyState as app } from './ducks/app'; -import { getEmptyState as audioPlayer } from './ducks/audioPlayer'; -import { getEmptyState as audioRecorder } from './ducks/audioRecorder'; -import { getEmptyState as callHistory } from './ducks/callHistory'; -import { getEmptyState as calling } from './ducks/calling'; -import { getEmptyState as composer } from './ducks/composer'; -import { getEmptyState as conversations } from './ducks/conversations'; -import { getEmptyState as crashReports } from './ducks/crashReports'; -import { getEmptyState as expiration } from './ducks/expiration'; -import { getEmptyState as globalModals } from './ducks/globalModals'; -import { getEmptyState as inbox } from './ducks/inbox'; -import { getEmptyState as lightbox } from './ducks/lightbox'; -import { getEmptyState as linkPreviews } from './ducks/linkPreviews'; -import { getEmptyState as mediaGallery } from './ducks/mediaGallery'; -import { getEmptyState as nav } from './ducks/nav'; -import { getEmptyState as network } from './ducks/network'; -import { getEmptyState as preferredReactions } from './ducks/preferredReactions'; -import { getEmptyState as safetyNumber } from './ducks/safetyNumber'; -import { getEmptyState as search } from './ducks/search'; -import { getEmptyState as getStoriesEmptyState } from './ducks/stories'; -import { getEmptyState as getStoryDistributionListsEmptyState } from './ducks/storyDistributionLists'; -import { getEmptyState as getToastEmptyState } from './ducks/toast'; -import { getEmptyState as updates } from './ducks/updates'; -import { getEmptyState as user } from './ducks/user'; -import { getEmptyState as username } from './ducks/username'; +import { getEmptyState as accountsEmptyState } from './ducks/accounts'; +import { getEmptyState as appEmptyState } from './ducks/app'; +import { getEmptyState as audioPlayerEmptyState } from './ducks/audioPlayer'; +import { getEmptyState as audioRecorderEmptyState } from './ducks/audioRecorder'; +import { getEmptyState as badgesEmptyState } from './ducks/badges'; +import { getEmptyState as callHistoryEmptyState } from './ducks/callHistory'; +import { getEmptyState as callingEmptyState } from './ducks/calling'; +import { getEmptyState as composerEmptyState } from './ducks/composer'; +import { getEmptyState as conversationsEmptyState } from './ducks/conversations'; +import { getEmptyState as crashReportsEmptyState } from './ducks/crashReports'; +import { getEmptyState as emojiEmptyState } from './ducks/emojis'; +import { getEmptyState as itemsEmptyState } from './ducks/items'; +import { getEmptyState as stickersEmptyState } from './ducks/stickers'; +import { getEmptyState as expirationEmptyState } from './ducks/expiration'; +import { getEmptyState as globalModalsEmptyState } from './ducks/globalModals'; +import { getEmptyState as inboxEmptyState } from './ducks/inbox'; +import { getEmptyState as lightboxEmptyState } from './ducks/lightbox'; +import { getEmptyState as linkPreviewsEmptyState } from './ducks/linkPreviews'; +import { getEmptyState as mediaGalleryEmptyState } from './ducks/mediaGallery'; +import { getEmptyState as navEmptyState } from './ducks/nav'; +import { getEmptyState as networkEmptyState } from './ducks/network'; +import { getEmptyState as preferredReactionsEmptyState } from './ducks/preferredReactions'; +import { getEmptyState as safetyNumberEmptyState } from './ducks/safetyNumber'; +import { getEmptyState as searchEmptyState } from './ducks/search'; +import { getEmptyState as storiesEmptyState } from './ducks/stories'; +import { getEmptyState as storyDistributionListsEmptyState } from './ducks/storyDistributionLists'; +import { getEmptyState as toastEmptyState } from './ducks/toast'; +import { getEmptyState as updatesEmptyState } from './ducks/updates'; +import { getEmptyState as userEmptyState } from './ducks/user'; +import { getEmptyState as usernameEmptyState } from './ducks/username'; -import type { StateType } from './reducer'; -import type { BadgesStateType } from './ducks/badges'; -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 OS from '../util/os/osMain'; -import { getEmojiReducerState as emojis } from '../util/loadRecentEmojis'; -import { getInitialState as stickers } from '../types/Stickers'; import { getInteractionMode } from '../services/InteractionMode'; import { makeLookup } from '../util/makeLookup'; -import type { CallHistoryDetails } from '../types/CallDisposition'; -import type { ThemeType } from '../types/Util'; -import type { CallLinkType } from '../types/CallLink'; -export function getInitialState({ - badges, - callLinks, - callsHistory, - callsHistoryUnreadCount, - stories, - storyDistributionLists, - mainWindowStats, - menuOptions, - theme, -}: { - badges: BadgesStateType; - callLinks: ReadonlyArray; - callsHistory: ReadonlyArray; - callsHistoryUnreadCount: number; - stories: Array; - storyDistributionLists: Array; - mainWindowStats: MainWindowStatsType; - menuOptions: MenuOptionsType; - theme: ThemeType; -}): StateType { +import type { StateType } from './reducer'; +import type { MainWindowStatsType } from '../windows/context'; +import type { ConversationsStateType } from './ducks/conversations'; +import type { MenuOptionsType } from '../types/menu'; +import type { + StoryDistributionListDataType, + StoryDistributionListStateType, +} from './ducks/storyDistributionLists'; +import type { ThemeType } from '../types/Util'; +import type { UserStateType } from './ducks/user'; +import type { ReduxInitData } from './initializeRedux'; + +export function getInitialState( + { + badgesState, + callLinks, + callHistory: calls, + callHistoryUnreadCount, + mainWindowStats, + menuOptions, + recentEmoji, + stickers, + stories, + storyDistributionLists, + theme, + }: ReduxInitData, + existingState?: StateType +): StateType { const items = window.storage.getItemsState(); + const baseState: StateType = existingState ?? getEmptyState(); + + return { + ...baseState, + badges: badgesState, + callHistory: { + ...callHistoryEmptyState(), + callHistoryByCallId: makeLookup(calls, 'callId'), + unreadCount: callHistoryUnreadCount, + }, + calling: { + ...callingEmptyState(), + callLinks: makeLookup(callLinks, 'roomId'), + }, + emojis: recentEmoji, + items, + stickers, + stories: { + ...storiesEmptyState(), + stories, + }, + storyDistributionLists: generateStoryDistributionListState( + storyDistributionLists + ), + user: generateUserState({ + mainWindowStats, + menuOptions, + theme, + }), + }; +} + +export function generateConversationsState(): ConversationsStateType { const convoCollection = window.getConversations(); const formattedConversations = convoCollection.map(conversation => conversation.format() ); + + return { + ...conversationsEmptyState(), + conversationLookup: makeLookup(formattedConversations, 'id'), + conversationsByE164: makeLookup(formattedConversations, 'e164'), + conversationsByServiceId: { + ...makeLookup(formattedConversations, 'serviceId'), + ...makeLookup(formattedConversations, 'pni'), + }, + conversationsByGroupId: makeLookup(formattedConversations, 'groupId'), + conversationsByUsername: makeLookup(formattedConversations, 'username'), + }; +} + +function getEmptyState(): StateType { + return { + accounts: accountsEmptyState(), + app: appEmptyState(), + audioPlayer: audioPlayerEmptyState(), + audioRecorder: audioRecorderEmptyState(), + badges: badgesEmptyState(), + callHistory: callHistoryEmptyState(), + calling: callingEmptyState(), + composer: composerEmptyState(), + conversations: generateConversationsState(), + crashReports: crashReportsEmptyState(), + emojis: emojiEmptyState(), + expiration: expirationEmptyState(), + globalModals: globalModalsEmptyState(), + inbox: inboxEmptyState(), + items: itemsEmptyState(), + lightbox: lightboxEmptyState(), + linkPreviews: linkPreviewsEmptyState(), + mediaGallery: mediaGalleryEmptyState(), + nav: navEmptyState(), + network: networkEmptyState(), + preferredReactions: preferredReactionsEmptyState(), + safetyNumber: safetyNumberEmptyState(), + search: searchEmptyState(), + stickers: stickersEmptyState(), + stories: storiesEmptyState(), + storyDistributionLists: storyDistributionListsEmptyState(), + toast: toastEmptyState(), + updates: updatesEmptyState(), + user: userEmptyState(), + username: usernameEmptyState(), + }; +} + +export function generateStoryDistributionListState( + storyDistributionLists: ReadonlyArray +): StoryDistributionListStateType { + return { + ...storyDistributionListsEmptyState(), + distributionLists: storyDistributionLists || [], + }; +} + +export function generateUserState({ + mainWindowStats, + menuOptions, + theme, +}: { + mainWindowStats: MainWindowStatsType; + menuOptions: MenuOptionsType; + theme: ThemeType; +}): UserStateType { const ourNumber = window.textsecure.storage.user.getNumber(); const ourAci = window.textsecure.storage.user.getAci(); const ourPni = window.textsecure.storage.user.getPni(); @@ -88,79 +188,25 @@ export function getInitialState({ } return { - accounts: accounts(), - app: app(), - audioPlayer: audioPlayer(), - audioRecorder: audioRecorder(), - badges, - callHistory: { - ...callHistory(), - callHistoryByCallId: makeLookup(callsHistory, 'callId'), - unreadCount: callsHistoryUnreadCount, - }, - calling: { - ...calling(), - callLinks: makeLookup(callLinks, 'roomId'), - }, - composer: composer(), - conversations: { - ...conversations(), - conversationLookup: makeLookup(formattedConversations, 'id'), - conversationsByE164: makeLookup(formattedConversations, 'e164'), - conversationsByServiceId: { - ...makeLookup(formattedConversations, 'serviceId'), - ...makeLookup(formattedConversations, 'pni'), - }, - conversationsByGroupId: makeLookup(formattedConversations, 'groupId'), - conversationsByUsername: makeLookup(formattedConversations, 'username'), - }, - crashReports: crashReports(), - emojis: emojis(), - expiration: expiration(), - globalModals: globalModals(), - inbox: inbox(), - items, - lightbox: lightbox(), - linkPreviews: linkPreviews(), - mediaGallery: mediaGallery(), - nav: nav(), - network: network(), - preferredReactions: preferredReactions(), - safetyNumber: safetyNumber(), - search: search(), - stickers: stickers(), - stories: { - ...getStoriesEmptyState(), - stories, - }, - storyDistributionLists: { - ...getStoryDistributionListsEmptyState(), - distributionLists: storyDistributionLists || [], - }, - toast: getToastEmptyState(), - updates: updates(), - user: { - ...user(), - attachmentsPath: window.BasePaths.attachments, - i18n: window.i18n, - interactionMode: getInteractionMode(), - isMainWindowFullScreen: mainWindowStats.isFullScreen, - isMainWindowMaximized: mainWindowStats.isMaximized, - localeMessages: window.i18n.getLocaleMessages(), - menuOptions, - osName, - ourAci, - ourConversationId, - ourDeviceId, - ourNumber, - ourPni, - platform: window.platform, - regionCode: window.storage.get('regionCode'), - stickersPath: window.BasePaths.stickers, - tempPath: window.BasePaths.temp, - theme, - version: window.getVersion(), - }, - username: username(), + ...userEmptyState(), + attachmentsPath: window.BasePaths.attachments, + i18n: window.i18n, + interactionMode: getInteractionMode(), + isMainWindowFullScreen: mainWindowStats.isFullScreen, + isMainWindowMaximized: mainWindowStats.isMaximized, + localeMessages: window.i18n.getLocaleMessages(), + menuOptions, + osName, + ourAci, + ourConversationId, + ourDeviceId, + ourNumber, + ourPni, + platform: window.platform, + regionCode: window.storage.get('regionCode'), + stickersPath: window.BasePaths.stickers, + tempPath: window.BasePaths.temp, + theme, + version: window.getVersion(), }; } diff --git a/ts/state/initializeRedux.ts b/ts/state/initializeRedux.ts index c8a07d4a0550..778c878d723f 100644 --- a/ts/state/initializeRedux.ts +++ b/ts/state/initializeRedux.ts @@ -2,50 +2,37 @@ // SPDX-License-Identifier: AGPL-3.0-only import { bindActionCreators } from 'redux'; +import { actionCreators } from './actions'; +import { createStore } from './createStore'; +import { getInitialState } from './getInitialState'; + 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'; import type { ThemeType } from '../types/Util'; import type { CallLinkType } from '../types/CallLink'; +import type { RecentEmojiObjectType } from '../util/loadRecentEmojis'; +import type { StickersStateType } from './ducks/stickers'; -export function initializeRedux({ - callLinks, - callsHistory, - callsHistoryUnreadCount, - initialBadgesState, - mainWindowStats, - menuOptions, - stories, - storyDistributionLists, - theme, -}: { +export type ReduxInitData = { + badgesState: BadgesStateType; + callHistory: ReadonlyArray; + callHistoryUnreadCount: number; callLinks: ReadonlyArray; - callsHistory: ReadonlyArray; - callsHistoryUnreadCount: number; - initialBadgesState: BadgesStateType; mainWindowStats: MainWindowStatsType; menuOptions: MenuOptionsType; + recentEmoji: RecentEmojiObjectType; + stickers: StickersStateType; stories: Array; storyDistributionLists: Array; theme: ThemeType; -}): void { - const initialState = getInitialState({ - badges: initialBadgesState, - callLinks, - callsHistory, - callsHistoryUnreadCount, - mainWindowStats, - menuOptions, - stories, - storyDistributionLists, - theme, - }); +}; + +export function initializeRedux(data: ReduxInitData): void { + const initialState = getInitialState(data); const store = createStore(initialState); window.reduxStore = store; diff --git a/ts/state/reinitializeRedux.ts b/ts/state/reinitializeRedux.ts new file mode 100644 index 000000000000..4d512b5afc18 --- /dev/null +++ b/ts/state/reinitializeRedux.ts @@ -0,0 +1,57 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AnyAction } from 'redux'; + +import * as log from '../logging/log'; +import { getInitialState } from './getInitialState'; +import { reducer as normalReducer } from './reducer'; + +import type { StateType } from './reducer'; +import type { ReduxInitData } from './initializeRedux'; + +const REPLACE_STATE = 'resetReducer/REPLACE'; + +export function reinitializeRedux(options: ReduxInitData): void { + const logId = 'initializeRedux'; + const existingState = window.reduxStore.getState(); + const newInitialState = getInitialState(options, existingState); + + const resetReducer = ( + state: StateType | undefined, + action: AnyAction + ): StateType => { + if (state == null) { + log.info( + `${logId}/resetReducer: Got null incoming state, returning newInitialState` + ); + return newInitialState; + } + + const { type } = action; + if (type === REPLACE_STATE) { + log.info( + `${logId}/resetReducer: Got REPLACE_STATE action, returning newInitialState` + ); + return newInitialState; + } + + log.info( + `${logId}/resetReducer: Got action with type ${type}, returning original state` + ); + return state; + }; + + log.info(`${logId}: installing resetReducer`); + window.reduxStore.replaceReducer(resetReducer); + + log.info(`${logId}: dispatching REPLACE_STATE event`); + window.reduxStore.dispatch({ + type: REPLACE_STATE, + }); + + log.info(`${logId}: restoring original reducer`); + window.reduxStore.replaceReducer(normalReducer); + + log.info(`${logId}: complete!`); +} diff --git a/ts/test-electron/backup/attachments_test.ts b/ts/test-electron/backup/attachments_test.ts index 541e62741131..34c5a47c8f4a 100644 --- a/ts/test-electron/backup/attachments_test.ts +++ b/ts/test-electron/backup/attachments_test.ts @@ -14,7 +14,6 @@ import { DataWriter } from '../../sql/Client'; import { type AciString, generateAci } from '../../types/ServiceId'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; -import { loadCallsHistory } from '../../services/callHistoryLoader'; import { setupBasics, asymmetricRoundtripHarness } from './helpers'; import { AUDIO_MP3, @@ -31,6 +30,7 @@ import { isVoiceMessage, type AttachmentType } from '../../types/Attachment'; import { strictAssert } from '../../util/assert'; import { SignalService } from '../../protobuf'; import { getRandomBytes } from '../../Crypto'; +import { loadAll } from '../../services/allLoaders'; const CONTACT_A = generateAci(); @@ -51,7 +51,7 @@ describe('backup/attachments', () => { { systemGivenName: 'CONTACT_A' } ); - await loadCallsHistory(); + await loadAll(); sandbox = sinon.createSandbox(); const getAbsoluteAttachmentPath = sandbox.stub( diff --git a/ts/test-electron/backup/backup_groupv2_notifications_test.ts b/ts/test-electron/backup/backup_groupv2_notifications_test.ts index 711f5cb895d9..ab199e23a649 100644 --- a/ts/test-electron/backup/backup_groupv2_notifications_test.ts +++ b/ts/test-electron/backup/backup_groupv2_notifications_test.ts @@ -12,7 +12,6 @@ import type { MessageAttributesType } from '../../model-types'; import type { GroupV2ChangeType } from '../../groups'; import { getRandomBytes } from '../../Crypto'; import * as Bytes from '../../Bytes'; -import { loadCallsHistory } from '../../services/callHistoryLoader'; import { strictAssert } from '../../util/assert'; import { DurationInSeconds } from '../../util/durations'; import { @@ -24,6 +23,7 @@ import { } from './helpers'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; +import { loadAll } from '../../services/allLoaders'; // Note: this should be kept up to date with GroupV2Change.stories.tsx, to // maintain the comprehensive set of GroupV2 notifications we need to handle @@ -114,7 +114,7 @@ describe('backup/groupv2/notifications', () => { name: 'Rock Enthusiasts', }); - await loadCallsHistory(); + await loadAll(); }); afterEach(async () => { await DataWriter.removeAll(); diff --git a/ts/test-electron/backup/bubble_test.ts b/ts/test-electron/backup/bubble_test.ts index 103bfc2f37a9..770113c54b63 100644 --- a/ts/test-electron/backup/bubble_test.ts +++ b/ts/test-electron/backup/bubble_test.ts @@ -13,7 +13,6 @@ import * as Bytes from '../../Bytes'; import { generateAci } from '../../types/ServiceId'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; -import { loadCallsHistory } from '../../services/callHistoryLoader'; import { ID_V1_LENGTH } from '../../groups'; import { DurationInSeconds, WEEK } from '../../util/durations'; import { @@ -22,6 +21,7 @@ import { symmetricRoundtripHarness, OUR_ACI, } from './helpers'; +import { loadAll } from '../../services/allLoaders'; const CONTACT_A = generateAci(); const CONTACT_B = generateAci(); @@ -67,7 +67,7 @@ describe('backup/bubble messages', () => { } ); - await loadCallsHistory(); + await loadAll(); }); it('roundtrips incoming edited message', async () => { diff --git a/ts/test-electron/backup/calling_test.ts b/ts/test-electron/backup/calling_test.ts index fe89a9d45a84..cf563f877cb2 100644 --- a/ts/test-electron/backup/calling_test.ts +++ b/ts/test-electron/backup/calling_test.ts @@ -14,7 +14,6 @@ import * as Bytes from '../../Bytes'; import { getRandomBytes } from '../../Crypto'; import { DataReader, DataWriter } from '../../sql/Client'; import { generateAci } from '../../types/ServiceId'; -import { loadCallsHistory } from '../../services/callHistoryLoader'; import { setupBasics, symmetricRoundtripHarness } from './helpers'; import { AdhocCallStatus, @@ -30,6 +29,7 @@ import { fromAdminKeyBytes } from '../../util/callLinks'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; import { deriveGroupID, deriveGroupSecretParams } from '../../util/zkgroup'; +import { loadAll } from '../../services/allLoaders'; const CONTACT_A = generateAci(); const GROUP_MASTER_KEY = getRandomBytes(32); @@ -78,7 +78,7 @@ describe('backup/calling', () => { await DataWriter.insertCallLink(callLink); - await loadCallsHistory(); + await loadAll(); }); after(async () => { await DataWriter.removeAll(); @@ -99,7 +99,7 @@ describe('backup/calling', () => { timestamp: now, }; await DataWriter.saveCallHistory(callHistory); - await loadCallsHistory(); + await loadAll(); const messageUnseen: MessageAttributesType = { id: generateGuid(), @@ -146,7 +146,7 @@ describe('backup/calling', () => { timestamp: now, }; await DataWriter.saveCallHistory(callHistory); - await loadCallsHistory(); + await loadAll(); const messageUnseen: MessageAttributesType = { id: generateGuid(), @@ -231,7 +231,7 @@ describe('backup/calling', () => { timestamp: now, }; await DataWriter.saveCallHistory(callHistory); - await loadCallsHistory(); + await loadAll(); await symmetricRoundtripHarness([]); @@ -255,7 +255,7 @@ describe('backup/calling', () => { timestamp: now, }; await DataWriter.saveCallHistory(callHistory); - await loadCallsHistory(); + await loadAll(); await symmetricRoundtripHarness([]); diff --git a/ts/test-electron/backup/non_bubble_test.ts b/ts/test-electron/backup/non_bubble_test.ts index f20f5c6cf96b..641280b0ba1e 100644 --- a/ts/test-electron/backup/non_bubble_test.ts +++ b/ts/test-electron/backup/non_bubble_test.ts @@ -18,13 +18,13 @@ import { MessageRequestResponseEvent } from '../../types/MessageRequestResponseE import { DurationInSeconds } from '../../util/durations'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; -import { loadCallsHistory } from '../../services/callHistoryLoader'; import { setupBasics, asymmetricRoundtripHarness, symmetricRoundtripHarness, OUR_ACI, } from './helpers'; +import { loadAll } from '../../services/allLoaders'; const CONTACT_A = generateAci(); const GROUP_ID = Bytes.toBase64(getRandomBytes(32)); @@ -56,7 +56,7 @@ describe('backup/non-bubble messages', () => { } ); - await loadCallsHistory(); + await loadAll(); }); it('roundtrips END_SESSION simple update', async () => { diff --git a/ts/test-mock/bootstrap.ts b/ts/test-mock/bootstrap.ts index 519e12619b58..17e92ef0b66b 100644 --- a/ts/test-mock/bootstrap.ts +++ b/ts/test-mock/bootstrap.ts @@ -344,6 +344,11 @@ export class Bootstrap { } } + if (extraConfig?.ciBackupPath) { + debug('waiting for backup import to complete'); + await app.waitForBackupImportComplete(); + } + await this.phone.waitForSync(this.desktop); this.phone.resetSyncState(this.desktop); @@ -512,7 +517,9 @@ export class Bootstrap { return; } - debug('screenshot difference', numPixels); + debug( + `screenshot difference for ${name}: ${numPixels}/${width * height}` + ); const outDir = await this.getArtifactsDir(test?.fullTitle()); if (outDir != null) { diff --git a/ts/test-mock/playwright.ts b/ts/test-mock/playwright.ts index d90563d8064e..40cf81b866d7 100644 --- a/ts/test-mock/playwright.ts +++ b/ts/test-mock/playwright.ts @@ -114,6 +114,10 @@ export class App extends EventEmitter { return this.waitForEvent('app-loaded'); } + public async waitForBackupImportComplete(): Promise { + return this.waitForEvent('backupImportComplete'); + } + public async waitForMessageSend(): Promise { return this.waitForEvent('message:send-complete'); } diff --git a/ts/util/loadRecentEmojis.ts b/ts/util/loadRecentEmojis.ts index f863e35314fc..fa991535de09 100644 --- a/ts/util/loadRecentEmojis.ts +++ b/ts/util/loadRecentEmojis.ts @@ -4,7 +4,7 @@ import { take } from 'lodash'; import { DataReader } from '../sql/Client'; -type RecentEmojiObjectType = { +export type RecentEmojiObjectType = { recents: Array; }; diff --git a/ts/windows/main/preload_test.ts b/ts/windows/main/preload_test.ts index a86dae3371a9..e8168f020a52 100644 --- a/ts/windows/main/preload_test.ts +++ b/ts/windows/main/preload_test.ts @@ -99,10 +99,10 @@ window.testUtilities = { await Stickers.load(); initializeRedux({ + badgesState: { byId: {} }, callLinks: [], - callsHistory: [], - callsHistoryUnreadCount: 0, - initialBadgesState: { byId: {} }, + callHistory: [], + callHistoryUnreadCount: 0, mainWindowStats: { isFullScreen: false, isMaximized: false, @@ -114,8 +114,17 @@ window.testUtilities = { isProduction: false, platform: 'test', }, + recentEmoji: { + recents: [], + }, stories: [], storyDistributionLists: [], + stickers: { + installedPack: null, + packs: {}, + recentStickers: [], + blessedPacks: {}, + }, theme: ThemeType.dark, }); },