diff --git a/native/data/core-data-provider.react.js b/native/data/core-data-provider.react.js index bb833aa8d..f3ee882e9 100644 --- a/native/data/core-data-provider.react.js +++ b/native/data/core-data-provider.react.js @@ -1,141 +1,142 @@ // @flow import * as React from 'react'; import { useSelector } from 'react-redux'; +import { commCoreModule } from '../native-modules'; import { type CoreData, defaultCoreData, CoreDataContext } from './core-data'; type Props = { +children: React.Node, }; function CoreDataProvider(props: Props): React.Node { const [draftCache, setDraftCache] = React.useState< $PropertyType<$PropertyType, 'data'>, >(defaultCoreData.drafts.data); React.useEffect(() => { (async () => { - const fetchedDrafts = await global.CommCoreModule.getAllDrafts(); + const fetchedDrafts = await commCoreModule.getAllDrafts(); setDraftCache(prevDrafts => { const mergedDrafts = {}; for (const draftObj of fetchedDrafts) { mergedDrafts[draftObj.key] = draftObj.text; } for (const key in prevDrafts) { const value = prevDrafts[key]; if (!value) { continue; } mergedDrafts[key] = value; } return mergedDrafts; }); })(); }, []); const removeAllDrafts = React.useCallback(async () => { const oldDrafts = draftCache; setDraftCache({}); try { - return await global.CommCoreModule.removeAllDrafts(); + return await commCoreModule.removeAllDrafts(); } catch (e) { setDraftCache(oldDrafts); throw e; } }, [draftCache]); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const prevViewerIDRef = React.useRef(); React.useEffect(() => { if (!viewerID) { return; } if (prevViewerIDRef.current === viewerID) { return; } if (prevViewerIDRef.current) { removeAllDrafts(); } prevViewerIDRef.current = viewerID; }, [viewerID, removeAllDrafts]); /** * wrapper for updating the draft state receiving an array of drafts * if you want to add/update the draft, pass the draft with non-empty text * if you pass a draft with !!text == false * it will remove this entry from the cache */ const setDrafts = React.useCallback( (newDrafts: $ReadOnlyArray<{ +key: string, +text: ?string }>) => { setDraftCache(prevDrafts => { const result = { ...prevDrafts }; newDrafts.forEach(draft => { if (draft.text) { result[draft.key] = draft.text; } else { delete result[draft.key]; } }); return result; }); }, [], ); const updateDraft = React.useCallback( async (draft: { +key: string, +text: string }) => { const prevDraftText = draftCache[draft.key]; setDrafts([draft]); try { - return await global.CommCoreModule.updateDraft(draft); + return await commCoreModule.updateDraft(draft); } catch (e) { setDrafts([{ key: draft.key, text: prevDraftText }]); throw e; } }, [draftCache, setDrafts], ); const moveDraft = React.useCallback( async (prevKey: string, newKey: string) => { const value = draftCache[prevKey]; if (!value) { return false; } setDrafts([ { key: newKey, text: value }, { key: prevKey, text: null }, ]); try { - return await global.CommCoreModule.moveDraft(prevKey, newKey); + return await commCoreModule.moveDraft(prevKey, newKey); } catch (e) { setDrafts([ { key: newKey, text: null }, { key: prevKey, text: value }, ]); throw e; } }, [draftCache, setDrafts], ); const coreData = React.useMemo( () => ({ drafts: { data: draftCache, updateDraft, moveDraft, }, }), [draftCache, updateDraft, moveDraft], ); return ( {props.children} ); } export default CoreDataProvider; diff --git a/native/data/core-data.js b/native/data/core-data.js index 9af70555b..6910e0bb2 100644 --- a/native/data/core-data.js +++ b/native/data/core-data.js @@ -1,53 +1,55 @@ // @flow import * as React from 'react'; import { draftKeyFromThreadID } from 'lib/shared/thread-utils'; +import { commCoreModule } from '../native-modules'; + export type UpdateDraft = (draft: { +key: string, +text: string, }) => Promise; export type MoveDraft = (prevKey: string, nextKey: string) => Promise; export type CoreData = { +drafts: { +data: { +[key: string]: string }, +updateDraft: UpdateDraft, +moveDraft: MoveDraft, }, }; const defaultCoreData = Object.freeze({ drafts: { data: ({}: { +[key: string]: string }), - updateDraft: global.CommCoreModule.updateDraft, - moveDraft: global.CommCoreModule.moveDraft, + updateDraft: commCoreModule.updateDraft, + moveDraft: commCoreModule.moveDraft, }, }); const CoreDataContext: React.Context = React.createContext( defaultCoreData, ); type ThreadDrafts = { +draft: string, +moveDraft: MoveDraft, +updateDraft: UpdateDraft, }; const useDrafts = (threadID: ?string): ThreadDrafts => { const coreData = React.useContext(CoreDataContext); return React.useMemo( () => ({ draft: threadID ? coreData.drafts.data[draftKeyFromThreadID(threadID)] ?? '' : '', updateDraft: coreData.drafts.updateDraft, moveDraft: coreData.drafts.moveDraft, }), [coreData, threadID], ); }; export { defaultCoreData, CoreDataContext, useDrafts }; diff --git a/native/data/sensitive-data-cleaner.react.js b/native/data/sensitive-data-cleaner.react.js index e9f31c996..bd634e9e6 100644 --- a/native/data/sensitive-data-cleaner.react.js +++ b/native/data/sensitive-data-cleaner.react.js @@ -1,38 +1,39 @@ // @flow import * as React from 'react'; import ExitApp from 'react-native-exit-app'; +import { commCoreModule } from '../native-modules'; import { useSelector } from '../redux/redux-utils'; function SensitiveDataCleaner(): null { const currentLoggedInUserID = useSelector(state => state.currentUserInfo?.anonymous ? null : state.currentUserInfo?.id, ); React.useEffect(() => { (async () => { try { - const databaseCurrentUserInfoID = await global.CommCoreModule.getCurrentUserID(); + const databaseCurrentUserInfoID = await commCoreModule.getCurrentUserID(); if ( databaseCurrentUserInfoID && databaseCurrentUserInfoID !== currentLoggedInUserID ) { - await global.CommCoreModule.clearSensitiveData(); + await commCoreModule.clearSensitiveData(); } if (currentLoggedInUserID) { - await global.CommCoreModule.setCurrentUserID(currentLoggedInUserID); + await commCoreModule.setCurrentUserID(currentLoggedInUserID); } } catch (e) { if (__DEV__) { throw e; } else { console.log(e); ExitApp.exitApp(); } } })(); }, [currentLoggedInUserID]); return null; } export { SensitiveDataCleaner }; diff --git a/native/data/sqlite-context-provider.js b/native/data/sqlite-context-provider.js index 8b27a153d..168014a71 100644 --- a/native/data/sqlite-context-provider.js +++ b/native/data/sqlite-context-provider.js @@ -1,75 +1,76 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { setMessageStoreMessages } from 'lib/actions/message-actions.js'; import { setThreadStoreActionType } from 'lib/actions/thread-actions'; import { sqliteLoadFailure } from 'lib/actions/user-actions'; import { fetchNewCookieFromNativeCredentials } from 'lib/utils/action-utils'; import { convertClientDBThreadInfosToRawThreadInfos } from 'lib/utils/thread-ops-utils'; +import { commCoreModule } from '../native-modules'; import { useSelector } from '../redux/redux-utils'; import { SQLiteContext } from './sqlite-context'; type Props = { +children: React.Node, }; function SQLiteContextProvider(props: Props): React.Node { const [storeLoaded, setStoreLoaded] = React.useState(false); const dispatch = useDispatch(); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated), ); const cookie = useSelector(state => state.cookie); const urlPrefix = useSelector(state => state.urlPrefix); React.useEffect(() => { if (storeLoaded || !rehydrateConcluded) { return; } (async () => { try { - const threads = await global.CommCoreModule.getAllThreads(); + const threads = await commCoreModule.getAllThreads(); const threadInfosFromDB = convertClientDBThreadInfosToRawThreadInfos( threads, ); dispatch({ type: setThreadStoreActionType, payload: { threadInfos: threadInfosFromDB }, }); - const messages = await global.CommCoreModule.getAllMessages(); + const messages = await commCoreModule.getAllMessages(); dispatch({ type: setMessageStoreMessages, payload: messages, }); } catch { await fetchNewCookieFromNativeCredentials( dispatch, cookie, urlPrefix, sqliteLoadFailure, ); } finally { setStoreLoaded(true); } })(); }, [storeLoaded, urlPrefix, rehydrateConcluded, cookie, dispatch]); const contextValue = React.useMemo( () => ({ storeLoaded, }), [storeLoaded], ); return ( {props.children} ); } export { SQLiteContextProvider }; diff --git a/native/native-modules.js b/native/native-modules.js new file mode 100644 index 000000000..0822561a2 --- /dev/null +++ b/native/native-modules.js @@ -0,0 +1,5 @@ +// @flow + +import type { Spec } from './schema/CommCoreModuleSchema'; + +export const commCoreModule: Spec = global.CommCoreModule; diff --git a/native/redux/persist.js b/native/redux/persist.js index ab433c8a2..17d9da075 100644 --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -1,455 +1,456 @@ // @flow import AsyncStorage from '@react-native-community/async-storage'; import invariant from 'invariant'; import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createMigrate, createTransform } from 'redux-persist'; import type { Transform } from 'redux-persist/es/types'; import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils'; import { getContainingThreadID, getCommunity } from 'lib/shared/thread-utils'; import { unshimMessageStore } from 'lib/shared/unshim-utils'; import { defaultEnabledApps } from 'lib/types/enabled-apps'; import { defaultCalendarFilters } from 'lib/types/filter-types'; import { type LocalMessageInfo, type MessageStore, messageTypes, type ClientDBMessageStoreOperation, type RawMessageInfo, } from 'lib/types/message-types'; import { defaultConnectionInfo } from 'lib/types/socket-types'; import { translateRawMessageInfoToClientDBMessageInfo } from 'lib/utils/message-ops-utils'; import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils'; +import { commCoreModule } from '../native-modules'; import { defaultNotifPermissionAlertInfo } from '../push/alerts'; import { defaultDeviceCameraInfo } from '../types/camera'; import { defaultGlobalThemeInfo } from '../types/themes'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration'; import type { AppState } from './state-types'; const migrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, }), [2]: (state: AppState) => ({ ...state, messageSentFromRoute: [], }), [3]: state => ({ 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, threadIDsToNotifIDs: state.threadIDsToNotifIDs, 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 => ({ ...state, threadInfos: undefined, threadStore: { threadInfos: state.threadInfos, inconsistencyResponses: [], }, }), [7]: state => ({ ...state, lastUserInteraction: undefined, sessionID: undefined, entryStore: { ...state.entryStore, inconsistencyResponses: [], }, }), [8]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, connection: defaultConnectionInfo(Platform.OS), watchedThreadIDs: [], entryStore: { ...state.entryStore, actualizedCalendarQuery: undefined, }, }), [9]: (state: AppState) => ({ ...state, connection: { ...state.connection, lateResponses: [], }, }), [10]: (state: AppState) => ({ ...state, nextLocalID: highestLocalIDSelector(state) + 1, connection: { ...state.connection, showDisconnectedBar: false, }, messageStore: { ...state.messageStore, local: {}, }, }), [11]: (state: AppState) => ({ ...state, messageStore: 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 => ({ ...state, threadStore: { ...state.threadStore, inconsistencyReports: inconsistencyResponsesToReports( state.threadStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: inconsistencyResponsesToReports( state.entryStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, queuedReports: [], }), [16]: state => { const result = { ...state, messageSentFromRoute: undefined, dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous, }; if (state.navInfo) { result.navInfo = { ...state.navInfo, navigationState: undefined, }; } return result; }, [17]: state => ({ ...state, userInfos: undefined, userStore: { userInfos: state.userInfos, inconsistencyResponses: [], }, }), [18]: state => ({ ...state, userStore: { userInfos: state.userStore.userInfos, inconsistencyReports: [], }, }), [19]: state => { const threadInfos = {}; 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: unshimMessageStore(state.messageStore, [ messageTypes.UPDATE_RELATIONSHIP, ]), }), [21]: (state: AppState) => ({ ...state, messageStore: unshimMessageStore(state.messageStore, [ messageTypes.CREATE_SIDEBAR, messageTypes.SIDEBAR_SOURCE, ]), }), [22]: state => { for (const key in state.drafts) { const value = state.drafts[key]; - global.CommCoreModule.updateDraft({ + commCoreModule.updateDraft({ key, text: value, }); } return { ...state, drafts: undefined, }; }, [23]: state => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [24]: state => ({ ...state, enabledApps: defaultEnabledApps, }), [25]: state => ({ ...state, crashReportsEnabled: __DEV__, }), [26]: state => { 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 => ({ ...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 => { const threadParentToChildren = {}; 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 = {}; 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 updatedThreadInfos = migrateThreadStoreForEditThreadPermissions( state.threadStore.threadInfos, ); 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] }, })), ]; - const processingResult: boolean = global.CommCoreModule.processThreadStoreOperationsSync( + const processingResult: boolean = commCoreModule.processThreadStoreOperationsSync( convertThreadStoreOperationsToClientDBOperations(operations), ); if (!processingResult) { 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]), })), ]; - const processingResult: boolean = global.CommCoreModule.processMessageStoreOperationsSync( + const processingResult: boolean = commCoreModule.processMessageStoreOperationsSync( operations, ); if (!processingResult) { return { ...state, cookie: null }; } 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(...)`. // eslint-disable-next-line no-unused-vars type PersistedThreadMessageInfo = { +startReached: boolean, +lastNavigatedTo: number, +lastPruned: number, }; type PersistedMessageStore = { +local: { +[id: string]: LocalMessageInfo }, +currentAsOf: number, +threads: { +[threadID: string]: PersistedThreadMessageInfo }, }; type RehydratedMessageStore = $Diff< MessageStore, { +messages: { +[id: string]: RawMessageInfo } }, >; const messageStoreMessagesBlocklistTransform: Transform = createTransform( (state: MessageStore): PersistedMessageStore => { const { messages, threads, ...messageStoreSansMessages } = state; // We also do not want to persist `messageStore.threads[ID].messageIDs` // because they can be deterministically computed based on messages we have // from SQLite const threadsToPersist = {}; for (const threadID in threads) { const { messageIDs, ...threadsData } = threads[threadID]; threadsToPersist[threadID] = threadsData; } return { ...messageStoreSansMessages, threads: threadsToPersist }; }, (state: PersistedMessageStore): RehydratedMessageStore => { const { threads: persistedThreads, ...messageStore } = state; const threads = {}; for (const threadID in persistedThreads) { threads[threadID] = { ...persistedThreads[threadID], messageIDs: [] }; } return { ...messageStore, threads }; }, { whitelist: ['messageStore'] }, ); const persistConfig = { key: 'root', storage: AsyncStorage, blacklist: [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', ], debug: __DEV__, version: 31, transforms: [messageStoreMessagesBlocklistTransform], migrate: (createMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void), }; -const codeVersion: number = global.CommCoreModule.getCodeVersion(); +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 b14ada5ff..79467ba13 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,493 +1,494 @@ // @flow import { AppState as NativeAppState, Platform, Alert } from 'react-native'; import ExitApp from 'react-native-exit-app'; 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 { setDeviceTokenActionTypes } from 'lib/actions/device-actions'; import { logOutActionTypes, deleteAccountActionTypes, logInActionTypes, sqliteOpFailure, } from 'lib/actions/user-actions'; import baseReducer from 'lib/reducers/master-reducer'; import { processThreadStoreOperations } from 'lib/reducers/thread-reducer'; import { invalidSessionDowngrade, invalidSessionRecovery, } from 'lib/shared/account-utils'; import { defaultEnabledApps } from 'lib/types/enabled-apps'; import { defaultCalendarFilters } from 'lib/types/filter-types'; import type { Dispatch, BaseAction } from 'lib/types/redux-types'; import type { SetSessionPayload } from 'lib/types/session-types'; import { defaultConnectionInfo, incrementalStateSyncActionType, } from 'lib/types/socket-types'; import type { ThreadStoreOperation } from 'lib/types/thread-types'; import { updateTypes } from 'lib/types/update-types'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger'; import { setNewSessionActionType } from 'lib/utils/action-utils'; import { convertMessageStoreOperationsToClientDBOperations } from 'lib/utils/message-ops-utils'; import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils'; +import { commCoreModule } from '../native-modules'; import { defaultNavInfo } from '../navigation/default-state'; import { getGlobalNavContext } from '../navigation/icky-global'; import { activeMessageListSelector } from '../navigation/nav-selectors'; import { defaultNotifPermissionAlertInfo } from '../push/alerts'; import { reduceThreadIDsToNotifIDs } from '../push/reducer'; import reactotron from '../reactotron'; import { defaultDeviceCameraInfo } from '../types/camera'; import { defaultConnectivityInfo } from '../types/connectivity'; import { defaultGlobalThemeInfo } from '../types/themes'; import { defaultURLPrefix, natNodeServer, setCustomServer, getDevServerHostname, } from '../utils/url-utils'; import { resetUserStateActionType, recordNotifPermissionAlertActionType, recordAndroidNotificationActionType, clearAndroidNotificationsActionType, rescindAndroidNotificationActionType, updateDimensionsActiveType, updateConnectivityActiveType, updateThemeInfoActionType, updateDeviceCameraInfoActionType, updateDeviceOrientationActionType, updateThreadLastNavigatedActionType, backgroundActionTypes, setReduxStateActionType, type Action, } from './action-types'; import { remoteReduxDevServerConfig } from './dev-tools'; import { defaultDimensionsInfo } from './dimensions-updater.react'; import { persistConfig, setPersistor } from './persist'; import type { AppState } from './state-types'; const defaultState = ({ navInfo: defaultNavInfo, currentUserInfo: null, entryStore: { entryInfos: {}, daysToEntries: {}, lastUserInteractionCalendar: 0, }, threadStore: { threadInfos: {}, }, userStore: { userInfos: {}, inconsistencyReports: [], }, messageStore: { messages: {}, threads: {}, local: {}, currentAsOf: 0, }, updatesCurrentAsOf: 0, loadingStatuses: {}, calendarFilters: defaultCalendarFilters, cookie: null, deviceToken: null, dataLoaded: false, urlPrefix: defaultURLPrefix, customServer: natNodeServer, threadIDsToNotifIDs: {}, 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, }: AppState); function reducer(state: AppState = defaultState, action: Action) { if (action.type === setReduxStateActionType) { return action.payload.state; } 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.source, )) || (action.type === logInActionTypes.success && invalidSessionRecovery( state, action.payload.currentUserInfo, action.payload.source, )) ) { return state; } const threadIDsToNotifIDs = reduceThreadIDsToNotifIDs( state.threadIDsToNotifIDs, action, ); state = { ...state, threadIDsToNotifIDs }; if ( action.type === recordAndroidNotificationActionType || action.type === clearAndroidNotificationsActionType || action.type === rescindAndroidNotificationActionType ) { return state; } if (action.type === setCustomServer) { return { ...state, customServer: action.payload, }; } else if (action.type === recordNotifPermissionAlertActionType) { return { ...state, notifPermissionAlertInfo: { totalAlerts: state.notifPermissionAlertInfo.totalAlerts + 1, lastAlertTime: action.payload.time, }, }; } 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) { return { ...state, globalThemeInfo: { ...state.globalThemeInfo, ...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 === setDeviceTokenActionTypes.success) { return { ...state, deviceToken: action.payload, }; } else if (action.type === updateThreadLastNavigatedActionType) { const { threadID, time } = action.payload; if (state.messageStore.threads[threadID]) { state = { ...state, messageStore: { ...state.messageStore, threads: { ...state.messageStore.threads, [threadID]: { ...state.messageStore.threads[threadID], lastNavigatedTo: time, }, }, }, }; } return state; } if (action.type === setNewSessionActionType) { sessionInvalidationAlert(action.payload); state = { ...state, cookie: action.payload.sessionChange.cookie, }; } else if (action.type === incrementalStateSyncActionType) { let wipeDeviceToken = false; for (const update of action.payload.updatesResult.newUpdates) { if ( update.type === updateTypes.BAD_DEVICE_TOKEN && update.deviceToken === state.deviceToken ) { wipeDeviceToken = true; break; } } if (wipeDeviceToken) { state = { ...state, deviceToken: null, }; } } const baseReducerResult = baseReducer(state, (action: BaseAction)); state = baseReducerResult.state; const { storeOperations } = baseReducerResult; const { threadStoreOperations, messageStoreOperations } = storeOperations; const fixUnreadActiveThreadResult = fixUnreadActiveThread(state, action); state = fixUnreadActiveThreadResult.state; const threadStoreOperationsWithUnreadFix = [ ...threadStoreOperations, ...fixUnreadActiveThreadResult.threadStoreOperations, ]; const convertedThreadStoreOperations = convertThreadStoreOperationsToClientDBOperations( threadStoreOperationsWithUnreadFix, ); const convertedMessageStoreOperations = convertMessageStoreOperationsToClientDBOperations( messageStoreOperations, ); (async () => { try { const promises = []; if (convertedThreadStoreOperations.length > 0) { promises.push( - global.CommCoreModule.processThreadStoreOperations( + commCoreModule.processThreadStoreOperations( convertedThreadStoreOperations, ), ); } if (convertedMessageStoreOperations.length > 0) { promises.push( - global.CommCoreModule.processMessageStoreOperations( + commCoreModule.processMessageStoreOperations( convertedMessageStoreOperations, ), ); } await Promise.all(promises); } catch { dispatch({ type: setNewSessionActionType, payload: { sessionChange: { cookie: null, cookieInvalidated: false, currentUserInfo: state.currentUserInfo, }, preRequestUserState: { currentUserInfo: state.currentUserInfo, sessionID: undefined, cookie: state.cookie, }, error: null, source: sqliteOpFailure, }, }); await persistor.flush(); ExitApp.exitApp(); } })(); 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 middlewares = [thunk, reduxLoggerMiddleware]; if (__DEV__) { const createDebugger = require('redux-flipper').default; middlewares.push(createDebugger()); } const middleware = applyMiddleware(...middlewares); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { const { composeWithDevTools } = require('remote-redux-devtools/src'); 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/selectors/socket-selectors.js b/native/selectors/socket-selectors.js index 5c6df72cf..216163f38 100644 --- a/native/selectors/socket-selectors.js +++ b/native/selectors/socket-selectors.js @@ -1,87 +1,87 @@ // @flow import { createSelector } from 'reselect'; import { getClientResponsesSelector, sessionStateFuncSelector, } from 'lib/selectors/socket-selectors'; import { createOpenSocketFunction } from 'lib/shared/socket-utils'; import type { ClientServerRequest, ClientClientResponse, } from 'lib/types/request-types'; import type { SessionIdentification, SessionState, } from 'lib/types/session-types'; import type { OneTimeKeyGenerator } from 'lib/types/socket-types'; import { calendarActiveSelector } from '../navigation/nav-selectors'; import type { AppState } from '../redux/state-types'; import type { NavPlusRedux } from '../types/selector-types'; const openSocketSelector: (state: AppState) => () => WebSocket = createSelector( (state: AppState) => state.urlPrefix, // We don't actually use the cookie in the socket open function, but we do use // it in the initial message, and when the cookie changes the socket needs to // be reopened. By including the cookie here, whenever the cookie changes this // function will change, which tells the Socket component to restart the // connection. (state: AppState) => state.cookie, createOpenSocketFunction, ); const sessionIdentificationSelector: ( state: AppState, ) => SessionIdentification = createSelector( (state: AppState) => state.cookie, (cookie: ?string): SessionIdentification => ({ cookie }), ); function oneTimeKeyGenerator(inc: number): string { // todo replace this hard code with something like - // global.CommCoreModule.generateOneTimeKeys() + // commCoreModule.generateOneTimeKeys() let str = Date.now().toString() + '_' + inc.toString() + '_'; while (str.length < 43) { str += Math.random().toString(36).substr(2, 5); } str = str.substr(0, 43); return str; } const nativeGetClientResponsesSelector: ( input: NavPlusRedux, ) => ( serverRequests: $ReadOnlyArray, ) => $ReadOnlyArray = createSelector( (input: NavPlusRedux) => getClientResponsesSelector(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( getClientResponsesFunc: ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, serverRequests: $ReadOnlyArray, ) => $ReadOnlyArray, calendarActive: boolean, ) => (serverRequests: $ReadOnlyArray) => getClientResponsesFunc(calendarActive, oneTimeKeyGenerator, serverRequests), ); const nativeSessionStateFuncSelector: ( input: NavPlusRedux, ) => () => SessionState = createSelector( (input: NavPlusRedux) => sessionStateFuncSelector(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( sessionStateFunc: (calendarActive: boolean) => SessionState, calendarActive: boolean, ) => () => sessionStateFunc(calendarActive), ); export { openSocketSelector, sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, };