diff --git a/.buildkite/eslint_flow_jest.yml b/.buildkite/eslint_flow_jest.yml index 50983d2d6..9e8958e5c 100644 --- a/.buildkite/eslint_flow_jest.yml +++ b/.buildkite/eslint_flow_jest.yml @@ -1,16 +1,16 @@ steps: - label: ':eslint: :jest: ESLint & Flow & Jest' command: - '(pkill flow || true)' - 'curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y' - '. /root/.cargo/env' - 'yarn cleaninstall --frozen-lockfile --skip-optional --network-timeout 180000' - - 'yarn eslint --max-warnings=0 && yarn workspace lib flow && yarn workspace web flow && yarn workspace landing flow && yarn workspace native flow && yarn workspace keyserver flow' + - 'yarn eslint --max-warnings=0 && yarn workspace lib flow && yarn workspace web flow && yarn workspace landing flow && yarn workspace native flow && yarn workspace keyserver flow && yarn workspace desktop flow' - 'yarn workspace lib test && yarn workspace keyserver test' plugins: - docker#v3.13.0: image: 'node:16.13-bullseye' always-pull: true workdir: /comm agents: - 'autoscaling=true' diff --git a/.github/workflows/eslint_flow_jest.yml b/.github/workflows/eslint_flow_jest.yml index e34d0de93..15ac27703 100644 --- a/.github/workflows/eslint_flow_jest.yml +++ b/.github/workflows/eslint_flow_jest.yml @@ -1,50 +1,54 @@ name: ESLint & Flow & Jest on: push: branches: [master] pull_request: types: [opened, reopened] jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: npm install -g yarn run: npm install -g yarn - name: yarn --frozen-lockfile --network-timeout 180000 run: yarn --frozen-lockfile --network-timeout 180000 - name: yarn eslint --max-warnings=0 run: yarn eslint --max-warnings=0 - name: '[lib] flow' working-directory: ./lib run: ./node_modules/.bin/flow - name: '[keyserver] flow' working-directory: ./keyserver run: ./node_modules/.bin/flow - name: '[web] flow' working-directory: ./web run: ./node_modules/.bin/flow - name: '[landing] flow' working-directory: ./landing run: ./node_modules/.bin/flow - name: '[native] flow' working-directory: ./native run: ./node_modules/.bin/flow + - name: '[desktop] flow' + working-directory: ./desktop + run: ./node_modules/.bin/flow + - name: '[lib] test' working-directory: ./lib run: yarn test - name: '[keyserver] test' working-directory: ./keyserver run: yarn test diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 4d508d774..21badc964 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,64 +1,67 @@ const { CLIEngine } = require('eslint'); const { getClangPaths } = require('./scripts/get_clang_paths'); const cli = new CLIEngine({}); module.exports = { '*.{js,mjs,cjs}': function eslint(files) { // This logic is likely broken and needs to be updated. see ENG-1011 return ( 'eslint --cache --fix --max-warnings=0 ' + files.filter(file => !cli.isPathIgnored(file)).join(' ') ); }, '*.{css,html,md,json}': function prettier(files) { return 'prettier --write ' + files.join(' '); }, '*.sh': function shellCheck(files) { return 'shellcheck -x -P SCRIPTDIR ' + files.join(' '); }, 'lib/**/*.js': function libFlow(files) { return 'yarn workspace lib flow --quiet'; }, 'lib/**/*.js': function libTest(files) { return 'yarn workspace lib test'; }, '{web,lib}/**/*.js': function webFlow(files) { return 'yarn workspace web flow --quiet'; }, '{native,lib}/**/*.js': function nativeFlow(files) { return 'yarn workspace native flow --quiet'; }, '{keyserver,web,lib}/**/*.js': function keyserverFlow(files) { return 'yarn workspace keyserver flow --quiet'; }, '{keyserver,web,lib}/**/*.js': function keyserverTest(files) { return 'yarn workspace keyserver test'; }, '{landing,lib}/**/*.js': function landingFlow(files) { return 'yarn workspace landing flow --quiet'; }, + '{desktop,lib}/**/*.js': function desktopFlow(files) { + return 'yarn workspace desktop flow --quiet'; + }, 'services/identity/**/*.rs': function testIdentity(files) { return 'bash -c "cd services/identity && cargo test"'; }, '{native,services}/**/*.{h,cpp,java,mm}': function clangFormat(files) { files = files.filter(path => { if (path.indexOf('generated') !== -1) { return false; } for (const allowedPath of getClangPaths()) { if (path.indexOf(allowedPath) !== -1) { return true; } } return false; }); return 'clang-format -i ' + files.join(' '); }, 'services/commtest/**/*.rs': function rustFormat(files) { return 'yarn rust-pre-commit'; }, 'services/terraform/*.tf': function checkTerraform(files) { return 'yarn terraform-pre-commit'; }, }; diff --git a/desktop/.eslintrc.json b/desktop/.eslintrc.json index c81e1d23f..436dbb587 100644 --- a/desktop/.eslintrc.json +++ b/desktop/.eslintrc.json @@ -1,6 +1,3 @@ { - "rules": { - "flowtype/require-valid-file-annotation": "off" - }, "env": { "node": true } } diff --git a/desktop/.flowconfig b/desktop/.flowconfig new file mode 100644 index 000000000..66b7173dc --- /dev/null +++ b/desktop/.flowconfig @@ -0,0 +1,29 @@ +[include] +../lib + +[libs] +../lib/flow-typed + +[options] +exact_by_default=true + +format.bracket_spacing=false + +[lints] +sketchy-null-number=warn +sketchy-null-mixed=warn +sketchy-number=warn +untyped-type-import=warn +nonstrict-import=warn +deprecated-type=warn +unsafe-getters-setters=warn +unnecessary-invariant=warn + +[strict] +deprecated-type +nonstrict-import +sketchy-null +unclear-type +unsafe-getters-setters +untyped-import +untyped-type-import diff --git a/desktop/babel.config.cjs b/desktop/babel.config.cjs index 7d2e9f9d0..3676ea0e2 100644 --- a/desktop/babel.config.cjs +++ b/desktop/babel.config.cjs @@ -1,12 +1,13 @@ module.exports = { presets: [ ['@babel/preset-env', { targets: { electron: 22 }, modules: 'commonjs' }], + '@babel/preset-flow', ], plugins: [ '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', '@babel/plugin-transform-runtime', ], }; diff --git a/desktop/flow-typed/npm/electron_vx.x.x.js b/desktop/flow-typed/npm/electron_vx.x.x.js new file mode 100644 index 000000000..68fecb568 --- /dev/null +++ b/desktop/flow-typed/npm/electron_vx.x.x.js @@ -0,0 +1,19 @@ +// @flow +// flow-typed signature: f8bfa3876f1890f644b65b1ebd801ed8 +// flow-typed version: <>/electron_v22.0.0/flow_v0.182.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'electron' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'electron' { + declare module.exports: any; +} diff --git a/desktop/forge.config.cjs b/desktop/forge.config.cjs index 36157e0fa..a247b358d 100644 --- a/desktop/forge.config.cjs +++ b/desktop/forge.config.cjs @@ -1,132 +1,138 @@ const babel = require('@babel/core'); const { PluginBase } = require('@electron-forge/plugin-base'); const fs = require('fs-extra'); const klaw = require('klaw'); const path = require('path'); const transformDirectoryWithBabel = async (dirPath, outDirPath) => { for await (const { path: filePath, stats } of klaw(dirPath)) { if (stats.isFile()) { const outPath = path.resolve( outDirPath, path.relative(dirPath, filePath), ); const { code } = await new Promise(resolve => babel.transformFile(filePath, (err, result) => { if (err) { console.error(err); } resolve(result); }), ); if (code) { await fs.outputFile(outPath, code); } } } }; const runBabel = async () => { await Promise.all([ fs.outputFile('./dist/package.json', JSON.stringify({ type: 'commonjs' })), transformDirectoryWithBabel('./src', './dist'), ]); }; class BabelPlugin extends PluginBase { name = 'BabelPlugin'; getHooks() { return { // This hook runs during the packaging of the final executable prePackage: [runBabel], }; } // This function runs only in development mode, just before the // application starts async startLogic() { await runBabel(); // startLogic allows us to run electron ourselves and return the process // object. Electron Forge (package which handles bundling, packaging and // running dev mode) will then watch it instead of spawing electron by // itself. But we are fine with the default behaviour (Electron Forge // spawning electron) so we return false. return false; } } module.exports = { packagerConfig: { name: 'Comm', icon: 'icons/icon', - ignore: ['src', '.*config\\.cjs', '\\.eslintrc\\.json'], + ignore: [ + 'src', + '.*config\\.cjs', + '\\.eslintrc\\.json', + '\\.flowconfig', + 'flow-typed', + ], appBundleId: 'app.comm.macos', osxSign: { identity: 'Developer ID Application' }, osxNotarize: { tool: 'notarytool', appleId: process.env?.APPLE_USER_NAME, appleIdPassword: process.env?.APPLE_APP_SPECIFIC_PASSWORD, teamId: process.env?.TEAM_ID, }, }, makers: [ { name: '@electron-forge/maker-dmg', platforms: ['darwin'], config: { icon: 'icons/icon.icns', background: 'icons/dmg_background.png', additionalDMGOptions: { 'code-sign': { 'signing-identity': 'Developer ID Application', 'identifier': 'app.comm.macos', }, }, contents: opts => [ { x: 340, y: 100, type: 'link', path: '/Applications' }, { x: 100, y: 100, type: 'file', path: opts.appPath }, ], }, }, { name: '@electron-forge/maker-squirrel', platforms: ['win32'], config: { name: 'Comm', title: 'Comm', authors: 'Comm Technologies, Inc.', description: 'Comm is a private messaging app for communities!', iconUrl: 'https://comm-external.s3.amazonaws.com/icon.ico', setupIcon: 'icons/icon.ico', loadingGif: 'icons/win_installer.gif', certificateFile: process.env?.WINDOWS_CERTIFICATE, certificatePassword: process.env?.WINDOWS_PASSWORD, }, }, ], plugins: [new BabelPlugin()], hooks: { generateAssets: async () => { await Promise.all([ fs.copy('../keyserver/fonts', './assets/fonts'), fs.copy('../web/theme.css', './assets/theme.css'), fs.copy('../web/typography.css', './assets/typography.css'), ]); }, prePackage: async (forgeConfig, platform, arch) => { if ( arch === 'universal' && (fs.existsSync('./out/Comm-darwin-x64') || fs.existsSync('./out/Comm-darwin-arm64')) ) { throw new Error( 'Due to a bug in @electron/universal, please first run ' + '`yarn clean-build` or remove previous builds artifacts: ' + '"out/Comm-darwin-x64" and/or "out/Comm-darwin-arm64"\n', ); } }, }, }; diff --git a/desktop/package.json b/desktop/package.json index 80c1935e3..09d14fdd1 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,40 +1,42 @@ { "workspaces": { "nohoist": [ "**" ] }, "name": "desktop", "version": "0.0.1", "type": "module", "main": "./dist/main.js", "private": true, "license": "BSD-3-Clause", "scripts": { "dev": "ENV=dev electron-forge start", "package": "electron-forge package", "make": "electron-forge make", "clean": "rm -rf assets/ && rm -rf dist/ && yarn clean-build && rm -rf node_modules/", "clean-build": "rm -rf out/" }, "dependencies": { "@babel/runtime": "^7.20.1" }, "devDependencies": { "@babel/core": "^7.13.14", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-object-rest-spread": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-transform-runtime": "^7.13.10", "@babel/preset-env": "^7.13.12", "@electron-forge/cli": "^6.0.4", "@electron-forge/maker-dmg": "^6.0.4", "@electron-forge/maker-squirrel": "^6.0.4", "@electron-forge/plugin-base": "^6.0.4", "electron": "^22.0.0", + "flow-bin": "^0.182.0", + "flow-typed": "^3.2.1", "fs-extra": "^10.1.0", "klaw": "^4.0.1" } } diff --git a/desktop/src/handle-squirrel-event.js b/desktop/src/handle-squirrel-event.js index 260d538b9..a6aefc6ea 100644 --- a/desktop/src/handle-squirrel-event.js +++ b/desktop/src/handle-squirrel-event.js @@ -1,41 +1,43 @@ +// @flow + import { spawn } from 'child_process'; import { app } from 'electron'; import path from 'path'; // Squirrel will start the app with additional flags during installing, // uninstalling and updating so we can for example create or delete shortcuts. // After handling some of these events the app will be closed. If this function // returns false, the app should start normally. -export function handleSquirrelEvent() { +export function handleSquirrelEvent(): boolean { if (process.argv.length === 1) { return false; } const updateExe = path.resolve(process.execPath, '..', '..', 'Update.exe'); const commExeName = path.basename(process.execPath); const spawnUpdate = args => { return spawn(updateExe, args, { detached: true }).on('close', app.quit); }; const squirrelEvent = process.argv[1]; switch (squirrelEvent) { case '--squirrel-install': case '--squirrel-updated': spawnUpdate(['--createShortcut', commExeName]); return true; case '--squirrel-uninstall': spawnUpdate(['--removeShortcut', commExeName]); return true; case '--squirrel-obsolete': app.quit(); return true; case '--squirrel-firstrun': return false; } return false; } diff --git a/desktop/src/main.js b/desktop/src/main.js index 9bab5789b..12e20185c 100644 --- a/desktop/src/main.js +++ b/desktop/src/main.js @@ -1,266 +1,268 @@ +// @flow + import { app, BrowserWindow, shell, Menu, ipcMain, systemPreferences, } from 'electron'; import fs from 'fs'; import path from 'path'; import { handleSquirrelEvent } from './handle-squirrel-event'; 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); win.on('closed', () => { ipcMain.removeListener('clear-history', clearHistory); ipcMain.removeListener('double-click-top-bar', doubleClickTopBar); }); 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(); ipcMain.on('set-badge', (event, value) => { if (isMac) { app.dock.setBadge(value?.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 851f8a727..e4f06e0da 100644 --- a/desktop/src/preload.js +++ b/desktop/src/preload.js @@ -1,14 +1,16 @@ +// @flow + import { contextBridge, ipcRenderer } from 'electron'; const bridge = { 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), }; contextBridge.exposeInMainWorld('electronContextBridge', bridge); diff --git a/native/.flowconfig b/native/.flowconfig index f6b73a5bb..e763d17ef 100644 --- a/native/.flowconfig +++ b/native/.flowconfig @@ -1,77 +1,78 @@ [ignore] ; We fork some components by platform .*/*[.]android.js .*/node_modules/react-native-fast-image/src/index.js.flow .*/node_modules/react-native-fs/FS.common.js .*/node_modules/react-native-gesture-handler/Swipeable.js .*/fbjs/lib/keyMirrorRecursive.js.flow ; Flow doesn't support platforms .*/Libraries/Utilities/LoadingView.js .*/node_modules/resolve/test/resolver/malformed_package_json/package\.json$ .*/comm/web/.* .*/comm/keyserver/.* +.*/comm/desktop/.* .*/android/app/build/.* [untyped] .*/node_modules/@react-native-community/cli/.*/.* [declarations] .*/node_modules/react-native-camera/* .*/node_modules/react-native-firebase/* [include] ../node_modules ../lib [libs] ../node_modules/react-native/interface.js ../node_modules/react-native/flow/ ../lib/flow-typed/ [options] emoji=true exact_by_default=true format.bracket_spacing=false module.file_ext=.js module.file_ext=.json module.file_ext=.ios.js munge_underscores=true module.name_mapper='^react-native/\(.*\)$' -> '/../node_modules/react-native/\1' module.name_mapper='^@?[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> '/node_modules/react-native/Libraries/Image/RelativeImageStub' suppress_type=$FlowIssue suppress_type=$FlowFixMe suppress_type=$FlowFixMeProps suppress_type=$FlowFixMeState [lints] sketchy-null-number=warn sketchy-null-mixed=warn sketchy-number=warn untyped-type-import=warn nonstrict-import=warn deprecated-type=warn unsafe-getters-setters=warn unnecessary-invariant=warn [strict] deprecated-type nonstrict-import sketchy-null unclear-type unsafe-getters-setters untyped-import untyped-type-import [version] ^0.182.0