diff --git a/lib/reducers/master-reducer.js b/lib/reducers/master-reducer.js index ed097a388..a857ee524 100644 --- a/lib/reducers/master-reducer.js +++ b/lib/reducers/master-reducer.js @@ -1,135 +1,138 @@ // @flow import reduceCalendarFilters from './calendar-filters-reducer.js'; import reduceConnectionInfo from './connection-reducer.js'; import reduceDataLoaded from './data-loaded-reducer.js'; import { reduceDeviceToken } from './device-token-reducer.js'; import { reduceDraftStore } from './draft-reducer.js'; import reduceEnabledApps from './enabled-apps-reducer.js'; import { reduceEntryInfos } from './entry-reducer.js'; import reduceInviteLinks from './invite-links-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 { reduceThreadInfos } from './thread-reducer.js'; import reduceUpdatesCurrentAsOf from './updates-reducer.js'; import reduceURLPrefix from './url-prefix-reducer.js'; import { reduceCurrentUserInfo, reduceUserInfos } from './user-reducer.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { registerActionTypes, logInActionTypes, } from '../actions/user-actions.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'; export default function baseReducer>( state: T, action: BaseAction, ): { state: T, storeOperations: StoreOperations } { const { threadStore, newThreadInconsistencies, threadStoreOperations } = reduceThreadInfos(state.threadStore, action); const { threadInfos } = threadStore; const [entryStore, newEntryInconsistencies] = reduceEntryInfos( state.entryStore, action, threadInfos, ); const newInconsistencies = [ ...newEntryInconsistencies, ...newThreadInconsistencies, ]; // 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 updatesCurrentAsOf = reduceUpdatesCurrentAsOf( state.updatesCurrentAsOf, action, ); const connection = reduceConnectionInfo(state.connection, action); if ( connection.status !== 'connected' && action.type !== incrementalStateSyncActionType && action.type !== fullStateSyncActionType && action.type !== registerActionTypes.success && action.type !== logInActionTypes.success && action.type !== siweAuthActionTypes.success ) { if (messageStore.currentAsOf !== state.messageStore.currentAsOf) { messageStore = { ...messageStore, currentAsOf: state.messageStore.currentAsOf, }; } if (updatesCurrentAsOf !== state.updatesCurrentAsOf) { updatesCurrentAsOf = state.updatesCurrentAsOf; } } const { draftStore, draftStoreOperations } = reduceDraftStore( state.draftStore, action, ); + const { reportStore, reportStoreOperations } = reduceReportStore( + state.reportStore, + action, + newInconsistencies, + ); + return { state: { ...state, navInfo: reduceBaseNavInfo(state.navInfo, action), draftStore, entryStore, loadingStatuses: reduceLoadingStatuses(state.loadingStatuses, action), currentUserInfo: reduceCurrentUserInfo(state.currentUserInfo, action), threadStore, userStore: reduceUserInfos(state.userStore, action), messageStore, updatesCurrentAsOf, urlPrefix: reduceURLPrefix(state.urlPrefix, action), calendarFilters: reduceCalendarFilters( state.calendarFilters, action, threadStore, ), notifPermissionAlertInfo: reduceNotifPermissionAlertInfo( state.notifPermissionAlertInfo, action, ), connection, lifecycleState: reduceLifecycleState(state.lifecycleState, action), enabledApps: reduceEnabledApps(state.enabledApps, action), - reportStore: reduceReportStore( - state.reportStore, - action, - newInconsistencies, - ), + reportStore, nextLocalID: reduceNextLocalID(state.nextLocalID, action), dataLoaded: reduceDataLoaded(state.dataLoaded, action), userPolicies: policiesReducer(state.userPolicies, action), deviceToken: reduceDeviceToken(state.deviceToken, action), commServicesAccessToken: reduceServicesAccessToken( state.commServicesAccessToken, action, ), inviteLinksStore: reduceInviteLinks(state.inviteLinksStore, action), }, storeOperations: { draftStoreOperations, threadStoreOperations, messageStoreOperations, + reportStoreOperations, }, }; } diff --git a/lib/reducers/report-store-reducer.js b/lib/reducers/report-store-reducer.js index 1b291ffa3..776475f91 100644 --- a/lib/reducers/report-store-reducer.js +++ b/lib/reducers/report-store-reducer.js @@ -1,96 +1,120 @@ // @flow import { sendReportActionTypes, sendReportsActionTypes, queueReportsActionType, } from '../actions/report-actions.js'; import { siweAuthActionTypes } from '../actions/siwe-actions.js'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, } from '../actions/user-actions.js'; +import type { ReportStoreOperation } from '../ops/report-store-ops.js'; import { isStaff } from '../shared/staff-utils.js'; import type { BaseAction } from '../types/redux-types.js'; import { type ReportStore, defaultEnabledReports, defaultDevEnabledReports, type ClientReportCreationRequest, } from '../types/report-types.js'; import { setNewSessionActionType } from '../utils/action-utils.js'; import { isDev } from '../utils/dev-utils.js'; import { isReportEnabled } from '../utils/report-utils.js'; export const updateReportsEnabledActionType = 'UPDATE_REPORTS_ENABLED'; export default function reduceReportStore( state: ReportStore, action: BaseAction, newInconsistencies: $ReadOnlyArray, -): ReportStore { +): { + reportStore: ReportStore, + reportStoreOperations: $ReadOnlyArray, +} { const updatedReports = newInconsistencies.length > 0 ? [...state.queuedReports, ...newInconsistencies].filter(report => isReportEnabled(report, state.enabledReports), ) : state.queuedReports; if (action.type === updateReportsEnabledActionType) { const newEnabledReports = { ...state.enabledReports, ...action.payload }; const filteredReports = updatedReports.filter(report => isReportEnabled(report, newEnabledReports), ); return { - queuedReports: filteredReports, - enabledReports: newEnabledReports, + reportStore: { + queuedReports: filteredReports, + enabledReports: newEnabledReports, + }, + reportStoreOperations: [], }; } else if ( action.type === logOutActionTypes.success || action.type === deleteAccountActionTypes.success || (action.type === setNewSessionActionType && action.payload.sessionChange.cookieInvalidated) ) { return { - queuedReports: [], - enabledReports: isDev ? defaultDevEnabledReports : defaultEnabledReports, + reportStore: { + queuedReports: [], + enabledReports: isDev + ? defaultDevEnabledReports + : defaultEnabledReports, + }, + reportStoreOperations: [], }; } else if ( action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success ) { return { - queuedReports: [], - enabledReports: - isStaff(action.payload.currentUserInfo.id) || isDev - ? defaultDevEnabledReports - : defaultEnabledReports, + reportStore: { + queuedReports: [], + enabledReports: + isStaff(action.payload.currentUserInfo.id) || isDev + ? defaultDevEnabledReports + : defaultEnabledReports, + }, + reportStoreOperations: [], }; } else if ( (action.type === sendReportActionTypes.success || action.type === sendReportsActionTypes.success) && action.payload ) { const { payload } = action; const unsentReports = updatedReports.filter( response => !payload.reports.includes(response), ); if (unsentReports.length === updatedReports.length) { - return state; + return { reportStore: state, reportStoreOperations: [] }; } - return { ...state, queuedReports: unsentReports }; + return { + reportStore: { ...state, queuedReports: unsentReports }, + reportStoreOperations: [], + }; } else if (action.type === queueReportsActionType) { const { reports } = action.payload; const filteredReports = [...updatedReports, ...reports].filter(report => isReportEnabled(report, state.enabledReports), ); return { - ...state, - queuedReports: filteredReports, + reportStore: { + ...state, + queuedReports: filteredReports, + }, + reportStoreOperations: [], }; } - return updatedReports !== state.queuedReports - ? { ...state, queuedReports: updatedReports } - : state; + const reportStore = + updatedReports !== state.queuedReports + ? { ...state, queuedReports: updatedReports } + : state; + + return { reportStore, reportStoreOperations: [] }; } diff --git a/lib/reducers/report-store-reducer.test.js b/lib/reducers/report-store-reducer.test.js index 0f91078f6..5bddd5162 100644 --- a/lib/reducers/report-store-reducer.test.js +++ b/lib/reducers/report-store-reducer.test.js @@ -1,287 +1,311 @@ // @flow import reduceReportStore from './report-store-reducer.js'; import type { LogInResult } from '../types/account-types.js'; import type { LoadingInfo } from '../types/loading-types.js'; import type { AppState, BaseAction } from '../types/redux-types.js'; import { type ReportStore, reportTypes, type MediaMissionReportCreationRequest, type ErrorReportCreationRequest, type EnabledReports, type ClientThreadInconsistencyReportCreationRequest, } from '../types/report-types.js'; const loadingInfo: LoadingInfo = { fetchIndex: 0, trackMultipleRequests: false, customKeyName: undefined, }; // this is only for types compatibility and `any` will not have any influence // on tests correctness const defaultState: AppState = ({}: any); const defaultBaseAction: BaseAction = ({ payload: ({}: any), loadingInfo, }: any); const defaultAction = { payload: ({}: any), loadingInfo, }; const mockErrorReport: ErrorReportCreationRequest = { type: reportTypes.ERROR, platformDetails: { platform: 'web' }, errors: [], preloadedState: defaultState, currentState: defaultState, actions: [], id: '1-1', }; const mockInconsistencyReport: ClientThreadInconsistencyReportCreationRequest = { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: { platform: 'web' }, beforeAction: {}, action: defaultBaseAction, pushResult: {}, lastActions: [], time: 0, id: '1-2', }; const mockMediaReport: MediaMissionReportCreationRequest = { type: reportTypes.MEDIA_MISSION, platformDetails: { platform: 'web' }, time: Date.now(), mediaMission: { steps: [], result: { success: true }, totalTime: 0, userTime: 0, }, id: '1-3', }; const defaultEnabledReports: EnabledReports = { crashReports: true, inconsistencyReports: true, mediaReports: true, }; const defaultEmptyReportStore: ReportStore = { queuedReports: [], enabledReports: defaultEnabledReports, }; const defaultReportStore: ReportStore = { queuedReports: [mockErrorReport, mockInconsistencyReport, mockMediaReport], enabledReports: defaultEnabledReports, }; describe('session change test', () => { const mockLogInResult: LogInResult = ({ currentUserInfo: { id: '-1', username: 'test' }, }: any); test('should handle log out', () => { const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; - const result = reduceReportStore(defaultReportStore, action, []); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [], + ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log out with new inconsistencies', () => { const action = { ...defaultAction, type: 'LOG_OUT_SUCCESS' }; - const result = reduceReportStore(defaultReportStore, action, [ - mockErrorReport, - ]); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [mockErrorReport], + ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log in', () => { const action = { type: 'LOG_IN_SUCCESS', payload: mockLogInResult, loadingInfo, }; - const result = reduceReportStore(defaultReportStore, action, []); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [], + ); expect(result.queuedReports).toHaveLength(0); }); test('should handle log in with new inconsistencies', () => { const action = { type: 'LOG_IN_SUCCESS', payload: mockLogInResult, loadingInfo, }; - const result = reduceReportStore(defaultReportStore, action, [ - mockErrorReport, - ]); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [mockErrorReport], + ); expect(result.queuedReports).toHaveLength(0); }); }); describe('updateReportsEnabledActionType test', () => { test('should handle the same enabled reports', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: defaultEnabledReports, }; - const result = reduceReportStore(defaultReportStore, action, []); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [], + ); expect(result.queuedReports).toStrictEqual( defaultReportStore.queuedReports, ); }); test('should handle changing enabled reports', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; - const result = reduceReportStore(defaultReportStore, action, []); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [], + ); expect(result.queuedReports).toHaveLength(1); const enabledReportsExist = result.queuedReports.some( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReportsExist).toBeTruthy(); expect(notEnabledReportsExist).toBeFalsy(); }); test('should handle changing enabled reports with new inconsistencies', () => { const action = { type: 'UPDATE_REPORTS_ENABLED', payload: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; - const result = reduceReportStore(defaultReportStore, action, [ - { ...mockErrorReport, id: 'new-id-error' }, - { ...mockMediaReport, id: 'new-id-media' }, - ]); + const { reportStore: result } = reduceReportStore( + defaultReportStore, + action, + [ + { ...mockErrorReport, id: 'new-id-error' }, + { ...mockMediaReport, id: 'new-id-media' }, + ], + ); expect(result.queuedReports).toHaveLength(2); const enabledReports = result.queuedReports.filter( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReports).toHaveLength(2); expect(notEnabledReportsExist).toBeFalsy(); }); }); describe('queueReportsActionType test', () => { test('should handle adding enabled report', () => { const action = { type: 'QUEUE_REPORTS', payload: { reports: [ { ...mockErrorReport, id: 'new-id-error' }, { ...mockMediaReport, id: 'new-id-media' }, ], }, }; const reportStore = { queuedReports: [mockErrorReport], enabledReports: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }; - const result = reduceReportStore(reportStore, action, [ + const { reportStore: result } = reduceReportStore(reportStore, action, [ { ...mockErrorReport, id: 'new-id-error-inc' }, { ...mockMediaReport, id: 'new-id-media-inc' }, ]); expect(result.queuedReports).toHaveLength(3); const enabledReports = result.queuedReports.filter( report => report.type === reportTypes.ERROR, ); const notEnabledReportsExist = result.queuedReports.some( report => report.type !== reportTypes.ERROR, ); expect(enabledReports).toHaveLength(3); expect(notEnabledReportsExist).toBeFalsy(); }); }); describe('sending report test', () => { test('should remove sent report', () => { - const reportStore = reduceReportStore( + const { reportStore } = reduceReportStore( defaultEmptyReportStore, defaultBaseAction, [mockErrorReport, mockMediaReport], ); expect(reportStore.queuedReports).toHaveLength(2); const [sentReport, notSentReport] = reportStore.queuedReports; const action = { type: 'SEND_REPORT_SUCCESS', payload: { reports: [sentReport], }, loadingInfo, }; - const result = reduceReportStore(reportStore, action, []); + const { reportStore: result } = reduceReportStore(reportStore, action, []); expect(result.queuedReports).toHaveLength(1); expect(result.queuedReports).toContain(notSentReport); expect(result.queuedReports).not.toContain(sentReport); }); test('should remove sent report and handle new inconsistencies', () => { - const reportStore = reduceReportStore( + const { reportStore } = reduceReportStore( defaultEmptyReportStore, defaultBaseAction, [mockErrorReport, mockMediaReport], ); expect(reportStore.queuedReports).toHaveLength(2); const [sentReport, notSentReport] = reportStore.queuedReports; const action = { type: 'SEND_REPORT_SUCCESS', payload: { reports: [sentReport], }, loadingInfo, }; - const result = reduceReportStore(reportStore, action, [ + const { reportStore: result } = reduceReportStore(reportStore, action, [ mockInconsistencyReport, ]); expect(result.queuedReports).toHaveLength(2); expect(result.queuedReports).toContain(notSentReport); expect(result.queuedReports).not.toContain(sentReport); }); }); describe('new inconsistencies test', () => { test('should handle new inconsistencies without any action', () => { - const reportStore = reduceReportStore( + const { reportStore } = reduceReportStore( { queuedReports: [mockErrorReport], enabledReports: { crashReports: true, inconsistencyReports: false, mediaReports: false, }, }, defaultBaseAction, [ { ...mockErrorReport, id: 'new-id-error' }, { ...mockMediaReport, id: 'new-id-media' }, ], ); expect(reportStore.queuedReports).toHaveLength(2); }); }); diff --git a/lib/types/store-ops-types.js b/lib/types/store-ops-types.js index df4641191..0743251d9 100644 --- a/lib/types/store-ops-types.js +++ b/lib/types/store-ops-types.js @@ -1,37 +1,39 @@ // @flow import type { DraftStoreOperation, ClientDBDraftStoreOperation, ClientDBDraftInfo, } from './draft-types.js'; import type { ClientDBMessageInfo, ClientDBMessageStoreOperation, MessageStoreOperation, ClientDBThreadMessageInfo, } from './message-types.js'; import type { ClientDBThreadInfo, ClientDBThreadStoreOperation, ThreadStoreOperation, } from './thread-types.js'; +import type { ReportStoreOperation } from '../ops/report-store-ops.js'; export type StoreOperations = { +draftStoreOperations: $ReadOnlyArray, +threadStoreOperations: $ReadOnlyArray, +messageStoreOperations: $ReadOnlyArray, + +reportStoreOperations: $ReadOnlyArray, }; export type ClientDBStoreOperations = { +draftStoreOperations?: $ReadOnlyArray, +threadStoreOperations?: $ReadOnlyArray, +messageStoreOperations?: $ReadOnlyArray, }; export type ClientDBStore = { +messages: $ReadOnlyArray, +drafts: $ReadOnlyArray, +threads: $ReadOnlyArray, +messageStoreThreads: $ReadOnlyArray, }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index 697be14aa..75ce0af65 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,498 +1,500 @@ // @flow import { AppState as NativeAppState, Platform, Alert } from 'react-native'; import Orientation from 'react-native-orientation-locker'; 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, } from 'lib/actions/user-actions.js'; import baseReducer from 'lib/reducers/master-reducer.js'; import { processThreadStoreOperations } from 'lib/reducers/thread-reducer.js'; import { invalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/session-utils.js'; import { isStaff } from 'lib/shared/staff-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.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 { defaultConnectionInfo } from 'lib/types/socket-types.js'; import type { ThreadStoreOperation } from 'lib/types/thread-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { resetUserStateActionType, updateDimensionsActiveType, updateConnectivityActiveType, updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, setStoreLoadedActionType, type Action, } from './action-types.js'; import { remoteReduxDevServerConfig } from './dev-tools.js'; import { defaultDimensionsInfo } from './dimensions-updater.react.js'; import { persistConfig, setPersistor } from './persist.js'; import { processDBStoreOperations } from './redux-utils.js'; import type { AppState } from './state-types.js'; import reduceGlobalThemeInfo from './theme-reducer.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import reactotron from '../reactotron.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultConnectivityInfo } from '../types/connectivity.js'; import { defaultGlobalThemeInfo } from '../types/themes.js'; import { isStaffRelease } from '../utils/staff-utils.js'; import { defaultURLPrefix, natNodeServer, setCustomServer, getDevServerHostname, } from '../utils/url-utils.js'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, draftStore: { drafts: {} }, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, inconsistencyReports: [], }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: 0, }, storeLoaded: false, updatesCurrentAsOf: 0, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, cookie: null, deviceToken: null, dataLoaded: false, urlPrefix: defaultURLPrefix, customServer: natNodeServer, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, connection: defaultConnectionInfo(Platform.OS), 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: {}, }, }: AppState); function reducer(state: AppState = defaultState, action: Action) { 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 = 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 = 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.type === logOutActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) || (action.type === deleteAccountActionTypes.success && invalidSessionDowngrade( state, action.payload.currentUserInfo, action.payload.preRequestUserState, )) ) { return state; } if ( (action.type === setNewSessionActionType && action.payload.sessionChange.currentUserInfo && invalidSessionRecovery( state, action.payload.sessionChange.currentUserInfo, action.payload.logInActionSource, )) || ((action.type === logInActionTypes.success || action.type === siweAuthActionTypes.success) && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.logInActionSource, )) ) { return state; } state = { ...state, globalThemeInfo: reduceGlobalThemeInfo(state.globalThemeInfo, action), }; if (action.type === setCustomServer) { return { ...state, customServer: action.payload, }; } else if (action.type === resetUserStateActionType) { const cookie = state.cookie && state.cookie.startsWith('anonymous=') ? state.cookie : null; const currentUserInfo = state.currentUserInfo && state.currentUserInfo.anonymous ? state.currentUserInfo : null; return { ...state, currentUserInfo, cookie, }; } else 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 === updateThemeInfoActionType) { // Handled above by reduceGlobalThemeInfo return state; } 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 === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; if (state.messageStore.threads[threadID]) { const updatedThreads = { [threadID]: { ...state.messageStore.threads[threadID], lastNavigatedTo: time, }, }; state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, ...updatedThreads, }, }, }; processDBStoreOperations({ draftStoreOperations: [], messageStoreOperations: [ { type: 'replace_threads', payload: { threads: updatedThreads, }, }, ], threadStoreOperations: [], + reportStoreOperations: [], }); } return state; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); state = { ...state, cookie: action.payload.sessionChange.cookie, }; } 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)); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { draftStoreOperations, threadStoreOperations, messageStoreOperations, } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; processDBStoreOperations({ draftStoreOperations, messageStoreOperations, threadStoreOperations: threadStoreOperationsWithUnreadFix, + reportStoreOperations: [], }); 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') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how to ' + `speak to it anymore. Please use the ${app} app to update!`, [{ 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 updatedActiveThreadInfo = { ...state.threadStore.threadInfos[activeThread], currentUser: { ...state.threadStore.threadInfos[activeThread].currentUser, unread: false, }, }; const threadStoreOperations = [ { type: 'replace', payload: { id: activeThread, threadInfo: updatedActiveThreadInfo, }, }, ]; const updatedThreadStore = processThreadStoreOperations( 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 };