diff --git a/.eslintrc.json b/.eslintrc.json index 8a0166333..cf07108d5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,77 +1,78 @@ { "root": true, "env": { "es6": true }, "extends": [ "eslint:recommended", "plugin:react/recommended", "plugin:flowtype/recommended", "plugin:import/errors", "plugin:import/warnings", "plugin:prettier/recommended", "prettier" ], "parser": "babel-eslint", "plugins": ["react", "react-hooks", "flowtype", "monorepo", "import"], "rules": { // Prettier is configured to keep lines to 80 chars, but there are two issues: // - It doesn't handle comments (leaves them as-is) // - It makes all import statements take one line (reformats them) // We want ESLint to warn us in the first case, but not in the second case // since Prettier forces us in the second case. By setting code to 420, we // make sure ESLint defers to Prettier for import statements. "max-len": ["error", { "code": 420, "comments": 80, "ignoreUrls": true }], "flowtype/require-valid-file-annotation": ["error", "always"], "flowtype/require-exact-type": ["error", "never"], "curly": "error", "linebreak-style": "error", "semi": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "monorepo/no-relative-import": "error", "no-empty": ["error", { "allowEmptyCatch": true }], "import/no-unresolved": 0, "no-unused-vars": ["error", { "ignoreRestSiblings": true }], "react/prop-types": ["error", { "skipUndeclared": true }], "no-shadow": 1, "no-var": "error", + "import/extensions": ["error", "always"], "import/order": [ "warn", { "newlines-between": "always", "alphabetize": { "order": "asc", "caseInsensitive": true }, "groups": [["builtin", "external"], "internal"] } ], "prefer-const": "error", "react/jsx-curly-brace-presence": [ "error", { "props": "never", "children": "ignore" } ], "eqeqeq": ["error", "always"] }, "settings": { "react": { "version": "detect" }, "import/ignore": ["react-native"], "import/internal-regex": "^(lib|native|keyserver|web)/" }, "overrides": [ { "files": "*.cjs", "env": { "node": true, "commonjs": true }, "rules": { "flowtype/require-valid-file-annotation": 0, "flowtype/require-exact-type": 0 } } ] } diff --git a/desktop/src/auto-update.js b/desktop/src/auto-update.js index a14817eca..9d8a16712 100644 --- a/desktop/src/auto-update.js +++ b/desktop/src/auto-update.js @@ -1,27 +1,28 @@ // @flow +// eslint-disable-next-line import/extensions import { app, ipcMain, autoUpdater } from 'electron/main'; const getUpdateUrl = version => `https://electron-update.commtechnologies.org/update/${process.platform}/${version}`; export function initAutoUpdate(): void { autoUpdater.setFeedURL({ url: getUpdateUrl(app.getVersion()) }); // Check for new updates every 10 minutes const updateIntervalMs = 10 * 60_000; const scheduleCheckForUpdates = () => { setTimeout(() => autoUpdater.checkForUpdates(), updateIntervalMs); }; scheduleCheckForUpdates(); autoUpdater.on('update-not-available', scheduleCheckForUpdates); autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { autoUpdater.setFeedURL({ url: getUpdateUrl(releaseName) }); scheduleCheckForUpdates(); }); ipcMain.on('update-to-new-version', () => autoUpdater.quitAndInstall()); } diff --git a/desktop/src/main.js b/desktop/src/main.js index 4fcd31e53..7c1700729 100644 --- a/desktop/src/main.js +++ b/desktop/src/main.js @@ -1,287 +1,288 @@ // @flow import { app, BrowserWindow, shell, Menu, ipcMain, systemPreferences, autoUpdater, + // eslint-disable-next-line import/extensions } from 'electron/main'; import fs from 'fs'; import path from 'path'; import { initAutoUpdate } from './auto-update.js'; import { handleSquirrelEvent } from './handle-squirrel-event.js'; const isDev = process.env.ENV === 'dev'; const url = isDev ? 'http://localhost/comm/' : 'https://web.comm.app'; const isMac = process.platform === 'darwin'; const scrollbarCSS = fs.promises.readFile( path.resolve(__dirname, '../scrollbar.css'), 'utf8', ); const setApplicationMenu = () => { let mainMenu = []; if (isMac) { mainMenu = [ { label: app.name, submenu: [ { role: 'about' }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit' }, ], }, ]; } const viewMenu = { label: 'View', submenu: [ { role: 'reload' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, { role: 'toggleDevTools' }, ], }; const windowMenu = { label: 'Window', submenu: [ { role: 'minimize' }, ...(isMac ? [ { type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }, ] : [{ role: 'close' }]), ], }; const menu = Menu.buildFromTemplate([ ...mainMenu, { role: 'fileMenu' }, { role: 'editMenu' }, viewMenu, windowMenu, ]); Menu.setApplicationMenu(menu); }; const createMainWindow = () => { const win = new BrowserWindow({ show: false, width: 1300, height: 800, minWidth: 1100, minHeight: 600, titleBarStyle: 'hidden', trafficLightPosition: { x: 20, y: 24 }, titleBarOverlay: { color: '#0A0A0A', symbolColor: '#FFFFFF', height: 64, }, backgroundColor: '#0A0A0A', webPreferences: { preload: path.resolve(__dirname, 'preload.js'), }, }); const updateNavigationState = () => { win.webContents.send('on-navigate', { canGoBack: win.webContents.canGoBack(), canGoForward: win.webContents.canGoForward(), }); }; win.webContents.on('did-navigate-in-page', updateNavigationState); const clearHistory = () => { win.webContents.clearHistory(); updateNavigationState(); }; ipcMain.on('clear-history', clearHistory); const doubleClickTopBar = () => { if (isMac) { // Possible values for AppleActionOnDoubleClick are Maximize, // Minimize or None. We handle the last two inside this if. // Maximize (which is the only behaviour for other platforms) // is handled in the later block. const action = systemPreferences.getUserDefault( 'AppleActionOnDoubleClick', 'string', ); if (action === 'None') { return; } else if (action === 'Minimize') { win.minimize(); return; } } if (win.isMaximized()) { win.unmaximize(); } else { win.maximize(); } }; ipcMain.on('double-click-top-bar', doubleClickTopBar); const updateDownloaded = (event, releaseNotes, releaseName) => { win.webContents.send('on-new-version-available', releaseName); }; autoUpdater.on('update-downloaded', updateDownloaded); win.on('closed', () => { ipcMain.removeListener('clear-history', clearHistory); ipcMain.removeListener('double-click-top-bar', doubleClickTopBar); autoUpdater.removeListener('update-downloaded', updateDownloaded); }); win.webContents.setWindowOpenHandler(({ url: openURL }) => { shell.openExternal(openURL); // Returning 'deny' prevents a new electron window from being created return { action: 'deny' }; }); (async () => { const css = await scrollbarCSS; win.webContents.insertCSS(css); })(); win.loadURL(url); return win; }; const createSplashWindow = () => { const win = new BrowserWindow({ width: 300, height: 300, resizable: false, frame: false, alwaysOnTop: true, center: true, backgroundColor: '#111827', }); win.loadFile(path.resolve(__dirname, '../pages/splash.html')); return win; }; const createErrorWindow = () => { const win = new BrowserWindow({ show: false, width: 400, height: 300, resizable: false, center: true, titleBarStyle: 'hidden', trafficLightPosition: { x: 20, y: 24 }, backgroundColor: '#111827', }); win.on('close', () => { app.quit(); }); win.loadFile(path.resolve(__dirname, '../pages/error.html')); return win; }; const show = () => { const splash = createSplashWindow(); const error = createErrorWindow(); const main = createMainWindow(); let loadedSuccessfully = true; main.webContents.on('did-fail-load', () => { loadedSuccessfully = false; if (!splash.isDestroyed()) { splash.destroy(); } if (!error.isDestroyed()) { error.show(); } setTimeout(() => { loadedSuccessfully = true; main.loadURL(url); }, 1000); }); main.webContents.on('did-finish-load', () => { if (loadedSuccessfully) { if (!splash.isDestroyed()) { splash.destroy(); } if (!error.isDestroyed()) { error.destroy(); } main.show(); } }); }; const run = () => { app.setName('Comm'); setApplicationMenu(); (async () => { await app.whenReady(); if (app.isPackaged) { try { initAutoUpdate(); } catch (error) { console.error(error); } } ipcMain.on('set-badge', (event, value) => { if (isMac) { app.dock.setBadge(value?.toString() ?? ''); } }); ipcMain.on('get-version', event => { event.returnValue = app.getVersion().toString(); }); show(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { show(); } }); })(); app.on('window-all-closed', () => { if (!isMac) { app.quit(); } }); }; if (app.isPackaged && process.platform === 'win32') { if (!handleSquirrelEvent()) { run(); } } else { run(); } diff --git a/desktop/src/preload.js b/desktop/src/preload.js index 825756158..e62610431 100644 --- a/desktop/src/preload.js +++ b/desktop/src/preload.js @@ -1,26 +1,27 @@ // @flow +// eslint-disable-next-line import/extensions import { contextBridge, ipcRenderer } from 'electron/renderer'; import type { ElectronBridge } from 'lib/types/electron-types.js'; const bridge: ElectronBridge = { onNavigate: callback => { const withEvent = (event, ...args) => callback(...args); ipcRenderer.on('on-navigate', withEvent); return () => ipcRenderer.removeListener('on-navigate', withEvent); }, clearHistory: () => ipcRenderer.send('clear-history'), doubleClickTopBar: () => ipcRenderer.send('double-click-top-bar'), setBadge: value => ipcRenderer.send('set-badge', value), version: ipcRenderer.sendSync('get-version'), onNewVersionAvailable: callback => { const withEvent = (event, ...args) => callback(...args); ipcRenderer.on('on-new-version-available', withEvent); return () => ipcRenderer.removeListener('on-new-version-available', withEvent); }, updateToNewVersion: () => ipcRenderer.send('update-to-new-version'), }; contextBridge.exposeInMainWorld('electronContextBridge', bridge); diff --git a/keyserver/src/responders/landing-handler.js b/keyserver/src/responders/landing-handler.js index 64f421a05..9c5ec9505 100644 --- a/keyserver/src/responders/landing-handler.js +++ b/keyserver/src/responders/landing-handler.js @@ -1,218 +1,219 @@ // @flow -import html from 'common-tags/lib/html'; +import html from 'common-tags/lib/html/index.js'; import type { $Response, $Request } from 'express'; import fs from 'fs'; import * as React from 'react'; +// eslint-disable-next-line import/extensions import ReactDOMServer from 'react-dom/server'; import { promisify } from 'util'; import { isValidPrimaryIdentityPublicKey, isValidSIWENonce, } from 'lib/utils/siwe-utils.js'; import { getMessageForException } from './utils.js'; import { type LandingSSRProps } from '../landing/landing-ssr.react.js'; import { waitForStream } from '../utils/json-stream.js'; import { getAndAssertLandingURLFacts } from '../utils/urls.js'; async function landingHandler(req: $Request, res: $Response) { try { await landingResponder(req, res); } catch (e) { console.warn(e); if (!res.headersSent) { res.status(500).send(getMessageForException(e)); } } } const access = promisify(fs.access); const readFile = promisify(fs.readFile); const googleFontsURL = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@500&family=IBM+Plex+Sans:wght@400;500&display=swap'; const iaDuoFontsURL = 'fonts/duo.css'; const localFontsURL = 'fonts/local-fonts.css'; async function getDevFontURLs(): Promise<$ReadOnlyArray> { try { await access(localFontsURL); return [localFontsURL, iaDuoFontsURL]; } catch { return [googleFontsURL, iaDuoFontsURL]; } } type AssetInfo = { +jsURL: string, +fontURLs: $ReadOnlyArray, +cssInclude: string, }; let assetInfo: ?AssetInfo = null; async function getAssetInfo() { if (assetInfo) { return assetInfo; } if (process.env.NODE_ENV === 'development') { const fontURLs = await getDevFontURLs(); assetInfo = { jsURL: 'http://localhost:8082/dev.build.js', fontURLs, cssInclude: '', }; return assetInfo; } try { const assetsString = await readFile('../landing/dist/assets.json', 'utf8'); const assets = JSON.parse(assetsString); assetInfo = { jsURL: `compiled/${assets.browser.js}`, fontURLs: [googleFontsURL, iaDuoFontsURL], cssInclude: html` `, }; return assetInfo; } catch { throw new Error( 'Could not load assets.json for landing build. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } type LandingApp = React.ComponentType; let webpackCompiledRootComponent: ?LandingApp = null; async function getWebpackCompiledRootComponentForSSR() { if (webpackCompiledRootComponent) { return webpackCompiledRootComponent; } try { // $FlowFixMe landing/dist doesn't always exist const webpackBuild = await import('landing/dist/landing.build.cjs'); webpackCompiledRootComponent = webpackBuild.default.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load landing.build.cjs. ' + 'Did you forget to run `yarn dev` in the landing folder?', ); } } const { renderToNodeStream } = ReactDOMServer; async function landingResponder(req: $Request, res: $Response) { const siweNonce = req.header('siwe-nonce'); if ( siweNonce !== null && siweNonce !== undefined && !isValidSIWENonce(siweNonce) ) { res.status(400).send({ message: 'Invalid nonce in siwe-nonce header.', }); return; } const siwePrimaryIdentityPublicKey = req.header( 'siwe-primary-identity-public-key', ); if ( siwePrimaryIdentityPublicKey !== null && siwePrimaryIdentityPublicKey !== undefined && !isValidPrimaryIdentityPublicKey(siwePrimaryIdentityPublicKey) ) { res.status(400).send({ message: 'Invalid primary identity public key in siwe-primary-identity-public-key header.', }); return; } const [{ jsURL, fontURLs, cssInclude }, LandingSSR] = await Promise.all([ getAssetInfo(), getWebpackCompiledRootComponentForSSR(), ]); const fontsInclude = fontURLs .map(url => ``) .join(''); const urlFacts = getAndAssertLandingURLFacts(); const { basePath } = urlFacts; // prettier-ignore res.write(html` Comm ${fontsInclude} ${cssInclude}
`); // We remove trailing slash for `react-router` const routerBasename = basePath.replace(/\/$/, ''); const clientPath = routerBasename + req.url; const reactStream = renderToNodeStream( , ); reactStream.pipe(res, { end: false }); await waitForStream(reactStream); const siweNonceString = siweNonce ? `"${siweNonce}"` : 'null'; const siwePrimaryIdentityPublicKeyString = siwePrimaryIdentityPublicKey ? `"${siwePrimaryIdentityPublicKey}"` : 'null'; // prettier-ignore res.end(html`
`); } export default landingHandler; diff --git a/keyserver/src/responders/website-responders.js b/keyserver/src/responders/website-responders.js index b458b6f98..8972c5bb6 100644 --- a/keyserver/src/responders/website-responders.js +++ b/keyserver/src/responders/website-responders.js @@ -1,403 +1,404 @@ // @flow -import html from 'common-tags/lib/html'; +import html from 'common-tags/lib/html/index.js'; import type { $Response, $Request } from 'express'; import fs from 'fs'; 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 { promisify } from 'util'; import { baseLegalPolicies } from 'lib/facts/policies.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 { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { defaultNumberPerThread } from 'lib/types/message-types.js'; import { defaultEnabledReports } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import { threadPermissions, threadTypes } from 'lib/types/thread-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 getTitle from 'web/title/getTitle.js'; import { navInfoFromURL } from 'web/url-utils.js'; import { fetchEntryInfos } from '../fetchers/entry-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, } from '../fetchers/user-fetchers.js'; import { setNewSession } from '../session/cookies.js'; import { Viewer } from '../session/viewer.js'; import { streamJSON, waitForStream } from '../utils/json-stream.js'; import { getAppURLFactsFromRequestURL } from '../utils/urls.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 }; 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: '', }; return assetInfo; } try { const assetsString = await readFile('../web/dist/assets.json', 'utf8'); const assets = JSON.parse(assetsString); assetInfo = { jsURL: `compiled/${assets.browser.js}`, fontsURL: googleFontsURL, cssInclude: html` `, }; return assetInfo; } catch { throw new Error( 'Could not load assets.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.default.default; return webpackCompiledRootComponent; } catch { throw new Error( 'Could not load app.build.cjs. ' + 'Did you forget to run `yarn dev` in the web folder?', ); } } async function websiteResponder( viewer: Viewer, req: $Request, res: $Response, ): Promise { const appURLFacts = getAppURLFactsFromRequestURL(req.originalUrl); const { basePath, baseDomain } = appURLFacts; const baseURL = basePath.replace(/\/$/, ''); const baseHref = baseDomain + baseURL; const loadingPromise = getWebpackCompiledRootComponentForSSR(); const hasNotAcknowledgedPoliciesPromise = hasAnyNotAcknowledgedPolicies( viewer.id, baseLegalPolicies, ); let initialNavInfo; try { initialNavInfo = navInfoFromURL(req.url, { now: currentDateInTimeZone(viewer.timeZone), }); } catch (e) { throw new ServerError(e.message); } const calendarQuery = { 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 = fetchEntryInfos(viewer, [calendarQuery]); const currentUserInfoPromise = fetchCurrentUserInfo(viewer); const userInfoPromise = fetchKnownUserInfos(viewer); const sessionIDPromise = (async () => { 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: 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, ] = await Promise.all([ threadInfoPromise, messageStorePromise, currentUserInfoPromise, userStorePromise, ]); const finalNavInfo = initialNavInfo; 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 .map(id => userInfos[id]) .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 { jsURL, fontsURL, cssInclude } = 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`
`); } export { websiteResponder }; diff --git a/landing/script.js b/landing/script.js index c14f06c71..62004ffa9 100644 --- a/landing/script.js +++ b/landing/script.js @@ -1,16 +1,17 @@ // @flow localStorage.clear(); import 'isomorphic-fetch'; import invariant from 'invariant'; import React from 'react'; +// eslint-disable-next-line import/extensions import { hydrateRoot } from 'react-dom/client'; import Root from './root.js'; const root = document.getElementById('react-root'); invariant(root, "cannot find id='react-root' element!"); hydrateRoot(root, ); diff --git a/lib/utils/wagmi-utils.js b/lib/utils/wagmi-utils.js index 8effa0a0d..825565761 100644 --- a/lib/utils/wagmi-utils.js +++ b/lib/utils/wagmi-utils.js @@ -1,37 +1,39 @@ // @flow import { chain, configureChains, createClient } from 'wagmi'; +// eslint-disable-next-line import/extensions import { alchemyProvider } from 'wagmi/providers/alchemy'; +// eslint-disable-next-line import/extensions import { publicProvider } from 'wagmi/providers/public'; // details can be found https://0.6.x.wagmi.sh/docs/providers/configuring-chains type WagmiChainConfiguration = { +chains: mixed, +provider: mixed, }; function configureWagmiChains(alchemyKey: ?string): WagmiChainConfiguration { const availableProviders = alchemyKey ? [alchemyProvider({ apiKey: alchemyKey })] : [publicProvider()]; const { chains, provider } = configureChains( [chain.mainnet], availableProviders, ); return { chains, provider }; } type WagmiClientInput = { +provider: mixed, +connectors?: mixed, }; function createWagmiClient(input: WagmiClientInput): mixed { const { provider, connectors } = input; return createClient({ autoConnect: true, connectors, provider, }); } export { configureWagmiChains, createWagmiClient }; diff --git a/native/components/swipeable.js b/native/components/swipeable.js index 0c5d8f8cd..5bed21967 100644 --- a/native/components/swipeable.js +++ b/native/components/swipeable.js @@ -1,117 +1,118 @@ // @flow import * as React from 'react'; import { Animated, View } from 'react-native'; +// eslint-disable-next-line import/extensions import SwipeableComponent from 'react-native-gesture-handler/Swipeable'; import { useSelector } from 'react-redux'; import Button from './button.react.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; type BaseProps = { +buttonWidth: number, +rightActions: $ReadOnlyArray<{ +key: string, +onPress: () => mixed, +color: ?string, +content: React.Node, }>, +onSwipeableRightWillOpen?: () => void, +innerRef: { current: ?SwipeableComponent, }, +children?: React.Node, }; type Props = { ...BaseProps, +windowWidth: number, +colors: Colors, +styles: typeof unboundStyles, }; class Swipeable extends React.PureComponent { static defaultProps = { rightActions: [], }; renderRightActions = progress => { const actions = this.props.rightActions.map( ({ key, content, color, onPress }, i) => { const translation = progress.interpolate({ inputRange: [0, 1], outputRange: [ (this.props.rightActions.length - i) * this.props.buttonWidth, 0, ], }); return ( ); }, ); return {actions}; }; render() { return ( {this.props.children} ); } } const unboundStyles = { action: { height: '100%', alignItems: 'center', justifyContent: 'center', }, actionsContainer: { flexDirection: 'row', }, }; const ConnectedSwipeable: React.ComponentType = React.memo( function ConnectedSwipeable(props: BaseProps) { const styles = useStyles(unboundStyles); const windowWidth = useSelector(state => state.dimensions.width); const colors = useColors(); return ( ); }, ); export default ConnectedSwipeable; diff --git a/native/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js index c1e3b4a14..5d33dfc8c 100644 --- a/native/navigation/app-navigator.react.js +++ b/native/navigation/app-navigator.react.js @@ -1,147 +1,147 @@ // @flow import * as SplashScreen from 'expo-splash-screen'; import * as React from 'react'; import { useSelector } from 'react-redux'; -import { PersistGate } from 'redux-persist/integration/react'; +import { PersistGate } from 'redux-persist/es/integration/react.js'; import ActionResultModal from './action-result-modal.react.js'; import { CommunityDrawerNavigator } from './community-drawer-navigator.react.js'; import { createOverlayNavigator } from './overlay-navigator.react.js'; import type { OverlayNavigationProp, OverlayNavigationHelpers, } from './overlay-navigator.react.js'; import type { RootNavigationProp } from './root-navigator.react.js'; import { ImageModalRouteName, MultimediaMessageTooltipModalRouteName, ActionResultModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, RelationshipListItemTooltipModalRouteName, RobotextMessageTooltipModalRouteName, CameraModalRouteName, VideoPlaybackModalRouteName, CommunityDrawerNavigatorRouteName, type ScreenParamList, type OverlayParamList, } from './route-names.js'; import MultimediaMessageTooltipModal from '../chat/multimedia-message-tooltip-modal.react.js'; import RobotextMessageTooltipModal from '../chat/robotext-message-tooltip-modal.react.js'; import ThreadSettingsMemberTooltipModal from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import TextMessageTooltipModal from '../chat/text-message-tooltip-modal.react.js'; import KeyboardStateContainer from '../keyboard/keyboard-state-container.react.js'; import CameraModal from '../media/camera-modal.react.js'; import ImageModal from '../media/image-modal.react.js'; import VideoPlaybackModal from '../media/video-playback-modal.react.js'; import RelationshipListItemTooltipModal from '../profile/relationship-list-item-tooltip-modal.react.js'; import PushHandler from '../push/push-handler.react.js'; import { getPersistor } from '../redux/persist.js'; import { RootContext } from '../root-context.js'; import { useLoadCommFonts } from '../themes/fonts.js'; import { waitForInteractions } from '../utils/timers.js'; let splashScreenHasHidden = false; export type AppNavigationProp< RouteName: $Keys = $Keys, > = OverlayNavigationProp; const App = createOverlayNavigator< ScreenParamList, OverlayParamList, OverlayNavigationHelpers, >(); type AppNavigatorProps = { navigation: RootNavigationProp<'App'>, ... }; function AppNavigator(props: AppNavigatorProps): React.Node { const { navigation } = props; const fontsLoaded = useLoadCommFonts(); const rootContext = React.useContext(RootContext); const storeLoadedFromLocalDatabase = useSelector(state => state.storeLoaded); const setNavStateInitialized = rootContext && rootContext.setNavStateInitialized; React.useEffect(() => { setNavStateInitialized && setNavStateInitialized(); }, [setNavStateInitialized]); const [ localSplashScreenHasHidden, setLocalSplashScreenHasHidden, ] = React.useState(splashScreenHasHidden); React.useEffect(() => { if (localSplashScreenHasHidden || !fontsLoaded) { return; } splashScreenHasHidden = true; (async () => { await waitForInteractions(); try { await SplashScreen.hideAsync(); } finally { setLocalSplashScreenHasHidden(true); } })(); }, [localSplashScreenHasHidden, fontsLoaded]); let pushHandler; if (localSplashScreenHasHidden) { pushHandler = ( ); } if (!storeLoadedFromLocalDatabase) { return null; } return ( {pushHandler} ); } export default AppNavigator; diff --git a/native/push/push-handler.react.js b/native/push/push-handler.react.js index d27b2ae63..9322867de 100644 --- a/native/push/push-handler.react.js +++ b/native/push/push-handler.react.js @@ -1,613 +1,613 @@ // @flow import * as Haptics from 'expo-haptics'; import * as React from 'react'; import { Platform, Alert, LogBox } from 'react-native'; import { Notification as InAppNotification } from 'react-native-in-app-message'; import { useDispatch } from 'react-redux'; import { setDeviceTokenActionTypes, setDeviceToken, } from 'lib/actions/device-actions.js'; import { saveMessagesActionType } from 'lib/actions/message-actions.js'; import { unreadCount, threadInfoSelector, } from 'lib/selectors/thread-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { mergePrefixIntoBody } from 'lib/shared/notif-utils.js'; import type { RawMessageInfo } from 'lib/types/message-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { type ConnectionInfo } from 'lib/types/socket-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import { useServerCall, useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/action-utils.js'; import { type NotifPermissionAlertInfo } from './alerts.js'; import { androidNotificationChannelID, handleAndroidMessage, getCommAndroidNotificationsEventEmitter, type AndroidForegroundMessage, CommAndroidNotifications, } from './android.js'; import { CommIOSNotification, type CoreIOSNotificationData, type CoreIOSNotificationDataWithRequestIdentifier, } from './comm-ios-notification.js'; import InAppNotif from './in-app-notif.react.js'; import { requestIOSPushPermissions, iosPushPermissionResponseReceived, CommIOSNotifications, getCommIOSNotificationsEventEmitter, -} from './ios'; +} from './ios.js'; import { type MessageListParams, useNavigateToThread, } from '../chat/message-list-types.js'; import { addLifecycleListener, getCurrentLifecycleState, } from '../lifecycle/lifecycle.js'; import { replaceWithThreadActionType } from '../navigation/action-types.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { recordNotifPermissionAlertActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { RootContext, type RootContextType } from '../root-context.js'; import type { EventSubscription } from '../types/react-native.js'; import { type GlobalTheme } from '../types/themes.js'; LogBox.ignoreLogs([ // react-native-in-app-message 'ForceTouchGestureHandler is not available', ]); const msInDay = 24 * 60 * 60 * 1000; type BaseProps = { +navigation: RootNavigationProp<'App'>, }; type Props = { ...BaseProps, // Navigation state +activeThread: ?string, // Redux state +unreadCount: number, +deviceToken: ?string, +threadInfos: { +[id: string]: ThreadInfo }, +notifPermissionAlertInfo: NotifPermissionAlertInfo, +connection: ConnectionInfo, +updatesCurrentAsOf: number, +activeTheme: ?GlobalTheme, +loggedIn: boolean, +navigateToThread: (params: MessageListParams) => void, // Redux dispatch functions +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +setDeviceToken: (deviceToken: ?string) => Promise, // withRootContext +rootContext: ?RootContextType, }; type State = { +inAppNotifProps: ?{ +customComponent: React.Node, +blurType: ?('xlight' | 'dark'), +onPress: () => void, }, }; class PushHandler extends React.PureComponent { state: State = { inAppNotifProps: null, }; currentState: ?string = getCurrentLifecycleState(); appStarted = 0; androidNotificationsEventSubscriptions: Array = []; initialAndroidNotifHandled = false; openThreadOnceReceived: Set = new Set(); lifecycleSubscription: ?EventSubscription; iosNotificationEventSubscriptions: Array = []; componentDidMount() { this.appStarted = Date.now(); this.lifecycleSubscription = addLifecycleListener( this.handleAppStateChange, ); this.onForeground(); if (Platform.OS === 'ios') { const commIOSNotificationsEventEmitter = getCommIOSNotificationsEventEmitter(); this.iosNotificationEventSubscriptions.push( commIOSNotificationsEventEmitter.addListener( 'remoteNotificationsRegistered', registration => this.registerPushPermissions(registration?.deviceToken), ), commIOSNotificationsEventEmitter.addListener( 'remoteNotificationsRegistrationFailed', this.failedToRegisterPushPermissions, ), commIOSNotificationsEventEmitter.addListener( 'notificationReceivedForeground', this.iosForegroundNotificationReceived, ), commIOSNotificationsEventEmitter.addListener( 'notificationOpened', this.iosNotificationOpened, ), ); } else if (Platform.OS === 'android') { CommAndroidNotifications.createChannel( androidNotificationChannelID, 'Default', CommAndroidNotifications.getConstants().NOTIFICATIONS_IMPORTANCE_HIGH, 'Comm notifications channel', ); const commAndroidNotificationsEventEmitter = getCommAndroidNotificationsEventEmitter(); this.androidNotificationsEventSubscriptions.push( commAndroidNotificationsEventEmitter.addListener( 'commAndroidNotificationsToken', this.handleAndroidDeviceToken, ), commAndroidNotificationsEventEmitter.addListener( 'commAndroidNotificationsForegroundMessage', this.androidMessageReceived, ), commAndroidNotificationsEventEmitter.addListener( 'commAndroidNotificationsNotificationOpened', this.androidNotificationOpened, ), ); } if (this.props.connection.status === 'connected') { this.updateBadgeCount(); } } componentWillUnmount() { if (this.lifecycleSubscription) { this.lifecycleSubscription.remove(); } if (Platform.OS === 'ios') { for (const iosNotificationEventSubscription of this .iosNotificationEventSubscriptions) { iosNotificationEventSubscription.remove(); } } else if (Platform.OS === 'android') { for (const androidNotificationsEventSubscription of this .androidNotificationsEventSubscriptions) { androidNotificationsEventSubscription.remove(); } this.androidNotificationsEventSubscriptions = []; } } handleAppStateChange = (nextState: ?string) => { if (!nextState || nextState === 'unknown') { return; } const lastState = this.currentState; this.currentState = nextState; if (lastState === 'background' && nextState === 'active') { this.onForeground(); this.clearNotifsOfThread(); } }; onForeground() { if (this.props.loggedIn) { this.ensurePushNotifsEnabled(); } else if (this.props.deviceToken) { // We do this in case there was a crash, so we can clear deviceToken from // any other cookies it might be set for this.setDeviceToken(this.props.deviceToken); } } componentDidUpdate(prevProps: Props, prevState: State) { if (this.props.activeThread !== prevProps.activeThread) { this.clearNotifsOfThread(); } if ( this.props.connection.status === 'connected' && (prevProps.connection.status !== 'connected' || this.props.unreadCount !== prevProps.unreadCount) ) { this.updateBadgeCount(); } for (const threadID of this.openThreadOnceReceived) { const threadInfo = this.props.threadInfos[threadID]; if (threadInfo) { this.navigateToThread(threadInfo, false); this.openThreadOnceReceived.clear(); break; } } if ( (this.props.loggedIn && !prevProps.loggedIn) || (!this.props.deviceToken && prevProps.deviceToken) ) { this.ensurePushNotifsEnabled(); } if (!this.props.loggedIn && prevProps.loggedIn) { this.clearAllNotifs(); } if ( this.state.inAppNotifProps && this.state.inAppNotifProps !== prevState.inAppNotifProps ) { Haptics.notificationAsync(); InAppNotification.show(); } } updateBadgeCount() { const curUnreadCount = this.props.unreadCount; if (Platform.OS === 'ios') { CommIOSNotifications.setBadgesCount(curUnreadCount); } else if (Platform.OS === 'android') { CommAndroidNotifications.setBadge(curUnreadCount); } } clearAllNotifs() { if (Platform.OS === 'ios') { CommIOSNotifications.removeAllDeliveredNotifications(); } else if (Platform.OS === 'android') { CommAndroidNotifications.removeAllDeliveredNotifications(); } } clearNotifsOfThread() { const { activeThread } = this.props; if (!activeThread) { return; } if (Platform.OS === 'ios') { CommIOSNotifications.getDeliveredNotifications(notifications => PushHandler.clearDeliveredIOSNotificationsForThread( activeThread, notifications, ), ); } else if (Platform.OS === 'android') { CommAndroidNotifications.removeAllActiveNotificationsForThread( activeThread, ); } } static clearDeliveredIOSNotificationsForThread( threadID: string, notifications: $ReadOnlyArray, ) { const identifiersToClear = []; for (const notification of notifications) { if (notification.threadID === threadID) { identifiersToClear.push(notification.identifier); } } if (identifiersToClear) { CommIOSNotifications.removeDeliveredNotifications(identifiersToClear); } } async ensurePushNotifsEnabled() { if (!this.props.loggedIn) { return; } if (Platform.OS === 'ios') { const missingDeviceToken = this.props.deviceToken === null || this.props.deviceToken === undefined; await requestIOSPushPermissions(missingDeviceToken); } else if (Platform.OS === 'android') { await this.ensureAndroidPushNotifsEnabled(); } } async ensureAndroidPushNotifsEnabled() { const hasPermission = await CommAndroidNotifications.hasPermission(); if (!hasPermission) { this.failedToRegisterPushPermissions(); return; } try { const fcmToken = await CommAndroidNotifications.getToken(); await this.handleAndroidDeviceToken(fcmToken); } catch (e) { this.failedToRegisterPushPermissions(); } } handleAndroidDeviceToken = async (deviceToken: string) => { this.registerPushPermissions(deviceToken); await this.handleInitialAndroidNotification(); }; async handleInitialAndroidNotification() { if (this.initialAndroidNotifHandled) { return; } this.initialAndroidNotifHandled = true; const initialNotif = await CommAndroidNotifications.getInitialNotification(); if (initialNotif) { await this.androidNotificationOpened(initialNotif); } } registerPushPermissions = (deviceToken: ?string) => { const deviceType = Platform.OS; if (deviceType !== 'android' && deviceType !== 'ios') { return; } if (deviceType === 'ios') { iosPushPermissionResponseReceived(); } if (deviceToken !== this.props.deviceToken) { this.setDeviceToken(deviceToken); } }; setDeviceToken(deviceToken: ?string) { this.props.dispatchActionPromise( setDeviceTokenActionTypes, this.props.setDeviceToken(deviceToken), ); } failedToRegisterPushPermissions = () => { this.setDeviceToken(null); if (!this.props.loggedIn) { return; } const deviceType = Platform.OS; if (deviceType === 'ios') { iosPushPermissionResponseReceived(); } else { this.showNotifAlertOnAndroid(); } }; showNotifAlertOnAndroid() { const alertInfo = this.props.notifPermissionAlertInfo; if ( (alertInfo.totalAlerts > 3 && alertInfo.lastAlertTime > Date.now() - msInDay) || (alertInfo.totalAlerts > 6 && alertInfo.lastAlertTime > Date.now() - msInDay * 3) || (alertInfo.totalAlerts > 9 && alertInfo.lastAlertTime > Date.now() - msInDay * 7) ) { return; } this.props.dispatch({ type: recordNotifPermissionAlertActionType, payload: { time: Date.now() }, }); Alert.alert( 'Unable to initialize notifs!', 'Please check your network connection, make sure Google Play ' + 'services are installed and enabled, and confirm that your Google ' + 'Play credentials are valid in the Google Play Store.', undefined, { cancelable: true }, ); } navigateToThread(threadInfo: ThreadInfo, clearChatRoutes: boolean) { if (clearChatRoutes) { this.props.navigation.dispatch({ type: replaceWithThreadActionType, payload: { threadInfo }, }); } else { this.props.navigateToThread({ threadInfo }); } } onPressNotificationForThread(threadID: string, clearChatRoutes: boolean) { const threadInfo = this.props.threadInfos[threadID]; if (threadInfo) { this.navigateToThread(threadInfo, clearChatRoutes); } else { this.openThreadOnceReceived.add(threadID); } } saveMessageInfos(messageInfosString: ?string) { if (!messageInfosString) { return; } const rawMessageInfos: $ReadOnlyArray = JSON.parse( messageInfosString, ); const { updatesCurrentAsOf } = this.props; this.props.dispatch({ type: saveMessagesActionType, payload: { rawMessageInfos, updatesCurrentAsOf }, }); } iosForegroundNotificationReceived = ( rawNotification: CoreIOSNotificationData, ) => { const notification = new CommIOSNotification(rawNotification); if (Date.now() < this.appStarted + 1500) { // On iOS, when the app is opened from a notif press, for some reason this // callback gets triggered before iosNotificationOpened. In fact this // callback shouldn't be triggered at all. To avoid weirdness we are // ignoring any foreground notification received within the first second // of the app being started, since they are most likely to be erroneous. notification.finish( CommIOSNotifications.getConstants().FETCH_RESULT_NO_DATA, ); return; } const threadID = notification.getData().threadID; const messageInfos = notification.getData().messageInfos; this.saveMessageInfos(messageInfos); let title = notification.getData().title; let body = notification.getData().body; if (title && body) { ({ title, body } = mergePrefixIntoBody({ title, body })); } else { body = notification.getMessage(); } if (body) { this.showInAppNotification(threadID, body, title); } else { console.log( 'Non-rescind foreground notification without alert received!', ); } notification.finish( CommIOSNotifications.getConstants().FETCH_RESULT_NEW_DATA, ); }; onPushNotifBootsApp() { if ( this.props.rootContext && this.props.rootContext.detectUnsupervisedBackground ) { this.props.rootContext.detectUnsupervisedBackground(false); } } iosNotificationOpened = (rawNotification: CoreIOSNotificationData) => { const notification = new CommIOSNotification(rawNotification); this.onPushNotifBootsApp(); const threadID = notification.getData().threadID; const messageInfos = notification.getData().messageInfos; this.saveMessageInfos(messageInfos); this.onPressNotificationForThread(threadID, true); notification.finish( CommIOSNotifications.getConstants().FETCH_RESULT_NEW_DATA, ); }; showInAppNotification(threadID: string, message: string, title?: ?string) { if (threadID === this.props.activeThread) { return; } this.setState({ inAppNotifProps: { customComponent: ( ), blurType: this.props.activeTheme === 'dark' ? 'xlight' : 'dark', onPress: () => { InAppNotification.hide(); this.onPressNotificationForThread(threadID, false); }, }, }); } androidNotificationOpened = async ( notificationOpen: AndroidForegroundMessage, ) => { this.onPushNotifBootsApp(); const { threadID } = notificationOpen; this.onPressNotificationForThread(threadID, true); }; androidMessageReceived = async (message: AndroidForegroundMessage) => { this.onPushNotifBootsApp(); const { messageInfos } = message; this.saveMessageInfos(messageInfos); handleAndroidMessage( message, this.props.updatesCurrentAsOf, this.handleAndroidNotificationIfActive, ); }; handleAndroidNotificationIfActive = ( threadID: string, texts: { body: string, title: ?string }, ) => { if (this.currentState !== 'active') { return false; } this.showInAppNotification(threadID, texts.body, texts.title); return true; }; render() { return ( ); } } const ConnectedPushHandler: React.ComponentType = React.memo( function ConnectedPushHandler(props: BaseProps) { const navContext = React.useContext(NavContext); const activeThread = activeMessageListSelector(navContext); const boundUnreadCount = useSelector(unreadCount); const deviceToken = useSelector(state => state.deviceToken); const threadInfos = useSelector(threadInfoSelector); const notifPermissionAlertInfo = useSelector( state => state.notifPermissionAlertInfo, ); const connection = useSelector(state => state.connection); const updatesCurrentAsOf = useSelector(state => state.updatesCurrentAsOf); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const loggedIn = useSelector(isLoggedIn); const navigateToThread = useNavigateToThread(); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const boundSetDeviceToken = useServerCall(setDeviceToken); const rootContext = React.useContext(RootContext); return ( ); }, ); export default ConnectedPushHandler; diff --git a/native/redux/dev-tools.js b/native/redux/dev-tools.js index 14c287af0..1a6f317a7 100644 --- a/native/redux/dev-tools.js +++ b/native/redux/dev-tools.js @@ -1,25 +1,25 @@ // @flow import { getDevServerHostname } from '../utils/url-utils.js'; const remoteReduxDevServerConfig = { port: 8043, suppressConnectErrors: false, // we want to see error messages }; // Since there's no way to run the Hermes runtime externally, when debugging we // can no longer rely on the debugger to set these globals. We have to set them // ourselves to work with remote-redux-devtools if (__DEV__ && global.HermesInternal && !global.__REDUX_DEVTOOLS_EXTENSION__) { - const { connect } = require('remotedev/src'); + const { connect } = require('remotedev/src/index.js'); global.__REDUX_DEVTOOLS_EXTENSION__ = { connect: ({ name }) => connect({ name, hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }), }; } export { remoteReduxDevServerConfig }; diff --git a/native/redux/redux-setup.js b/native/redux/redux-setup.js index ef622bd6b..dc694e202 100644 --- a/native/redux/redux-setup.js +++ b/native/redux/redux-setup.js @@ -1,547 +1,547 @@ // @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 { setClientDBStoreActionType } from 'lib/actions/client-db-store-actions.js'; import { setDeviceTokenActionTypes } from 'lib/actions/device-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/account-utils.js'; import { isStaff } from 'lib/shared/user-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, incrementalStateSyncActionType, } from 'lib/types/socket-types.js'; import type { ThreadStoreOperation } from 'lib/types/thread-types.js'; import { updateTypes } from 'lib/types/update-types.js'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import { setNewSessionActionType } from 'lib/utils/action-utils.js'; import { convertMessageStoreOperationsToClientDBOperations } from 'lib/utils/message-ops-utils.js'; import { convertThreadStoreOperationsToClientDBOperations } from 'lib/utils/thread-ops-utils.js'; import { resetUserStateActionType, recordNotifPermissionAlertActionType, 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 type { AppState } from './state-types.js'; import { commCoreModule } from '../native-modules.js'; import { defaultNavInfo } from '../navigation/default-state.js'; import { getGlobalNavContext } from '../navigation/icky-global.js'; import { activeMessageListSelector } from '../navigation/nav-selectors.js'; import { defaultNotifPermissionAlertInfo } from '../push/alerts.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 { isTaskCancelledError } from '../utils/error-handling.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: {}, }: 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; } 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, }; } } 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, ]; const convertedThreadStoreOperations = convertThreadStoreOperationsToClientDBOperations( threadStoreOperationsWithUnreadFix, ); const convertedMessageStoreOperations = convertMessageStoreOperationsToClientDBOperations( messageStoreOperations, ); (async () => { try { const promises = []; if (convertedThreadStoreOperations.length > 0) { promises.push( commCoreModule.processThreadStoreOperations( convertedThreadStoreOperations, ), ); } if (convertedMessageStoreOperations.length > 0) { promises.push( commCoreModule.processMessageStoreOperations( convertedMessageStoreOperations, ), ); } if (draftStoreOperations.length > 0) { promises.push( commCoreModule.processDraftStoreOperations(draftStoreOperations), ); } await Promise.all(promises); } catch (e) { if (isTaskCancelledError(e)) { return; } 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 middleware = applyMiddleware(thunk, reduxLoggerMiddleware); let composeFunc = compose; if (__DEV__ && global.HermesInternal) { - const { composeWithDevTools } = require('remote-redux-devtools/src'); + const { composeWithDevTools } = require('remote-redux-devtools/src/index.js'); composeFunc = composeWithDevTools({ name: 'Redux', hostname: getDevServerHostname(), ...remoteReduxDevServerConfig, }); } else if (global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) { composeFunc = global.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ name: 'Redux', }); } let enhancers; if (reactotron) { enhancers = composeFunc(middleware, reactotron.createEnhancer()); } else { enhancers = composeFunc(middleware); } const store: Store = createStore( persistReducer(persistConfig, reducer), defaultState, enhancers, ); const persistor = persistStore(store); setPersistor(persistor); const unsafeDispatch: any = store.dispatch; const dispatch: Dispatch = unsafeDispatch; export { store, dispatch, appBecameInactive }; diff --git a/native/root.react.js b/native/root.react.js index 29bde4cf6..ce08f1882 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,297 +1,297 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import type { PossiblyStaleNavigationState } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, UIManager, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; -import { PersistGate as ReduxPersistGate } from 'redux-persist/integration/react'; +import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { defaultNavigationState } from './navigation/default-state.js'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import { NavContext } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import Socket from './socket.react.js'; import { StaffContextProvider } from './staff/staff-context.provider.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { provider } from './utils/ethers-utils.js'; if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef(); const navDispatchRef = React.useRef(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return; } return navContainer.addListener('__unsafe_action__', event => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const gated: React.Node = ( <> ); let navigation; if (initialState) { navigation = ( ); } return ( {gated} {navigation} ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( ); } export default AppRoot; diff --git a/native/types/react-native.js b/native/types/react-native.js index 8a8e4cf78..93eb7ca5f 100644 --- a/native/types/react-native.js +++ b/native/types/react-native.js @@ -1,34 +1,33 @@ // @flow import type ReactNativeAnimatedValue from 'react-native/Libraries/Animated/nodes/AnimatedValue.js'; import type { ViewToken } from 'react-native/Libraries/Lists/ViewabilityHelper.js'; export type { Layout, LayoutEvent, ScrollEvent, -} from 'react-native/Libraries/Types/CoreEventTypes'; +} from 'react-native/Libraries/Types/CoreEventTypes.js'; export type { ContentSizeChangeEvent, KeyPressEvent, BlurEvent, -} from 'react-native/Libraries/Components/TextInput/TextInput'; + SelectionChangeEvent, +} from 'react-native/Libraries/Components/TextInput/TextInput.js'; -export type { SelectionChangeEvent } from 'react-native/Libraries/Components/TextInput/TextInput'; +export type { NativeMethods } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes.js'; -export type { NativeMethods } from 'react-native/Libraries/Renderer/shims/ReactNativeTypes'; +export type { KeyboardEvent } from 'react-native/Libraries/Components/Keyboard/Keyboard.js'; -export type { KeyboardEvent } from 'react-native/Libraries/Components/Keyboard/Keyboard'; - -export type { EventSubscription } from 'react-native/Libraries/vendor/emitter/EventEmitter'; +export type { EventSubscription } from 'react-native/Libraries/vendor/emitter/EventEmitter.js'; export type AnimatedValue = ReactNativeAnimatedValue; export type ViewableItemsChange = { +viewableItems: ViewToken[], +changed: ViewToken[], ... }; export type EmitterSubscription = { +remove: () => void, ... }; diff --git a/native/utils/dev-hostname.js b/native/utils/dev-hostname.js index 8528335bd..1b90de03f 100644 --- a/native/utils/dev-hostname.js +++ b/native/utils/dev-hostname.js @@ -1,47 +1,47 @@ // @flow import Constants from 'expo-constants'; let warnNatDevHostnameUndefined = true; const defaultNatDevHostname = '192.168.1.1'; function readHostnameFromExpoConfig(): ?string { const { natDevHostname: detectedHostname } = Constants.expoConfig.extra || {}; if (!detectedHostname || typeof detectedHostname !== 'string') { return null; } warnNatDevHostnameUndefined = false; return detectedHostname; } function readHostnameFromNetworkJson(): string { try { // this is your machine's hostname in the local network // usually it looks like this: 192.168.1.x // to find it you may want to use `ifconfig | grep '192.168'` // command in the terminal // example of native/facts/network.json: // { "natDevHostname": "192.168.1.x" } // $FlowExpectedError: It's a conditional require so the file may not exist - const hostname: string = require('../facts/network').natDevHostname; + const hostname: string = require('../facts/network.json').natDevHostname; warnNatDevHostnameUndefined = false; return hostname; } catch (e) { return defaultNatDevHostname; } } const natDevHostname: string = readHostnameFromExpoConfig() ?? readHostnameFromNetworkJson(); function checkForMissingNatDevHostname() { if (!warnNatDevHostnameUndefined) { return; } console.warn( 'Failed to autodetect natDevHostname. ' + 'Please specify it manually in native/facts/network.json', ); warnNatDevHostnameUndefined = false; } export { natDevHostname, checkForMissingNatDevHostname }; diff --git a/scripts/get_clang_paths_cli.js b/scripts/get_clang_paths_cli.js index 83aaeb930..7e2e79c3c 100644 --- a/scripts/get_clang_paths_cli.js +++ b/scripts/get_clang_paths_cli.js @@ -1,20 +1,20 @@ // @flow -const { clangPaths } = require('./get_clang_paths'); +const { clangPaths } = require('./get_clang_paths.js'); (() => { let command = '('; clangPaths.forEach(pathItem => { const { path, extensions, excludes } = pathItem; command += `find ${path} `; command += extensions .map(extension => `-name '*.${extension}' `) .join('-o '); if (excludes) { command += `| grep -v '${excludes.map(exclude => exclude).join('\\|')}'`; } command += '; '; }); command += ')'; console.log(command); })(); diff --git a/web/account/siwe-button.react.js b/web/account/siwe-button.react.js index 256a67fc8..c0bfd2651 100644 --- a/web/account/siwe-button.react.js +++ b/web/account/siwe-button.react.js @@ -1,36 +1,36 @@ // @flow import * as React from 'react'; -import { FaEthereum } from 'react-icons/fa'; +import { FaEthereum } from 'react-icons/fa/index.js'; import css from './siwe.css'; import Button from '../components/button.react.js'; type SIWEButtonProps = { +onSIWEButtonClick: () => void, }; function SIWEButton(props: SIWEButtonProps): React.Node { const { onSIWEButtonClick } = props; const siweButtonColor = React.useMemo( () => ({ backgroundColor: 'white', color: 'black' }), [], ); return (
); } export default SIWEButton; diff --git a/web/chat/chat-thread-list-see-more-sidebars.react.js b/web/chat/chat-thread-list-see-more-sidebars.react.js index dd2c171ff..3e731a281 100644 --- a/web/chat/chat-thread-list-see-more-sidebars.react.js +++ b/web/chat/chat-thread-list-see-more-sidebars.react.js @@ -1,50 +1,50 @@ // @flow import classNames from 'classnames'; import * as React from 'react'; -import { IoIosMore } from 'react-icons/io'; +import { IoIosMore } from 'react-icons/io/index.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import css from './chat-thread-list.css'; import SidebarsModal from '../modals/threads/sidebars/sidebars-modal.react.js'; type Props = { +threadInfo: ThreadInfo, +unread: boolean, }; function ChatThreadListSeeMoreSidebars(props: Props): React.Node { const { unread, threadInfo } = props; const { pushModal, popModal } = useModalContext(); const onClick = React.useCallback( () => pushModal( , ), [popModal, pushModal, threadInfo.id], ); return (
See more...
); } export default ChatThreadListSeeMoreSidebars; diff --git a/web/root.js b/web/root.js index 95f66491f..9874beb2c 100644 --- a/web/root.js +++ b/web/root.js @@ -1,57 +1,57 @@ // @flow 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 { PersistGate } from 'redux-persist/integration/react'; -import storage from 'redux-persist/lib/storage'; +import { PersistGate } from 'redux-persist/es/integration/react.js'; +import storage from 'redux-persist/es/storage/index.js'; import thunk from 'redux-thunk'; import { reduxLoggerMiddleware } from 'lib/utils/action-logger.js'; import App from './app.react.js'; import ErrorBoundary from './error-boundary.react.js'; import Loading from './loading.react.js'; import { reducer } from './redux/redux-setup.js'; import type { AppState, Action } from './redux/redux-setup.js'; import history from './router-history.js'; import Socket from './socket.react.js'; const persistConfig = { key: 'root', storage, whitelist: [ 'enabledApps', 'deviceID', 'draftStore', 'primaryIdentityPublicKey', ], version: 0, }; declare var preloadedState: AppState; const persistedReducer = persistReducer(persistConfig, reducer); const store: Store = createStore( persistedReducer, preloadedState, composeWithDevTools({})(applyMiddleware(thunk, reduxLoggerMiddleware)), ); const persistor = persistStore(store); const RootProvider = (): React.Node => ( }> ); export default RootProvider; diff --git a/web/script.js b/web/script.js index 461c7b8f7..57b50d000 100644 --- a/web/script.js +++ b/web/script.js @@ -1,14 +1,15 @@ // @flow import 'isomorphic-fetch'; import invariant from 'invariant'; import React from 'react'; +// eslint-disable-next-line import/extensions import { hydrateRoot } from 'react-dom/client'; import Root from './root.js'; const root = document.getElementById('react-root'); invariant(root, "cannot find id='react-root' element!"); hydrateRoot(root, );