diff --git a/lib/reducers/db-ops-reducer.test.js b/lib/reducers/db-ops-reducer.test.js index 9cb8e3808..e6b4cfbba 100644 --- a/lib/reducers/db-ops-reducer.test.js +++ b/lib/reducers/db-ops-reducer.test.js @@ -1,34 +1,35 @@ // @flow import { reduceDBOpsStore } from './db-ops-reducer.js'; import { opsProcessingFinishedActionType } from '../actions/db-ops-actions.js'; import type { DBOpsStore } from '../types/db-ops-types.js'; const emptyOps = { draftStoreOperations: [], threadStoreOperations: [], messageStoreOperations: [], reportStoreOperations: [], userStoreOperations: [], keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], + syncedMetadataStoreOperations: [], }; describe('DB ops reducer', () => { const store: DBOpsStore = { queuedOps: [ { ops: emptyOps, messageID: '5' }, { ops: emptyOps }, { ops: null, messageID: '7' }, ], }; it('should remove the first entry', () => { const newState = reduceDBOpsStore(store, { type: opsProcessingFinishedActionType, }); expect(newState.queuedOps.length).toEqual(2); expect(newState.queuedOps[1].messageID).toEqual('7'); }); }); diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js index d1dc65402..1492ec84d 100644 --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -1,228 +1,234 @@ // @flow import reduceCalendarFilters from './calendar-filters-reducer.js'; import { reduceCommunityStore } from './community-reducer.js'; import reduceCustomerServer from './custom-server-reducer.js'; import reduceDataLoaded from './data-loaded-reducer.js'; import { reduceDBOpsStore } from './db-ops-reducer.js'; import { reduceDraftStore } from './draft-reducer.js'; import reduceEnabledApps from './enabled-apps-reducer.js'; import { reduceEntryInfos } from './entry-reducer.js'; import { reduceIntegrityStore } from './integrity-reducer.js'; import reduceInviteLinks from './invite-links-reducer.js'; import reduceKeyserverStore from './keyserver-reducer.js'; import reduceLifecycleState from './lifecycle-state-reducer.js'; import { reduceLoadingStatuses } from './loading-reducer.js'; import reduceNextLocalID from './local-id-reducer.js'; import { reduceMessageStore } from './message-reducer.js'; import reduceBaseNavInfo from './nav-reducer.js'; import { reduceNotifPermissionAlertInfo } from './notif-permission-alert-info-reducer.js'; import policiesReducer from './policies-reducer.js'; import reduceReportStore from './report-store-reducer.js'; import reduceServicesAccessToken from './services-access-token-reducer.js'; +import { reduceSyncedMetadataStore } from './synced-metadata-reducer.js'; import reduceGlobalThemeInfo from './theme-reducer.js'; import { reduceThreadActivity } from './thread-activity-reducer.js'; import { reduceThreadInfos } from './thread-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { addKeyserverActionType } from '../actions/keyserver-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { keyserverRegisterActionTypes, logInActionTypes, keyserverAuthActionTypes, } from '../actions/user-actions.js'; import { keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from '../ops/keyserver-store-ops.js'; import { isStaff } from '../shared/staff-utils.js'; import type { BaseNavInfo } from '../types/nav-types.js'; import type { BaseAppState, BaseAction } from '../types/redux-types.js'; import { fullStateSyncActionType, incrementalStateSyncActionType, } from '../types/socket-types.js'; import type { StoreOperations } from '../types/store-ops-types.js'; import { isDev } from '../utils/dev-utils.js'; export default function baseReducer>( state: T, action: BaseAction, onStateDifference: (message: string) => mixed, ): { state: T, storeOperations: StoreOperations } { const { threadStore, newThreadInconsistencies, threadStoreOperations } = reduceThreadInfos(state.threadStore, action); const { threadInfos } = threadStore; const [entryStore, newEntryInconsistencies] = reduceEntryInfos( state.entryStore, action, threadInfos, ); const onStateDifferenceForStaff = (message: string) => { const isCurrentUserStaff = state.currentUserInfo?.id ? isStaff(state.currentUserInfo.id) : false; if (isCurrentUserStaff || isDev) { onStateDifference(message); } }; const [userStore, newUserInconsistencies, userStoreOperations] = reduceUserInfos(state.userStore, action, onStateDifferenceForStaff); const newInconsistencies = [ ...newEntryInconsistencies, ...newThreadInconsistencies, ...newUserInconsistencies, ]; // Only allow checkpoints to increase if we are connected // or if the action is a STATE_SYNC const { messageStoreOperations, messageStore: reducedMessageStore } = reduceMessageStore(state.messageStore, action, threadInfos); let messageStore = reducedMessageStore; let { keyserverStore, keyserverStoreOperations } = reduceKeyserverStore( state.keyserverStore, action, onStateDifferenceForStaff, ); if ( action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && action.type !== keyserverRegisterActionTypes.success && action.type !== logInActionTypes.success && action.type !== siweAuthActionTypes.success && action.type !== keyserverAuthActionTypes.success && action.type !== addKeyserverActionType ) { const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverStore.keyserverInfos) { if ( keyserverStore.keyserverInfos[keyserverID].connection.status === 'connected' ) { continue; } if ( messageStore.currentAsOf[keyserverID] !== state.messageStore.currentAsOf[keyserverID] ) { messageStore = { ...messageStore, currentAsOf: { ...messageStore.currentAsOf, [keyserverID]: state.messageStore.currentAsOf[keyserverID], }, }; } if ( state.keyserverStore.keyserverInfos[keyserverID] && keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf !== state.keyserverStore.keyserverInfos[keyserverID].updatesCurrentAsOf ) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...keyserverStore.keyserverInfos[keyserverID], updatesCurrentAsOf: state.keyserverStore.keyserverInfos[keyserverID] .updatesCurrentAsOf, }, }, }); } } keyserverStore = keyserverStoreOpsHandlers.processStoreOperations( keyserverStore, replaceOperations, ); keyserverStoreOperations = [ ...keyserverStoreOperations, ...replaceOperations, ]; } const { draftStore, draftStoreOperations } = reduceDraftStore( state.draftStore, action, ); const { reportStore, reportStoreOperations } = reduceReportStore( state.reportStore, action, newInconsistencies, ); const { communityStore, communityStoreOperations } = reduceCommunityStore( state.communityStore, action, ); const { integrityStore, integrityStoreOperations } = reduceIntegrityStore( state.integrityStore, action, onStateDifferenceForStaff, threadInfos, threadStoreOperations, ); + const { syncedMetadataStore, syncedMetadataStoreOperations } = + reduceSyncedMetadataStore(state.syncedMetadataStore, action); + return { state: { ...state, navInfo: reduceBaseNavInfo(state.navInfo, action), draftStore, entryStore, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action), threadStore, userStore, messageStore, calendarFilters: reduceCalendarFilters( state.calendarFilters, action, threadStore, ), notifPermissionAlertInfo: reduceNotifPermissionAlertInfo( state.notifPermissionAlertInfo, action, ), lifecycleState: reduceLifecycleState(state.lifecycleState, action), enabledApps: reduceEnabledApps(state.enabledApps, action), reportStore, nextLocalID: reduceNextLocalID(state.nextLocalID, action), dataLoaded: reduceDataLoaded(state.dataLoaded, action), userPolicies: policiesReducer(state.userPolicies, action), commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), keyserverStore, threadActivityStore: reduceThreadActivity( state.threadActivityStore, action, ), integrityStore, globalThemeInfo: reduceGlobalThemeInfo(state.globalThemeInfo, action), customServer: reduceCustomerServer(state.customServer, action), communityStore, dbOpsStore: reduceDBOpsStore(state.dbOpsStore, action), + syncedMetadataStore, }, storeOperations: { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, + syncedMetadataStoreOperations, }, }; } diff --git a/lib/reducers/message-reducer.test.js b/lib/reducers/message-reducer.test.js index 831b266e9..3bd8f6bc8 100644 --- a/lib/reducers/message-reducer.test.js +++ b/lib/reducers/message-reducer.test.js @@ -1,320 +1,321 @@ // @flow import invariant from 'invariant'; import { reduceMessageStore } from './message-reducer.js'; import { createPendingThread } from '../shared/thread-utils.js'; import { messageTypes } from '../types/message-types-enum.js'; import type { MessageStore } from '../types/message-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import { authoritativeKeyserverID } from '../utils/authoritative-keyserver.js'; const messageStoreBeforeMediaUpdate: MessageStore = { messages: { local1: { type: 14, threadID: '91140', creatorID: '91097', time: 1639522317443, media: [ { id: 'localUpload2', uri: 'assets-library://asset/asset.HEIC?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=HEIC', type: 'photo', dimensions: { height: 3024, width: 4032 }, thumbHash: 'some_thumb_hash', localMediaSelection: { step: 'photo_library', dimensions: { height: 3024, width: 4032 }, uri: 'assets-library://asset/asset.HEIC?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=HEIC', filename: 'IMG_0006.HEIC', mediaNativeID: 'CC95F08C-88C3-4012-9D6D-64A413D254B3/L0/001', selectTime: 1639522317349, sendTime: 1639522317349, retries: 0, }, }, ], localID: 'local1', }, }, threads: { '91140': { messageIDs: ['local1'], startReached: true, }, }, local: {}, currentAsOf: { [authoritativeKeyserverID()]: 1639522292174 }, }; describe('UPDATE_MULTIMEDIA_MESSAGE_MEDIA', () => { const updateMultiMediaMessageMediaAction = { type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', payload: { messageID: 'local1', currentMediaID: 'localUpload2', mediaUpdate: { id: '91172', type: 'photo', uri: 'http://localhost/comm/upload/91172/dfa9b9fe7eb03fde', dimensions: { height: 1440, width: 1920 }, localMediaSelection: undefined, }, }, }; const { messageStore: updatedMessageStore } = reduceMessageStore( messageStoreBeforeMediaUpdate, updateMultiMediaMessageMediaAction, {}, ); test('replace local media with uploaded media', () => { expect( updatedMessageStore.messages[ updateMultiMediaMessageMediaAction.payload.messageID ], ).toStrictEqual({ type: 14, threadID: '91140', creatorID: '91097', time: 1639522317443, media: [ { id: '91172', type: 'photo', uri: 'http://localhost/comm/upload/91172/dfa9b9fe7eb03fde', dimensions: { height: 1440, width: 1920 }, thumbHash: 'some_thumb_hash', }, ], localID: 'local1', }); }); test('localMediaSelection is unset when undefined in update', () => { const msg = updatedMessageStore.messages[ updateMultiMediaMessageMediaAction.payload.messageID ]; expect(msg.type).toEqual(messageTypes.IMAGES); invariant(msg.type === messageTypes.IMAGES, 'message is of type IMAGES'); expect(msg.media[0]).not.toHaveProperty('localMediaSelection'); }); test('localMediaSelection is unchanged when missing in update', () => { const actionWithoutLocalMediaSelectionUpdate = { type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', payload: { messageID: 'local1', currentMediaID: 'localUpload2', mediaUpdate: { id: '91172', type: 'photo', uri: 'http://localhost/comm/upload/91172/dfa9b9fe7eb03fde', dimensions: { height: 1440, width: 1920 }, }, }, }; const { messageStore: storeWithoutLocalMediaSelectionUpdate } = reduceMessageStore( messageStoreBeforeMediaUpdate, actionWithoutLocalMediaSelectionUpdate, {}, ); const prevMsg = messageStoreBeforeMediaUpdate.messages[ actionWithoutLocalMediaSelectionUpdate.payload.messageID ]; const updatedMsg = storeWithoutLocalMediaSelectionUpdate.messages[ actionWithoutLocalMediaSelectionUpdate.payload.messageID ]; expect(updatedMsg.type).toEqual(messageTypes.IMAGES); expect(prevMsg.type).toEqual(messageTypes.IMAGES); invariant( updatedMsg.type === messageTypes.IMAGES && prevMsg.type === messageTypes.IMAGES, 'message is of type IMAGES', ); expect(updatedMsg.media[0].localMediaSelection).toStrictEqual( prevMsg.media[0].localMediaSelection, ); }); test('localMediaSelection is updated when included in update', () => { const updateMultiMediaMessageMediaActionWithReplacement = { type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', payload: { messageID: 'local1', currentMediaID: 'localUpload2', mediaUpdate: { id: '91172', type: 'photo', uri: 'http://localhost/comm/upload/91172/dfa9b9fe7eb03fde', dimensions: { height: 1440, width: 1920 }, localMediaSelection: { step: 'photo_library', dimensions: { height: 10, width: 10 }, uri: 'assets-library://asset/new/path', filename: 'NEWNAME.PNG', mediaNativeID: 'CC95F08C-88C3-4012-9D6D-64A413D254B3/L0/001', selectTime: 1639522317349, sendTime: 1639522317349, retries: 1, }, }, }, }; const { messageStore: updatedMessageStoreWithReplacement } = reduceMessageStore( messageStoreBeforeMediaUpdate, updateMultiMediaMessageMediaActionWithReplacement, {}, ); const updatedMsg = updatedMessageStoreWithReplacement.messages[ updateMultiMediaMessageMediaActionWithReplacement.payload.messageID ]; expect(updatedMsg.type).toEqual(messageTypes.IMAGES); invariant( updatedMsg.type === messageTypes.IMAGES, 'message is of type IMAGES', ); expect(updatedMsg.media[0].localMediaSelection).toStrictEqual({ step: 'photo_library', dimensions: { height: 10, width: 10 }, uri: 'assets-library://asset/new/path', filename: 'NEWNAME.PNG', mediaNativeID: 'CC95F08C-88C3-4012-9D6D-64A413D254B3/L0/001', selectTime: 1639522317349, sendTime: 1639522317349, retries: 1, }); }); }); describe('SET_MESSAGE_STORE_MESSAGES', () => { const clientDBMessages = [ { id: '103502', local_id: null, thread: '88471', user: '83809', type: '14', future_type: null, content: '[103501]', time: '1658168455316', media_infos: [ { id: '103501', uri: 'http://localhost/comm/upload/103501/425db25471f3acd5', type: 'photo', extras: '{"dimensions":{"width":1920,"height":1440},"loop":false}', }, ], }, { id: 'local10', local_id: 'local10', thread: '88471', user: '83809', type: '14', future_type: null, content: '[null]', time: '1658172650495', media_infos: [ { id: 'localUpload0', uri: 'assets-library://asset/asset.heic?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=heic', type: 'photo', extras: '{"dimensions":{"height":3024,"width":4032},"loop":false,"local_media_selection":{"step":"photo_library","dimensions":{"height":3024,"width":4032},"uri":"assets-library://asset/asset.heic?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=heic","filename":"IMG_0006.HEIC","mediaNativeID":"CC95F08C-88C3-4012-9D6D-64A413D254B3/L0/001","selectTime":1658172650370,"sendTime":1658172650370,"retries":0}}', }, ], }, { id: 'local11', local_id: 'local11', thread: '88471', user: '83809', type: '14', future_type: null, content: '[null,null]', time: '1658172656976', media_infos: [ { id: 'localUpload2', uri: 'assets-library://asset/asset.heic?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=heic', type: 'photo', extras: '{"dimensions":{"height":3024,"width":4032},"loop":false,"local_media_selection":{"step":"photo_library","dimensions":{"height":3024,"width":4032},"uri":"assets-library://asset/asset.heic?id=CC95F08C-88C3-4012-9D6D-64A413D254B3&ext=heic","filename":"IMG_0006.HEIC","mediaNativeID":"CC95F08C-88C3-4012-9D6D-64A413D254B3/L0/001","selectTime":1658172656826,"sendTime":1658172656826,"retries":0}}', }, { id: 'localUpload4', uri: 'assets-library://asset/asset.jpg?id=ED7AC36B-A150-4C38-BB8C-B6D696F4F2ED&ext=jpg', type: 'photo', extras: '{"dimensions":{"height":2002,"width":3000},"loop":false,"local_media_selection":{"step":"photo_library","dimensions":{"height":2002,"width":3000},"uri":"assets-library://asset/asset.jpg?id=ED7AC36B-A150-4C38-BB8C-B6D696F4F2ED&ext=jpg","filename":"IMG_0005.JPG","mediaNativeID":"ED7AC36B-A150-4C38-BB8C-B6D696F4F2ED/L0/001","selectTime":1658172656826,"sendTime":1658172656826,"retries":0}}', }, ], }, ]; const clientDBThreads = [ { id: '88471', start_reached: '0', }, ]; const { messageStore: updatedMessageStore } = reduceMessageStore( { messages: {}, threads: {}, local: {}, currentAsOf: { [authoritativeKeyserverID()]: 1234567890123 }, }, { type: 'SET_CLIENT_DB_STORE', payload: { currentUserID: '', drafts: [], threadStore: { threadInfos: {}, }, messageStoreThreads: clientDBThreads, messages: clientDBMessages, reports: [], users: {}, keyserverInfos: {}, communityInfos: {}, threadHashes: {}, + syncedMetadata: {}, }, }, { [88471]: createPendingThread({ viewerID: '', threadType: threadTypes.LOCAL, members: [{ id: '', username: '' }], }), }, ); test('removes local media when constructing messageStore.messages', () => { expect(updatedMessageStore.messages).toHaveProperty('103502'); expect(updatedMessageStore.messages).not.toHaveProperty('local10'); expect(updatedMessageStore.messages).not.toHaveProperty('local11'); }); test('removes local media when constructing messageStore.threads', () => { expect(updatedMessageStore).toBeDefined(); expect(updatedMessageStore.threads).toBeDefined(); expect(updatedMessageStore.threads['88471']).toBeDefined(); expect(updatedMessageStore.threads['88471'].messageIDs).toBeDefined(); expect(updatedMessageStore.threads['88471'].messageIDs).toEqual(['103502']); }); }); diff --git a/lib/reducers/synced-metadata-reducer.js b/lib/reducers/synced-metadata-reducer.js new file mode 100644 index 000000000..7d90980fb --- /dev/null +++ b/lib/reducers/synced-metadata-reducer.js @@ -0,0 +1,76 @@ +// @flow + +import { setClientDBStoreActionType } from '../actions/client-db-store-actions.js'; +import { + addSyncedMetadataEntryActionType, + removeSyncedMetadataEntryActionType, +} from '../actions/synced-metadata-actions.js'; +import { + syncedMetadataStoreOpsHandlers, + type SyncedMetadataStoreOperation, + type ReplaceSyncedMetadataEntryOperation, + type RemoveSyncedMetadataOperation, +} from '../ops/synced-metadata-store-ops.js'; +import type { BaseAction } from '../types/redux-types.js'; +import type { SyncedMetadataStore } from '../types/synced-metadata-types.js'; + +const { processStoreOperations: processStoreOps } = + syncedMetadataStoreOpsHandlers; + +function reduceSyncedMetadataStore( + state: SyncedMetadataStore, + action: BaseAction, +): { + +syncedMetadataStore: SyncedMetadataStore, + +syncedMetadataStoreOperations: $ReadOnlyArray, +} { + if (action.type === addSyncedMetadataEntryActionType) { + const replaceOperation: ReplaceSyncedMetadataEntryOperation = { + type: 'replace_synced_metadata_entry', + payload: action.payload, + }; + + return { + syncedMetadataStore: processStoreOps(state, [replaceOperation]), + syncedMetadataStoreOperations: [replaceOperation], + }; + } else if (action.type === removeSyncedMetadataEntryActionType) { + const removeOperation: RemoveSyncedMetadataOperation = { + type: 'remove_synced_metadata', + payload: { + names: [action.payload.name], + }, + }; + + return { + syncedMetadataStore: processStoreOps(state, [removeOperation]), + syncedMetadataStoreOperations: [removeOperation], + }; + } else if (action.type === setClientDBStoreActionType) { + const newSyncedMetadata = action.payload.syncedMetadata; + + if (!newSyncedMetadata) { + return { + syncedMetadataStore: state, + syncedMetadataStoreOperations: [], + }; + } + + const newSyncedMetadataStore: SyncedMetadataStore = { + ...state, + syncedMetadata: newSyncedMetadata, + }; + + return { + syncedMetadataStore: newSyncedMetadataStore, + syncedMetadataStoreOperations: [], + }; + } + + return { + syncedMetadataStore: state, + syncedMetadataStoreOperations: [], + }; +} + +export { reduceSyncedMetadataStore }; diff --git a/lib/types/redux-types.js b/lib/types/redux-types.js index 0c9b1881f..2c5c7003b 100644 --- a/lib/types/redux-types.js +++ b/lib/types/redux-types.js @@ -1,1406 +1,1408 @@ // @flow import type { LogOutResult, KeyserverLogOutResult, LogInStartingPayload, LogInResult, RegisterResult, DefaultNotificationPayload, ClaimUsernameResponse, KeyserverAuthResult, } from './account-types.js'; import type { ActivityUpdateSuccessPayload, QueueActivityUpdatesPayload, SetThreadUnreadStatusPayload, } from './activity-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from './avatar-types.js'; import type { CommunityStore, AddCommunityPayload } from './community-types.js'; import type { MessageID, DBOpsStore } from './db-ops-types.js'; import type { GetVersionActionPayload, LastCommunicatedPlatformDetails, } from './device-types.js'; import type { DraftStore } from './draft-types.js'; import type { EnabledApps, SupportedApps } from './enabled-apps.js'; import type { RawEntryInfo, EntryStore, SaveEntryPayload, CreateEntryPayload, DeleteEntryResult, RestoreEntryPayload, FetchEntryInfosResult, CalendarQueryUpdateResult, CalendarQueryUpdateStartingPayload, FetchRevisionsForEntryPayload, } from './entry-types.js'; import type { CalendarFilter, CalendarThreadFilter, SetCalendarDeletedFilterPayload, } from './filter-types.js'; import type { IdentityAuthResult } from './identity-service-types.js'; import type { IntegrityStore } from './integrity-types.js'; import type { KeyserverStore, AddKeyserverPayload, RemoveKeyserverPayload, } from './keyserver-types.js'; import type { LifecycleState } from './lifecycle-state-types.js'; import type { FetchInviteLinksResponse, InviteLink, InviteLinksStore, InviteLinkVerificationResponse, DisableInviteLinkPayload, } from './link-types.js'; import type { LoadingStatus, LoadingInfo } from './loading-types.js'; import type { UpdateMultimediaMessageMediaPayload } from './media-types.js'; import type { MessageReportCreationResult } from './message-report-types.js'; import type { MessageStore, RawMultimediaMessageInfo, FetchMessageInfosPayload, SendMessagePayload, EditMessagePayload, SaveMessagesPayload, NewMessagesPayload, MessageStorePrunePayload, LocallyComposedMessageInfo, SimpleMessagesPayload, FetchPinnedMessagesResult, SearchMessagesResponse, } from './message-types.js'; import type { RawReactionMessageInfo } from './messages/reaction.js'; import type { RawTextMessageInfo } from './messages/text.js'; import type { BaseNavInfo, WebNavInfo } from './nav-types.js'; import { type ForcePolicyAcknowledgmentPayload, type PolicyAcknowledgmentPayload, type UserPolicies, } from './policy-types.js'; import type { RelationshipErrors } from './relationship-types.js'; import type { EnabledReports, ClearDeliveredReportsPayload, QueueReportsPayload, ReportStore, } from './report-types.js'; import type { ProcessServerRequestAction, GetOlmSessionInitializationDataResponse, } from './request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from './search-types.js'; import type { SetSessionPayload } from './session-types.js'; import type { ConnectionIssue, StateSyncFullActionPayload, StateSyncIncrementalActionPayload, SetActiveSessionRecoveryPayload, } from './socket-types.js'; import { type ClientStore } from './store-ops-types.js'; import type { SubscriptionUpdateResult } from './subscription-types.js'; import type { + SyncedMetadataStore, AddSyncedMetadataEntryPayload, RemoveSyncedMetadataEntryPayload, } from './synced-metadata-types.js'; import type { GlobalThemeInfo } from './theme-types.js'; import type { ThreadActivityStore } from './thread-activity-types.js'; import type { ThreadStore, ChangeThreadSettingsPayload, LeaveThreadPayload, NewThreadResult, ThreadJoinPayload, ToggleMessagePinResult, LegacyThreadStore, RoleModificationPayload, RoleDeletionPayload, } from './thread-types.js'; import type { ClientUpdatesResultWithUserInfos } from './update-types.js'; import type { CurrentUserInfo, UserInfos, UserStore } from './user-types.js'; import type { SetDeviceTokenActionPayload } from '../actions/device-actions.js'; import type { UpdateConnectionStatusPayload, SetLateResponsePayload, UpdateKeyserverReachabilityPayload, } from '../keyserver-conn/keyserver-conn-types.js'; import type { NotifPermissionAlertInfo } from '../utils/push-alerts.js'; export type BaseAppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +dataLoaded: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, + +syncedMetadataStore: SyncedMetadataStore, ... }; export type NativeAppState = BaseAppState<>; export type WebAppState = BaseAppState<> & { +pushApiPublicKey: ?string, ... }; export type AppState = NativeAppState | WebAppState; export type ClientWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: ThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type ServerWebInitialReduxStateResponse = { +navInfo: WebNavInfo, +currentUserInfo: CurrentUserInfo, +entryStore: EntryStore, +threadStore: LegacyThreadStore, +userInfos: UserInfos, +messageStore: MessageStore, +pushApiPublicKey: ?string, +inviteLinksStore: InviteLinksStore, +keyserverInfo: WebInitialKeyserverInfo, }; export type WebInitialKeyserverInfo = { +sessionID: ?string, +updatesCurrentAsOf: number, }; export type BaseAction = $ReadOnly<{ +messageID?: MessageID, ... | { +type: '@@redux/INIT', +payload?: void, } | { +type: 'FETCH_ENTRIES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_ENTRIES_SUCCESS', +payload: FetchEntryInfosResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_OUT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CLAIM_USERNAME_SUCCESS', +payload: ClaimUsernameResponse, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_KEYSERVER_ACCOUNT_SUCCESS', +payload: KeyserverLogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ACCOUNT_SUCCESS', +payload: LogOutResult, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_LOCAL_ENTRY', +payload: RawEntryInfo, } | { +type: 'CREATE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_ENTRY_SUCCESS', +payload: CreateEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SAVE_ENTRY_SUCCESS', +payload: SaveEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'CONCURRENT_MODIFICATION_RESET', +payload: { +id: string, +dbText: string, }, } | { +type: 'DELETE_ENTRY_STARTED', +loadingInfo: LoadingInfo, +payload: { +localID: ?string, +serverID: ?string, }, } | { +type: 'DELETE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_ENTRY_SUCCESS', +payload: ?DeleteEntryResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload?: void, } | { +type: 'IDENTITY_LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_LOG_IN_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'KEYSERVER_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_AUTH_SUCCESS', +payload: KeyserverAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'LOG_IN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LOG_IN_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_REGISTER_STARTED', +loadingInfo: LoadingInfo, +payload: LogInStartingPayload, } | { +type: 'KEYSERVER_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'KEYSERVER_REGISTER_SUCCESS', +payload: RegisterResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_REGISTER_SUCCESS', +payload: IdentityAuthResult, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'IDENTITY_GENERATE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_KEYSERVER_USER_PASSWORD_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_SETTINGS_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'NEW_THREAD_SUCCESS', +payload: NewThreadResult, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REMOVE_USERS_FROM_THREAD_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CHANGE_THREAD_MEMBER_ROLES_SUCCESS', +payload: ChangeThreadSettingsPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_REVISIONS_FOR_ENTRY_SUCCESS', +payload: FetchRevisionsForEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RESTORE_ENTRY_SUCCESS', +payload: RestoreEntryPayload, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'JOIN_THREAD_SUCCESS', +payload: ThreadJoinPayload, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'LEAVE_THREAD_SUCCESS', +payload: LeaveThreadPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_NEW_SESSION', +payload: SetSessionPayload, } | { +type: 'persist/REHYDRATE', +payload: ?BaseAppState<>, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MESSAGES_BEFORE_CURSOR_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_MOST_RECENT_MESSAGES_SUCCESS', +payload: FetchMessageInfosPayload, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_SINGLE_MOST_RECENT_MESSAGES_FROM_THREADS_SUCCESS', +payload: SimpleMessagesPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawTextMessageInfo, } | { +type: 'SEND_TEXT_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_TEXT_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawMultimediaMessageInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, }, +loadingInfo?: LoadingInfo, } | { +type: 'SEND_MULTIMEDIA_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload: RawReactionMessageInfo, } | { +type: 'SEND_REACTION_MESSAGE_FAILED', +error: true, +payload: Error & { +localID: string, +threadID: string, +targetMessageID: string, +reaction: string, +action: string, }, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REACTION_MESSAGE_SUCCESS', +payload: SendMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_USERS_SUCCESS', +payload: UserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'EXACT_SEARCH_USER_SUCCESS', +payload: ExactUserSearchResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_DRAFT', +payload: { +key: string, +text: string, }, } | { +type: 'MOVE_DRAFT', +payload: { +oldKey: string, +newKey: string, }, } | { +type: 'SET_CLIENT_DB_STORE', +payload: ClientStore, } | { +type: 'UPDATE_ACTIVITY_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_ACTIVITY_SUCCESS', +payload: ActivityUpdateSuccessPayload, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DEVICE_TOKEN_SUCCESS', +payload: SetDeviceTokenActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORT_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_REPORTS_SUCCESS', +payload?: ClearDeliveredReportsPayload, +loadingInfo: LoadingInfo, } | { +type: 'QUEUE_REPORTS', +payload: QueueReportsPayload, } | { +type: 'SET_URL_PREFIX', +payload: string, } | { +type: 'SAVE_MESSAGES', +payload: SaveMessagesPayload, } | { +type: 'UPDATE_CALENDAR_THREAD_FILTER', +payload: CalendarThreadFilter, } | { +type: 'CLEAR_CALENDAR_THREAD_FILTER', +payload?: void, } | { +type: 'SET_CALENDAR_DELETED_FILTER', +payload: SetCalendarDeletedFilterPayload, } | { +type: 'UPDATE_SUBSCRIPTION_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_SUBSCRIPTION_SUCCESS', +payload: SubscriptionUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_STARTED', +loadingInfo: LoadingInfo, +payload?: CalendarQueryUpdateStartingPayload, } | { +type: 'UPDATE_CALENDAR_QUERY_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_QUERY_SUCCESS', +payload: CalendarQueryUpdateResult, +loadingInfo: LoadingInfo, } | { +type: 'FULL_STATE_SYNC', +payload: StateSyncFullActionPayload, } | { +type: 'INCREMENTAL_STATE_SYNC', +payload: StateSyncIncrementalActionPayload, } | ProcessServerRequestAction | { +type: 'UPDATE_CONNECTION_STATUS', +payload: UpdateConnectionStatusPayload, } | { +type: 'QUEUE_ACTIVITY_UPDATES', +payload: QueueActivityUpdatesPayload, } | { +type: 'UNSUPERVISED_BACKGROUND', +payload: { +keyserverID: string }, } | { +type: 'UPDATE_LIFECYCLE_STATE', +payload: LifecycleState, } | { +type: 'ENABLE_APP', +payload: SupportedApps, } | { +type: 'DISABLE_APP', +payload: SupportedApps, } | { +type: 'UPDATE_REPORTS_ENABLED', +payload: Partial, } | { +type: 'PROCESS_UPDATES', +payload: ClientUpdatesResultWithUserInfos, } | { +type: 'PROCESS_MESSAGES', +payload: NewMessagesPayload, } | { +type: 'MESSAGE_STORE_PRUNE', +payload: MessageStorePrunePayload, } | { +type: 'SET_LATE_RESPONSE', +payload: SetLateResponsePayload, } | { +type: 'UPDATE_KEYSERVER_REACHABILITY', +payload: UpdateKeyserverReachabilityPayload, } | { +type: 'REQUEST_ACCESS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'REQUEST_ACCESS_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_MULTIMEDIA_MESSAGE_MEDIA', +payload: UpdateMultimediaMessageMediaPayload, } | { +type: 'CREATE_LOCAL_MESSAGE', +payload: LocallyComposedMessageInfo, } | { +type: 'UPDATE_RELATIONSHIPS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_RELATIONSHIPS_SUCCESS', +payload: RelationshipErrors, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_STARTED', +payload: { +threadID: string, +unread: boolean, }, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_THREAD_UNREAD_STATUS_SUCCESS', +payload: SetThreadUnreadStatusPayload, } | { +type: 'SET_USER_SETTINGS_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SET_USER_SETTINGS_SUCCESS', +payload: DefaultNotificationPayload, } | { +type: 'SET_USER_SETTINGS_FAILED', +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_SUCCESS', +payload: MessageReportCreationResult, +loadingInfo: LoadingInfo, } | { +type: 'SEND_MESSAGE_REPORT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FORCE_POLICY_ACKNOWLEDGMENT', +payload: ForcePolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_SUCCESS', +payload: PolicyAcknowledgmentPayload, +loadingInfo: LoadingInfo, } | { +type: 'POLICY_ACKNOWLEDGMENT_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_STARTED', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_SUCCESS', +payload?: void, +loadingInfo: LoadingInfo, } | { +type: 'GET_SIWE_NONCE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_STARTED', +payload: LogInStartingPayload, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_SUCCESS', +payload: LogInResult, +loadingInfo: LoadingInfo, } | { +type: 'SIWE_AUTH_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'RECORD_NOTIF_PERMISSION_ALERT', +payload: { +time: number }, } | { +type: 'UPDATE_USER_AVATAR_STARTED', +payload: UpdateUserAvatarRequest, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_SUCCESS', +payload: UpdateUserAvatarResponse, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_USER_AVATAR_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'SEND_EDIT_MESSAGE_SUCCESS', +payload: EditMessagePayload, +loadingInfo: LoadingInfo, } | { +type: 'SEND_EDIT_MESSAGE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'TOGGLE_MESSAGE_PIN_SUCCESS', +payload: ToggleMessagePinResult, +loadingInfo: LoadingInfo, } | { +type: 'TOGGLE_MESSAGE_PIN_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PINNED_MESSAGES_SUCCESS', +payload: FetchPinnedMessagesResult, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PINNED_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'VERIFY_INVITE_LINK_SUCCESS', +payload: InviteLinkVerificationResponse, +loadingInfo: LoadingInfo, } | { +type: 'VERIFY_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_SUCCESS', +payload: FetchInviteLinksResponse, +loadingInfo: LoadingInfo, } | { +type: 'FETCH_PRIMARY_INVITE_LINKS_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_CALENDAR_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CALENDAR_COMMUNITY_FILTER', +payload: void, } | { +type: 'UPDATE_CHAT_COMMUNITY_FILTER', +payload: string, } | { +type: 'CLEAR_CHAT_COMMUNITY_FILTER', +payload: void, } | { +type: 'SEARCH_MESSAGES_STARTED', +payload: void, +loadingInfo?: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_SUCCESS', +payload: SearchMessagesResponse, +loadingInfo: LoadingInfo, } | { +type: 'SEARCH_MESSAGES_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_SUCCESS', +payload: InviteLink, +loadingInfo: LoadingInfo, } | { +type: 'CREATE_OR_UPDATE_PUBLIC_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DISABLE_INVITE_LINK_SUCCESS', +payload: DisableInviteLinkPayload, +loadingInfo: LoadingInfo, } | { +type: 'DISABLE_INVITE_LINK_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', +payload: GetOlmSessionInitializationDataResponse, +loadingInfo: LoadingInfo, } | { +type: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_DATA_LOADED', +payload: { +dataLoaded: boolean, }, } | { +type: 'GET_VERSION_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'GET_VERSION_SUCCESS', +payload: GetVersionActionPayload, +loadingInfo: LoadingInfo, } | { +type: 'GET_VERSION_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'UPDATE_LAST_COMMUNICATED_PLATFORM_DETAILS', +payload: LastCommunicatedPlatformDetails, } | { +type: 'RESET_USER_STATE', +payload?: void } | { +type: 'MODIFY_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'MODIFY_COMMUNITY_ROLE_SUCCESS', +payload: RoleModificationPayload, +loadingInfo: LoadingInfo, } | { +type: 'MODIFY_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_STARTED', +loadingInfo?: LoadingInfo, +payload?: void, } | { +type: 'DELETE_COMMUNITY_ROLE_SUCCESS', +payload: RoleDeletionPayload, +loadingInfo: LoadingInfo, } | { +type: 'DELETE_COMMUNITY_ROLE_FAILED', +error: true, +payload: Error, +loadingInfo: LoadingInfo, } | { +type: 'SET_ACCESS_TOKEN', +payload: ?string, } | { +type: 'UPDATE_THREAD_LAST_NAVIGATED', +payload: { +threadID: string, +time: number }, } | { +type: 'UPDATE_INTEGRITY_STORE', +payload: { +threadIDsToHash?: $ReadOnlyArray, +threadHashingStatus?: 'starting' | 'running' | 'completed', }, } | { +type: 'UPDATE_THEME_INFO', +payload: Partial, } | { +type: 'ADD_KEYSERVER', +payload: AddKeyserverPayload, } | { +type: 'REMOVE_KEYSERVER', +payload: RemoveKeyserverPayload, } | { +type: 'SET_CUSTOM_SERVER', +payload: string, } | { +type: 'SET_CONNECTION_ISSUE', +payload: { +connectionIssue: ?ConnectionIssue, +keyserverID: string }, } | { +type: 'ADD_COMMUNITY', +payload: AddCommunityPayload, } | { +type: 'ADD_SYNCED_METADATA_ENTRY', +payload: AddSyncedMetadataEntryPayload, } | { +type: 'REMOVE_SYNCED_METADATA_ENTRY', +payload: RemoveSyncedMetadataEntryPayload, } | { +type: 'SET_ACTIVE_SESSION_RECOVERY', +payload: SetActiveSessionRecoveryPayload, } | { +type: 'OPS_PROCESSING_FINISHED_ACTION_TYPE', +payload?: void, }, }>; export type ActionPayload = ?(Object | Array<*> | $ReadOnlyArray<*> | string); export type DispatchSource = 'tunnelbroker' | 'tab-sync'; export type SuperAction = { +type: string, +payload?: ActionPayload, +loadingInfo?: LoadingInfo, +error?: boolean, +dispatchSource?: DispatchSource, +messageID?: MessageID, }; type ThunkedAction = (dispatch: Dispatch) => void; export type PromisedAction = (dispatch: Dispatch) => Promise; export type Dispatch = ((promisedAction: PromisedAction) => Promise) & ((thunkedAction: ThunkedAction) => void) & ((action: SuperAction) => boolean); // This is lifted from redux-persist/lib/constants.js // I don't want to add redux-persist to the web/server bundles... // import { REHYDRATE } from 'redux-persist'; export const rehydrateActionType = 'persist/REHYDRATE'; diff --git a/lib/types/store-ops-types.js b/lib/types/store-ops-types.js index f566fd2a7..5b5ee361b 100644 --- a/lib/types/store-ops-types.js +++ b/lib/types/store-ops-types.js @@ -1,101 +1,104 @@ // @flow import type { CommunityInfos } from './community-types.js'; import type { DraftStoreOperation, ClientDBDraftStoreOperation, ClientDBDraftInfo, } from './draft-types.js'; import type { ThreadHashes } from './integrity-types.js'; import type { KeyserverInfos } from './keyserver-types.js'; import type { ClientDBMessageInfo, ClientDBThreadMessageInfo, } from './message-types.js'; import type { ClientReportCreationRequest } from './report-types.js'; +import type { SyncedMetadata } from './synced-metadata-types.js'; import type { ClientDBThreadInfo, ThreadStore } from './thread-types.js'; import type { UserInfos } from './user-types.js'; import type { ClientDBCommunityInfo, ClientDBCommunityStoreOperation, CommunityStoreOperation, } from '../ops/community-store-ops.js'; import type { ClientDBIntegrityThreadHash, ClientDBIntegrityStoreOperation, IntegrityStoreOperation, } from '../ops/integrity-store-ops.js'; import type { ClientDBKeyserverInfo, ClientDBKeyserverStoreOperation, KeyserverStoreOperation, } from '../ops/keyserver-store-ops.js'; import type { ClientDBMessageStoreOperation, MessageStoreOperation, } from '../ops/message-store-ops.js'; import type { ReportStoreOperation, ClientDBReport, ClientDBReportStoreOperation, } from '../ops/report-store-ops.js'; import type { ClientDBSyncedMetadataEntry, ClientDBSyncedMetadataStoreOperation, } from '../ops/synced-metadata-store-ops.js'; import type { ClientDBThreadStoreOperation, ThreadStoreOperation, } from '../ops/thread-store-ops.js'; import type { UserStoreOperation, ClientDBUserInfo, } from '../ops/user-store-ops.js'; export type StoreOperations = { +draftStoreOperations: $ReadOnlyArray, +threadStoreOperations: $ReadOnlyArray, +messageStoreOperations: $ReadOnlyArray, +reportStoreOperations: $ReadOnlyArray, +userStoreOperations: $ReadOnlyArray, +keyserverStoreOperations: $ReadOnlyArray, +communityStoreOperations: $ReadOnlyArray, +integrityStoreOperations: $ReadOnlyArray, + +syncedMetadataStoreOperations: $ReadOnlyArray, }; export type ClientDBStoreOperations = { +draftStoreOperations?: $ReadOnlyArray, +threadStoreOperations?: $ReadOnlyArray, +messageStoreOperations?: $ReadOnlyArray, +reportStoreOperations?: $ReadOnlyArray, +keyserverStoreOperations?: $ReadOnlyArray, +communityStoreOperations?: $ReadOnlyArray, +integrityStoreOperations?: $ReadOnlyArray, +syncedMetadataStoreOperations?: $ReadOnlyArray, }; export type ClientDBStore = { +messages: $ReadOnlyArray, +drafts: $ReadOnlyArray, +threads: $ReadOnlyArray, +messageStoreThreads: $ReadOnlyArray, +reports: $ReadOnlyArray, +users: $ReadOnlyArray, +keyservers: $ReadOnlyArray, +communities: $ReadOnlyArray, +integrityThreadHashes: $ReadOnlyArray, +syncedMetadata: $ReadOnlyArray, }; export type ClientStore = { +currentUserID: ?string, +drafts: $ReadOnlyArray, +messages: ?$ReadOnlyArray, +threadStore: ?ThreadStore, +messageStoreThreads: ?$ReadOnlyArray, +reports: ?$ReadOnlyArray, +users: ?UserInfos, +keyserverInfos: ?KeyserverInfos, +communityInfos: ?CommunityInfos, +threadHashes: ?ThreadHashes, + +syncedMetadata: ?SyncedMetadata, }; diff --git a/lib/utils/reducers-utils.test.js b/lib/utils/reducers-utils.test.js index 0c742a180..094f9dece 100644 --- a/lib/utils/reducers-utils.test.js +++ b/lib/utils/reducers-utils.test.js @@ -1,126 +1,129 @@ // @flow import { authoritativeKeyserverID } from './authoritative-keyserver.js'; import { defaultNotifPermissionAlertInfo } from './push-alerts.js'; import { resetUserSpecificState } from './reducers-utils.js'; import { defaultWebEnabledApps } from '../types/enabled-apps.js'; import { defaultCalendarFilters } from '../types/filter-types.js'; import { defaultKeyserverInfo } from '../types/keyserver-types.js'; import { defaultGlobalThemeInfo } from '../types/theme-types.js'; jest.mock('./config.js'); describe('resetUserSpecificState', () => { let defaultState; let state; beforeAll(() => { defaultState = { navInfo: { activeChatThreadID: null, startDate: '', endDate: '', tab: 'chat', }, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: { [authoritativeKeyserverID()]: 0 }, }, windowActive: true, pushApiPublicKey: null, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, dataLoaded: false, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultWebEnabledApps, reportStore: { enabledReports: { crashReports: false, inconsistencyReports: false, mediaReports: false, }, queuedReports: [], }, nextLocalID: 0, _persist: null, userPolicies: {}, commServicesAccessToken: null, inviteLinksStore: { links: {}, }, actualizedCalendarQuery: { startDate: '', endDate: '', filters: defaultCalendarFilters, }, communityPickerStore: { chat: null, calendar: null }, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID()]: defaultKeyserverInfo('url'), }, }, threadActivityStore: {}, initialStateLoaded: false, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, globalThemeInfo: defaultGlobalThemeInfo, customServer: null, communityStore: { communityInfos: {}, }, dbOpsStore: { queuedOps: [], }, + syncedMetadataStore: { + syncedMetadata: {}, + }, }; state = { ...defaultState, globalThemeInfo: { activeTheme: 'light', preference: 'light', systemTheme: null, }, communityPickerStore: { chat: '256|1', calendar: '256|1' }, nextLocalID: 9, navInfo: { activeChatThreadID: null, startDate: '2023-02-02', endDate: '2023-03-02', tab: 'calendar', }, }; }); it("doesn't reset fields named in nonUserSpecificFields", () => { const nonUserSpecificFields = [ 'globalThemeInfo', 'communityPickerStore', 'nextLocalID', 'navInfo', ]; expect( resetUserSpecificState(state, defaultState, nonUserSpecificFields), ).toEqual(state); }); it('resets fields not named in nonUserSpecificFields', () => { expect(resetUserSpecificState(state, defaultState, [])).toEqual( defaultState, ); }); }); diff --git a/native/redux/default-state.js b/native/redux/default-state.js index 878385bed..52560fa86 100644 --- a/native/redux/default-state.js +++ b/native/redux/default-state.js @@ -1,92 +1,95 @@ // @flow import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { defaultKeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { defaultDimensionsInfo } from './dimensions-updater.react.js'; import type { AppState } from './state-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultConnectivityInfo } from '../types/connectivity.js'; import { defaultURLPrefix, natNodeServer } from '../utils/url-utils.js'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: { [authoritativeKeyserverID]: 0 }, }, storeLoaded: false, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, dataLoaded: false, customServer: natNodeServer, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultEnabledApps, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [], }, nextLocalID: 0, _persist: null, dimensions: defaultDimensionsInfo, connectivity: defaultConnectivityInfo, globalThemeInfo: defaultGlobalThemeInfo, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), frozen: false, userPolicies: {}, commServicesAccessToken: null, inviteLinksStore: { links: {}, }, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID]: defaultKeyserverInfo( defaultURLPrefix, Platform.OS, ), }, }, localSettings: { isBackupEnabled: false, }, threadActivityStore: {}, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, communityStore: { communityInfos: {}, }, dbOpsStore: { queuedOps: [], }, + syncedMetadataStore: { + syncedMetadata: {}, + }, }: AppState); export { defaultState }; diff --git a/native/redux/persist.js b/native/redux/persist.js index 9ecad395a..f995414ef 100644 --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -1,1253 +1,1254 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import invariant from 'invariant'; import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createTransform } from 'redux-persist'; import type { Transform } from 'redux-persist/es/types.js'; import { convertEntryStoreToNewIDSchema, convertInviteLinksStoreToNewIDSchema, convertMessageStoreToNewIDSchema, convertRawMessageInfoToNewIDSchema, convertCalendarFilterToNewIDSchema, convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js'; import { type ClientDBIntegrityStoreOperation, integrityStoreOpsHandlers, type ReplaceIntegrityThreadHashesOperation, } from 'lib/ops/integrity-store-ops.js'; import { type ClientDBKeyserverStoreOperation, keyserverStoreOpsHandlers, type ReplaceKeyserverOperation, } from 'lib/ops/keyserver-store-ops.js'; import { type ClientDBMessageStoreOperation, messageStoreOpsHandlers, } from 'lib/ops/message-store-ops.js'; import { type ReportStoreOperation, type ClientDBReportStoreOperation, convertReportsToReplaceReportOps, reportStoreOpsHandlers, } from 'lib/ops/report-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { type ClientDBUserStoreOperation, type UserStoreOperation, convertUserInfosToReplaceUserOps, userStoreOpsHandlers, } from 'lib/ops/user-store-ops.js'; import { filterThreadIDsInFilterList } from 'lib/reducers/calendar-filters-reducer.js'; import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js'; import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js'; import { getContainingThreadID, getCommunity, assertAllThreadInfosAreLegacy, } from 'lib/shared/thread-utils.js'; import { keyserverStoreTransform } from 'lib/shared/transforms/keyserver-store-transform.js'; import { DEPRECATED_unshimMessageStore, unshimFunc, } from 'lib/shared/unshim-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarQuery } from 'lib/types/entry-types.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import type { KeyserverInfo } from 'lib/types/keyserver-types.js'; import { messageTypes, type MessageType, } from 'lib/types/message-types-enum.js'; import { type LocalMessageInfo, type MessageStore, type MessageStoreThreads, type RawMessageInfo, } from 'lib/types/message-types.js'; import { minimallyEncodeRawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { ReportStore, ClientReportCreationRequest, } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ClientDBThreadInfo, LegacyRawThreadInfo, MixedRawThreadInfos, } from 'lib/types/thread-types.js'; import { wipeKeyserverStore } from 'lib/utils/keyserver-store-utils.js'; import { translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { deprecatedConvertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, } from 'lib/utils/thread-ops-utils.js'; import { getUUID } from 'lib/utils/uuid.js'; import { createUpdateDBOpsForMessageStoreMessages, createUpdateDBOpsForMessageStoreThreads, } from './client-db-utils.js'; import { defaultState } from './default-state.js'; import { deprecatedCreateUpdateDBOpsForThreadStoreThreadInfos, deprecatedUpdateClientDBThreadStoreThreadInfos, } from './deprecated-client-db-utils.js'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js'; import { legacyUpdateRolesAndPermissions } from './legacy-update-roles-and-permissions.js'; import { persistMigrationForManagePinsThreadPermission } from './manage-pins-permission-migration.js'; import { persistMigrationToRemoveSelectRolePermissions } from './remove-select-role-permissions.js'; import type { AppState } from './state-types.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import { unshimClientDB } from './unshim-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import { commCoreModule } from '../native-modules.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; import { defaultURLPrefix } from '../utils/url-utils.js'; const persistBlacklist = [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'draftStore', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', 'storeLoaded', 'dbOpsStore', + 'syncedMetadataStore', ]; function handleReduxMigrationFailure(oldState: AppState): AppState { const persistedNonUserSpecificFields = nonUserSpecificFieldsNative.filter( field => !persistBlacklist.includes(field) || field === '_persist', ); const stateAfterReset = resetUserSpecificState( oldState, defaultState, persistedNonUserSpecificFields, ); return { ...stateAfterReset, keyserverStore: wipeKeyserverStore(stateAfterReset.keyserverStore), }; } const migrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, }), [2]: (state: AppState) => ({ ...state, messageSentFromRoute: [], }), [3]: (state: any) => ({ currentUserInfo: state.currentUserInfo, entryStore: state.entryStore, threadInfos: state.threadInfos, userInfos: state.userInfos, messageStore: { ...state.messageStore, currentAsOf: state.currentAsOf, }, updatesCurrentAsOf: state.currentAsOf, cookie: state.cookie, deviceToken: state.deviceToken, urlPrefix: state.urlPrefix, customServer: state.customServer, notifPermissionAlertInfo: state.notifPermissionAlertInfo, messageSentFromRoute: state.messageSentFromRoute, _persist: state._persist, }), [4]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, }), [5]: (state: AppState) => ({ ...state, calendarFilters: defaultCalendarFilters, }), [6]: (state: any) => ({ ...state, threadInfos: undefined, threadStore: { threadInfos: state.threadInfos, inconsistencyResponses: [], }, }), [7]: (state: AppState) => ({ ...state, lastUserInteraction: undefined, sessionID: undefined, entryStore: { ...state.entryStore, inconsistencyResponses: [], }, }), [8]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, connection: { ...defaultConnectionInfo, actualizedCalendarQuery: defaultCalendarQuery(Platform.OS), }, watchedThreadIDs: [], entryStore: { ...state.entryStore, actualizedCalendarQuery: undefined, }, }), [9]: (state: any) => ({ ...state, connection: { ...state.connection, lateResponses: [], }, }), [10]: (state: any) => ({ ...state, nextLocalID: highestLocalIDSelector(state) + 1, connection: { ...state.connection, showDisconnectedBar: false, }, messageStore: { ...state.messageStore, local: {}, }, }), [11]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.IMAGES, ]), }), [12]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [13]: (state: AppState) => ({ ...state, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), }), [14]: (state: AppState) => state, [15]: (state: any) => ({ ...state, threadStore: { ...state.threadStore, inconsistencyReports: inconsistencyResponsesToReports( state.threadStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: inconsistencyResponsesToReports( state.entryStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, queuedReports: [], }), [16]: (state: any) => { const result = { ...state, messageSentFromRoute: undefined, dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous, }; if (state.navInfo) { result.navInfo = { ...state.navInfo, navigationState: undefined, }; } return result; }, [17]: (state: any) => ({ ...state, userInfos: undefined, userStore: { userInfos: state.userInfos, inconsistencyResponses: [], }, }), [18]: (state: AppState) => ({ ...state, userStore: { userInfos: state.userStore.userInfos, inconsistencyReports: [], }, }), [19]: (state: any) => { const threadInfos: { [string]: LegacyRawThreadInfo } = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const { visibilityRules, ...rest } = threadInfo; threadInfos[threadID] = rest; } return { ...state, threadStore: { ...state.threadStore, threadInfos, }, }; }, [20]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.LEGACY_UPDATE_RELATIONSHIP, ]), }), [21]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.CREATE_SIDEBAR, messageTypes.SIDEBAR_SOURCE, ]), }), [22]: (state: any) => { for (const key in state.drafts) { const value = state.drafts[key]; try { void commCoreModule.updateDraft(key, value); } catch (e) { if (!isTaskCancelledError(e)) { throw e; } } } return { ...state, drafts: undefined, }; }, [23]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [24]: (state: AppState) => ({ ...state, enabledApps: defaultEnabledApps, }), [25]: (state: AppState) => ({ ...state, crashReportsEnabled: __DEV__, }), [26]: (state: any) => { const { currentUserInfo } = state; if (currentUserInfo.anonymous) { return state; } return { ...state, crashReportsEnabled: undefined, currentUserInfo: { id: currentUserInfo.id, username: currentUserInfo.username, }, enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, }; }, [27]: (state: any) => ({ ...state, queuedReports: undefined, enabledReports: undefined, threadStore: { ...state.threadStore, inconsistencyReports: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: undefined, }, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [ ...state.entryStore.inconsistencyReports, ...state.threadStore.inconsistencyReports, ...state.queuedReports, ], }, }), [28]: (state: AppState) => { const threadParentToChildren: { [string]: string[] } = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? state.threadStore.threadInfos[threadInfo.parentThreadID] : null; const parentIndex = parentThreadInfo ? parentThreadInfo.id : '-1'; if (!threadParentToChildren[parentIndex]) { threadParentToChildren[parentIndex] = []; } threadParentToChildren[parentIndex].push(threadID); } const rootIDs = threadParentToChildren['-1']; if (!rootIDs) { // This should never happen, but if it somehow does we'll let the state // check mechanism resolve it... return state; } const threadInfos: { [string]: LegacyRawThreadInfo | RawThreadInfo, } = {}; const stack = [...rootIDs]; while (stack.length > 0) { const threadID = stack.shift(); const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; threadInfos[threadID] = { ...threadInfo, containingThreadID: getContainingThreadID( parentThreadInfo, threadInfo.type, ), community: getCommunity(parentThreadInfo), }; const children = threadParentToChildren[threadID]; if (children) { stack.push(...children); } } return { ...state, threadStore: { ...state.threadStore, threadInfos } }; }, [29]: (state: AppState) => { const legacyRawThreadInfos: { +[id: string]: LegacyRawThreadInfo, } = assertAllThreadInfosAreLegacy(state.threadStore.threadInfos); const updatedThreadInfos = migrateThreadStoreForEditThreadPermissions(legacyRawThreadInfos); return { ...state, threadStore: { ...state.threadStore, threadInfos: updatedThreadInfos, }, }; }, [30]: (state: AppState) => { const threadInfos = state.threadStore.threadInfos; const operations = [ { type: 'remove_all', }, ...Object.keys(threadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: threadInfos[id] }, })), ]; try { commCoreModule.processThreadStoreOperationsSync( threadStoreOpsHandlers.convertOpsToClientDBOps(operations), ); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [31]: (state: AppState) => { const messages = state.messageStore.messages; const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...Object.keys(messages).map((id: string) => ({ type: 'replace', payload: translateRawMessageInfoToClientDBMessageInfo(messages[id]), })), ]; try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [32]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [33]: (state: AppState) => unshimClientDB(state, [messageTypes.REACTION]), [34]: (state: any) => { const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state; return stateSansThreadIDsToNotifIDs; }, [35]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [36]: (state: AppState) => { // 1. Get threads and messages from SQLite `threads` and `messages` tables. const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); // 2. Translate `ClientDBThreadInfo`s to `RawThreadInfo`s and // `ClientDBMessageInfo`s to `RawMessageInfo`s. const rawThreadInfos = clientDBThreadInfos.map( deprecatedConvertClientDBThreadInfoToRawThreadInfo, ); const rawMessageInfos = clientDBMessageInfos.map( translateClientDBMessageInfoToRawMessageInfo, ); // 3. Unshim translated `RawMessageInfos` to get the TOGGLE_PIN messages const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => unshimFunc(messageInfo, new Set([messageTypes.TOGGLE_PIN])), ); // 4. Filter out non-TOGGLE_PIN messages const filteredRawMessageInfos = unshimmedRawMessageInfos .map(messageInfo => messageInfo.type === messageTypes.TOGGLE_PIN ? messageInfo : null, ) .filter(Boolean); // 5. We want only the last TOGGLE_PIN message for each message ID, // so 'pin', 'unpin', 'pin' don't count as 3 pins, but only 1. const lastMessageIDToRawMessageInfoMap = new Map(); for (const messageInfo of filteredRawMessageInfos) { const { targetMessageID } = messageInfo; lastMessageIDToRawMessageInfoMap.set(targetMessageID, messageInfo); } const lastMessageIDToRawMessageInfos = Array.from( lastMessageIDToRawMessageInfoMap.values(), ); // 6. Create a Map of threadIDs to pinnedCount const threadIDsToPinnedCount = new Map(); for (const messageInfo of lastMessageIDToRawMessageInfos) { const { threadID, type } = messageInfo; if (type === messageTypes.TOGGLE_PIN) { const pinnedCount = threadIDsToPinnedCount.get(threadID) || 0; threadIDsToPinnedCount.set(threadID, pinnedCount + 1); } } // 7. Include a pinnedCount for each rawThreadInfo const rawThreadInfosWithPinnedCount = rawThreadInfos.map(threadInfo => ({ ...threadInfo, pinnedCount: threadIDsToPinnedCount.get(threadInfo.id) || 0, })); // 8. Convert rawThreadInfos to a map of threadID to threadInfo const threadIDToThreadInfo = rawThreadInfosWithPinnedCount.reduce( ( acc: { [string]: LegacyRawThreadInfo }, threadInfo: LegacyRawThreadInfo, ) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); // 9. Add threadPermission to each threadInfo const rawThreadInfosWithThreadPermission = persistMigrationForManagePinsThreadPermission(threadIDToThreadInfo); // 10. Convert the new threadInfos back into an array const rawThreadInfosWithCountAndPermission = Object.keys( rawThreadInfosWithThreadPermission, ).map(id => rawThreadInfosWithThreadPermission[id]); // 11. Translate `RawThreadInfo`s to `ClientDBThreadInfo`s. const convertedClientDBThreadInfos = rawThreadInfosWithCountAndPermission.map( convertRawThreadInfoToClientDBThreadInfo, ); // 12. Construct `ClientDBThreadStoreOperation`s to clear SQLite `threads` // table and repopulate with `ClientDBThreadInfo`s. const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; // 13. Try processing `ClientDBThreadStoreOperation`s and log out if // `processThreadStoreOperationsSync(...)` throws an exception. try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return state; }, [37]: (state: AppState) => { const operations = messageStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_threads', }, { type: 'replace_threads', payload: { threads: state.messageStore.threads }, }, ]); try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.error(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [38]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, ), [39]: (state: AppState) => unshimClientDB(state, [messageTypes.EDIT_MESSAGE]), [40]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, ), [41]: (state: AppState) => { const queuedReports = state.reportStore.queuedReports.map(report => ({ ...report, id: getUUID(), })); return { ...state, reportStore: { ...state.reportStore, queuedReports }, }; }, [42]: (state: AppState) => { const reportStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_reports' }, ...convertReportsToReplaceReportOps(state.reportStore.queuedReports), ]; const dbOperations: $ReadOnlyArray = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [43]: async (state: any) => { const { messages, drafts, threads, messageStoreThreads } = await commCoreModule.getClientDBStore(); const messageStoreThreadsOperations = createUpdateDBOpsForMessageStoreThreads( messageStoreThreads, convertMessageStoreThreadsToNewIDSchema, ); const messageStoreMessagesOperations = createUpdateDBOpsForMessageStoreMessages(messages, messageInfos => messageInfos.map(convertRawMessageInfoToNewIDSchema), ); const threadOperations = deprecatedCreateUpdateDBOpsForThreadStoreThreadInfos( threads, convertThreadStoreThreadInfosToNewIDSchema, ); const draftOperations = generateIDSchemaMigrationOpsForDrafts(drafts); try { await Promise.all([ commCoreModule.processMessageStoreOperations([ ...messageStoreMessagesOperations, ...messageStoreThreadsOperations, ]), commCoreModule.processThreadStoreOperations(threadOperations), commCoreModule.processDraftStoreOperations(draftOperations), ]); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } const inviteLinksStore = state.inviteLinksStore ?? defaultState.inviteLinksStore; return { ...state, entryStore: convertEntryStoreToNewIDSchema(state.entryStore), messageStore: convertMessageStoreToNewIDSchema(state.messageStore), calendarFilters: state.calendarFilters.map( convertCalendarFilterToNewIDSchema, ), connection: convertConnectionInfoToNewIDSchema(state.connection), watchedThreadIDs: state.watchedThreadIDs.map( id => `${authoritativeKeyserverID}|${id}`, ), inviteLinksStore: convertInviteLinksStoreToNewIDSchema(inviteLinksStore), }; }, [44]: async (state: any) => { const { cookie, ...rest } = state; return { ...rest, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID]: { cookie } }, }, }; }, [45]: async (state: any) => { const { updatesCurrentAsOf, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], updatesCurrentAsOf, }, }, }, }; }, [46]: async (state: AppState) => { const { currentAsOf } = state.messageStore; return { ...state, messageStore: { ...state.messageStore, currentAsOf: { [authoritativeKeyserverID]: currentAsOf }, }, }; }, [47]: async (state: any) => { const { urlPrefix, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], urlPrefix, }, }, }, }; }, [48]: async (state: any) => { const { connection, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], connection, }, }, }, }; }, [49]: async (state: AppState) => { const { keyserverStore, ...rest } = state; const { connection, ...keyserverRest } = keyserverStore.keyserverInfos[authoritativeKeyserverID]; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverRest, }, }, }, connection, }; }, [50]: async (state: any) => { const { connection, ...rest } = state; const { actualizedCalendarQuery, ...connectionRest } = connection; return { ...rest, connection: connectionRest, actualizedCalendarQuery, }; }, [51]: async (state: any) => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [52]: async (state: AppState) => ({ ...state, integrityStore: { threadHashes: {}, threadHashingStatus: 'data_not_loaded', }, }), [53]: (state: any) => { if (!state.userStore.inconsistencyReports) { return state; } const reportStoreOperations = convertReportsToReplaceReportOps( state.userStore.inconsistencyReports, ); const dbOperations: $ReadOnlyArray = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } const { inconsistencyReports, ...newUserStore } = state.userStore; const queuedReports = reportStoreOpsHandlers.processStoreOperations( state.reportStore.queuedReports, reportStoreOperations, ); return { ...state, userStore: newUserStore, reportStore: { ...state.reportStore, queuedReports, }, }; }, [54]: (state: any) => { let updatedMessageStoreThreads: MessageStoreThreads = {}; for (const threadID: string in state.messageStore.threads) { const { lastNavigatedTo, lastPruned, ...rest } = state.messageStore.threads[threadID]; updatedMessageStoreThreads = { ...updatedMessageStoreThreads, [threadID]: rest, }; } return { ...state, messageStore: { ...state.messageStore, threads: updatedMessageStoreThreads, }, }; }, [55]: async (state: AppState) => __DEV__ ? { ...state, keyserverStore: { ...state.keyserverStore, keyserverInfos: { ...state.keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...state.keyserverStore.keyserverInfos[ authoritativeKeyserverID ], urlPrefix: defaultURLPrefix, }, }, }, } : state, [56]: (state: any) => { const { deviceToken, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [authoritativeKeyserverID]: { ...keyserverStore.keyserverInfos[authoritativeKeyserverID], deviceToken, }, }, }, }; }, [57]: async (state: any) => { const { connection, keyserverStore: { keyserverInfos }, ...rest } = state; const newKeyserverInfos: { [string]: KeyserverInfo } = {}; for (const key in keyserverInfos) { newKeyserverInfos[key] = { ...keyserverInfos[key], connection: { ...defaultConnectionInfo }, }; } return { ...rest, keyserverStore: { ...state.keyserverStore, keyserverInfos: newKeyserverInfos, }, }; }, [58]: async (state: AppState) => { const userStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_users' }, ...convertUserInfosToReplaceUserOps(state.userStore.userInfos), ]; const dbOperations: $ReadOnlyArray = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); try { await commCoreModule.processUserStoreOperations(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [59]: (state: AppState) => { const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const rawThreadInfos = clientDBThreadInfos.map( deprecatedConvertClientDBThreadInfoToRawThreadInfo, ); const rawThreadInfosObject = rawThreadInfos.reduce( ( acc: { [string]: LegacyRawThreadInfo }, threadInfo: LegacyRawThreadInfo, ) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); const migratedRawThreadInfos = persistMigrationToRemoveSelectRolePermissions(rawThreadInfosObject); const migratedThreadInfosArray = Object.keys(migratedRawThreadInfos).map( id => migratedRawThreadInfos[id], ); const convertedClientDBThreadInfos = migratedThreadInfosArray.map( convertRawThreadInfoToClientDBThreadInfo, ); const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return handleReduxMigrationFailure(state); } return state; }, [60]: (state: AppState) => deprecatedUpdateClientDBThreadStoreThreadInfos( state, legacyUpdateRolesAndPermissions, handleReduxMigrationFailure, ), [61]: (state: AppState) => { const minimallyEncodeThreadInfosFunc = ( threadStoreInfos: MixedRawThreadInfos, ): MixedRawThreadInfos => Object.keys(threadStoreInfos).reduce( ( acc: { [string]: LegacyRawThreadInfo | RawThreadInfo, }, key: string, ) => { const threadInfo = threadStoreInfos[key]; acc[threadInfo.id] = threadInfo.minimallyEncoded ? threadInfo : minimallyEncodeRawThreadInfo(threadInfo); return acc; }, {}, ); return deprecatedUpdateClientDBThreadStoreThreadInfos( state, minimallyEncodeThreadInfosFunc, handleReduxMigrationFailure, ); }, [62]: async (state: AppState) => { const replaceOps: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo, }, })); const dbOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_keyservers' }, ...replaceOps, ]); try { await commCoreModule.processKeyserverStoreOperations(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, [63]: async (state: any) => { const { actualizedCalendarQuery, ...rest } = state; const operations: $ReadOnlyArray = entries( state.keyserverStore.keyserverInfos, ).map(([id, keyserverInfo]) => ({ type: 'replace_keyserver', payload: { id, keyserverInfo: { ...keyserverInfo, actualizedCalendarQuery: { ...actualizedCalendarQuery, filters: filterThreadIDsInFilterList( actualizedCalendarQuery.filters, (threadID: string) => extractKeyserverIDFromID(threadID) === id, ), }, }, }, })); const dbOperations: $ReadOnlyArray = keyserverStoreOpsHandlers.convertOpsToClientDBOps(operations); const newState = { ...rest, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( rest.keyserverStore, operations, ), }; try { await commCoreModule.processKeyserverStoreOperations(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return newState; } return handleReduxMigrationFailure(newState); } return newState; }, // Migration 64 is a noop to unblock a `native` release since the previous // contents are not ready to be deployed to prod and we don't want to // decrement migration 65. [64]: (state: AppState) => state, [65]: async (state: AppState) => { const replaceOp: ReplaceIntegrityThreadHashesOperation = { type: 'replace_integrity_thread_hashes', payload: { threadHashes: state.integrityStore.threadHashes, }, }; const dbOperations: $ReadOnlyArray = integrityStoreOpsHandlers.convertOpsToClientDBOps([ { type: 'remove_all_integrity_thread_hashes' }, replaceOp, ]); try { await commCoreModule.processIntegrityStoreOperations(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return handleReduxMigrationFailure(state); } return state; }, }; // After migration 31, we'll no longer want to persist `messageStore.messages` // via redux-persist. However, we DO want to continue persisting everything in // `messageStore` EXCEPT for `messages`. The `blacklist` property in // `persistConfig` allows us to specify top-level keys that shouldn't be // persisted. However, we aren't able to specify nested keys in `blacklist`. // As a result, if we want to prevent nested keys from being persisted we'll // need to use `createTransform(...)` to specify an `inbound` function that // allows us to modify the `state` object before it's passed through // `JSON.stringify(...)` and written to disk. We specify the keys for which // this transformation should be executed in the `whitelist` property of the // `config` object that's passed to `createTransform(...)`. type PersistedMessageStore = { +local: { +[id: string]: LocalMessageInfo }, +currentAsOf: { +[keyserverID: string]: number }, }; const messageStoreMessagesBlocklistTransform: Transform = createTransform( (state: MessageStore): PersistedMessageStore => { const { messages, threads, ...messageStoreSansMessages } = state; return { ...messageStoreSansMessages }; }, (state: MessageStore): MessageStore => { // We typically expect `messageStore.messages` to be `undefined` because // messages are persisted in the SQLite `messages` table rather than via // `redux-persist`. In this case we want to set `messageStore.messages` // to {} so we don't run into issues with `messageStore.messages` being // `undefined` (https://phab.comm.dev/D5545). // // However, in the case that a user is upgrading from a client where // `persistConfig.version` < 31, we expect `messageStore.messages` to // contain messages stored via `redux-persist` that we need in order // to correctly populate the SQLite `messages` table in migration 31 // (https://phab.comm.dev/D2600). // // However, because `messageStoreMessagesBlocklistTransform` modifies // `messageStore` before migrations are run, we need to make sure we aren't // inadvertently clearing `messageStore.messages` (by setting to {}) before // messages are stored in SQLite (https://linear.app/comm/issue/ENG-2377). return { ...state, threads: state.threads ?? {}, messages: state.messages ?? {}, }; }, { whitelist: ['messageStore'] }, ); type PersistedReportStore = $Diff< ReportStore, { +queuedReports: $ReadOnlyArray }, >; const reportStoreTransform: Transform = createTransform( (state: ReportStore): PersistedReportStore => { return { enabledReports: state.enabledReports }; }, (state: PersistedReportStore): ReportStore => { return { ...state, queuedReports: [] }; }, { whitelist: ['reportStore'] }, ); const persistConfig = { key: 'root', storage: AsyncStorage, blacklist: persistBlacklist, debug: __DEV__, version: 65, transforms: [ messageStoreMessagesBlocklistTransform, reportStoreTransform, keyserverStoreTransform, ], migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void), }; const codeVersion: number = commCoreModule.getCodeVersion(); // This local exists to avoid a circular dependency where redux-setup needs to // import all the navigation and screen stuff, but some of those screens want to // access the persistor to purge its state. let storedPersistor = null; function setPersistor(persistor: *) { storedPersistor = persistor; } function getPersistor(): empty { invariant(storedPersistor, 'should be set'); return storedPersistor; } export { persistConfig, codeVersion, setPersistor, getPersistor }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index db93bf990..6090c9c90 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,458 +1,460 @@ // @flow import { AppState as NativeAppState, Alert } from 'react-native'; import { createStore, applyMiddleware, type Store, compose } from 'redux'; import { persistStore, persistReducer } from 'redux-persist'; import thunk from 'redux-thunk'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { siweAuthActionTypes } from 'lib/actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, keyserverAuthActionTypes, deleteKeyserverAccountActionTypes, identityRegisterActionTypes, identityLogInActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { invalidSessionDowngrade, invalidSessionRecovery, identityInvalidSessionDowngrade, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import type { Dispatch, BaseAction } from 'lib/types/redux-types.js'; import { rehydrateActionType } from 'lib/types/redux-types.js'; import type { SetSessionPayload } from 'lib/types/session-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateDimensionsActiveType, updateConnectivityActiveType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, setLocalSettingsActionType, } from './action-types.js'; import { defaultState } from './default-state.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { persistConfig, setPersistor } from './persist.js'; import { onStateDifference } from './redux-debug-utils.js'; import { nonUserSpecificFieldsNative } from './state-types.js'; import type { AppState } from './state-types.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { AppOutOfDateAlertDetails } from '../utils/alert-messages.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { getDevServerHostname } from '../utils/url-utils.js'; function reducer(state: AppState = defaultState, inputAction: Action) { let action = inputAction; if (action.type === setReduxStateActionType) { return action.payload.state; } // We want to alert staff/developers if there's a difference between the keys // we expect to see REHYDRATED and the keys that are actually REHYDRATED. // Context: https://linear.app/comm/issue/ENG-2127/ if ( action.type === rehydrateActionType && (__DEV__ || isStaffRelease || (state.currentUserInfo && state.currentUserInfo.id && isStaff(state.currentUserInfo.id))) ) { // 1. Construct set of keys expected to be REHYDRATED const defaultKeys: $ReadOnlyArray = Object.keys(defaultState); const expectedKeys = defaultKeys.filter( each => !persistConfig.blacklist.includes(each), ); const expectedKeysSet = new Set(expectedKeys); // 2. Construct set of keys actually REHYDRATED const rehydratedKeys: $ReadOnlyArray = Object.keys( action.payload ?? {}, ); const rehydratedKeysSet = new Set(rehydratedKeys); // 3. Determine the difference between the two sets const expectedKeysNotRehydrated = expectedKeys.filter( each => !rehydratedKeysSet.has(each), ); const rehydratedKeysNotExpected = rehydratedKeys.filter( each => !expectedKeysSet.has(each), ); // 4. Display alerts with the differences between the two sets if (expectedKeysNotRehydrated.length > 0) { Alert.alert( `EXPECTED KEYS NOT REHYDRATED: ${JSON.stringify( expectedKeysNotRehydrated, )}`, ); } if (rehydratedKeysNotExpected.length > 0) { Alert.alert( `REHYDRATED KEYS NOT EXPECTED: ${JSON.stringify( rehydratedKeysNotExpected, )}`, ); } } if ( action.type === setNewSessionActionType && invalidSessionDowngrade( state, action.payload.sessionChange.currentUserInfo, action.payload.preRequestUserState, action.payload.keyserverID, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { const { currentUserInfo, preRequestUserState } = action.payload; if ( identityInvalidSessionDowngrade( state, currentUserInfo, preRequestUserState, ) ) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } else if (action.type === identityRegisterActionTypes.success) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } else if ( action.type === identityLogInActionTypes.success && action.payload.userID !== action.payload.preRequestUserState?.id ) { state = resetUserSpecificState( state, defaultState, nonUserSpecificFieldsNative, ); } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.authActionSource, )) || ((action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if (action.type === updateDimensionsActiveType) { return { ...state, dimensions: { ...state.dimensions, ...action.payload, }, }; } else if (action.type === updateConnectivityActiveType) { return { ...state, connectivity: action.payload, }; } else if (action.type === updateDeviceCameraInfoActionType) { return { ...state, deviceCameraInfo: { ...state.deviceCameraInfo, ...action.payload, }, }; } else if (action.type === updateDeviceOrientationActionType) { return { ...state, deviceOrientation: action.payload, }; } else if (action.type === setLocalSettingsActionType) { return { ...state, localSettings: { ...state.localSettings, ...action.payload }, }; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); } if (action.type === setStoreLoadedActionType) { return { ...state, storeLoaded: true, }; } if (action.type === setClientDBStoreActionType) { state = { ...state, storeLoaded: true, }; const currentLoggedInUserID = state.currentUserInfo?.anonymous ? undefined : state.currentUserInfo?.id; const actionCurrentLoggedInUserID = action.payload.currentUserID; if ( !currentLoggedInUserID || !actionCurrentLoggedInUserID || actionCurrentLoggedInUserID !== currentLoggedInUserID ) { // If user is logged out now, was logged out at the time action was // dispatched or their ID changed between action dispatch and a // call to reducer we ignore the SQLite data since it is not valid return state; } } const baseReducerResult = baseReducer( state, (action: BaseAction), onStateDifference, ); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, + syncedMetadataStoreOperations, } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const ops = { draftStoreOperations, messageStoreOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, reportStoreOperations, userStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, + syncedMetadataStoreOperations, }; state = { ...state, dbOpsStore: queueDBOps(state.dbOpsStore, action.messageID, ops), }; return state; } function sessionInvalidationAlert(payload: SetSessionPayload) { if ( !payload.sessionChange.cookieInvalidated || !payload.preRequestUserState || !payload.preRequestUserState.currentUserInfo || payload.preRequestUserState.currentUserInfo.anonymous ) { return; } if (payload.error === 'client_version_unsupported') { Alert.alert( AppOutOfDateAlertDetails.title, AppOutOfDateAlertDetails.message, [{ text: 'OK' }], { cancelable: true, }, ); } else { Alert.alert( 'Session invalidated', 'We’re sorry, but your session was invalidated by the server. ' + 'Please log in again.', [{ text: 'OK' }], { cancelable: true }, ); } } // Makes sure a currently focused thread is never unread. Note that we consider // a backgrounded NativeAppState to actually be active if it last changed to // inactive more than 10 seconds ago. This is because there is a delay when // NativeAppState is updating in response to a foreground, and actions don't get // processed more than 10 seconds after a backgrounding anyways. However we // don't consider this for action types that can be expected to happen while the // app is backgrounded. type FixUnreadActiveThreadResult = { +state: AppState, +threadStoreOperations: $ReadOnlyArray, }; function fixUnreadActiveThread( state: AppState, action: *, ): FixUnreadActiveThreadResult { const navContext = getGlobalNavContext(); const activeThread = activeMessageListSelector(navContext); if ( !activeThread || !state.threadStore.threadInfos[activeThread]?.currentUser.unread || (NativeAppState.currentState !== 'active' && (appLastBecameInactive + 10000 >= Date.now() || backgroundActionTypes.has(action.type))) ) { return { state, threadStoreOperations: [] }; } const activeThreadInfo = state.threadStore.threadInfos[activeThread]; const updatedActiveThreadInfo = { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = threadStoreOpsHandlers.processStoreOperations( state.threadStore, threadStoreOperations, ); return { state: { ...state, threadStore: updatedThreadStore }, threadStoreOperations, }; } let appLastBecameInactive = 0; function appBecameInactive() { appLastBecameInactive = Date.now(); } const middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/redux/redux-utils.js b/native/redux/redux-utils.js index cd07d8799..036d15283 100644 --- a/native/redux/redux-utils.js +++ b/native/redux/redux-utils.js @@ -1,132 +1,145 @@ // @flow import { useSelector as reactReduxUseSelector } from 'react-redux'; import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers, getKeyserversToRemoveFromNotifsStore, } from 'lib/ops/keyserver-store-ops.js'; import { messageStoreOpsHandlers } from 'lib/ops/message-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; +import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { userStoreOpsHandlers } from 'lib/ops/user-store-ops.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; import type { AppState } from './state-types.js'; import { commCoreModule } from '../native-modules.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; function useSelector( selector: (state: AppState) => SS, equalityFn?: (a: SS, b: SS) => boolean, ): SS { return reactReduxUseSelector(selector, equalityFn); } async function processDBStoreOperations( storeOperations: StoreOperations, ): Promise { const { draftStoreOperations, threadStoreOperations, messageStoreOperations, reportStoreOperations, userStoreOperations, keyserverStoreOperations, integrityStoreOperations, communityStoreOperations, + syncedMetadataStoreOperations, } = storeOperations; const convertedThreadStoreOperations = threadStoreOpsHandlers.convertOpsToClientDBOps(threadStoreOperations); const convertedMessageStoreOperations = messageStoreOpsHandlers.convertOpsToClientDBOps(messageStoreOperations); const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); const convertedUserStoreOperations = userStoreOpsHandlers.convertOpsToClientDBOps(userStoreOperations); const convertedKeyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations); const convertedCommunityStoreOperations = communityStoreOpsHandlers.convertOpsToClientDBOps(communityStoreOperations); + const convertedSyncedMetadataStoreOperations = + syncedMetadataStoreOpsHandlers.convertOpsToClientDBOps( + syncedMetadataStoreOperations, + ); const keyserversToRemoveFromNotifsStore = getKeyserversToRemoveFromNotifsStore(keyserverStoreOperations); const convertedIntegrityStoreOperations = integrityStoreOpsHandlers.convertOpsToClientDBOps(integrityStoreOperations); try { const promises = []; if (convertedThreadStoreOperations.length > 0) { promises.push( commCoreModule.processThreadStoreOperations( convertedThreadStoreOperations, ), ); } if (convertedMessageStoreOperations.length > 0) { promises.push( commCoreModule.processMessageStoreOperations( convertedMessageStoreOperations, ), ); } if (draftStoreOperations.length > 0) { promises.push( commCoreModule.processDraftStoreOperations(draftStoreOperations), ); } if (convertedReportStoreOperations.length > 0) { promises.push( commCoreModule.processReportStoreOperations( convertedReportStoreOperations, ), ); } if (convertedUserStoreOperations.length > 0) { promises.push( commCoreModule.processUserStoreOperations(convertedUserStoreOperations), ); } if (convertedKeyserverStoreOperations.length > 0) { promises.push( commCoreModule.processKeyserverStoreOperations( convertedKeyserverStoreOperations, ), ); } if (convertedCommunityStoreOperations.length > 0) { promises.push( commCoreModule.processCommunityStoreOperations( convertedCommunityStoreOperations, ), ); } if (keyserversToRemoveFromNotifsStore.length > 0) { promises.push( commCoreModule.removeKeyserverDataFromNotifStorage( keyserversToRemoveFromNotifsStore, ), ); } if (convertedIntegrityStoreOperations.length > 0) { promises.push( commCoreModule.processIntegrityStoreOperations( convertedIntegrityStoreOperations, ), ); } + if (convertedSyncedMetadataStoreOperations.length > 0) { + promises.push( + commCoreModule.processSyncedMetadataStoreOperations( + convertedSyncedMetadataStoreOperations, + ), + ); + } await Promise.all(promises); } catch (e) { if (isTaskCancelledError(e)) { return; } // this code will make an entry in SecureStore and cause re-creating // database when user will open app again commCoreModule.reportDBOperationsFailure(); commCoreModule.terminate(); } } export { useSelector, processDBStoreOperations }; diff --git a/native/redux/state-types.js b/native/redux/state-types.js index e71b0dec1..4576243b4 100644 --- a/native/redux/state-types.js +++ b/native/redux/state-types.js @@ -1,84 +1,86 @@ // @flow import type { Orientations } from 'react-native-orientation-locker'; import type { PersistState } from 'redux-persist/es/types.js'; import type { CommunityStore } from 'lib/types/community-types.js'; import type { DBOpsStore } from 'lib/types/db-ops-types'; import type { DraftStore } from 'lib/types/draft-types.js'; import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore } from 'lib/types/entry-types.js'; import type { CalendarFilter } from 'lib/types/filter-types.js'; import type { IntegrityStore } from 'lib/types/integrity-types.js'; import type { KeyserverStore } from 'lib/types/keyserver-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; +import type { SyncedMetadataStore } from 'lib/types/synced-metadata-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import type { DimensionsInfo } from './dimensions-updater.react.js'; import type { NavInfo } from '../navigation/default-state.js'; import type { DeviceCameraInfo } from '../types/camera.js'; import type { ConnectivityInfo } from '../types/connectivity.js'; import type { LocalSettings } from '../types/local-settings-types.js'; const nonUserSpecificFieldsNative = [ 'storeLoaded', 'loadingStatuses', 'customServer', 'lifecycleState', 'nextLocalID', 'dimensions', 'connectivity', 'deviceCameraInfo', 'deviceOrientation', 'frozen', 'keyserverStore', '_persist', ]; export type AppState = { +navInfo: NavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +storeLoaded: boolean, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +dataLoaded: boolean, +customServer: ?string, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +_persist: ?PersistState, +dimensions: DimensionsInfo, +connectivity: ConnectivityInfo, +globalThemeInfo: GlobalThemeInfo, +deviceCameraInfo: DeviceCameraInfo, +deviceOrientation: Orientations, +frozen: boolean, +userPolicies: UserPolicies, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +localSettings: LocalSettings, +integrityStore: IntegrityStore, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, + +syncedMetadataStore: SyncedMetadataStore, }; export { nonUserSpecificFieldsNative }; diff --git a/web/redux/default-state.js b/web/redux/default-state.js index 255af554e..608da4ebf 100644 --- a/web/redux/default-state.js +++ b/web/redux/default-state.js @@ -1,88 +1,91 @@ // @flow import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { defaultKeyserverInfo } from 'lib/types/keyserver-types.js'; import { defaultGlobalThemeInfo } from 'lib/types/theme-types.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import type { AppState } from './redux-setup.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import electron from '../electron.js'; declare var keyserverURL: string; const defaultWebState: AppState = Object.freeze({ navInfo: { activeChatThreadID: null, startDate: '', endDate: '', tab: 'chat', }, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: { [authoritativeKeyserverID]: 0 }, }, windowActive: true, pushApiPublicKey: null, windowDimensions: { width: window.width, height: window.height }, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, dataLoaded: false, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, watchedThreadIDs: [], lifecycleState: 'active', enabledApps: defaultWebEnabledApps, reportStore: { enabledReports: { crashReports: false, inconsistencyReports: false, mediaReports: false, }, queuedReports: [], }, nextLocalID: 0, _persist: null, userPolicies: {}, commServicesAccessToken: null, inviteLinksStore: { links: {}, }, communityPickerStore: { chat: null, calendar: null }, keyserverStore: { keyserverInfos: { [authoritativeKeyserverID]: defaultKeyserverInfo( keyserverURL, electron?.platform ?? 'web', ), }, }, threadActivityStore: {}, initialStateLoaded: false, integrityStore: { threadHashes: {}, threadHashingStatus: 'starting' }, globalThemeInfo: defaultGlobalThemeInfo, customServer: null, communityStore: { communityInfos: {}, }, dbOpsStore: { queuedOps: [], }, + syncedMetadataStore: { + syncedMetadata: {}, + }, }); export { defaultWebState }; diff --git a/web/redux/initial-state-gate.js b/web/redux/initial-state-gate.js index f17ea1fd5..75fa5ab41 100644 --- a/web/redux/initial-state-gate.js +++ b/web/redux/initial-state-gate.js @@ -1,158 +1,159 @@ // @flow import * as React from 'react'; import { PersistGate } from 'redux-persist/es/integration/react.js'; import type { Persistor } from 'redux-persist/es/types'; import { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import type { ThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import { allUpdatesCurrentAsOfSelector } from 'lib/selectors/keyserver-selectors.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { RawThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { LegacyRawThreadInfo } from 'lib/types/thread-types.js'; import { convertIDToNewSchema } from 'lib/utils/migration-utils.js'; import { entries } from 'lib/utils/objects.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { setInitialReduxState, useGetInitialReduxState, } from './action-types.js'; import { useSelector } from './redux-utils.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import Loading from '../loading.react.js'; import { getClientDBStore, processDBStoreOperations, } from '../shared-worker/utils/store.js'; type Props = { +persistor: Persistor, +children: React.Node, }; function InitialReduxStateGate(props: Props): React.Node { const { children, persistor } = props; const callGetInitialReduxState = useGetInitialReduxState(); const dispatch = useDispatch(); const [initError, setInitError] = React.useState(null); React.useEffect(() => { if (initError) { throw initError; } }, [initError]); const isRehydrated = useSelector(state => !!state._persist?.rehydrated); const allUpdatesCurrentAsOf = useSelector(allUpdatesCurrentAsOfSelector); const prevIsRehydrated = React.useRef(false); React.useEffect(() => { if (prevIsRehydrated.current || !isRehydrated) { return; } prevIsRehydrated.current = isRehydrated; void (async () => { try { let urlInfo = infoFromURL(decodeURI(window.location.href)); // Handle older links if (urlInfo.thread) { urlInfo = { ...urlInfo, thread: convertIDToNewSchema( urlInfo.thread, authoritativeKeyserverID, ), }; } const clientDBStore = await getClientDBStore(); dispatch({ type: setClientDBStoreActionType, payload: clientDBStore, }); const payload = await callGetInitialReduxState({ urlInfo, excludedData: { threadStore: !!clientDBStore.threadStore, }, allUpdatesCurrentAsOf, }); const currentLoggedInUserID = payload.currentUserInfo?.anonymous ? null : payload.currentUserInfo?.id; const useDatabase = canUseDatabaseOnWeb(currentLoggedInUserID); if (!currentLoggedInUserID || !useDatabase) { dispatch({ type: setInitialReduxState, payload }); return; } if (clientDBStore.threadStore) { const { threadStore, ...rest } = payload; dispatch({ type: setInitialReduxState, payload: rest }); return; } // When there is no data in the DB, it's necessary to migrate data // from the keyserver payload to the DB const { threadStore: { threadInfos }, } = payload; const threadStoreOperations: ThreadStoreOperation[] = entries( threadInfos, ).map( ([id, threadInfo]: [ string, LegacyRawThreadInfo | RawThreadInfo, ]) => ({ type: 'replace', payload: { id, threadInfo, }, }), ); await processDBStoreOperations( { threadStoreOperations, draftStoreOperations: [], messageStoreOperations: [], reportStoreOperations: [], userStoreOperations: [], keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], + syncedMetadataStoreOperations: [], }, currentLoggedInUserID, ); dispatch({ type: setInitialReduxState, payload }); } catch (err) { setInitError(err); } })(); }, [callGetInitialReduxState, dispatch, isRehydrated, allUpdatesCurrentAsOf]); const initialStateLoaded = useSelector(state => state.initialStateLoaded); const childFunction = React.useCallback( // This argument is passed from `PersistGate`. It means that the state is // rehydrated and we can start fetching the initial info. (bootstrapped: boolean) => { if (bootstrapped && initialStateLoaded) { return children; } else { return ; } }, [children, initialStateLoaded], ); return {childFunction}; } export default InitialReduxStateGate; diff --git a/web/redux/redux-setup.js b/web/redux/redux-setup.js index 7a020f7c3..4d5570eb4 100644 --- a/web/redux/redux-setup.js +++ b/web/redux/redux-setup.js @@ -1,522 +1,525 @@ // @flow import invariant from 'invariant'; import type { PersistState } from 'redux-persist/es/types.js'; import { logOutActionTypes, deleteKeyserverAccountActionTypes, deleteAccountActionTypes, identityRegisterActionTypes, identityLogInActionTypes, keyserverAuthActionTypes, } from 'lib/actions/user-actions.js'; import { setNewSessionActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { type ReplaceKeyserverOperation, keyserverStoreOpsHandlers, } from 'lib/ops/keyserver-store-ops.js'; import { type ThreadStoreOperation, threadStoreOpsHandlers, } from 'lib/ops/thread-store-ops.js'; import { queueDBOps } from 'lib/reducers/db-ops-reducer.js'; import { reduceLoadingStatuses } from 'lib/reducers/loading-reducer.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { mostRecentlyReadThreadSelector } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { invalidSessionDowngrade, identityInvalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/session-utils.js'; import type { CommunityStore } from 'lib/types/community-types.js'; import type { MessageID, DBOpsStore } from 'lib/types/db-ops-types.js'; import type { DraftStore } from 'lib/types/draft-types.js'; import type { EnabledApps } from 'lib/types/enabled-apps.js'; import type { EntryStore } from 'lib/types/entry-types.js'; import { type CalendarFilter } from 'lib/types/filter-types.js'; import type { IntegrityStore } from 'lib/types/integrity-types.js'; import type { KeyserverStore } from 'lib/types/keyserver-types.js'; import type { LifecycleState } from 'lib/types/lifecycle-state-types.js'; import type { InviteLinksStore } from 'lib/types/link-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { MessageStore } from 'lib/types/message-types.js'; import type { WebNavInfo } from 'lib/types/nav-types.js'; import type { UserPolicies } from 'lib/types/policy-types.js'; import type { BaseAction } from 'lib/types/redux-types.js'; import type { ReportStore } from 'lib/types/report-types.js'; import type { StoreOperations } from 'lib/types/store-ops-types.js'; +import type { SyncedMetadataStore } from 'lib/types/synced-metadata-types.js'; import type { GlobalThemeInfo } from 'lib/types/theme-types.js'; import type { ThreadActivityStore } from 'lib/types/thread-activity-types'; import type { ThreadStore } from 'lib/types/thread-types.js'; import type { CurrentUserInfo, UserStore } from 'lib/types/user-types.js'; import type { NotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { resetUserSpecificState } from 'lib/utils/reducers-utils.js'; import { updateWindowActiveActionType, updateNavInfoActionType, updateWindowDimensionsActionType, setInitialReduxState, } from './action-types.js'; import { reduceCommunityPickerStore } from './community-picker-reducer.js'; import { defaultWebState } from './default-state.js'; import reduceNavInfo from './nav-reducer.js'; import { onStateDifference } from './redux-debug-utils.js'; import { reduceServicesAccessToken } from './services-access-token-reducer.js'; import { getVisibility } from './visibility.js'; import { activeThreadSelector } from '../selectors/nav-selectors.js'; import type { InitialReduxState } from '../types/redux-types.js'; export type WindowDimensions = { width: number, height: number }; export type CommunityPickerStore = { +chat: ?string, +calendar: ?string, }; const nonUserSpecificFieldsWeb = [ 'loadingStatuses', 'windowDimensions', 'lifecycleState', 'nextLocalID', 'windowActive', 'pushApiPublicKey', 'keyserverStore', 'initialStateLoaded', '_persist', 'customServer', ]; export type AppState = { +navInfo: WebNavInfo, +currentUserInfo: ?CurrentUserInfo, +draftStore: DraftStore, +entryStore: EntryStore, +threadStore: ThreadStore, +userStore: UserStore, +messageStore: MessageStore, +loadingStatuses: { [key: string]: { [idx: number]: LoadingStatus } }, +calendarFilters: $ReadOnlyArray, +communityPickerStore: CommunityPickerStore, +windowDimensions: WindowDimensions, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +watchedThreadIDs: $ReadOnlyArray, +lifecycleState: LifecycleState, +enabledApps: EnabledApps, +reportStore: ReportStore, +nextLocalID: number, +dataLoaded: boolean, +windowActive: boolean, +userPolicies: UserPolicies, +pushApiPublicKey: ?string, +_persist: ?PersistState, +commServicesAccessToken: ?string, +inviteLinksStore: InviteLinksStore, +keyserverStore: KeyserverStore, +threadActivityStore: ThreadActivityStore, +initialStateLoaded: boolean, +integrityStore: IntegrityStore, +globalThemeInfo: GlobalThemeInfo, +customServer: ?string, +communityStore: CommunityStore, +dbOpsStore: DBOpsStore, + +syncedMetadataStore: SyncedMetadataStore, }; export type Action = $ReadOnly< | BaseAction | { +messageID?: MessageID, ... | { +type: 'UPDATE_NAV_INFO', +payload: Partial } | { +type: 'UPDATE_WINDOW_DIMENSIONS', +payload: WindowDimensions, } | { +type: 'UPDATE_WINDOW_ACTIVE', +payload: boolean, } | { +type: 'SET_INITIAL_REDUX_STATE', +payload: InitialReduxState }, }, >; function reducer(oldState: AppState | void, action: Action): AppState { invariant(oldState, 'should be set'); let state = oldState; let storeOperations: StoreOperations = { draftStoreOperations: [], threadStoreOperations: [], messageStoreOperations: [], reportStoreOperations: [], userStoreOperations: [], keyserverStoreOperations: [], communityStoreOperations: [], integrityStoreOperations: [], + syncedMetadataStoreOperations: [], }; if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.authActionSource, )) || (action.type === keyserverAuthActionTypes.success && invalidSessionRecovery( state, action.payload.preRequestUserInfo, action.payload.authActionSource, )) ) { return state; } if (action.type === setInitialReduxState) { const { userInfos, keyserverInfos, actualizedCalendarQuery, ...rest } = action.payload; const replaceOperations: ReplaceKeyserverOperation[] = []; for (const keyserverID in keyserverInfos) { replaceOperations.push({ type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], ...keyserverInfos[keyserverID], actualizedCalendarQuery, }, }, }); } return validateStateAndQueueOpsProcessing( action, oldState, { ...state, ...rest, userStore: { userInfos }, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, replaceOperations, ), initialStateLoaded: true, }, { ...storeOperations, keyserverStoreOperations: [ ...storeOperations.keyserverStoreOperations, ...replaceOperations, ], }, ); } else if (action.type === updateWindowDimensionsActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowDimensions: action.payload, }, storeOperations, ); } else if (action.type === updateWindowActiveActionType) { return validateStateAndQueueOpsProcessing( action, oldState, { ...state, windowActive: action.payload, }, storeOperations, ); } else if (action.type === setNewSessionActionType) { const { keyserverID, sessionChange } = action.payload; if (!state.keyserverStore.keyserverInfos[keyserverID]) { if (sessionChange.cookie?.startsWith('user=')) { console.log( 'received sessionChange with user cookie, ' + `but keyserver ${keyserverID} is not in KeyserverStore!`, ); } return state; } if ( invalidSessionDowngrade( oldState, sessionChange.currentUserInfo, action.payload.preRequestUserState, keyserverID, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } const replaceOperation: ReplaceKeyserverOperation = { type: 'replace_keyserver', payload: { id: keyserverID, keyserverInfo: { ...state.keyserverStore.keyserverInfos[keyserverID], sessionID: sessionChange.sessionID, }, }, }; state = { ...state, keyserverStore: keyserverStoreOpsHandlers.processStoreOperations( state.keyserverStore, [replaceOperation], ), }; storeOperations = { ...storeOperations, keyserverStoreOperations: [ ...storeOperations.keyserverStoreOperations, replaceOperation, ], }; } else if (action.type === deleteKeyserverAccountActionTypes.success) { const { currentUserInfo, preRequestUserState } = action.payload; const newKeyserverIDs = []; for (const keyserverID of action.payload.keyserverIDs) { if ( invalidSessionDowngrade( state, currentUserInfo, preRequestUserState, keyserverID, ) ) { continue; } newKeyserverIDs.push(keyserverID); } if (newKeyserverIDs.length === 0) { return { ...state, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } action = { ...action, payload: { ...action.payload, keyserverIDs: newKeyserverIDs, }, }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success ) { const { currentUserInfo, preRequestUserState } = action.payload; if ( identityInvalidSessionDowngrade( oldState, currentUserInfo, preRequestUserState, ) ) { return { ...oldState, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), }; } state = resetUserSpecificState( state, defaultWebState, nonUserSpecificFieldsWeb, ); } else if (action.type === identityRegisterActionTypes.success) { state = resetUserSpecificState( state, defaultWebState, nonUserSpecificFieldsWeb, ); } else if ( action.type === identityLogInActionTypes.success && action.payload.userID !== action.payload.preRequestUserState?.id ) { state = resetUserSpecificState( state, defaultWebState, nonUserSpecificFieldsWeb, ); } if (action.type !== updateNavInfoActionType) { const baseReducerResult = baseReducer(state, action, onStateDifference); state = baseReducerResult.state; storeOperations = { ...baseReducerResult.storeOperations, keyserverStoreOperations: [ ...storeOperations.keyserverStoreOperations, ...baseReducerResult.storeOperations.keyserverStoreOperations, ], }; } const communityPickerStore = reduceCommunityPickerStore( state.communityPickerStore, action, ); state = { ...state, navInfo: reduceNavInfo( state.navInfo, action, state.threadStore.threadInfos, ), communityPickerStore, commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), }; return validateStateAndQueueOpsProcessing( action, oldState, state, storeOperations, ); } function validateStateAndQueueOpsProcessing( action: Action, oldState: AppState, state: AppState, storeOperations: StoreOperations, ): AppState { const updateActiveThreadOps: ThreadStoreOperation[] = []; if ( (state.navInfo.activeChatThreadID && !state.navInfo.pendingThread && !state.threadStore.threadInfos[state.navInfo.activeChatThreadID]) || (!state.navInfo.activeChatThreadID && isLoggedIn(state)) ) { // Makes sure the active thread always exists state = { ...state, navInfo: { ...state.navInfo, activeChatThreadID: mostRecentlyReadThreadSelector(state), }, }; } const activeThread = activeThreadSelector(state); if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && getVisibility().hidden() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but visibilityjs reports the window is not visible', ); } if ( activeThread && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread && typeof document !== 'undefined' && document && 'hasFocus' in document && !document.hasFocus() ) { console.warn( `thread ${activeThread} is active and unread, ` + 'but document.hasFocus() is false', ); } if ( activeThread && !getVisibility().hidden() && typeof document !== 'undefined' && document && 'hasFocus' in document && document.hasFocus() && !state.navInfo.pendingThread && state.threadStore.threadInfos[activeThread].currentUser.unread ) { // Makes sure a currently focused thread is never unread const activeThreadInfo = state.threadStore.threadInfos[activeThread]; updateActiveThreadOps.push({ type: 'replace', payload: { id: activeThread, threadInfo: { ...activeThreadInfo, currentUser: { ...activeThreadInfo.currentUser, unread: false, }, }, }, }); } const oldActiveThread = activeThreadSelector(oldState); if ( activeThread && oldActiveThread !== activeThread && state.messageStore.threads[activeThread] ) { const now = Date.now(); state = { ...state, threadActivityStore: { ...state.threadActivityStore, [(activeThread: string)]: { ...state.threadActivityStore[activeThread], lastNavigatedTo: now, }, }, }; } if (updateActiveThreadOps.length > 0) { state = { ...state, threadStore: threadStoreOpsHandlers.processStoreOperations( state.threadStore, updateActiveThreadOps, ), }; storeOperations = { ...storeOperations, threadStoreOperations: [ ...storeOperations.threadStoreOperations, ...updateActiveThreadOps, ], }; } // The operations were already dispatched from the main tab // For now the `dispatchSource` field is not included in any of the // redux actions and this causes flow to throw an error. // As soon as one of the actions is updated, this fix (and the corresponding // one in tab-synchronization.js) can be removed. // $FlowFixMe if (action.dispatchSource === 'tab-sync') { return state; } return { ...state, dbOpsStore: queueDBOps(state.dbOpsStore, action.messageID, storeOperations), }; } export { nonUserSpecificFieldsWeb, reducer }; diff --git a/web/shared-worker/utils/store.js b/web/shared-worker/utils/store.js index e44c3eb23..6e4a77064 100644 --- a/web/shared-worker/utils/store.js +++ b/web/shared-worker/utils/store.js @@ -1,151 +1,160 @@ // @flow import { communityStoreOpsHandlers } from 'lib/ops/community-store-ops.js'; import { integrityStoreOpsHandlers } from 'lib/ops/integrity-store-ops.js'; import { keyserverStoreOpsHandlers } from 'lib/ops/keyserver-store-ops.js'; import { reportStoreOpsHandlers } from 'lib/ops/report-store-ops.js'; +import { syncedMetadataStoreOpsHandlers } from 'lib/ops/synced-metadata-store-ops.js'; import { threadStoreOpsHandlers } from 'lib/ops/thread-store-ops.js'; import { canUseDatabaseOnWeb } from 'lib/shared/web-database.js'; import type { ClientStore, StoreOperations, } from 'lib/types/store-ops-types.js'; import { defaultWebState } from '../../redux/default-state.js'; import { workerRequestMessageTypes } from '../../types/worker-types.js'; import { getCommSharedWorker } from '../shared-worker-provider.js'; async function getClientDBStore(): Promise { const sharedWorker = await getCommSharedWorker(); let result: ClientStore = { currentUserID: null, drafts: [], messages: null, threadStore: null, messageStoreThreads: null, reports: null, users: null, keyserverInfos: defaultWebState.keyserverStore.keyserverInfos, communityInfos: null, threadHashes: null, + syncedMetadata: null, }; const data = await sharedWorker.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); if (data?.store?.drafts) { result = { ...result, drafts: data.store.drafts, }; } if (data?.store?.reports) { result = { ...result, reports: reportStoreOpsHandlers.translateClientDBData(data.store.reports), }; } if (data?.store?.threads && data.store.threads.length > 0) { result = { ...result, threadStore: { threadInfos: threadStoreOpsHandlers.translateClientDBData( data.store.threads, ), }, }; } if (data?.store?.keyservers?.length) { result = { ...result, keyserverInfos: keyserverStoreOpsHandlers.translateClientDBData( data.store.keyservers, ), }; } if (data?.store?.communities) { result = { ...result, communityInfos: communityStoreOpsHandlers.translateClientDBData( data.store.communities, ), }; } if (data?.store?.integrityThreadHashes) { result = { ...result, threadHashes: integrityStoreOpsHandlers.translateClientDBData( data.store.integrityThreadHashes, ), }; } return result; } async function processDBStoreOperations( storeOperations: StoreOperations, userID: null | string, ): Promise { const { draftStoreOperations, threadStoreOperations, reportStoreOperations, keyserverStoreOperations, communityStoreOperations, integrityStoreOperations, + syncedMetadataStoreOperations, } = storeOperations; const canUseDatabase = canUseDatabaseOnWeb(userID); const convertedThreadStoreOperations = canUseDatabase ? threadStoreOpsHandlers.convertOpsToClientDBOps(threadStoreOperations) : []; const convertedReportStoreOperations = reportStoreOpsHandlers.convertOpsToClientDBOps(reportStoreOperations); const convertedKeyserverStoreOperations = keyserverStoreOpsHandlers.convertOpsToClientDBOps(keyserverStoreOperations); const convertedCommunityStoreOperations = communityStoreOpsHandlers.convertOpsToClientDBOps(communityStoreOperations); const convertedIntegrityStoreOperations = integrityStoreOpsHandlers.convertOpsToClientDBOps(integrityStoreOperations); + const convertedSyncedMetadataStoreOperations = + syncedMetadataStoreOpsHandlers.convertOpsToClientDBOps( + syncedMetadataStoreOperations, + ); if ( convertedThreadStoreOperations.length === 0 && convertedReportStoreOperations.length === 0 && draftStoreOperations.length === 0 && convertedKeyserverStoreOperations.length === 0 && convertedCommunityStoreOperations.length === 0 && - convertedIntegrityStoreOperations.length === 0 + convertedIntegrityStoreOperations.length === 0 && + convertedSyncedMetadataStoreOperations.length === 0 ) { return; } const sharedWorker = await getCommSharedWorker(); const isSupported = await sharedWorker.isSupported(); if (!isSupported) { return; } try { await sharedWorker.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations, reportStoreOperations: convertedReportStoreOperations, threadStoreOperations: convertedThreadStoreOperations, keyserverStoreOperations: convertedKeyserverStoreOperations, communityStoreOperations: convertedCommunityStoreOperations, integrityStoreOperations: convertedIntegrityStoreOperations, + syncedMetadataStoreOperations: convertedSyncedMetadataStoreOperations, }, }); } catch (e) { console.log(e); if (canUseDatabase) { window.alert(e.message); if (threadStoreOperations.length > 0) { await sharedWorker.init({ clearDatabase: true }); location.reload(); } } } } export { getClientDBStore, processDBStoreOperations };