diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index 5281bc214..ab556e88e 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,827 +1,386 @@ // @flow import html from 'common-tags/lib/html/index.js'; import { detect as detectBrowser } from 'detect-browser'; import type { $Response, $Request } from 'express'; import fs from 'fs'; -import _isEqual from 'lodash/fp/isEqual.js'; -import _keyBy from 'lodash/fp/keyBy.js'; import * as React from 'react'; // eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; -import t from 'tcomb'; import { promisify } from 'util'; import { inviteLinkURL } from 'lib/facts/links.js'; -import { baseLegalPolicies } from 'lib/facts/policies.js'; import stores from 'lib/facts/stores.js'; -import { daysToEntriesFromEntryInfos } from 'lib/reducers/entry-reducer.js'; -import { freshMessageStore } from 'lib/reducers/message-reducer.js'; -import { mostRecentlyReadThread } from 'lib/selectors/thread-selectors.js'; -import { mostRecentMessageTimestamp } from 'lib/shared/message-utils.js'; -import { - threadHasPermission, - threadIsPending, - parsePendingThreadID, - createPendingThread, -} from 'lib/shared/thread-utils.js'; -import { defaultWebEnabledApps } from 'lib/types/enabled-apps.js'; -import { entryStoreValidator } from 'lib/types/entry-types.js'; -import { defaultCalendarFilters } from 'lib/types/filter-types.js'; -import { keyserverStoreValidator } from 'lib/types/keyserver-types.js'; -import { inviteLinksStoreValidator } from 'lib/types/link-types.js'; -import { - defaultNumberPerThread, - messageStoreValidator, -} from 'lib/types/message-types.js'; -import { defaultEnabledReports } from 'lib/types/report-types.js'; -import { defaultConnectionInfo } from 'lib/types/socket-types.js'; -import { threadPermissions } from 'lib/types/thread-permission-types.js'; -import { threadTypes } from 'lib/types/thread-types-enum.js'; -import { threadStoreValidator } from 'lib/types/thread-types.js'; -import { - currentUserInfoValidator, - userInfosValidator, -} from 'lib/types/user-types.js'; -import { currentDateInTimeZone } from 'lib/utils/date-utils.js'; -import { ServerError } from 'lib/utils/errors.js'; -import { promiseAll } from 'lib/utils/promises.js'; -import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; -import { infoFromURL, urlInfoValidator } from 'lib/utils/url-utils.js'; -import { - tBool, - tNumber, - tShape, - tString, - ashoatKeyserverID, -} from 'lib/utils/validation-utils.js'; import getTitle from 'web/title/getTitle.js'; -import { navInfoValidator } from 'web/types/nav-types.js'; -import { navInfoFromURL } from 'web/url-utils.js'; - -import { fetchEntryInfos } from '../fetchers/entry-fetchers.js'; -import { fetchPrimaryInviteLinks } from '../fetchers/link-fetchers.js'; -import { fetchMessageInfos } from '../fetchers/message-fetchers.js'; -import { hasAnyNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; -import { fetchThreadInfos } from '../fetchers/thread-fetchers.js'; -import { - fetchCurrentUserInfo, - fetchKnownUserInfos, - fetchUserInfos, -} from '../fetchers/user-fetchers.js'; -import { getWebPushConfig } from '../push/providers.js'; -import { setNewSession } from '../session/cookies.js'; + import { Viewer } from '../session/viewer.js'; -import { streamJSON, waitForStream } from '../utils/json-stream.js'; +import { waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL, getCommAppURLFacts, } from '../utils/urls.js'; -import { validateOutput, validateInput } from '../utils/validation-utils.js'; const { renderToNodeStream } = ReactDOMServer; const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Inter:wght@400;500;600&display=swap'; const localFontsURL = 'fonts/local-fonts.css'; async function getFontsURL() { try { await access(localFontsURL); return localFontsURL; } catch { return googleFontsURL; } } type AssetInfo = { +jsURL: string, +fontsURL: string, +cssInclude: string, +olmFilename: string, +commQueryExecutorFilename: string, +opaqueURL: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontsURL = await getFontsURL(); assetInfo = { jsURL: 'http://localhost:8080/dev.build.js', fontsURL, cssInclude: '', olmFilename: '', commQueryExecutorFilename: '', opaqueURL: 'http://localhost:8080/opaque-ke.wasm', }; return assetInfo; } try { const manifestString = await readFile('../web/dist/manifest.json', 'utf8'); const manifest = JSON.parse(manifestString); const webworkersManifestString = await readFile( '../web/dist/webworkers/manifest.json', 'utf8', ); const webworkersManifest = JSON.parse(webworkersManifestString); assetInfo = { jsURL: `compiled/${manifest['browser.js']}`, fontsURL: googleFontsURL, cssInclude: html` `, olmFilename: manifest['olm.wasm'], commQueryExecutorFilename: webworkersManifest['comm_query_executor.wasm'], opaqueURL: `compiled/${manifest['comm_opaque2_wasm_bg.wasm']}`, }; return assetInfo; } catch { throw new Error( 'Could not load manifest.json for web build. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } let webpackCompiledRootComponent: ?React.ComponentType<{}> = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe web/dist doesn't always exist const webpackBuild = await import('web/dist/app.build.cjs'); webpackCompiledRootComponent = webpackBuild.app.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } -const initialReduxStateValidator = tShape({ - navInfo: navInfoValidator, - deviceID: t.Nil, - currentUserInfo: currentUserInfoValidator, - draftStore: t.irreducible('default draftStore', _isEqual({ drafts: {} })), - entryStore: entryStoreValidator, - threadStore: threadStoreValidator, - userStore: tShape({ - userInfos: userInfosValidator, - inconsistencyReports: t.irreducible( - 'default inconsistencyReports', - _isEqual([]), - ), - }), - messageStore: messageStoreValidator, - loadingStatuses: t.irreducible('default loadingStatuses', _isEqual({})), - calendarFilters: t.irreducible( - 'defaultCalendarFilters', - _isEqual(defaultCalendarFilters), - ), - communityPickerStore: t.irreducible( - 'default communityPickerStore', - _isEqual({ chat: null, calendar: null }), - ), - windowDimensions: t.irreducible( - 'default windowDimensions', - _isEqual({ width: 0, height: 0 }), - ), - notifPermissionAlertInfo: t.irreducible( - 'default notifPermissionAlertInfo', - _isEqual(defaultNotifPermissionAlertInfo), - ), - actualizedCalendarQuery: tShape({ - startDate: t.String, - endDate: t.String, - filters: t.irreducible('default filters', _isEqual(defaultCalendarFilters)), - }), - watchedThreadIDs: t.irreducible('default watchedThreadIDs', _isEqual([])), - lifecycleState: tString('active'), - enabledApps: t.irreducible( - 'defaultWebEnabledApps', - _isEqual(defaultWebEnabledApps), - ), - reportStore: t.irreducible( - 'default reportStore', - _isEqual({ - enabledReports: defaultEnabledReports, - queuedReports: [], - }), - ), - nextLocalID: tNumber(0), - deviceToken: t.Nil, - dataLoaded: t.Boolean, - windowActive: tBool(true), - userPolicies: t.irreducible('default userPolicies', _isEqual({})), - cryptoStore: t.irreducible( - 'default cryptoStore', - _isEqual({ - primaryIdentityKeys: null, - notificationIdentityKeys: null, - }), - ), - pushApiPublicKey: t.maybe(t.String), - _persist: t.Nil, - commServicesAccessToken: t.Nil, - inviteLinksStore: inviteLinksStoreValidator, - keyserverStore: keyserverStoreValidator, - initialStateLoaded: tBool(false), -}); - async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { - const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); - const { basePath, baseDomain } = appURLFacts; + const { basePath } = getAppURLFactsFromRequestURL(req.originalUrl); const baseURL = basePath.replace(/\/$/, ''); - const urlPrefix = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); - const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( - viewer.id, - baseLegalPolicies, - ); - - const initialNavInfoPromise = (async () => { - try { - let urlInfo = infoFromURL(decodeURI(req.url)); - - try { - urlInfo = await validateInput(viewer, urlInfoValidator, urlInfo, true); - } catch (exc) { - // We should still be able to handle older links - if (exc.message !== 'invalid_client_id_prefix') { - throw exc; - } - } - - let backupInfo = { - now: currentDateInTimeZone(viewer.timeZone), - }; - // Some user ids in selectedUserList might not exist in the userStore - // (e.g. they were included in the results of the user search endpoint) - // Because of that we keep their userInfos inside the navInfo. - if (urlInfo.selectedUserList) { - const fetchedUserInfos = await fetchUserInfos(urlInfo.selectedUserList); - const userInfos = {}; - for (const userID in fetchedUserInfos) { - const userInfo = fetchedUserInfos[userID]; - if (userInfo.username) { - userInfos[userID] = userInfo; - } - } - backupInfo = { userInfos, ...backupInfo }; - } - return navInfoFromURL(urlInfo, backupInfo); - } catch (e) { - throw new ServerError(e.message); - } - })(); - - const calendarQueryPromise = (async () => { - const initialNavInfo = await initialNavInfoPromise; - return { - startDate: initialNavInfo.startDate, - endDate: initialNavInfo.endDate, - filters: defaultCalendarFilters, - }; - })(); - const messageSelectionCriteria = { joinedThreads: true }; - const initialTime = Date.now(); const assetInfoPromise = getAssetInfo(); - const threadInfoPromise = fetchThreadInfos(viewer); - const messageInfoPromise = fetchMessageInfos( - viewer, - messageSelectionCriteria, - defaultNumberPerThread, - ); - const entryInfoPromise = (async () => { - const calendarQuery = await calendarQueryPromise; - return await fetchEntryInfos(viewer, [calendarQuery]); - })(); - const currentUserInfoPromise = fetchCurrentUserInfo(viewer); - const userInfoPromise = fetchKnownUserInfos(viewer); - - const sessionIDPromise = (async () => { - const calendarQuery = await calendarQueryPromise; - if (viewer.loggedIn) { - await setNewSession(viewer, calendarQuery, initialTime); - } - return viewer.sessionID; - })(); - - const threadStorePromise = (async () => { - const [{ threadInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ - threadInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - return { threadInfos: hasNotAcknowledgedPolicies ? {} : threadInfos }; - })(); - const messageStorePromise = (async () => { - const [ - { threadInfos }, - { rawMessageInfos, truncationStatuses }, - hasNotAcknowledgedPolicies, - ] = await Promise.all([ - threadInfoPromise, - messageInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - if (hasNotAcknowledgedPolicies) { - return { - messages: {}, - threads: {}, - local: {}, - currentAsOf: { [ashoatKeyserverID]: 0 }, - }; - } - const { messageStore: freshStore } = freshMessageStore( - rawMessageInfos, - truncationStatuses, - mostRecentMessageTimestamp(rawMessageInfos, initialTime), - threadInfos, - ); - return freshStore; - })(); - const entryStorePromise = (async () => { - const [{ rawEntryInfos }, hasNotAcknowledgedPolicies] = await Promise.all([ - entryInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - if (hasNotAcknowledgedPolicies) { - return { - entryInfos: {}, - daysToEntries: {}, - lastUserInteractionCalendar: 0, - }; - } - return { - entryInfos: _keyBy('id')(rawEntryInfos), - daysToEntries: daysToEntriesFromEntryInfos(rawEntryInfos), - lastUserInteractionCalendar: initialTime, - }; - })(); - const userStorePromise = (async () => { - const [userInfos, hasNotAcknowledgedPolicies] = await Promise.all([ - userInfoPromise, - hasNotAcknowledgedPoliciesPromise, - ]); - return { - userInfos: hasNotAcknowledgedPolicies ? {} : userInfos, - inconsistencyReports: [], - }; - })(); - - const navInfoPromise = (async () => { - const [ - { threadInfos }, - messageStore, - currentUserInfo, - userStore, - finalNavInfo, - ] = await Promise.all([ - threadInfoPromise, - messageStorePromise, - currentUserInfoPromise, - userStorePromise, - initialNavInfoPromise, - ]); - - const requestedActiveChatThreadID = finalNavInfo.activeChatThreadID; - if ( - requestedActiveChatThreadID && - !threadIsPending(requestedActiveChatThreadID) && - !threadHasPermission( - threadInfos[requestedActiveChatThreadID], - threadPermissions.VISIBLE, - ) - ) { - finalNavInfo.activeChatThreadID = null; - } - - if (!finalNavInfo.activeChatThreadID) { - const mostRecentThread = mostRecentlyReadThread( - messageStore, - threadInfos, - ); - if (mostRecentThread) { - finalNavInfo.activeChatThreadID = mostRecentThread; - } - } - - if ( - finalNavInfo.activeChatThreadID && - threadIsPending(finalNavInfo.activeChatThreadID) && - finalNavInfo.pendingThread?.id !== finalNavInfo.activeChatThreadID - ) { - const pendingThreadData = parsePendingThreadID( - finalNavInfo.activeChatThreadID, - ); - if ( - pendingThreadData && - pendingThreadData.threadType !== threadTypes.SIDEBAR && - currentUserInfo.id - ) { - const { userInfos } = userStore; - const members = [...pendingThreadData.memberIDs, currentUserInfo.id] - .map(id => { - const userInfo = userInfos[id]; - if (!userInfo || !userInfo.username) { - return undefined; - } - const { username } = userInfo; - return { id, username }; - }) - .filter(Boolean); - const newPendingThread = createPendingThread({ - viewerID: currentUserInfo.id, - threadType: pendingThreadData.threadType, - members, - }); - finalNavInfo.activeChatThreadID = newPendingThread.id; - finalNavInfo.pendingThread = newPendingThread; - } - } - - return finalNavInfo; - })(); - const currentAsOfPromise = (async () => { - const hasNotAcknowledgedPolicies = await hasNotAcknowledgedPoliciesPromise; - return hasNotAcknowledgedPolicies ? 0 : initialTime; - })(); - - const pushApiPublicKeyPromise = (async () => { - const pushConfig = await getWebPushConfig(); - if (!pushConfig) { - if (process.env.NODE_ENV !== 'development') { - console.warn('keyserver/secrets/web_push_config.json should exist'); - } - return null; - } - return pushConfig.publicKey; - })(); - - const inviteLinksStorePromise = (async () => { - const primaryInviteLinks = await fetchPrimaryInviteLinks(viewer); - const links = {}; - for (const link of primaryInviteLinks) { - if (link.primary) { - links[link.communityID] = { - primaryLink: link, - }; - } - } - return { - links, - }; - })(); - - const keyserverStorePromise = (async () => { - const { sessionID, updatesCurrentAsOf } = await promiseAll({ - sessionID: sessionIDPromise, - updatesCurrentAsOf: currentAsOfPromise, - }); - - return { - keyserverInfos: { - [ashoatKeyserverID]: { - cookie: undefined, - sessionID, - updatesCurrentAsOf, - urlPrefix, - connection: defaultConnectionInfo, - lastCommunicatedPlatformDetails: null, - }, - }, - }; - })(); - const { jsURL, fontsURL, cssInclude, olmFilename, opaqueURL, commQueryExecutorFilename, } = await assetInfoPromise; // prettier-ignore res.write(html` ${getTitle(0)} ${cssInclude}
`); const Loading = await loadingPromise; const reactStream = renderToNodeStream(); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); - res.write(html` + res.end(html`
`); } const inviteSecretRegex = /^[a-z0-9]+$/i; // On native, if this responder is called, it means that the app isn't // installed. async function inviteResponder(req: $Request, res: $Response): Promise { const { secret } = req.params; const userAgent = req.get('User-Agent'); const detectionResult = detectBrowser(userAgent); if (detectionResult.os === 'Android OS') { const isSecretValid = inviteSecretRegex.test(secret); const referrer = isSecretValid ? `&referrer=${encodeURIComponent(`utm_source=invite/${secret}`)}` : ''; const redirectUrl = `${stores.googlePlayUrl}${referrer}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } else if (detectionResult.os !== 'iOS') { const urlFacts = getCommAppURLFacts(); const baseDomain = urlFacts?.baseDomain ?? ''; const basePath = urlFacts?.basePath ?? '/'; const redirectUrl = `${baseDomain}${basePath}handle/invite/${secret}`; res.writeHead(301, { Location: redirectUrl, }); res.end(); return; } const fontsURL = await getFontsURL(); res.end(html` Comm

Comm

To join this community, download the Comm app and reopen this invite link

Download Comm Invite Link
Visit Comm’s website arrow up right `); } export { websiteResponder, inviteResponder }; diff --git a/web/redux/default-state.js b/web/redux/default-state.js new file mode 100644 index 000000000..1c59131b9 --- /dev/null +++ b/web/redux/default-state.js @@ -0,0 +1,93 @@ +// @flow + +import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; +import { defaultCalendarFilters } from 'lib/types/filter-types.js'; +import { defaultConnectionInfo } from 'lib/types/socket-types.js'; +import { isDev } from 'lib/utils/dev-utils.js'; +import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; + +import type { AppState } from './redux-setup.js'; + +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: {}, + inconsistencyReports: [], + }, + messageStore: { + messages: {}, + threads: {}, + local: {}, + currentAsOf: { [ashoatKeyserverID]: 0 }, + }, + windowActive: true, + pushApiPublicKey: null, + cryptoStore: { + primaryAccount: null, + primaryIdentityKeys: null, + notificationAccount: null, + notificationIdentityKeys: null, + }, + deviceID: null, + windowDimensions: { width: window.width, height: window.height }, + loadingStatuses: {}, + calendarFilters: defaultCalendarFilters, + deviceToken: null, + dataLoaded: false, + notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, + watchedThreadIDs: [], + lifecycleState: 'active', + enabledApps: defaultEnabledApps, + 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: { + [ashoatKeyserverID]: { + updatesCurrentAsOf: 0, + urlPrefix: isDev + ? 'http://localhost:3000/comm' + : 'https://web.comm.app', + connection: { ...defaultConnectionInfo }, + lastCommunicatedPlatformDetails: null, + }, + }, + }, + initialStateLoaded: false, +}); + +export { defaultWebState }; diff --git a/web/redux/persist.js b/web/redux/persist.js index d0eebde9a..dba11b497 100644 --- a/web/redux/persist.js +++ b/web/redux/persist.js @@ -1,254 +1,249 @@ // @flow import invariant from 'invariant'; import { getStoredState, purgeStoredState, createTransform, } from 'redux-persist'; import storage from 'redux-persist/es/storage/index.js'; import type { Transform } from 'redux-persist/es/types.js'; import type { PersistConfig } from 'redux-persist/src/types.js'; import { createAsyncMigrate, type StorageMigrationFunction, } from 'lib/shared/create-async-migrate.js'; import type { KeyserverInfo, KeyserverStore, } from 'lib/types/keyserver-types.js'; import { defaultConnectionInfo, type ConnectionInfo, } from 'lib/types/socket-types.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertDraftStoreToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import commReduxStorageEngine from './comm-redux-storage-engine.js'; import type { AppState } from './redux-setup.js'; import { getDatabaseModule } from '../database/database-module-provider.js'; import { isSQLiteSupported } from '../database/utils/db-utils.js'; import { workerRequestMessageTypes } from '../types/worker-types.js'; -declare var preloadedState: AppState; - const migrations = { [1]: async state => { const { primaryIdentityPublicKey, ...stateWithoutPrimaryIdentityPublicKey } = state; return { ...stateWithoutPrimaryIdentityPublicKey, cryptoStore: { primaryAccount: null, primaryIdentityKeys: null, notificationAccount: null, notificationIdentityKeys: null, }, }; }, [2]: async state => { return state; }, [3]: async (state: AppState) => { let newState = state; if (state.draftStore) { newState = { ...newState, draftStore: convertDraftStoreToNewIDSchema(state.draftStore), }; } const databaseModule = await getDatabaseModule(); const isDatabaseSupported = await databaseModule.isDatabaseSupported(); if (!isDatabaseSupported) { return newState; } const stores = await databaseModule.schedule({ type: workerRequestMessageTypes.GET_CLIENT_STORE, }); invariant(stores?.store, 'Stores should exist'); await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations: generateIDSchemaMigrationOpsForDrafts( stores.store.drafts, ), }, }); return newState; }, [4]: async state => { const { lastCommunicatedPlatformDetails, keyserverStore, ...rest } = state; return { ...rest, keyserverStore: { ...keyserverStore, keyserverInfos: { ...keyserverStore.keyserverInfos, [ashoatKeyserverID]: { ...keyserverStore.keyserverInfos[ashoatKeyserverID], lastCommunicatedPlatformDetails, }, }, }, }; }, [5]: async state => { const databaseModule = await getDatabaseModule(); const isDatabaseSupported = await databaseModule.isDatabaseSupported(); if (!isDatabaseSupported) { return state; } if (!state.draftStore) { return state; } const { drafts } = state.draftStore; const draftStoreOperations = []; for (const key in drafts) { const text = drafts[key]; draftStoreOperations.push({ type: 'update', payload: { key, text }, }); } await databaseModule.schedule({ type: workerRequestMessageTypes.PROCESS_STORE_OPERATIONS, storeOperations: { draftStoreOperations }, }); return state; }, }; const persistWhitelist = [ 'enabledApps', 'deviceID', 'cryptoStore', 'notifPermissionAlertInfo', 'commServicesAccessToken', 'keyserverStore', ]; const rootKey = 'root'; const migrateStorageToSQLite: StorageMigrationFunction = async debug => { const databaseModule = await getDatabaseModule(); const isSupported = await databaseModule.isDatabaseSupported(); if (!isSupported) { return undefined; } const oldStorage = await getStoredState({ storage, key: rootKey }); if (!oldStorage) { return undefined; } purgeStoredState({ storage, key: rootKey }); if (debug) { console.log('redux-persist: migrating state to SQLite storage'); } // We need to simulate the keyserverStoreTransform for data stored in the // old local storage (because redux persist will only run it for the // sqlite storage which is empty in this case). // We don't just use keyserverStoreTransform.out(oldStorage) because // the transform might change in the future, but we need to treat // this code like migration code (it shouldn't change). if (oldStorage?._persist?.version === 4) { - const { connection, updatesCurrentAsOf, sessionID } = - preloadedState.keyserverStore.keyserverInfos[ashoatKeyserverID]; - + const defaultConnection = defaultConnectionInfo; return { ...oldStorage, keyserverStore: { ...oldStorage.keyserverStore, keyserverInfos: { ...oldStorage.keyserverStore.keyserverInfos, [ashoatKeyserverID]: { ...oldStorage.keyserverStore.keyserverInfos[ashoatKeyserverID], - connection, - updatesCurrentAsOf, - sessionID, + connection: { ...defaultConnection }, + updatesCurrentAsOf: 0, + sessionID: null, }, }, }, }; } return oldStorage; }; type PersistedKeyserverInfo = $Diff< KeyserverInfo, { +connection: ConnectionInfo, +updatesCurrentAsOf: number, +sessionID?: ?string, }, >; type PersistedKeyserverStore = { +keyserverInfos: { +[key: string]: PersistedKeyserverInfo }, }; const keyserverStoreTransform: Transform = createTransform( (state: KeyserverStore): PersistedKeyserverStore => { const keyserverInfos = {}; for (const key in state.keyserverInfos) { const { connection, updatesCurrentAsOf, sessionID, ...rest } = state.keyserverInfos[key]; keyserverInfos[key] = rest; } return { ...state, keyserverInfos, }; }, (state: PersistedKeyserverStore): KeyserverStore => { const keyserverInfos = {}; const defaultConnection = defaultConnectionInfo; for (const key in state.keyserverInfos) { keyserverInfos[key] = { ...state.keyserverInfos[key], connection: { ...defaultConnection }, - updatesCurrentAsOf: - preloadedState.keyserverStore.keyserverInfos[key].updatesCurrentAsOf, - sessionID: preloadedState.keyserverStore.keyserverInfos[key].sessionID, + updatesCurrentAsOf: 0, + sessionID: null, }; } return { ...state, keyserverInfos, }; }, { whitelist: ['keyserverStore'] }, ); const persistConfig: PersistConfig = { key: rootKey, storage: commReduxStorageEngine, whitelist: isSQLiteSupported() ? persistWhitelist : [...persistWhitelist, 'draftStore'], migrate: (createAsyncMigrate( migrations, { debug: isDev }, migrateStorageToSQLite, ): any), version: 5, transforms: [keyserverStoreTransform], }; export { persistConfig }; diff --git a/web/root.js b/web/root.js index e9870b8d8..f3ebcc459 100644 --- a/web/root.js +++ b/web/root.js @@ -1,50 +1,49 @@ // @flow import localforage from 'localforage'; import * as React from 'react'; import { Provider } from 'react-redux'; import { Router, Route } from 'react-router'; import { createStore, applyMiddleware, type Store } from 'redux'; import { composeWithDevTools } from 'redux-devtools-extension/logOnlyInProduction.js'; import { persistReducer, persistStore } from 'redux-persist'; import thunk from 'redux-thunk'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import App from './app.react.js'; import { SQLiteDataHandler } from './database/sqlite-data-handler.js'; import { localforageConfig } from './database/utils/constants.js'; import ErrorBoundary from './error-boundary.react.js'; +import { defaultWebState } from './redux/default-state.js'; import InitialReduxStateGate from './redux/initial-state-gate.js'; import { persistConfig } from './redux/persist.js'; import { type AppState, type Action, reducer } from './redux/redux-setup.js'; import history from './router-history.js'; import Socket from './socket.react.js'; localforage.config(localforageConfig); -declare var preloadedState: AppState; - const persistedReducer = persistReducer(persistConfig, reducer); const store: Store = createStore( persistedReducer, - preloadedState, + defaultWebState, composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), ); const persistor = persistStore(store); const RootProvider = (): React.Node => ( ); export default RootProvider;