diff --git a/.github/workflows/git-commit-message-style.yml b/.github/workflows/git-commit-message-style.yml index 7cb2d03765fe..850a563c814e 100644 --- a/.github/workflows/git-commit-message-style.yml +++ b/.github/workflows/git-commit-message-style.yml @@ -34,4 +34,4 @@ jobs: # This action defaults to 50 char subjects, but 72 is fine. max-subject-line-length: '72' # The action's wordlist is a bit short. Add more accepted verbs - additional-verbs: 'tidy, wrap, obfuscate, bias' + additional-verbs: 'tidy, wrap, obfuscate, bias, prohibit, forbid' diff --git a/gui/eslint.config.mjs b/gui/eslint.config.mjs index 75f28f60912c..5aafcb40414c 100644 --- a/gui/eslint.config.mjs +++ b/gui/eslint.config.mjs @@ -1,6 +1,7 @@ import eslint from '@eslint/js'; import prettier from 'eslint-plugin-prettier/recommended'; import react from 'eslint-plugin-react'; +import reactcompiler from 'eslint-plugin-react-compiler'; import reactHooks from 'eslint-plugin-react-hooks'; import simpleImportSort from 'eslint-plugin-simple-import-sort'; import globals from 'globals'; @@ -130,6 +131,7 @@ export default tseslint.config( plugins: { 'simple-import-sort': simpleImportSort, 'react-hooks': reactHooks, + 'react-compiler': reactcompiler, }, rules: { quotes: ['error', 'single', { avoidEscape: true }], @@ -143,10 +145,12 @@ export default tseslint.config( 'no-return-await': 'error', 'react/jsx-no-bind': 'error', '@typescript-eslint/naming-convention': ['error', ...namingConvention], - '@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': false }], + '@typescript-eslint/ban-ts-comment': 'error', 'simple-import-sort/imports': 'error', 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + 'react-compiler/react-compiler': 'error', '@typescript-eslint/no-use-before-define': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', diff --git a/gui/package-lock.json b/gui/package-lock.json index 3f6bddbe34b2..e5c9a0294bbf 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -56,6 +56,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react-compiler": "^0.0.0-experimental-42acc6a-20241001", "eslint-plugin-react-hooks": "^0.0.0-experimental-2d16326d-20240930", "eslint-plugin-simple-import-sort": "^12.1.1", "gettext-extractor": "^3.5.4", @@ -87,6 +88,504 @@ "nseventmonitor": "^1.0.5" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ampproject/remapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.4", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", @@ -98,6 +597,84 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -792,6 +1369,30 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -801,6 +1402,15 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -3103,6 +3713,38 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", @@ -3330,6 +3972,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001666", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", + "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, "node_modules/chai": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", @@ -4783,6 +5445,12 @@ "mime": "^2.5.2" } }, + "node_modules/electron-to-chromium": { + "version": "1.5.31", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", + "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "dev": true + }, "node_modules/elliptic": { "version": "6.5.7", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", @@ -5065,9 +5733,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "engines": { "node": ">=6" } @@ -5217,6 +5885,26 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-compiler": { + "version": "0.0.0-experimental-42acc6a-20241001", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-42acc6a-20241001.tgz", + "integrity": "sha512-pzkTsWowlHK4yKHsK1d9tTKOUtApZzL7wI6jT5iN31d00DhI9JGDD0pkLohQ6Wfkll+2aiqTPGj9esJoGYmRaw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "hermes-parser": "^0.20.1", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.0.0 || >= 18.0.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "0.0.0-experimental-2d16326d-20240930", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-2d16326d-20240930.tgz", @@ -6295,6 +6983,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -7155,6 +7852,21 @@ "he": "bin/he" } }, + "node_modules/hermes-estree": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.20.1.tgz", + "integrity": "sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg==", + "dev": true + }, + "node_modules/hermes-parser": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.20.1.tgz", + "integrity": "sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA==", + "dev": true, + "dependencies": { + "hermes-estree": "0.20.1" + } + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -8160,6 +8872,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9444,6 +10168,12 @@ "lodash.get": "^4.4.2" } }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -12548,6 +13278,15 @@ "node": ">=0.10.0" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -13120,6 +13859,42 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/update-browserslist-db/node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, "node_modules/uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -13726,144 +14501,556 @@ "node": ">=8" } }, - "node_modules/yargs/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", + "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "dev": true, + "peer": true, + "dependencies": { + "archiver-utils": "^3.0.4", + "compress-commons": "^4.1.2", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/archiver-utils": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", + "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "dev": true, + "peer": true, + "dependencies": { + "glob": "^7.2.3", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/zip-stream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, + "@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "dependencies": { + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + } + } + }, + "@babel/compat-data": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", + "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "dev": true + }, + "@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", + "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "dev": true, + "requires": { + "@babel/types": "^7.25.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "requires": { + "yallist": "^3.0.2" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz", + "integrity": "sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.4", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + } + }, + "@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, - "node_modules/yargs/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" + "@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" } }, - "node_modules/yargs/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" + "@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "requires": { + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", "dev": true, - "engines": { - "node": ">=6" + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "requires": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" } }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", + "@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true + }, + "@babel/helpers": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz", + "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==", "dev": true, - "peer": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" + "requires": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6" } }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", + "@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, - "peer": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "requires": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, - "engines": { - "node": ">= 10" + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, - "node_modules/zip-stream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "@babel/parser": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz", + "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==", "dev": true, - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "requires": { + "@babel/types": "^7.25.6" } }, - "node_modules/zip-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "@babel/plugin-proposal-private-methods": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", + "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", "dev": true, - "peer": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" + "requires": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" } - } - }, - "dependencies": { + }, "@babel/runtime": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.0.tgz", @@ -13872,6 +15059,66 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + } + }, + "@babel/traverse": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", + "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.6", + "@babel/parser": "^7.25.6", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.25.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz", + "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -14371,12 +15618,41 @@ } } }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + } + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -16256,6 +17532,18 @@ "pako": "~1.0.5" } }, + "browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + } + }, "buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", @@ -16438,6 +17726,12 @@ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" }, + "caniuse-lite": { + "version": "1.0.30001666", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001666.tgz", + "integrity": "sha512-gD14ICmoV5ZZM1OdzPWmpx+q4GyefaK06zi8hmfHV5xe4/2nOQX3+Dw5o+fSqOws2xVwL9j+anOPFwHzdEdV4g==", + "dev": true + }, "chai": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", @@ -17618,6 +18912,12 @@ "mime": "^2.5.2" } }, + "electron-to-chromium": { + "version": "1.5.31", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", + "integrity": "sha512-QcDoBbQeYt0+3CWcK/rEbuHvwpbT/8SV9T3OSgs6cX1FlcUAkgrkqbg9zLnDrMM/rLamzQwal4LYFCiWk861Tg==", + "dev": true + }, "elliptic": { "version": "6.5.7", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", @@ -17868,9 +19168,9 @@ } }, "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, "escape-string-regexp": { "version": "4.0.0", @@ -18045,6 +19345,20 @@ } } }, + "eslint-plugin-react-compiler": { + "version": "0.0.0-experimental-42acc6a-20241001", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-0.0.0-experimental-42acc6a-20241001.tgz", + "integrity": "sha512-pzkTsWowlHK4yKHsK1d9tTKOUtApZzL7wI6jT5iN31d00DhI9JGDD0pkLohQ6Wfkll+2aiqTPGj9esJoGYmRaw==", + "dev": true, + "requires": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/plugin-proposal-private-methods": "^7.18.6", + "hermes-parser": "^0.20.1", + "zod": "^3.22.4", + "zod-validation-error": "^3.0.3" + } + }, "eslint-plugin-react-hooks": { "version": "0.0.0-experimental-2d16326d-20240930", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-0.0.0-experimental-2d16326d-20240930.tgz", @@ -18806,6 +20120,12 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-assigned-identifiers": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", @@ -19464,6 +20784,21 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "hermes-estree": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.20.1.tgz", + "integrity": "sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg==", + "dev": true + }, + "hermes-parser": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.20.1.tgz", + "integrity": "sha512-BL5P83cwCogI8D7rrDCgsFY0tdYUtmFP9XaXtl2IQjC+2Xo+4okjfXintlTxcIwl4qeGddEl28Z11kbVIw0aNA==", + "dev": true, + "requires": { + "hermes-estree": "0.20.1" + } + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -20193,6 +21528,12 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -21216,6 +22557,12 @@ "lodash.get": "^4.4.2" } }, + "node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -23629,6 +24976,12 @@ "is-negated-glob": "^1.0.0" } }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -24052,6 +25405,24 @@ "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, + "update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "dependencies": { + "picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + } + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -24656,6 +26027,19 @@ } } } + }, + "zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true + }, + "zod-validation-error": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", + "dev": true, + "requires": {} } } } diff --git a/gui/package.json b/gui/package.json index 6298c0128612..c078ade9721f 100644 --- a/gui/package.json +++ b/gui/package.json @@ -62,6 +62,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-react": "^7.36.1", + "eslint-plugin-react-compiler": "^0.0.0-experimental-42acc6a-20241001", "eslint-plugin-react-hooks": "^0.0.0-experimental-2d16326d-20240930", "eslint-plugin-simple-import-sort": "^12.1.1", "gettext-extractor": "^3.5.4", diff --git a/gui/src/renderer/components/Accordion.tsx b/gui/src/renderer/components/Accordion.tsx index 70ae35c988ba..27a5be53bc36 100644 --- a/gui/src/renderer/components/Accordion.tsx +++ b/gui/src/renderer/components/Accordion.tsx @@ -93,7 +93,8 @@ export default class Accordion extends React.Component { private collapse() { // First change height to height in px since it's not possible to transition to/from auto this.setState({ containerHeight: this.getContentHeight() + 'px' }, () => { - // Make sure new height has been applied + // Make sure new height has been applied. By reading offsetHeight we force the browser to + // apply the height before returning. // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.containerRef.current?.offsetHeight; this.setState({ containerHeight: '0' }); diff --git a/gui/src/renderer/components/Account.tsx b/gui/src/renderer/components/Account.tsx index c92e56ab1bb5..1cc666910ef4 100644 --- a/gui/src/renderer/components/Account.tsx +++ b/gui/src/renderer/components/Account.tsx @@ -5,6 +5,7 @@ import { formatDate, hasExpired } from '../../shared/account-expiry'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; +import { useEffectEvent } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import AccountNumberLabel from './AccountNumberLabel'; import { @@ -33,11 +34,10 @@ export default function Account() { const onBuyMore = useCallback(async () => { await openLinkWithAuth(links.purchase); - }, []); + }, [openLinkWithAuth]); - useEffect(() => { - updateAccountData(); - }, []); + const onMount = useEffectEvent(() => updateAccountData()); + useEffect(() => onMount(), []); // Hack needed because if we just call `logout` directly in `onClick` // then it is run with the wrong `this`. diff --git a/gui/src/renderer/components/ApiAccessMethods.tsx b/gui/src/renderer/components/ApiAccessMethods.tsx index 78f0a11e7711..57668df787c6 100644 --- a/gui/src/renderer/components/ApiAccessMethods.tsx +++ b/gui/src/renderer/components/ApiAccessMethods.tsx @@ -10,7 +10,7 @@ import { useApiAccessMethodTest } from '../lib/api-access-methods'; import { useHistory } from '../lib/history'; import { generateRoutePath } from '../lib/routeHelpers'; import { RoutePath } from '../lib/routes'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import * as Cell from './cell'; import { @@ -168,7 +168,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { updateApiAccessMethod, removeApiAccessMethod, } = useAppContext(); - const history = useHistory(); + const { push } = useHistory(); const [testing, testResult, testApiAccessMethod] = useApiAccessMethodTest(); @@ -177,7 +177,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { const confirmRemove = useCallback(() => { void removeApiAccessMethod(props.method.id); hideRemoveConfirmation(); - }, [props.method.id]); + }, [hideRemoveConfirmation, props.method.id, removeApiAccessMethod]); // Toggle on/off on an access method. const toggle = useCallback( @@ -186,7 +186,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { updatedMethod.enabled = value; await updateApiAccessMethod(updatedMethod); }, - [props.method], + [props.method, updateApiAccessMethod], ); const setApiAccessMethod = useCallback(async () => { @@ -194,7 +194,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { if (reachable) { await setApiAccessMethodImpl(props.method.id); } - }, [testApiAccessMethod, props.method.id]); + }, [testApiAccessMethod, props.method.id, setApiAccessMethodImpl]); const menuItems = useMemo>(() => { const items: Array = [ @@ -219,9 +219,7 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { type: 'item' as const, label: messages.gettext('Edit'), onClick: () => - history.push( - generateRoutePath(RoutePath.editApiAccessMethods, { id: props.method.id }), - ), + push(generateRoutePath(RoutePath.editApiAccessMethods, { id: props.method.id })), }, { type: 'item' as const, @@ -232,7 +230,15 @@ function ApiAccessMethod(props: ApiAccessMethodProps) { } return items; - }, [props.method.id, props.inUse, setApiAccessMethod, testApiAccessMethod, history.push]); + }, [ + props.inUse, + props.custom, + props.method.id, + setApiAccessMethod, + testApiAccessMethod, + showRemoveConfirmation, + push, + ]); return ( diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx index 753f64745e3a..f82b997cc4ec 100644 --- a/gui/src/renderer/components/AppButton.tsx +++ b/gui/src/renderer/components/AppButton.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import log from '../../shared/logging'; -import { useMounted } from '../lib/utilityHooks'; +import { useMounted } from '../lib/utility-hooks'; import { StyledButtonContent, StyledHiddenSide, @@ -127,13 +127,15 @@ interface IBlockingProps { } export function BlockingButton(props: IBlockingProps) { + const { onClick: propsOnClick } = props; + const isMounted = useMounted(); const [isBlocked, setIsBlocked] = useState(false); const onClick = useCallback(async () => { setIsBlocked(true); try { - await props.onClick(); + await propsOnClick(); } catch (error) { log.error(`onClick() failed - ${error}`); } @@ -141,7 +143,7 @@ export function BlockingButton(props: IBlockingProps) { if (isMounted()) { setIsBlocked(false); } - }, [props.onClick]); + }, [isMounted, propsOnClick]); const contextValue = useMemo( () => ({ diff --git a/gui/src/renderer/components/AppRouter.tsx b/gui/src/renderer/components/AppRouter.tsx index e83b6f55cd55..3541c095d69a 100644 --- a/gui/src/renderer/components/AppRouter.tsx +++ b/gui/src/renderer/components/AppRouter.tsx @@ -48,23 +48,23 @@ export default function AppRouter() { const { setNavigationHistory } = useAppContext(); const focusRef = createRef(); - let unobserveHistory: () => void; - useEffect(() => { // React throttles updates, so it's impossible to capture the intermediate navigation without // listening to the history directly. - unobserveHistory = history.listen((location, _, transition) => { + const unobserveHistory = history.listen((location, _, transition) => { setNavigationHistory(history.asObject); setCurrentLocation(location); setTransition(transition); }); - return () => unobserveHistory?.(); - }, []); + return () => { + unobserveHistory?.(); + }; + }, [history, setNavigationHistory]); const onNavigation = useCallback(() => { focusRef.current?.resetFocus(); - }, []); + }, [focusRef]); return ( diff --git a/gui/src/renderer/components/AriaGroup.tsx b/gui/src/renderer/components/AriaGroup.tsx index 8adf3243afc3..9e58283933ba 100644 --- a/gui/src/renderer/components/AriaGroup.tsx +++ b/gui/src/renderer/components/AriaGroup.tsx @@ -1,9 +1,4 @@ -import React, { useContext, useEffect, useMemo, useState } from 'react'; - -let groupCounter = 0; -function getNewId() { - return groupCounter++; -} +import React, { useContext, useEffect, useId, useMemo, useState } from 'react'; interface IAriaControlContext { controlledId: string; @@ -21,8 +16,8 @@ interface IAriaGroupProps { } export function AriaControlGroup(props: IAriaGroupProps) { - const id = useMemo(getNewId, []); - const contextValue = useMemo(() => ({ controlledId: `${id}-controlled` }), []); + const id = useId(); + const contextValue = useMemo(() => ({ controlledId: `${id}-controlled` }), [id]); return ( {props.children} @@ -45,7 +40,7 @@ const AriaDescriptionContext = React.createContext({ }); export function AriaDescriptionGroup(props: IAriaGroupProps) { - const id = useMemo(getNewId, []); + const id = useId(); const [hasDescription, setHasDescription] = useState(false); const contextValue = useMemo( @@ -54,7 +49,7 @@ export function AriaDescriptionGroup(props: IAriaGroupProps) { descriptionId: hasDescription ? `${id}-description` : undefined, setHasDescription, }), - [hasDescription, props.describedId], + [hasDescription, id, props.describedId], ); return ( @@ -81,7 +76,7 @@ const AriaInputContext = React.createContext({ }); export function AriaInputGroup(props: IAriaGroupProps) { - const id = useMemo(getNewId, []); + const id = useId(); const [hasLabel, setHasLabel] = useState(false); @@ -91,7 +86,7 @@ export function AriaInputGroup(props: IAriaGroupProps) { labelId: hasLabel ? `${id}-label` : undefined, setHasLabel, }), - [hasLabel], + [hasLabel, id], ); return ( @@ -134,7 +129,7 @@ export function AriaLabel(props: IAriaElementProps) { useEffect(() => { setHasLabel(true); return () => setHasLabel(false); - }, []); + }, [setHasLabel]); return React.cloneElement(props.children, { id: labelId, @@ -157,7 +152,7 @@ export function AriaDescription(props: IAriaElementProps) { useEffect(() => { setHasDescription(true); return () => setHasDescription(false); - }, []); + }, [setHasDescription]); return React.cloneElement(props.children, { id: descriptionId, diff --git a/gui/src/renderer/components/Changelog.tsx b/gui/src/renderer/components/Changelog.tsx index 716daba75d35..ea1b42f5c229 100644 --- a/gui/src/renderer/components/Changelog.tsx +++ b/gui/src/renderer/components/Changelog.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { hugeText, smallText } from './common-styles'; @@ -44,7 +44,7 @@ export function Changelog() { const close = useCallback(() => { setDisplayedChangelog(); stopForceShowChanges(); - }, []); + }, [setDisplayedChangelog, stopForceShowChanges]); const visible = forceShowChanges || diff --git a/gui/src/renderer/components/ClipboardLabel.tsx b/gui/src/renderer/components/ClipboardLabel.tsx index e9f760fd07a3..7f1970ebfcb7 100644 --- a/gui/src/renderer/components/ClipboardLabel.tsx +++ b/gui/src/renderer/components/ClipboardLabel.tsx @@ -5,7 +5,7 @@ import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { useScheduler } from '../../shared/scheduler'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import ImageView from './ImageView'; const COPIED_ICON_DURATION = 2000; diff --git a/gui/src/renderer/components/ContextMenu.tsx b/gui/src/renderer/components/ContextMenu.tsx index 165c284ecab6..fd84a8f0d921 100644 --- a/gui/src/renderer/components/ContextMenu.tsx +++ b/gui/src/renderer/components/ContextMenu.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react'; import styled from 'styled-components'; import { colors } from '../../config.json'; -import { useBoolean, useStyledRef } from '../lib/utilityHooks'; +import { useBoolean, useStyledRef } from '../lib/utility-hooks'; import { smallText } from './common-styles'; import { BackAction } from './KeyboardNavigation'; @@ -47,7 +47,7 @@ export function ContextMenuContainer(props: React.PropsWithChildren) { throw new Error('No trigger bounds available'); } return ref.current.getBoundingClientRect(); - }, [ref.current]); + }, [ref]); const contextValue = useMemo( () => ({ @@ -56,7 +56,7 @@ export function ContextMenuContainer(props: React.PropsWithChildren) { visible, hide, }), - [getTriggerBounds, visible], + [getTriggerBounds, hide, toggleVisibility, visible], ); const clickOutsideListener = useCallback( @@ -69,7 +69,7 @@ export function ContextMenuContainer(props: React.PropsWithChildren) { hide(); } }, - [visible], + [hide, ref, visible], ); useEffect(() => { @@ -204,12 +204,14 @@ interface ContextMenuItemRowProps { } function ContextMenuItemRow(props: ContextMenuItemRowProps) { + const { closeMenu } = props; + const onClick = useCallback(() => { if (!props.item.disabled) { - props.closeMenu(); + closeMenu(); props.item.onClick(); } - }, [props.closeMenu, props.item.disabled, props.item.onClick]); + }, [closeMenu, props.item]); return ( diff --git a/gui/src/renderer/components/CustomDnsSettings.tsx b/gui/src/renderer/components/CustomDnsSettings.tsx index 35acb88a6c75..914f7b84066d 100644 --- a/gui/src/renderer/components/CustomDnsSettings.tsx +++ b/gui/src/renderer/components/CustomDnsSettings.tsx @@ -6,7 +6,7 @@ import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { formatHtml } from '../lib/html-formatter'; import { IpAddress } from '../lib/ip'; -import { useBoolean, useMounted, useStyledRef } from '../lib/utilityHooks'; +import { useBoolean, useMounted, useStyledRef } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; @@ -32,6 +32,8 @@ import { import List, { stringValueAsKey } from './List'; import { ModalAlert, ModalAlertType } from './Modal'; +const manualLocal = window.env.platform === 'win32' || window.env.platform === 'linux'; + export default function CustomDnsSettings() { const { setDnsOptions } = useAppContext(); const dns = useSelector((state) => state.settings.dns); @@ -43,7 +45,6 @@ export default function CustomDnsSettings() { const [savingEdit, setSavingEdit] = useState(false); const willShowConfirmationDialog = useRef(false); const addingLocalIp = useRef(false); - const manualLocal = window.env.platform === 'win32' || window.env.platform === 'linux'; const featureAvailable = useMemo( () => @@ -67,7 +68,7 @@ export default function CustomDnsSettings() { }, [confirmAction]); const abortConfirmation = useCallback(() => { setConfirmAction(undefined); - }, [confirmAction]); + }, []); const setCustomDnsEnabled = useCallback( async (enabled: boolean) => { @@ -81,7 +82,7 @@ export default function CustomDnsSettings() { hideInput(); } }, - [dns], + [dns, hideInput, setDnsOptions, showInput], ); // The input field should be hidden when it loses focus unless something on the same row or the @@ -100,7 +101,7 @@ export default function CustomDnsSettings() { hideInput(); } }, - [confirmAction, willShowConfirmationDialog], + [addButtonRef, hideInput, inputContainerRef, switchRef], ); const onAdd = useCallback( @@ -146,7 +147,7 @@ export default function CustomDnsSettings() { } } }, - [inputVisible, dns, setDnsOptions], + [dns, setInvalid, setDnsOptions, inputVisible, hideInput], ); const onEdit = useCallback( @@ -319,6 +320,8 @@ interface ICellListItemProps { } function CellListItem(props: ICellListItemProps) { + const { onRemove: propsOnRemove, onChange } = props; + const [editing, startEditing, stopEditing] = useBoolean(false); const [invalid, setInvalid, setValid] = useBoolean(false); const isMounted = useMounted(); @@ -326,8 +329,8 @@ function CellListItem(props: ICellListItemProps) { const inputContainerRef = useStyledRef(); const onRemove = useCallback( - () => props.onRemove(props.children), - [props.onRemove, props.children], + () => propsOnRemove(props.children), + [propsOnRemove, props.children], ); const onSubmit = useCallback( @@ -336,7 +339,7 @@ function CellListItem(props: ICellListItemProps) { stopEditing(); } else { try { - await props.onChange(props.children, value); + await onChange(props.children, value); if (isMounted()) { stopEditing(); } @@ -345,17 +348,20 @@ function CellListItem(props: ICellListItemProps) { } } }, - [props.onChange, props.children, invalid], + [props.children, stopEditing, onChange, isMounted, setInvalid], ); - const onBlur = useCallback((event?: React.FocusEvent) => { - const relatedTarget = event?.relatedTarget as Node | undefined; - if (relatedTarget && inputContainerRef.current?.contains(relatedTarget)) { - event?.target.focus(); - } else if (!props.willShowConfirmationDialog.current) { - stopEditing(); - } - }, []); + const onBlur = useCallback( + (event?: React.FocusEvent) => { + const relatedTarget = event?.relatedTarget as Node | undefined; + if (relatedTarget && inputContainerRef.current?.contains(relatedTarget)) { + event?.target.focus(); + } else if (!props.willShowConfirmationDialog.current) { + stopEditing(); + } + }, + [inputContainerRef, props.willShowConfirmationDialog, stopEditing], + ); return ( diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx index 4fd3ae0059cc..bcfca0331633 100644 --- a/gui/src/renderer/components/DaitaSettings.tsx +++ b/gui/src/renderer/components/DaitaSettings.tsx @@ -6,7 +6,7 @@ import { strings } from '../../config.json'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; @@ -150,22 +150,28 @@ function DaitaToggle() { const unavailable = 'normal' in relaySettings ? relaySettings.normal.tunnelProtocol === 'openvpn' : true; - const setDaita = useCallback((value: boolean) => { - void setEnableDaita(value); - }, []); + const setDaita = useCallback( + (value: boolean) => { + void setEnableDaita(value); + }, + [setEnableDaita], + ); - const setDirectOnly = useCallback((value: boolean) => { - if (value) { - showConfirmationDialog(); - } else { - void setDaitaDirectOnly(value); - } - }, []); + const setDirectOnly = useCallback( + (value: boolean) => { + if (value) { + showConfirmationDialog(); + } else { + void setDaitaDirectOnly(value); + } + }, + [setDaitaDirectOnly, showConfirmationDialog], + ); const confirmEnableDirectOnly = useCallback(() => { void setDaitaDirectOnly(true); hideConfirmationDialog(); - }, []); + }, [hideConfirmationDialog, setDaitaDirectOnly]); const directOnlyString = messages.gettext('Direct only'); diff --git a/gui/src/renderer/components/Debug.tsx b/gui/src/renderer/components/Debug.tsx index f81fb8540219..58e446be4765 100644 --- a/gui/src/renderer/components/Debug.tsx +++ b/gui/src/renderer/components/Debug.tsx @@ -2,7 +2,7 @@ import { useCallback } from 'react'; import styled from 'styled-components'; import { useHistory } from '../lib/history'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import * as AppButton from './AppButton'; import { measurements } from './common-styles'; import { BackAction } from './KeyboardNavigation'; diff --git a/gui/src/renderer/components/DeviceInfoButton.tsx b/gui/src/renderer/components/DeviceInfoButton.tsx index 48398669721e..090bb9c8df66 100644 --- a/gui/src/renderer/components/DeviceInfoButton.tsx +++ b/gui/src/renderer/components/DeviceInfoButton.tsx @@ -1,7 +1,7 @@ import styled from 'styled-components'; import { messages } from '../../shared/gettext'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import * as AppButton from './AppButton'; import { InfoIcon } from './InfoButton'; import { ModalAlert, ModalAlertType, ModalMessage } from './Modal'; diff --git a/gui/src/renderer/components/EditApiAccessMethod.tsx b/gui/src/renderer/components/EditApiAccessMethod.tsx index a8602675a06e..8a11404b1c95 100644 --- a/gui/src/renderer/components/EditApiAccessMethod.tsx +++ b/gui/src/renderer/components/EditApiAccessMethod.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useState } from 'react'; import { useParams } from 'react-router'; import { sprintf } from 'sprintf-js'; @@ -12,6 +12,7 @@ import { useScheduler } from '../../shared/scheduler'; import { useAppContext } from '../context'; import { useApiAccessMethodTest } from '../lib/api-access-methods'; import { useHistory } from '../lib/history'; +import { useLastDefinedValue } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import { SettingsForm } from './cell/SettingsForm'; import { BackAction } from './KeyboardNavigation'; @@ -32,7 +33,7 @@ export function EditApiAccessMethod() { } function AccessMethodForm() { - const history = useHistory(); + const { pop } = useHistory(); const { addApiAccessMethod, updateApiAccessMethod } = useAppContext(); const methods = useSelector((state) => state.settings.apiAccessMethods.custom); @@ -46,40 +47,52 @@ function AccessMethodForm() { const { id } = useParams<{ id: string | undefined }>(); const method = methods.find((method) => method.id === id); - const updatedMethod = useRef | undefined>(method); - - const save = useCallback(() => { - if (updatedMethod.current !== undefined) { - resetTestResult(); - if (id === undefined) { - void addApiAccessMethod(updatedMethod.current); - } else { - void updateApiAccessMethod({ ...updatedMethod.current, id }); + const [updatedMethod, setUpdatedMethod] = useState< + NewAccessMethodSetting | undefined + >(method); + + const save = useCallback( + (method: NewAccessMethodSetting) => { + if (method !== undefined) { + resetTestResult(); + if (id === undefined) { + void addApiAccessMethod(method); + } else { + void updateApiAccessMethod({ ...method, id }); + } + pop(); } - history.pop(); - } - }, [updatedMethod.current, id]); + }, + [resetTestResult, id, pop, addApiAccessMethod, updateApiAccessMethod], + ); const onSave = useCallback( async (newMethod: NamedCustomProxy) => { const enabled = id === undefined ? true : (method?.enabled ?? true); - updatedMethod.current = { ...newMethod, enabled }; + const updatedMethod = { ...newMethod, enabled }; + setUpdatedMethod(updatedMethod); if ( - updatedMethod.current !== undefined && - (await testApiAccessMethod(updatedMethod.current as CustomProxy)) + updatedMethod !== undefined && + (await testApiAccessMethod(updatedMethod as CustomProxy)) ) { // Hide the save dialog after 1.5 seconds. - saveScheduler.schedule(save, 1500); + saveScheduler.schedule(() => save(updatedMethod), 1500); } }, - [updatedMethod, save, history.pop], + [id, method?.enabled, testApiAccessMethod, saveScheduler, save], ); + const handleDialogSave = useCallback(() => { + if (updatedMethod !== undefined) { + save(updatedMethod); + } + }, [save, updatedMethod]); + const title = getTitle(id === undefined); const subtitle = getSubtitle(id === undefined); return ( - + @@ -100,17 +113,17 @@ function AccessMethodForm() { {id !== undefined && method === undefined ? ( Failed to open method ) : ( - + )} @@ -143,30 +156,26 @@ interface TestingDialogProps { } function TestingDialog(props: TestingDialogProps) { - const type = props.testing - ? ModalAlertType.loading - : props.testResult - ? ModalAlertType.success - : ModalAlertType.failure; - const prevType = useRef(type); - - const isOpen = props.testing || props.testResult !== undefined; - const typeValue = isOpen ? type : prevType.current; - - useEffect(() => { - if (isOpen) { - prevType.current = type; - } - }, [type]); + let currentType: ModalAlertType | undefined; + if (props.testing) { + currentType = ModalAlertType.loading; + } else if (props.testResult) { + currentType = ModalAlertType.success; + } else if (props.testResult === false) { + currentType = ModalAlertType.failure; + } + + const type = useLastDefinedValue(currentType); + const displayType = type ?? ModalAlertType.failure; return ( ); } diff --git a/gui/src/renderer/components/EditCustomBridge.tsx b/gui/src/renderer/components/EditCustomBridge.tsx index 7cc403aa6ed4..7a0ad6f8d82e 100644 --- a/gui/src/renderer/components/EditCustomBridge.tsx +++ b/gui/src/renderer/components/EditCustomBridge.tsx @@ -4,7 +4,7 @@ import { CustomProxy } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { useBridgeSettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import { SettingsForm } from './cell/SettingsForm'; import { BackAction } from './KeyboardNavigation'; @@ -25,7 +25,7 @@ export function EditCustomBridge() { } function CustomBridgeForm() { - const history = useHistory(); + const { pop } = useHistory(); const bridgeSettingsUpdater = useBridgeSettingsUpdater(); const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); @@ -45,9 +45,9 @@ function CustomBridgeForm() { bridgeSettings.custom = newBridge; return bridgeSettings; }); - history.pop(); + pop(); }, - [bridgeSettingsUpdater, history.pop], + [bridgeSettingsUpdater, pop], ); const onDelete = useCallback(() => { @@ -58,12 +58,12 @@ function CustomBridgeForm() { delete bridgeSettings.custom; return bridgeSettings; }); - history.pop(); + pop(); } - }, [bridgeSettingsUpdater, history.pop]); + }, [bridgeSettings.custom, bridgeSettingsUpdater, hideDeleteDialog, pop]); return ( - + @@ -83,7 +83,7 @@ function CustomBridgeForm() { diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx index 3b54642978e1..8dd48d44d0cc 100644 --- a/gui/src/renderer/components/ExpiredAccountAddTime.tsx +++ b/gui/src/renderer/components/ExpiredAccountAddTime.tsx @@ -134,7 +134,7 @@ interface ITimeAddedProps { } export function TimeAdded(props: ITimeAddedProps) { - const history = useHistory(); + const { push } = useHistory(); const finish = useFinishedCallback(); const expiry = useSelector((state) => state.account.expiry); const isNewAccount = useSelector( @@ -144,11 +144,11 @@ export function TimeAdded(props: ITimeAddedProps) { const navigateToSetupFinished = useCallback(() => { if (isNewAccount) { - history.push(RoutePath.setupFinished); + push(RoutePath.setupFinished); } else { finish(); } - }, [history, finish]); + }, [isNewAccount, push, finish]); const duration = props.secondsAdded !== undefined diff --git a/gui/src/renderer/components/ExpiredAccountErrorView.tsx b/gui/src/renderer/components/ExpiredAccountErrorView.tsx index b15d74329a08..a98d3e441c3d 100644 --- a/gui/src/renderer/components/ExpiredAccountErrorView.tsx +++ b/gui/src/renderer/components/ExpiredAccountErrorView.tsx @@ -47,7 +47,7 @@ export default function ExpiredAccountErrorView() { } function ExpiredAccountErrorViewComponent() { - const history = useHistory(); + const { push } = useHistory(); const { disconnectTunnel } = useAppContext(); const connection = useSelector((state) => state.connection); @@ -66,11 +66,11 @@ function ExpiredAccountErrorViewComponent() { const error = e as Error; log.error(`Failed to disconnect the tunnel: ${error.message}`); } - }, []); + }, [disconnectTunnel]); const navigateToRedeemVoucher = useCallback(() => { - history.push(RoutePath.redeemVoucher); - }, [history.push]); + push(RoutePath.redeemVoucher); + }, [push]); return ( @@ -192,7 +192,7 @@ function ExternalPaymentButton() { } else { await openLinkWithAuth(links.purchase); } - }, []); + }, [openLinkWithAuth, recoveryAction, setShowBlockWhenDisconnectedAlert]); return ( { setShowBlockWhenDisconnectedAlert(false); - }, []); - - const onChange = useCallback(async (blockWhenDisconnected: boolean) => { - try { - await setBlockWhenDisconnected(blockWhenDisconnected); - } catch (e) { - const error = e as Error; - log.error('Failed to update block when disconnected', error.message); - } - }, []); + }, [setShowBlockWhenDisconnectedAlert]); + + const onChange = useCallback( + async (blockWhenDisconnected: boolean) => { + try { + await setBlockWhenDisconnected(blockWhenDisconnected); + } catch (e) { + const error = e as Error; + log.error('Failed to update block when disconnected', error.message); + } + }, + [setBlockWhenDisconnected], + ); return ( - props.setProviders((providers) => { + setProviders((providers) => { const newProviders = { ...providers, [provider]: !providers[provider] }; return props.availableOptions.every((provider) => newProviders[provider]) ? toggleAllProviders(providers, true) : newProviders; }), - [props.availableOptions, props.setProviders], + [props.availableOptions, setProviders], ); const toggleAll = useCallback(() => { - props.setProviders((providers) => toggleAllProviders(providers)); - }, []); + setProviders((providers) => toggleAllProviders(providers)); + }, [setProviders]); return ( <> @@ -355,7 +358,9 @@ interface ICheckboxRowProps extends IStyledRowTitleProps { } function CheckboxRow(props: ICheckboxRowProps) { - const onToggle = useCallback(() => props.onChange(props.label), [props.onChange, props.label]); + const { onChange } = props; + + const onToggle = useCallback(() => onChange(props.label), [onChange, props.label]); return ( diff --git a/gui/src/renderer/components/FormattableTextInput.tsx b/gui/src/renderer/components/FormattableTextInput.tsx index f6c259c4c4cf..aae614ddd737 100644 --- a/gui/src/renderer/components/FormattableTextInput.tsx +++ b/gui/src/renderer/components/FormattableTextInput.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect } from 'react'; -import { useCombinedRefs, useStyledRef } from '../lib/utilityHooks'; +import { useCombinedRefs, useStyledRef } from '../lib/utility-hooks'; interface IFormattableTextInputProps extends React.InputHTMLAttributes { allowedCharacters: string; @@ -111,21 +111,23 @@ function FormattableTextInput( ); } }, - [unformat, format, handleChange, addTrailingSeparator], + [ref, unformat, maxLength, format, addTrailingSeparator, handleChange], ); // React doesn't fully support onBeforeInput currently and it's therefore set here. useEffect(() => { - ref.current?.addEventListener('beforeinput', onBeforeInput); - return () => ref.current?.removeEventListener('beforeinput', onBeforeInput); - }, [onBeforeInput]); + const input = ref.current; + input?.addEventListener('beforeinput', onBeforeInput); + return () => input?.removeEventListener('beforeinput', onBeforeInput); + }, [onBeforeInput, ref]); // Use value provided in props if it differs from current input value. useEffect(() => { if (typeof value === 'string' && ref.current && unformat(ref.current.value) !== value) { + // eslint-disable-next-line react-compiler/react-compiler ref.current.value = format(value, addTrailingSeparator); } - }, [format, value, addTrailingSeparator]); + }, [format, value, addTrailingSeparator, ref, unformat]); return ; } diff --git a/gui/src/renderer/components/InfoButton.tsx b/gui/src/renderer/components/InfoButton.tsx index e17e7b87b80e..4ef2aa3c6eda 100644 --- a/gui/src/renderer/components/InfoButton.tsx +++ b/gui/src/renderer/components/InfoButton.tsx @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; import { ModalAlert, ModalAlertType } from './Modal'; diff --git a/gui/src/renderer/components/KeyboardNavigation.tsx b/gui/src/renderer/components/KeyboardNavigation.tsx index 8ed3caa540ae..cbde4297bd0f 100644 --- a/gui/src/renderer/components/KeyboardNavigation.tsx +++ b/gui/src/renderer/components/KeyboardNavigation.tsx @@ -4,6 +4,7 @@ import { useLocation } from 'react-router'; import { useHistory } from '../lib/history'; import { disableDismissForRoutes } from '../lib/routeHelpers'; import { RoutePath } from '../lib/routes'; +import { useEffectEvent } from '../lib/utility-hooks'; interface IKeyboardNavigationProps { children: React.ReactElement | Array; @@ -11,7 +12,7 @@ interface IKeyboardNavigationProps { // Listens for and handles keyboard shortcuts export default function KeyboardNavigation(props: IKeyboardNavigationProps) { - const history = useHistory(); + const { pop } = useHistory(); const [backAction, setBackActionImpl] = useState(); const location = useLocation(); @@ -26,13 +27,13 @@ export default function KeyboardNavigation(props: IKeyboardNavigationProps) { if (event.key === 'Escape') { const path = location.pathname as RoutePath; if (event.shiftKey && !disableDismissForRoutes.includes(path)) { - history.pop(true); + pop(true); } else { backAction?.(); } } }, - [history.pop, backAction, location.pathname], + [pop, backAction, location.pathname], ); useEffect(() => { @@ -69,7 +70,7 @@ interface IBackActionProps { // Component for registering back actions, e.g. navigate back or close modal. These are called // either by pressing the back button in the navigation bar or by pressing escape. export function BackAction(props: IBackActionProps) { - const backActionContext = useContext(BackActionContext); + const { registerBackAction, removeBackAction } = useContext(BackActionContext); const [childrenBackAction, setChildrenBackActionImpl] = useState(); // Since the backaction is now a function we need to make sure it's not called when setting the @@ -88,10 +89,10 @@ export function BackAction(props: IBackActionProps) { // Every time the action or the disabled property changes the action needs to be reregistered. useEffect((): (() => void) | void => { if (!props.disabled && backAction) { - backActionContext.registerBackAction(backAction); - return () => backActionContext.removeBackAction(backAction); + registerBackAction(backAction); + return () => removeBackAction(backAction); } - }, [props.disabled, backAction]); + }, [props.disabled, backAction, registerBackAction, removeBackAction]); // Every back action keeps track of the back actions in its subtree. This makes it possible to // always use the action furthest down in the tree. @@ -121,10 +122,14 @@ function BackActionTracker(props: IBackActionTracker) { }, []); const backActionContext = useMemo( () => ({ parentBackAction: props.parentBackAction, registerBackAction, removeBackAction }), - [backActions], + [props.parentBackAction, registerBackAction, removeBackAction], ); - useEffect(() => props.registerBackAction(backActions.at(0)), [backActions]); + const registerBackActionEvent = useEffectEvent((backActions: Array) => { + props.registerBackAction(backActions.at(0)); + }); + + useEffect(() => registerBackActionEvent(backActions), [backActions]); return ( diff --git a/gui/src/renderer/components/Launch.tsx b/gui/src/renderer/components/Launch.tsx index 05bf6aeda60e..7a8f75bc9c6c 100644 --- a/gui/src/renderer/components/Launch.tsx +++ b/gui/src/renderer/components/Launch.tsx @@ -6,7 +6,7 @@ import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { transitions, useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import { measurements, tinyText } from './common-styles'; @@ -52,7 +52,7 @@ function MacOsPermissionFooter() { const openSettings = useCallback(async () => { await showLaunchDaemonSettings(); - }, []); + }, [showLaunchDaemonSettings]); return ( @@ -72,13 +72,13 @@ function MacOsPermissionFooter() { } function DefaultFooter() { - const history = useHistory(); + const { push } = useHistory(); const [dialogVisible, showDialog, hideDialog] = useBoolean(); const openSendProblemReport = useCallback(() => { hideDialog(); - history.push(RoutePath.problemReport, { transition: transitions.show }); - }, [hideDialog, history.push]); + push(RoutePath.problemReport, { transition: transitions.show }); + }, [hideDialog, push]); return ( <> diff --git a/gui/src/renderer/components/List.tsx b/gui/src/renderer/components/List.tsx index 3d1bd994789f..4961855fba49 100644 --- a/gui/src/renderer/components/List.tsx +++ b/gui/src/renderer/components/List.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import styled from 'styled-components'; import { Scheduler } from '../../shared/scheduler'; +import { useEffectEvent } from '../lib/utility-hooks'; import Accordion from './Accordion'; export const stringValueAsKey = (value: string): string => value; @@ -35,26 +36,30 @@ export default function List(props: ListProps) { convertToRowDisplayData(props.items, props.getKey), ); // Skip add transition on first render when initial items are added. - const skipAddTransition = useRef(props.skipInitialAddTransition ?? false); + const [skipAddTransition, setSkipAddTransition] = useState( + props.skipInitialAddTransition ?? false, + ); const removeFallbackSchedulers = useRef>({}); - useEffect(() => { + const itemChangeEvent = useEffectEvent((items: Array) => { setDisplayItems((prevItems) => { if (props.skipRemoveTransition) { - return convertToRowDisplayData(props.items, props.getKey); + return convertToRowDisplayData(items, props.getKey); } else { - const nextItems = convertToRowData(props.items, props.getKey); + const nextItems = convertToRowData(items, props.getKey); return calculateItemList(prevItems, nextItems); } }); - }, [props.items, props.getKey]); + }); + + useEffect(() => itemChangeEvent(props.items), [props.items]); useEffect(() => { // Set to animate accordion for added items after first render unless // props.skipAddTransition === true. - skipAddTransition.current = props.skipAddTransition ?? false; - }, []); + setSkipAddTransition(props.skipAddTransition ?? false); + }, [props.skipAddTransition]); const onRemoved = useCallback((key: string) => { removeFallbackSchedulers.current[key].cancel(); @@ -63,7 +68,7 @@ export default function List(props: ListProps) { setDisplayItems((items) => items.filter((item) => item.key !== key)); }, []); - useEffect(() => { + const handleDisplayItemsChange = useEffectEvent((displayItems: Array>) => { // Add scheduled item removal if `onTransitionEnd` doesn't trigger for some reason. displayItems .filter((item) => item.removing && removeFallbackSchedulers.current[item.key] === undefined) @@ -72,7 +77,9 @@ export default function List(props: ListProps) { scheduler.schedule(() => onRemoved(item.key), 400); removeFallbackSchedulers.current[item.key] = scheduler; }); - }, [displayItems]); + }); + + useEffect(() => handleDisplayItemsChange(displayItems), [displayItems]); useEffect( () => () => { @@ -90,7 +97,7 @@ export default function List(props: ListProps) { data={displayItem} onRemoved={onRemoved} render={props.children} - skipAddTransition={skipAddTransition.current} + skipAddTransition={skipAddTransition} /> ))} @@ -105,14 +112,16 @@ interface ListItemProps { } function ListItem(props: ListItemProps) { + const { onRemoved } = props; + // If skipAddTransition is true then the item is expanded from the beginning. const [expanded, setExpanded] = useState(props.skipAddTransition); const onTransitionEnd = useCallback(() => { if (props.data.removing) { - props.onRemoved(props.data.key); + onRemoved(props.data.key); } - }, [props.onRemoved, props.data.key, props.data.removing]); + }, [onRemoved, props.data.key, props.data.removing]); // Expands after initial render and collapses when item is set to being removed. useEffect(() => setExpanded(!props.data.removing), [props.data.removing]); diff --git a/gui/src/renderer/components/Login.tsx b/gui/src/renderer/components/Login.tsx index 430772c2c75c..0d234bd66b64 100644 --- a/gui/src/renderer/components/Login.tsx +++ b/gui/src/renderer/components/Login.tsx @@ -428,17 +428,19 @@ interface IAccountDropdownItemProps { } function AccountDropdownItem(props: IAccountDropdownItemProps) { + const { onSelect, onRemove } = props; + const handleSelect = useCallback(() => { - props.onSelect(props.value); - }, [props.onSelect, props.value]); + onSelect(props.value); + }, [onSelect, props.value]); const handleRemove = useCallback( (event: React.MouseEvent) => { // Prevent login form from submitting event.preventDefault(); - props.onRemove(props.value); + onRemove(props.value); }, - [props.onRemove, props.value], + [onRemove, props.value], ); return ( diff --git a/gui/src/renderer/components/MacOsScrollbarDetection.tsx b/gui/src/renderer/components/MacOsScrollbarDetection.tsx index aebb144f9b3b..520b6f3f3be2 100644 --- a/gui/src/renderer/components/MacOsScrollbarDetection.tsx +++ b/gui/src/renderer/components/MacOsScrollbarDetection.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { MacOsScrollbarVisibility } from '../../shared/ipc-schema'; import useActions from '../lib/actionsHook'; -import { useStyledRef } from '../lib/utilityHooks'; +import { useEffectEvent, useStyledRef } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import userInterface from '../redux/userinterface/actions'; @@ -24,7 +24,7 @@ export default function MacOsScrollbarDetection() { const { setMacOsScrollbarVisibility } = useActions(userInterface); const ref = useStyledRef(); - useEffect(() => { + const detectVisibility = useEffectEvent((visibility?: MacOsScrollbarVisibility) => { if (visibility === MacOsScrollbarVisibility.automatic) { // If the width is 0 then the 1 px width of the parent has been used by the scrollbar. const newVisibility = @@ -33,7 +33,9 @@ export default function MacOsScrollbarDetection() { : MacOsScrollbarVisibility.whenScrolling; setMacOsScrollbarVisibility(newVisibility); } - }, [visibility]); + }); + + useEffect(() => detectVisibility(visibility), [visibility]); return ( diff --git a/gui/src/renderer/components/Map.tsx b/gui/src/renderer/components/Map.tsx index b002cce5e827..9ea69251d99f 100644 --- a/gui/src/renderer/components/Map.tsx +++ b/gui/src/renderer/components/Map.tsx @@ -5,7 +5,12 @@ import { TunnelState } from '../../shared/daemon-rpc-types'; import log from '../../shared/logging'; import { useAppContext } from '../context'; import GlMap, { ConnectionState, Coordinate } from '../lib/3dmap'; -import { useCombinedRefs, useRerenderer } from '../lib/utilityHooks'; +import { + useCombinedRefs, + useEffectEvent, + useRefCallback, + useRerenderer, +} from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; // Default to Gothenburg when we don't know the actual location. @@ -29,7 +34,9 @@ export default function Map() { const hasLocationValue = hasLocation(connection); const location = useMemo(() => { return hasLocationValue ? connection : defaultLocation; - }, [hasLocationValue, connection.latitude, connection.longitude]); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [hasLocationValue, connection.longitude, connection.latitude]); if (window.env.e2e) { return null; @@ -83,43 +90,45 @@ function MapInner(props: MapInnerProps) { const mapRef = useRef(); const canvasRef = useRef(); + + // eslint-disable-next-line react-compiler/react-compiler const width = applyPixelRatio(canvasRef.current?.clientWidth ?? window.innerWidth); + // This constant is used for the height the first frame that is rendered only. + // eslint-disable-next-line react-compiler/react-compiler const height = applyPixelRatio(canvasRef.current?.clientHeight ?? 493); // Hack to rerender when window size changes or when ref is set. - const [onSizeChange, sizeChangeCounter] = useRerenderer(); + const [onSizeChangeImpl, sizeChangeCounter] = useRerenderer(); + const onSizeChange = useEffectEvent(onSizeChangeImpl); + + const animationFrameCallback = useEffectEvent((now: number) => { + now *= 0.001; // convert to seconds + + // Propagate location change to the map + if (newParams.current) { + mapRef.current?.setLocation( + newParams.current.location, + newParams.current.connectionState, + now, + props.animate, + ); + newParams.current = undefined; + } - const render = useCallback(() => requestAnimationFrame(animationFrameCallback), []); + mapRef.current?.draw(now); - const animationFrameCallback = useCallback( - (now: number) => { - now *= 0.001; // convert to seconds - - // Propagate location change to the map - if (newParams.current) { - mapRef.current?.setLocation( - newParams.current.location, - newParams.current.connectionState, - now, - props.animate, - ); - newParams.current = undefined; - } - - mapRef.current?.draw(now); - - // Stops rendering if pause is true. This happens when there is no ongoing movements - if (!pause.current) { - render(); - } - }, - [props.animate], - ); + // Stops rendering if pause is true. This happens when there is no ongoing movements + if (!pause.current) { + render(); + } + }); + + const render = useCallback(() => requestAnimationFrame(animationFrameCallback), []); // This is called when the canvas has been rendered the first time and initializes the gl context // and the map. - const canvasCallback = useCallback(async (canvas: HTMLCanvasElement | null) => { + const canvasCallback = useRefCallback(async (canvas: HTMLCanvasElement | null) => { if (!canvas) { return; } @@ -137,7 +146,7 @@ function MapInner(props: MapInnerProps) { ); render(); - }, []); + }); // Set new params when the location or connection state has changed, and unpause if paused useEffect(() => { @@ -150,12 +159,12 @@ function MapInner(props: MapInnerProps) { pause.current = false; render(); } - }, [props.location, props.connectionState]); + }, [props.location, props.connectionState, render]); useEffect(() => { mapRef.current?.updateViewport(); render(); - }, [width, height, sizeChangeCounter]); + }, [width, height, sizeChangeCounter, render]); // Resize canvas if window size changes useEffect(() => { @@ -168,10 +177,12 @@ function MapInner(props: MapInnerProps) { return () => unsubscribe(); }, []); + const devicePixelRatio = window.devicePixelRatio; + // Log new scale factor if it changes useEffect(() => { - log.verbose(`Map canvas scale factor: ${window.devicePixelRatio}, using: ${getPixelRatio()}`); - }, [window.devicePixelRatio]); + log.verbose(`Map canvas scale factor: ${devicePixelRatio}, using: ${getPixelRatio()}`); + }, [devicePixelRatio]); const combinedCanvasRef = useCombinedRefs(canvasRef, canvasCallback); diff --git a/gui/src/renderer/components/Modal.tsx b/gui/src/renderer/components/Modal.tsx index 1b83e105974f..e339566c243a 100644 --- a/gui/src/renderer/components/Modal.tsx +++ b/gui/src/renderer/components/Modal.tsx @@ -4,6 +4,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import log from '../../shared/logging'; +import { useEffectEvent } from '../lib/utility-hooks'; import { useWillExit } from '../lib/will-exit'; import * as AppButton from './AppButton'; import { measurements, normalText, tinyText } from './common-styles'; @@ -192,13 +193,15 @@ export function ModalAlert(props: IModalAlertProps & { isOpen: boolean }) { } }, [willExit, isOpen]); - useEffect(() => { + const onOpenStateChange = useEffectEvent((isOpen: boolean) => { setOpenState(({ isClosing, wasOpen }) => ({ isClosing: isClosing || (wasOpen && !isOpen), // Unmounting the Modal during view transitions result in a visual glitch. wasOpen: willExit ? wasOpen : isOpen, })); - }, [isOpen]); + }); + + useEffect(() => onOpenStateChange(isOpen), [isOpen]); if (!openState.wasOpen && !isOpen && !openState.isClosing) { return null; diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx index ee316de78331..680393a6c776 100644 --- a/gui/src/renderer/components/NavigationBar.tsx +++ b/gui/src/renderer/components/NavigationBar.tsx @@ -5,7 +5,7 @@ import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { transitions, useHistory } from '../lib/history'; -import { useCombinedRefs } from '../lib/utilityHooks'; +import { useCombinedRefs, useEffectEvent } from '../lib/utility-hooks'; import CustomScrollbars, { CustomScrollbarsRef, IScrollEvent } from './CustomScrollbars'; import InfoButton from './InfoButton'; import { BackActionContext } from './KeyboardNavigation'; @@ -97,37 +97,44 @@ export const NavigationScrollbars = React.forwardRef(function NavigationScrollba const ref = useRef(); const combinedRefs = useCombinedRefs(forwardedRef, ref); - useEffect(() => { - const beforeunload = () => { - if (ref.current) { - history.location.state.scrollPosition = ref.current.getScrollPosition(); - setNavigationHistory(history.asObject); - } - }; + const beforeunload = useEffectEvent(() => { + if (ref.current) { + history.recordScrollPosition(ref.current.getScrollPosition()); + setNavigationHistory(history.asObject); + } + }); + useEffect(() => { window.addEventListener('beforeunload', beforeunload); - return () => window.removeEventListener('beforeunload', beforeunload); }, []); - useLayoutEffect(() => { + const onMount = useEffectEvent(() => { const location = history.location; if (history.action === 'POP') { ref.current?.scrollTo(...location.state.scrollPosition); } + }); - return () => { - if (history.action === 'PUSH' && ref.current) { - location.state.scrollPosition = ref.current.getScrollPosition(); - setNavigationHistory(history.asObject); - } - }; - }, []); + const onUnmount = useEffectEvent(() => { + if (history.action === 'PUSH' && ref.current) { + history.recordScrollPosition(ref.current.getScrollPosition()); + setNavigationHistory(history.asObject); + } + }); - const handleScroll = useCallback((event: IScrollEvent) => { - onScroll(event); + useLayoutEffect(() => { + onMount(); + return () => onUnmount(); }, []); + const handleScroll = useCallback( + (event: IScrollEvent) => { + onScroll(event); + }, + [onScroll], + ); + return ( history.getPopTransition().name !== transitions.dismiss.name, []); + const backIcon = useMemo( + () => history.getPopTransition().name !== transitions.dismiss.name, + [history], + ); const { parentBackAction } = useContext(BackActionContext); const iconSource = backIcon ? 'icon-back' : 'icon-close-down'; const ariaLabel = backIcon ? messages.gettext('Back') : messages.gettext('Close'); diff --git a/gui/src/renderer/components/NotificationArea.tsx b/gui/src/renderer/components/NotificationArea.tsx index fff30300d0b4..dac62db192e6 100644 --- a/gui/src/renderer/components/NotificationArea.tsx +++ b/gui/src/renderer/components/NotificationArea.tsx @@ -127,7 +127,7 @@ interface INotificationActionWrapperProps { } function NotificationActionWrapper(props: INotificationActionWrapperProps) { - const history = useHistory(); + const { push } = useHistory(); const { openLinkWithAuth, openUrl } = useAppContext(); const [troubleshootInfo, setTroubleshootInfo] = useState(); @@ -150,12 +150,12 @@ function NotificationActionWrapper(props: INotificationActionWrapperProps) { } return Promise.resolve(); - }, [props.action]); + }, [openLinkWithAuth, openUrl, props.action]); const goToProblemReport = useCallback(() => { setTroubleshootInfo(undefined); - history.push(RoutePath.problemReport, { transition: transitions.show }); - }, []); + push(RoutePath.problemReport, { transition: transitions.show }); + }, [push]); const closeTroubleshootInfo = useCallback(() => setTroubleshootInfo(undefined), []); diff --git a/gui/src/renderer/components/NotificationBanner.tsx b/gui/src/renderer/components/NotificationBanner.tsx index f79855f00458..924f65ff9914 100644 --- a/gui/src/renderer/components/NotificationBanner.tsx +++ b/gui/src/renderer/components/NotificationBanner.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import { InAppNotificationIndicatorType } from '../../shared/notifications/notification'; -import { useStyledRef } from '../lib/utilityHooks'; +import { useEffectEvent, useLastDefinedValue, useStyledRef } from '../lib/utility-hooks'; import * as AppButton from './AppButton'; import { tinyText } from './common-styles'; import ImageView from './ImageView'; @@ -159,13 +159,9 @@ export function NotificationBanner(props: INotificationBannerProps) { const contentRef = useStyledRef(); - // Save last non-undefined children to be able to show them during the hide-transition. - const prevChildren = useRef(); - useEffect(() => { - prevChildren.current = props.children ?? prevChildren.current; - }, [props.children]); + const children = useLastDefinedValue(props.children); - useEffect(() => { + const updateHeightEvent = useEffectEvent(() => { const newHeight = props.children !== undefined ? (contentRef.current?.getBoundingClientRect().height ?? 0) : 0; if (newHeight !== contentHeight) { @@ -174,9 +170,11 @@ export function NotificationBanner(props: INotificationBannerProps) { } }); + useEffect(() => updateHeightEvent()); + return ( - {props.children ?? prevChildren.current} + {children} ); } diff --git a/gui/src/renderer/components/OpenVpnSettings.tsx b/gui/src/renderer/components/OpenVpnSettings.tsx index 5013b158f8eb..571e8e357103 100644 --- a/gui/src/renderer/components/OpenVpnSettings.tsx +++ b/gui/src/renderer/components/OpenVpnSettings.tsx @@ -16,7 +16,7 @@ import { useAppContext } from '../context'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import { AriaDescription, AriaInput, AriaInputGroup, AriaLabel } from './AriaGroup'; import * as Cell from './cell'; diff --git a/gui/src/renderer/components/PageSlider.tsx b/gui/src/renderer/components/PageSlider.tsx index 56d64954e79e..d03c90a0e297 100644 --- a/gui/src/renderer/components/PageSlider.tsx +++ b/gui/src/renderer/components/PageSlider.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { colors } from '../../config.json'; import { NonEmptyArray } from '../../shared/utils'; -import { useStyledRef } from '../lib/utilityHooks'; +import { useStyledRef } from '../lib/utility-hooks'; import { Icon } from './cell'; const PAGE_GAP = 16; @@ -92,7 +92,7 @@ export default function PageSlider(props: PageSliderProps) { // Trigger a rerender when the page number has changed. This needs to be done to update the // states of the arrows and page indicators. - const handleScroll = useCallback(() => setPageNumberState(getPageNumber()), []); + const handleScroll = useCallback(() => setPageNumberState(getPageNumber()), [getPageNumber]); useEffect(() => { document.addEventListener('keydown', handleKeyDown); @@ -229,9 +229,11 @@ interface PageIndicatorProps { } function PageIndicator(props: PageIndicatorProps) { + const { goToPage } = props; + const onClick = useCallback(() => { - props.goToPage(props.pageNumber); - }, [props.goToPage, props.pageNumber]); + goToPage(props.pageNumber); + }, [goToPage, props.pageNumber]); return ( diff --git a/gui/src/renderer/components/ProblemReport.tsx b/gui/src/renderer/components/ProblemReport.tsx index fd481cde07d7..36ddc09c4556 100644 --- a/gui/src/renderer/components/ProblemReport.tsx +++ b/gui/src/renderer/components/ProblemReport.tsx @@ -17,6 +17,7 @@ import { getDownloadUrl } from '../../shared/version'; import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; import { useHistory } from '../lib/history'; +import { useEffectEvent } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import support from '../redux/support/actions'; import * as AppButton from './AppButton'; @@ -142,15 +143,21 @@ function Form() { } finally { setDisableActions(false); } - }, []); + }, [collectLog, viewLog]); - const onChangeEmail = useCallback((event: ChangeEvent) => { - setEmail(event.target.value); - }, []); + const onChangeEmail = useCallback( + (event: ChangeEvent) => { + setEmail(event.target.value); + }, + [setEmail], + ); - const onChangeDescription = useCallback((event: ChangeEvent) => { - setMessage(event.target.value); - }, []); + const onChangeDescription = useCallback( + (event: ChangeEvent) => { + setMessage(event.target.value); + }, + [setMessage], + ); const validate = () => message.trim().length > 0; @@ -251,7 +258,7 @@ function Failed() { const handleEditMessage = useCallback(() => { setSendState(SendState.initial); - }, []); + }, [setSendState]); return ( @@ -291,7 +298,7 @@ function NoEmailDialog() { const onCancelNoEmailDialog = useCallback(() => { setSendState(SendState.initial); - }, []); + }, [setSendState]); return ( state.connection.isBlocked); @@ -327,14 +334,12 @@ function OutdatedVersionWarningDialog() { const openDownloadLink = useCallback(async () => { await openUrl(getDownloadUrl(suggestedIsBeta)); - }, [suggestedIsBeta]); - - const onClose = useCallback(() => history.pop(), [history.pop]); + }, [openUrl, suggestedIsBeta]); const outdatedVersionCancel = useCallback(() => { acknowledgeOutdatedVersion(); - onClose(); - }, [onClose]); + pop(); + }, [acknowledgeOutdatedVersion, pop]); const message = messages.pgettext( 'support-view', @@ -369,7 +374,7 @@ function OutdatedVersionWarningDialog() { {messages.gettext('Cancel')} , ]} - close={onClose} + close={pop} /> ); } @@ -396,7 +401,7 @@ const useCollectLog = () => { throw error; } } - }, [collectLogPromise]); + }, [accountHistory, collectProblemReport]); return { collectLog }; }; @@ -434,7 +439,7 @@ const ProblemReportContextProvider = ({ children }: { children: ReactNode }) => } catch { setSendState(SendState.failed); } - }, [email, message]); + }, [clearReportForm, collectLog, email, message, sendProblemReport]); const onSend = useCallback(async () => { if (sendState === SendState.initial && email.length === 0) { @@ -453,12 +458,14 @@ const ProblemReportContextProvider = ({ children }: { children: ReactNode }) => } }, [email, sendReport, sendState]); + const onMount = useEffectEvent((email: string, message: string) => { + saveReportForm({ email, message }); + }); + /** * Save the form whenever email or message gets updated */ - useEffect(() => { - saveReportForm({ email, message }); - }, [email, message]); + useEffect(() => onMount(email, message), [email, message]); const value: ProblemReportContextType = useMemo( () => ({ sendState, setSendState, email, setEmail, message, setMessage, onSend }), diff --git a/gui/src/renderer/components/ProxyForm.tsx b/gui/src/renderer/components/ProxyForm.tsx index 7e006f65c014..9a163ceebfd4 100644 --- a/gui/src/renderer/components/ProxyForm.tsx +++ b/gui/src/renderer/components/ProxyForm.tsx @@ -11,6 +11,7 @@ import { } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { IpAddress } from '../lib/ip'; +import { useEffectEvent } from '../lib/utility-hooks'; import * as Cell from './cell'; import { SettingsForm, useSettingsFormSubmittable } from './cell/SettingsForm'; import { SettingsGroup } from './cell/SettingsGroup'; @@ -59,13 +60,15 @@ interface ProxyFormContextProviderProps { } function ProxyFormContextProvider(props: React.PropsWithChildren) { + const { onSave: propsOnSave } = props; + const [proxy, setProxy] = useState(props.proxy); const onSave = useCallback(() => { if (proxy !== undefined) { - props.onSave(proxy); + propsOnSave(proxy); } - }, [proxy, props.onSave]); + }, [proxy, propsOnSave]); const value = useMemo( () => ({ proxy, setProxy, onSave, onCancel: props.onCancel, onDelete: props.onDelete }), @@ -274,18 +277,22 @@ function EditShadowsocks(props: EditProxyProps) { [], ); + const onUpdate = useEffectEvent( + (ip: string, port: number | undefined, password: string, cipher: string | undefined) => { + if (ip !== '' && port !== undefined && cipher !== undefined) { + props.onUpdate({ + type: 'shadowsocks', + ip, + port, + password, + cipher, + }); + } + }, + ); + // Report back to form component with the proxy values when all required values are set. - useEffect(() => { - if (ip !== '' && port !== undefined && cipher !== undefined) { - props.onUpdate({ - type: 'shadowsocks', - ip, - port, - password, - cipher, - }); - } - }, [ip, port, password, cipher]); + useEffect(() => onUpdate(ip, port, password, cipher), [ip, port, password, cipher]); return ( @@ -346,21 +353,25 @@ function EditSocks5Remote(props: EditProxyProps) { const [username, setUsername] = useState(props.proxy?.authentication?.username ?? ''); const [password, setPassword] = useState(props.proxy?.authentication?.password ?? ''); + const onUpdate = useEffectEvent( + (ip: string, port: number | undefined, username: string, password: string) => { + if ( + ip !== '' && + port !== undefined && + (!authentication || (username !== '' && password !== '')) + ) { + props.onUpdate({ + type: 'socks5-remote', + ip, + port, + authentication: authentication ? { username, password } : undefined, + }); + } + }, + ); + // Report back to form component with the proxy values when all required values are set. - useEffect(() => { - if ( - ip !== '' && - port !== undefined && - (!authentication || (username !== '' && password !== '')) - ) { - props.onUpdate({ - type: 'socks5-remote', - ip, - port, - authentication: authentication ? { username, password } : undefined, - }); - } - }, [ip, port, username, password]); + useEffect(() => onUpdate(ip, port, username, password), [ip, port, username, password]); return ( @@ -435,17 +446,29 @@ function EditSocks5Local(props: EditProxyProps) { [], ); - useEffect(() => { - if (remoteIp !== '' && remotePort !== undefined && localPort !== undefined) { - props.onUpdate({ - type: 'socks5-local', - remoteIp, - remotePort, - remoteTransportProtocol, - localPort, - }); - } - }, [remoteIp, remotePort, localPort, remoteTransportProtocol]); + const onUpdate = useEffectEvent( + ( + remoteIp: string, + remotePort: number | undefined, + localPort: number | undefined, + remoteTransportProtocol: RelayProtocol, + ) => { + if (remoteIp !== '' && remotePort !== undefined && localPort !== undefined) { + props.onUpdate({ + type: 'socks5-local', + remoteIp, + remotePort, + remoteTransportProtocol, + localPort, + }); + } + }, + ); + + useEffect( + () => onUpdate(remoteIp, remotePort, localPort, remoteTransportProtocol), + [remoteIp, remotePort, localPort, remoteTransportProtocol], + ); return ( <> diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx index 7f637ab12542..c88d68a087d9 100644 --- a/gui/src/renderer/components/RedeemVoucher.tsx +++ b/gui/src/renderer/components/RedeemVoucher.tsx @@ -6,8 +6,6 @@ import { VoucherResponse } from '../../shared/daemon-rpc-types'; import { formatRelativeDate } from '../../shared/date-helper'; import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; -import useActions from '../lib/actionsHook'; -import accountActions from '../redux/account/actions'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import ImageView from './ImageView'; @@ -69,7 +67,6 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) { const { onSubmit, onSuccess, onFailure } = props; const { submitVoucher } = useAppContext(); - const { updateAccountExpiry } = useActions(accountActions); const [value, setValue] = useState(''); const [submitting, setSubmitting] = useState(false); @@ -100,7 +97,7 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) { } else { onFailure?.(); } - }, [value, valueValid, onSubmit, submitVoucher, updateAccountExpiry, onSuccess, onFailure]); + }, [value, valueValid, onSubmit, submitVoucher, onSuccess, onFailure]); return ( (); const onInput = useCallback( (event: React.FormEvent) => { const element = event.target as HTMLInputElement; - props.onSearch(element.value); + onSearch(element.value); }, - [props.onSearch], + [onSearch], ); const onClear = useCallback(() => { - props.onSearch(''); + onSearch(''); inputRef.current?.blur(); - }, [props.onSearch]); + }, [inputRef, onSearch]); - useEffect(() => { + const focusInput = useEffectEvent(() => { if (!props.disableAutoFocus) { inputRef.current?.focus({ preventScroll: true }); } - }, []); + }); + + useEffect(() => focusInput(), []); return ( diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx index eb0ad038da34..9bbc110fc19c 100644 --- a/gui/src/renderer/components/SelectLanguage.tsx +++ b/gui/src/renderer/components/SelectLanguage.tsx @@ -24,7 +24,7 @@ const StyledSelector = styled(Selector)({ }); export default function SelectLanguage() { - const history = useHistory(); + const { pop } = useHistory(); const { preferredLocale, preferredLocalesList, setPreferredLocale } = usePreferredLocale(); const scrollView = useRef(null); const selectedCellRef = useRef(null); @@ -32,9 +32,9 @@ export default function SelectLanguage() { const selectLocale = useCallback( async (locale: string) => { await setPreferredLocale(locale); - history.pop(); + pop(); }, - [history.pop], + [pop, setPreferredLocale], ); const scrollToSelectedCell = () => { @@ -52,7 +52,7 @@ export default function SelectLanguage() { }, []); return ( - + @@ -97,7 +97,7 @@ function usePreferredLocale() { const preferredLocalesList: SelectorItem[] = useMemo(() => { return [...getPreferredLocaleList().map(({ name, code }) => ({ label: name, value: code }))]; - }, []); + }, [getPreferredLocaleList]); return { preferredLocale, preferredLocalesList, setPreferredLocale }; } diff --git a/gui/src/renderer/components/SettingsImport.tsx b/gui/src/renderer/components/SettingsImport.tsx index d93064ff2e36..8fefbedf20e8 100644 --- a/gui/src/renderer/components/SettingsImport.tsx +++ b/gui/src/renderer/components/SettingsImport.tsx @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; @@ -9,7 +9,7 @@ import { useAppContext } from '../context'; import useActions from '../lib/actionsHook'; import { transitions, useHistory } from '../lib/history'; import { RoutePath } from '../lib/routes'; -import { useAsyncEffect, useBoolean } from '../lib/utilityHooks'; +import { useBoolean, useEffectEvent } from '../lib/utility-hooks'; import settingsImportActions from '../redux/settings-import/actions'; import { useSelector } from '../redux/store'; import { measurements, normalText } from './common-styles'; @@ -70,22 +70,25 @@ export default function SettingsImport() { const [importStatus, setImportStatusImpl] = useState(); const importStatusResetScheduler = useScheduler(); - const setImportStatus = useCallback((status?: ImportStatus) => { - // Cancel scheduled status clearing. - importStatusResetScheduler.cancel(); - setImportStatusImpl(status); + const setImportStatus = useCallback( + (status?: ImportStatus) => { + // Cancel scheduled status clearing. + importStatusResetScheduler.cancel(); + setImportStatusImpl(status); - // The status text should be cleared after 10 seconds. - if (status !== undefined) { - importStatusResetScheduler.schedule(() => setImportStatusImpl(undefined), 10_000); - } - }, []); + // The status text should be cleared after 10 seconds. + if (status !== undefined) { + importStatusResetScheduler.schedule(() => setImportStatusImpl(undefined), 10_000); + } + }, + [importStatusResetScheduler], + ); const confirmClear = useCallback(() => { hideClearDialog(); void clearAllRelayOverrides(); setImportStatus(undefined); - }, []); + }, [clearAllRelayOverrides, hideClearDialog, setImportStatus]); const navigateTextImport = useCallback(() => { history.push(RoutePath.settingsTextImport, { transition: transitions.show }); @@ -105,9 +108,9 @@ export default function SettingsImport() { } catch { setImportStatus({ successful: false, type: 'file', name }); } - }, []); + }, [getPathBaseName, importSettingsFile, setImportStatus, showOpenDialog]); - useAsyncEffect(async () => { + const onMount = useEffectEvent(async () => { if (history.action === 'POP' && textForm.submit && textForm.value !== '') { try { await importSettingsText(textForm.value); @@ -118,7 +121,9 @@ export default function SettingsImport() { unsetSubmitSettingsImportForm(); } } - }, []); + }); + + useEffect(() => void onMount(), []); return ( diff --git a/gui/src/renderer/components/SettingsTextImport.tsx b/gui/src/renderer/components/SettingsTextImport.tsx index 7d51e7ac9325..c6cb325b0cd2 100644 --- a/gui/src/renderer/components/SettingsTextImport.tsx +++ b/gui/src/renderer/components/SettingsTextImport.tsx @@ -5,7 +5,7 @@ import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; import useActions from '../lib/actionsHook'; import { useHistory } from '../lib/history'; -import { useCombinedRefs, useStyledRef } from '../lib/utilityHooks'; +import { useCombinedRefs, useRefCallback, useStyledRef } from '../lib/utility-hooks'; import settingsImportActions from '../redux/settings-import/actions'; import { useSelector } from '../redux/store'; import ImageView from './ImageView'; @@ -21,18 +21,18 @@ const StyledTextArea = styled.textarea({ }); export default function SettingsTextImport() { - const history = useHistory(); + const { pop } = useHistory(); const { saveSettingsImportForm } = useActions(settingsImportActions); // The textarea value is saved in redux to make it persistent when leaving the view. const initialValue = useSelector((state) => state.settingsImport.value); const textareaRef = useStyledRef(); - const onTextareaLoad = useCallback((element?: HTMLTextAreaElement) => { + const onTextareaLoad = useRefCallback((element?: HTMLTextAreaElement) => { if (element) { element.value = initialValue; } - }, []); + }); const combinedTextAreaRef = useCombinedRefs(textareaRef, onTextareaLoad); @@ -40,15 +40,15 @@ export default function SettingsTextImport() { if (textareaRef.current?.value) { saveSettingsImportForm(textareaRef.current.value, true); } - history.pop(); - }, [history]); + pop(); + }, [pop, saveSettingsImportForm, textareaRef]); const back = useCallback(() => { if (textareaRef.current) { saveSettingsImportForm(textareaRef.current.value, false); } - history.pop(); - }, [history]); + pop(); + }, [pop, saveSettingsImportForm, textareaRef]); return ( diff --git a/gui/src/renderer/components/SimpleInput.tsx b/gui/src/renderer/components/SimpleInput.tsx index 3d2a1a63a745..8d4a51d8e9b7 100644 --- a/gui/src/renderer/components/SimpleInput.tsx +++ b/gui/src/renderer/components/SimpleInput.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'; import React from 'react'; import styled from 'styled-components'; -import { useCombinedRefs } from '../lib/utilityHooks'; +import { useCombinedRefs } from '../lib/utility-hooks'; import { normalText } from './common-styles'; const StyledInput = styled.input.attrs({ type: 'text' })(normalText, { @@ -19,41 +19,51 @@ interface SimpleInputProps extends Omit) { - const { onChangeValue, onSubmitValue, ...otherProps } = props; + const { + onChangeValue, + onSubmitValue, + onChange: propsOnChange, + onSubmit: propsOnSubmit, + onKeyPress: propsOnKeyPress, + ...otherProps + } = props; const [value, setValue] = useState((props.value as string) ?? ''); const onChange = useCallback( (event: React.ChangeEvent) => { setValue(event.target.value); - otherProps.onChange?.(event); + propsOnChange?.(event); onChangeValue?.(event.target.value); }, - [otherProps.onChange, onChangeValue], + [propsOnChange, onChangeValue], ); const onSubmit = useCallback( (event: React.FormEvent) => { - otherProps.onSubmit?.(event); + propsOnSubmit?.(event); onSubmitValue?.(value); }, - [otherProps.onSubmit, onSubmitValue, value], + [propsOnSubmit, onSubmitValue, value], ); const onKeyPress = useCallback( (event: React.KeyboardEvent) => { - props.onKeyPress?.(event); + propsOnKeyPress?.(event); if (event.key === 'Enter') { onSubmitValue?.(value); } }, - [props.onKeyPress, onSubmitValue, value], + [propsOnKeyPress, onSubmitValue, value], ); - const refCallback = useCallback((element: HTMLInputElement | null) => { - if (element && otherProps.autoFocus) { - setTimeout(() => element.focus()); - } - }, []); + const refCallback = useCallback( + (element: HTMLInputElement | null) => { + if (element && otherProps.autoFocus) { + setTimeout(() => element.focus()); + } + }, + [otherProps.autoFocus], + ); const combinedRef = useCombinedRefs(refCallback, ref); diff --git a/gui/src/renderer/components/SplitTunnelingSettings.tsx b/gui/src/renderer/components/SplitTunnelingSettings.tsx index 2cee823b3eb3..ed999ba867a5 100644 --- a/gui/src/renderer/components/SplitTunnelingSettings.tsx +++ b/gui/src/renderer/components/SplitTunnelingSettings.tsx @@ -12,7 +12,7 @@ import { messages } from '../../shared/gettext'; import { useAppContext } from '../context'; import { useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; -import { useAsyncEffect, useStyledRef } from '../lib/utilityHooks'; +import { useEffectEvent, useStyledRef } from '../lib/utility-hooks'; import { IReduxState } from '../redux/store'; import Accordion from './Accordion'; import * as AppButton from './AppButton'; @@ -116,7 +116,7 @@ function useFilePicker( if (file.filePaths[0]) { select(file.filePaths[0]); } - }, [buttonLabel, setOpen, select]); + }, [setOpen, showOpenDialog, buttonLabel, filter, select]); } function LinuxSplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps) { @@ -126,7 +126,12 @@ function LinuxSplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps const [applications, setApplications] = useState(); const [browseError, setBrowseError] = useState(); - useEffect(() => void getLinuxSplitTunnelingApplications().then(setApplications), []); + const updateApplications = useEffectEvent(async () => { + const applications = await getLinuxSplitTunnelingApplications(); + setApplications(applications); + }); + + useEffect(() => void updateApplications(), []); const launchApplication = useCallback( async (application: ILinuxSplitTunnelingApplication | string) => { @@ -220,12 +225,14 @@ interface ILinuxApplicationRowProps { } function LinuxApplicationRow(props: ILinuxApplicationRowProps) { + const { onSelect } = props; + const [showWarning, setShowWarning] = useState(false); const launch = useCallback(() => { setShowWarning(false); - props.onSelect?.(props.application); - }, [props.onSelect, props.application]); + onSelect?.(props.application); + }, [onSelect, props.application]); const showWarningDialog = useCallback(() => setShowWarning(true), []); const hideWarningDialog = useCallback(() => setShowWarning(false), []); @@ -299,6 +306,8 @@ function LinuxApplicationRow(props: ILinuxApplicationRowProps) { } export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsProps) { + const { scrollToTop } = props; + const { addSplitTunnelingApplication, removeSplitTunnelingApplication, @@ -313,7 +322,8 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro const [searchTerm, setSearchTerm] = useState(''); const [applications, setApplications] = useState(); - useAsyncEffect(async () => { + + const onMount = useEffectEvent(async () => { const { fromCache, applications } = await getSplitTunnelingApplications(); setApplications(applications); @@ -321,7 +331,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro const { applications } = await getSplitTunnelingApplications(true); setApplications(applications); } - }, []); + }); + + useEffect(() => void onMount(), []); const filteredSplitApplications = useMemo( () => @@ -377,7 +389,7 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro } removeSplitTunnelingApplication(application); }, - [removeSplitTunnelingApplication, splitTunnelingEnabled], + [removeSplitTunnelingApplication, setSplitTunnelingState, splitTunnelingEnabled], ); const filePickerCallback = useFilePicker( @@ -388,9 +400,9 @@ export function SplitTunnelingSettings(props: IPlatformSplitTunnelingSettingsPro ); const addWithFilePicker = useCallback(async () => { - props.scrollToTop(); + scrollToTop(); await filePickerCallback(); - }, [filePickerCallback, props.scrollToTop]); + }, [filePickerCallback, scrollToTop]); const excludedRowRenderer = useCallback( (application: ISplitTunnelingApplication) => ( @@ -522,17 +534,19 @@ interface IApplicationRowProps { } function ApplicationRow(props: IApplicationRowProps) { + const { onAdd: propsOnAdd, onRemove: propsOnRemove, onDelete: propsOnDelete } = props; + const onAdd = useCallback(() => { - props.onAdd?.(props.application); - }, [props.onAdd, props.application]); + propsOnAdd?.(props.application); + }, [propsOnAdd, props.application]); const onRemove = useCallback(() => { - props.onRemove?.(props.application); - }, [props.onRemove, props.application]); + propsOnRemove?.(props.application); + }, [propsOnRemove, props.application]); const onDelete = useCallback(() => { - props.onDelete?.(props.application); - }, [props.onDelete, props.application]); + propsOnDelete?.(props.application); + }, [propsOnDelete, props.application]); return ( diff --git a/gui/src/renderer/components/TooManyDevices.tsx b/gui/src/renderer/components/TooManyDevices.tsx index 1f1fcf924764..3e636bcb444e 100644 --- a/gui/src/renderer/components/TooManyDevices.tsx +++ b/gui/src/renderer/components/TooManyDevices.tsx @@ -11,7 +11,7 @@ import { useAppContext } from '../context'; import { transitions, useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; import { RoutePath } from '../lib/routes'; -import { useBoolean } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import * as Cell from './cell'; @@ -93,7 +93,7 @@ const StyledRemoveDeviceButton = styled.button({ }); export default function TooManyDevices() { - const history = useHistory(); + const { reset } = useHistory(); const { removeDevice, login, cancelLogin } = useAppContext(); const accountNumber = useSelector((state) => state.account.accountNumber)!; const devices = useSelector((state) => state.account.devices); @@ -108,12 +108,12 @@ export default function TooManyDevices() { const continueLogin = useCallback(() => { void login(accountNumber); - history.reset(RoutePath.login, { transition: transitions.pop }); - }, [login, accountNumber]); + reset(RoutePath.login, { transition: transitions.pop }); + }, [reset, login, accountNumber]); const cancel = useCallback(() => { cancelLogin(); - history.reset(RoutePath.login, { transition: transitions.pop }); - }, [history.reset, cancelLogin]); + reset(RoutePath.login, { transition: transitions.pop }); + }, [reset, cancelLogin]); const iconSource = getIconSource(devices); const title = getTitle(devices); @@ -188,6 +188,8 @@ interface IDeviceProps { } function Device(props: IDeviceProps) { + const { onRemove: propsOnRemove } = props; + const { fetchDevices } = useAppContext(); const accountNumber = useSelector((state) => state.account.accountNumber)!; const [confirmationVisible, showConfirmation, hideConfirmation] = useBoolean(false); @@ -211,18 +213,18 @@ function Device(props: IDeviceProps) { setError(); } }, - [fetchDevices, accountNumber, hideConfirmation, setError], + [fetchDevices, accountNumber, props.device.id, hideConfirmation, unsetDeleting, setError], ); const onRemove = useCallback(async () => { setDeleting(); hideConfirmation(); try { - await props.onRemove(props.device.id); + await propsOnRemove(props.device.id); } catch (e) { await handleError(e as Error); } - }, [props.onRemove, props.device.id, hideConfirmation, setDeleting, handleError]); + }, [propsOnRemove, props.device.id, hideConfirmation, setDeleting, handleError]); const capitalizedDeviceName = capitalizeEveryWord(props.device.name); const createdDate = props.device.created.toISOString().split('T')[0]; diff --git a/gui/src/renderer/components/VpnSettings.tsx b/gui/src/renderer/components/VpnSettings.tsx index d0253ef4805d..c23ecdbf4719 100644 --- a/gui/src/renderer/components/VpnSettings.tsx +++ b/gui/src/renderer/components/VpnSettings.tsx @@ -10,8 +10,9 @@ import { useAppContext } from '../context'; import { useRelaySettingsUpdater } from '../lib/constraint-updater'; import { useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; +import { useTunnelProtocol } from '../lib/relay-settings-hooks'; import { RoutePath } from '../lib/routes'; -import { useBoolean, useTunnelProtocol } from '../lib/utilityHooks'; +import { useBoolean } from '../lib/utility-hooks'; import { RelaySettingsRedux } from '../redux/settings/reducers'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; @@ -255,7 +256,7 @@ function useDns(setting: keyof IDnsOptions['defaultOptions']) { [setting]: enabled, }, }), - [dns, setDnsOptions], + [setting, dns, setDnsOptions], ); return [dns, updateBlockSetting] as const; @@ -730,7 +731,7 @@ function TunnelProtocolSetting() { disabled: openVpnDisabled, }, ], - [], + [openVpnDisabled], ); return ( diff --git a/gui/src/renderer/components/WireguardSettings.tsx b/gui/src/renderer/components/WireguardSettings.tsx index 9a79111ad7cb..b199bd5ee576 100644 --- a/gui/src/renderer/components/WireguardSettings.tsx +++ b/gui/src/renderer/components/WireguardSettings.tsx @@ -234,7 +234,11 @@ function ObfuscationSettings() { value: ObfuscationType.off, }, ], - [], + [ + obfuscationSettings.shadowsocksSettings.port, + obfuscationSettings.udp2tcpSettings.port, + subLabelTemplate, + ], ); const selectObfuscationType = useCallback( diff --git a/gui/src/renderer/components/cell/Input.tsx b/gui/src/renderer/components/cell/Input.tsx index 97fe01a18568..de2649e3c6f8 100644 --- a/gui/src/renderer/components/cell/Input.tsx +++ b/gui/src/renderer/components/cell/Input.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from 'react'; import styled from 'styled-components'; import { colors } from '../../../config.json'; -import { useBoolean, useCombinedRefs, useStyledRef } from '../../lib/utilityHooks'; +import { useBoolean, useCombinedRefs, useEffectEvent, useStyledRef } from '../../lib/utility-hooks'; import { normalText } from '../common-styles'; import ImageView from '../ImageView'; import { BackAction } from '../KeyboardNavigation'; @@ -59,6 +59,10 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref) => { setFocused(); - props.onFocus?.(event); + propsOnFocus?.(event); }, - [props.onFocus], + [propsOnFocus, setFocused], ); const onBlur = useCallback( (event: React.FocusEvent) => { setBlurred(); - props.onBlur?.(event); + propsOnBlur?.(event); if (submitOnBlur) { onSubmit(value); } }, - [value, props.onBlur, validateValue, onSubmit, submitOnBlur], + [setBlurred, propsOnBlur, submitOnBlur, onSubmit, value], ); const onChange = useCallback( @@ -110,10 +114,10 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref { + const handleInitialValueChange = useEffectEvent((initialValue?: string) => { if ( !isFocused && props.value === undefined && - props.initialValue !== undefined && - internalValue !== props.initialValue + initialValue !== undefined && + internalValue !== initialValue ) { - setInternalValue(props.initialValue); - onChangeValue?.(props.initialValue); + setInternalValue(initialValue); + onChangeValue?.(initialValue); } + }); + + // If the the initialValue changes in the uncontrolled mode when the user isn't currently writing, + // then we want to update the value. + useEffect(() => { + handleInitialValueChange(props.initialValue); }, [props.initialValue]); const valid = validateValue?.(value); @@ -205,7 +213,7 @@ function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref< setBlurred(); onBlur?.(event); }, - [onBlur], + [onBlur, setBlurred], ); const onFocusWrapper = useCallback( @@ -213,10 +221,10 @@ function AutoSizingTextInputWithRef(props: IInputProps, forwardedRef: React.Ref< setFocused(); onFocus?.(event); }, - [onFocus], + [onFocus, setFocused], ); - const blur = useCallback(() => inputRef.current?.blur(), []); + const blur = useCallback(() => inputRef.current?.blur(), [inputRef]); const value = inputRef.current?.value; @@ -303,18 +311,20 @@ interface IRowInputProps { } export function RowInput(props: IRowInputProps) { + const { onSubmit, onChange: propsOnChange, onFocus: propsOnFocus, onBlur: propsOnBlur } = props; + const [value, setValue] = useState(props.initialValue ?? ''); const textAreaRef = useStyledRef(); const [focused, setFocused, setBlurred] = useBoolean(false); - const submit = useCallback(() => props.onSubmit(value), [props.onSubmit, value]); + const submit = useCallback(() => onSubmit(value), [onSubmit, value]); const onChange = useCallback( (event: React.ChangeEvent) => { const value = event.target.value; setValue(value); - props.onChange?.(value); + propsOnChange?.(value); }, - [props.onChange], + [propsOnChange], ); const onKeyDown = useCallback( (event: React.KeyboardEvent) => { @@ -329,32 +339,37 @@ export function RowInput(props: IRowInputProps) { const onFocus = useCallback( (event: React.FocusEvent) => { setFocused(); - props.onFocus?.(event); + propsOnFocus?.(event); }, - [props.onFocus], + [propsOnFocus, setFocused], ); const onBlur = useCallback( (event: React.FocusEvent) => { setBlurred(); - props.onBlur?.(event); + propsOnBlur?.(event); }, - [props.onBlur], + [propsOnBlur, setBlurred], ); const focus = useCallback(() => { const input = textAreaRef.current; if (input) { input.focus(); + // eslint-disable-next-line react-compiler/react-compiler input.selectionStart = input.selectionEnd = value.length; } }, [textAreaRef, value.length]); - const blur = useCallback(() => textAreaRef.current?.blur(), []); + const blur = useCallback(() => textAreaRef.current?.blur(), [textAreaRef]); - useEffect(() => { + const focusOnMount = useEffectEvent(() => { if (props.autofocus) { focus(); } + }); + + useEffect(() => { + focusOnMount(); }, []); useEffect(() => { diff --git a/gui/src/renderer/components/cell/Section.tsx b/gui/src/renderer/components/cell/Section.tsx index 056aa8ceac8b..b7465ecb93cd 100644 --- a/gui/src/renderer/components/cell/Section.tsx +++ b/gui/src/renderer/components/cell/Section.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { colors } from '../../../config.json'; import { useAppContext } from '../../context'; import { useHistory } from '../../lib/history'; -import { useBoolean } from '../../lib/utilityHooks'; +import { useBoolean, useEffectEvent } from '../../lib/utility-hooks'; import Accordion from '../Accordion'; import ChevronButton from '../ChevronButton'; import { buttonText, openSans, sourceSansPro } from '../common-styles'; @@ -71,9 +71,13 @@ export function ExpandableSection(props: ExpandableSectionProps) { history.location.state.expandedSections[props.expandableId] ?? !!expandedInitially; const [expanded, , , toggleExpanded] = useBoolean(expandedValue); - useEffect(() => { - history.location.state.expandedSections[props.expandableId] = expanded; + const updateHistory = useEffectEvent((expanded: boolean) => { + history.recordSectionExpandedState(props.expandableId, expanded); setNavigationHistory(history.asObject); + }); + + useEffect(() => { + updateHistory(expanded); }, [expanded]); const title = ( diff --git a/gui/src/renderer/components/cell/Selector.tsx b/gui/src/renderer/components/cell/Selector.tsx index 3c1988a502f0..b1e20a0c4104 100644 --- a/gui/src/renderer/components/cell/Selector.tsx +++ b/gui/src/renderer/components/cell/Selector.tsx @@ -5,7 +5,7 @@ import { colors } from '../../../config.json'; import { messages } from '../../../shared/gettext'; import { useHistory } from '../../lib/history'; import { RoutePath } from '../../lib/routes'; -import { useStyledRef } from '../../lib/utilityHooks'; +import { useStyledRef } from '../../lib/utility-hooks'; import { AriaDetails, AriaInput, AriaLabel } from '../AriaGroup'; import ImageView from '../ImageView'; import InfoButton from '../InfoButton'; @@ -162,19 +162,21 @@ const StyledSideButton = styled(Cell.SideButton)({ }); function SelectorCell(props: SelectorCellProps) { - const history = useHistory(); + const { onSelect } = props; + + const { push } = useHistory(); const handleClick = useCallback(() => { if (!props.isSelected) { - props.onSelect(props.value); + onSelect(props.value); } - }, [props.isSelected, props.onSelect, props.value]); + }, [props.isSelected, onSelect, props.value]); const navigate = useCallback(() => { if (props.details) { - history.push(props.details.path); + push(props.details.path); } - }, [history.push, props.details?.path]); + }, [props.details, push]); return ( @@ -270,8 +272,11 @@ export function SelectorWithCustomItem(props: SelectorWithCustomItemProps< // Disables submitting of custom input when another item has been pressed. const allowSubmitCustom = useRef(false); - const isNonCustomItem = (value: T | U | undefined) => - props.items.some((item) => item.value === value) || props.automaticValue === value; + const isNonCustomItem = useCallback( + (value: T | U | undefined) => + props.items.some((item) => item.value === value) || props.automaticValue === value, + [props.automaticValue, props.items], + ); const itemIsSelected = isNonCustomItem(value); // Value of custom input. The value is undefined when custom isn't picked. @@ -285,7 +290,7 @@ export function SelectorWithCustomItem(props: SelectorWithCustomItemProps< // After focusing the input it should be allowed to submit custom values. allowSubmitCustom.current = true; setCustomValue((customValue) => customValue ?? ''); - }, [customValue, inputRef.current]); + }, [inputRef]); const handleSelectItem = useCallback( (newValue: T | U | undefined) => { @@ -298,7 +303,7 @@ export function SelectorWithCustomItem(props: SelectorWithCustomItemProps< onSelect(newValue!); }, - [onSelect], + [inputRef, onSelect], ); const validateCustomValue = useCallback( @@ -319,7 +324,7 @@ export function SelectorWithCustomItem(props: SelectorWithCustomItemProps< } } }, - [parseValue, onSelect], + [parseValue, isNonCustomItem, handleSelectItem, onSelect], ); const handleInvalidCustom = useCallback( @@ -330,11 +335,14 @@ export function SelectorWithCustomItem(props: SelectorWithCustomItemProps< // Delay blur event until onMouseUp resulting in handleSelectItem being called before // handleSubmitCustomValue and handleInvalidCustom. Clicking on the input should still move the // cursor and therefore needs to be an exception to this. - const handleMouseDown = useCallback((event: React.MouseEvent) => { - if (event.target !== inputRef.current) { - event.preventDefault(); - } - }, []); + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + if (event.target !== inputRef.current) { + event.preventDefault(); + } + }, + [inputRef], + ); return (
diff --git a/gui/src/renderer/components/cell/SettingsForm.tsx b/gui/src/renderer/components/cell/SettingsForm.tsx index 1f2be529c193..9a901d88d828 100644 --- a/gui/src/renderer/components/cell/SettingsForm.tsx +++ b/gui/src/renderer/components/cell/SettingsForm.tsx @@ -1,5 +1,7 @@ import React, { useCallback, useContext, useEffect, useId, useMemo, useState } from 'react'; +import { useEffectEvent } from '../../lib/utility-hooks'; + interface SettingsFormContext { formSubmittable: boolean; reportInputSubmittable: (key: string, submittable: boolean) => void; @@ -31,11 +33,17 @@ export function useSettingsFormSubmittableReporter() { (submittable: boolean) => { context?.reportInputSubmittable(key, submittable); }, - [context?.reportInputSubmittable], + [context, key], ); - // Remove from required fields if unmounted. - useEffect(() => () => context?.removeInput(key), []); + const clearRequiredFields = useEffectEvent(() => { + context?.removeInput(key); + }); + + useEffect(() => { + // Remove from required fields if unmounted. + return () => clearRequiredFields(); + }, []); return reportInputSubmittable; } diff --git a/gui/src/renderer/components/cell/SettingsGroup.tsx b/gui/src/renderer/components/cell/SettingsGroup.tsx index f430d96a3205..7ca8d0dff1c8 100644 --- a/gui/src/renderer/components/cell/SettingsGroup.tsx +++ b/gui/src/renderer/components/cell/SettingsGroup.tsx @@ -44,9 +44,9 @@ export function useSettingsGroupContext() { [setError, key], ); - const unsetErrorImpl = useCallback(() => unsetError?.(key), [unsetError]); + const unsetErrorImpl = useCallback(() => unsetError?.(key), [key, unsetError]); - useEffect(() => () => unsetErrorImpl(), []); + useEffect(() => () => unsetErrorImpl(), [unsetErrorImpl]); return { reportError, unsetError: unsetErrorImpl }; } diff --git a/gui/src/renderer/components/cell/SettingsRadioGroup.tsx b/gui/src/renderer/components/cell/SettingsRadioGroup.tsx index 46f3ccded7c4..6f4f1a0d90d0 100644 --- a/gui/src/renderer/components/cell/SettingsRadioGroup.tsx +++ b/gui/src/renderer/components/cell/SettingsRadioGroup.tsx @@ -17,13 +17,18 @@ interface SettingsSelectProps { } export function SettingsRadioGroup(props: SettingsSelectProps) { + const { onUpdate } = props; + const [value, setValue] = useState(props.defaultValue ?? props.items[0]?.value ?? ''); const key = useId(); - const onSelect = useCallback((value: T) => { - setValue(value); - props.onUpdate(value); - }, []); + const onSelect = useCallback( + (value: T) => { + setValue(value); + onUpdate(value); + }, + [onUpdate], + ); return ( @@ -92,11 +97,13 @@ interface RadioButtonProps { } function RadioButton(props: RadioButtonProps) { + const { onSelect } = props; + const onChange = useCallback( (event: React.ChangeEvent) => { - props.onSelect(event.target.value as T); + onSelect(event.target.value as T); }, - [props.onSelect], + [onSelect], ); return ( diff --git a/gui/src/renderer/components/cell/SettingsRow.tsx b/gui/src/renderer/components/cell/SettingsRow.tsx index 1211098a70d6..f242106c9237 100644 --- a/gui/src/renderer/components/cell/SettingsRow.tsx +++ b/gui/src/renderer/components/cell/SettingsRow.tsx @@ -104,10 +104,13 @@ export function SettingsRow(props: React.PropsWithChildren) { unsetError?.(); } }, - [reportError, unsetError], + [props.errorMessage, reportError, unsetError], ); - const contextValue = useMemo(() => ({ invalid, setInvalid: setInvalidImpl }), [invalid]); + const contextValue = useMemo( + () => ({ invalid, setInvalid: setInvalidImpl }), + [invalid, setInvalidImpl], + ); return ( diff --git a/gui/src/renderer/components/cell/SettingsSelect.tsx b/gui/src/renderer/components/cell/SettingsSelect.tsx index f0d918ded6c7..7b5e1d7ab4ef 100644 --- a/gui/src/renderer/components/cell/SettingsSelect.tsx +++ b/gui/src/renderer/components/cell/SettingsSelect.tsx @@ -3,7 +3,7 @@ import styled from 'styled-components'; import { colors } from '../../../config.json'; import { useScheduler } from '../../../shared/scheduler'; -import { useBoolean } from '../../lib/utilityHooks'; +import { useBoolean, useEffectEvent } from '../../lib/utility-hooks'; import { AriaInput } from '../AriaGroup'; import { smallNormalText } from '../common-styles'; import CustomScrollbars from '../CustomScrollbars'; @@ -99,10 +99,13 @@ export function SettingsSelect(props: SettingsSelectProps) // Scheduler for clearing the search string after the user has stopped typing. const searchClearScheduler = useScheduler(); - const onSelect = useCallback((value: T) => { - setValue(value); - closeDropdown(); - }, []); + const onSelect = useCallback( + (value: T) => { + setValue(value); + closeDropdown(); + }, + [closeDropdown], + ); // Handle keyboard shortcuts and type search const onKeyDown = useCallback( @@ -132,12 +135,16 @@ export function SettingsSelect(props: SettingsSelectProps) break; } }, - [props.items], + [props.items, searchClearScheduler], ); + const updateEvent = useEffectEvent((value: T) => { + props.onUpdate(value); + }); + // Update the parent when the value changes. useEffect(() => { - props.onUpdate(value); + updateEvent(value); }, [value]); return ( @@ -229,9 +236,11 @@ interface ItemProps { } function Item(props: ItemProps) { + const { onSelect } = props; + const onClick = useCallback(() => { - props.onSelect(props.item.value); - }, [props.onSelect, props.item.value]); + onSelect(props.item.value); + }, [onSelect, props.item.value]); return ( { onUpdate(parse(value)); }, - [onUpdate], + [onUpdate, parse], ); const validateNumber = useCallback( @@ -57,7 +58,7 @@ export function SettingsNumberInput(props: SettingsNumberInputProps) { const parsedValue = parse(value); return (parsedValue === undefined || validate?.(parsedValue)) ?? true; }, - [validate], + [parse, validate], ); return ( @@ -105,15 +106,19 @@ function Input(props: InputProps) { reportSubmittable(value !== '' || optionalInForm === true); } }, - [onUpdate, propsOnChange, validate, optionalInForm], + [propsOnChange, onUpdate, validate, setInvalid, reportSubmittable, optionalInForm], ); - // Report submittability to form context on load. - useEffect(() => { + const updateReportSubmittable = useEffectEvent(() => { const value = props.value ?? props.defaultValue ?? ''; reportSubmittable( (value !== '' || optionalInForm === true) && validate?.(`${value}`) !== false, ); + }); + + // Report submittability to form context on load. + useEffect(() => { + updateReportSubmittable(); }, []); return ( diff --git a/gui/src/renderer/components/main-view/ConnectionActionButton.tsx b/gui/src/renderer/components/main-view/ConnectionActionButton.tsx index ad579da671ec..437fa933216a 100644 --- a/gui/src/renderer/components/main-view/ConnectionActionButton.tsx +++ b/gui/src/renderer/components/main-view/ConnectionActionButton.tsx @@ -31,7 +31,7 @@ function ConnectButton(props: Partial[0]>) { const error = e as Error; log.error(`Failed to connect the tunnel: ${error.message}`); } - }, []); + }, [connectTunnel]); return ( @@ -51,7 +51,7 @@ function DisconnectButton() { const error = e as Error; log.error(`Failed to disconnect the tunnel: ${error.message}`); } - }, []); + }, [disconnectTunnel]); const displayAsCancel = tunnelState !== 'connected'; diff --git a/gui/src/renderer/components/main-view/ConnectionPanel.tsx b/gui/src/renderer/components/main-view/ConnectionPanel.tsx index 5b4e576f763a..34e98abeeaa0 100644 --- a/gui/src/renderer/components/main-view/ConnectionPanel.tsx +++ b/gui/src/renderer/components/main-view/ConnectionPanel.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect } from 'react'; import styled from 'styled-components'; -import { useBoolean } from '../../lib/utilityHooks'; +import { useBoolean } from '../../lib/utility-hooks'; import { useSelector } from '../../redux/store'; import CustomScrollbars from '../CustomScrollbars'; import { BackAction } from '../KeyboardNavigation'; diff --git a/gui/src/renderer/components/main-view/FeatureIndicators.tsx b/gui/src/renderer/components/main-view/FeatureIndicators.tsx index 2d7914fd8742..5b7e60ade00d 100644 --- a/gui/src/renderer/components/main-view/FeatureIndicators.tsx +++ b/gui/src/renderer/components/main-view/FeatureIndicators.tsx @@ -5,7 +5,7 @@ import styled from 'styled-components'; import { colors, strings } from '../../../config.json'; import { FeatureIndicator } from '../../../shared/daemon-rpc-types'; import { messages } from '../../../shared/gettext'; -import { useStyledRef } from '../../lib/utilityHooks'; +import { useStyledRef } from '../../lib/utility-hooks'; import { useSelector } from '../../redux/store'; import { tinyText } from '../common-styles'; import { InfoIcon } from '../InfoButton'; @@ -169,6 +169,7 @@ export default function FeatureIndicators(props: FeatureIndicatorsProps) { // Place the ellipsis at the end of the last visible indicator. const left = lastVisibleIndicatorRect.right - containerRect.left; + // eslint-disable-next-line react-compiler/react-compiler ellipsisRef.current.style.left = `${left}px`; ellipsisRef.current.style.visibility = 'visible'; diff --git a/gui/src/renderer/components/main-view/SelectLocationButton.tsx b/gui/src/renderer/components/main-view/SelectLocationButton.tsx index 30b132a84eeb..50508a319270 100644 --- a/gui/src/renderer/components/main-view/SelectLocationButton.tsx +++ b/gui/src/renderer/components/main-view/SelectLocationButton.tsx @@ -33,7 +33,7 @@ export default function SelectLocationButtons() { } function SelectLocationButton(props: MultiButtonCompatibleProps) { - const history = useHistory(); + const { push } = useHistory(); const tunnelState = useSelector((state) => state.connection.status.state); const relaySettings = useSelector((state) => state.settings.relaySettings); @@ -46,8 +46,8 @@ function SelectLocationButton(props: MultiButtonCompatibleProps) { ); const onSelectLocation = useCallback(() => { - history.push(RoutePath.selectLocation, { transition: transitions.show }); - }, [history.push]); + push(RoutePath.selectLocation, { transition: transitions.show }); + }, [push]); return ( state.settings.customLists); @@ -51,9 +53,9 @@ export function AddToListDialog(props: AddToListDialogProps) { log.error(`Failed to edit custom list ${list.id}: ${error.message}`); } - props.hide(); + hide(); }, - [location, updateCustomList], + [hide, props.location, updateCustomList], ); let locationType: string; @@ -114,7 +116,9 @@ interface SelectListProps { } function SelectList(props: SelectListProps) { - const onAdd = useCallback(() => props.add(props.list), [props.list]); + const { add } = props; + + const onAdd = useCallback(() => add(props.list), [add, props.list]); // List should be disabled if location already is in list. const disabled = props.list.locations.some((location) => @@ -144,6 +148,8 @@ interface EditListProps { // Dialog for changing the name of a custom list. export function EditListDialog(props: EditListProps) { + const { hide } = props; + const { updateCustomList } = useAppContext(); const [newName, setNewName] = useState(props.list.name); @@ -160,20 +166,23 @@ export function EditListDialog(props: EditListProps) { if (result && result.type === 'name already exists') { setError(); } else { - props.hide(); + hide(); } } catch (e) { const error = e as Error; log.error(`Failed to edit custom list ${props.list.id}: ${error.message}`); } } - }, [props.list, newName, props.hide]); + }, [newNameValid, props.list, newNameTrimmed, updateCustomList, setError, hide]); // Errors should be reset when editing the value - const onChange = useCallback((value: string) => { - setNewName(value); - unsetError(); - }, []); + const onChange = useCallback( + (value: string) => { + setNewName(value); + unsetError(); + }, + [unsetError], + ); return ( { - props.confirm(); - props.hide(); - }, []); + propsConfirm(); + hide(); + }, [hide, propsConfirm]); return ( => { - const result = await createCustomList(name); - // If an error is returned it should be passed as the return value. - if (result) { - return result; - } + const createList = useCallback( + async (name: string): Promise => { + const result = await createCustomList(name); + // If an error is returned it should be passed as the return value. + if (result) { + return result; + } - hideAddList(); - }, []); + hideAddList(); + }, + [createCustomList, hideAddList], + ); if (searchTerm !== '' && !customLists.some((list) => list.visible)) { return null; @@ -121,6 +124,8 @@ interface AddListFormProps { } function AddListForm(props: AddListFormProps) { + const { onCreateList, cancel } = props; + const [name, setName] = useState(''); const nameTrimmed = name.trim(); const nameValid = nameTrimmed !== ''; @@ -129,15 +134,18 @@ function AddListForm(props: AddListFormProps) { const inputRef = useStyledRef(); // Errors should be reset when editing the value - const onChange = useCallback((value: string) => { - setName(value); - unsetError(); - }, []); + const onChange = useCallback( + (value: string) => { + setName(value); + unsetError(); + }, + [unsetError], + ); const createList = useCallback(async () => { if (nameValid) { try { - const result = await props.onCreateList(nameTrimmed); + const result = await onCreateList(nameTrimmed); if (result) { setError(); } @@ -146,16 +154,16 @@ function AddListForm(props: AddListFormProps) { log.error('Failed to create list:', error.message); } } - }, [name, props.onCreateList, nameValid]); + }, [nameValid, onCreateList, nameTrimmed, setError]); const onBlur = useCallback( (event: React.FocusEvent) => { // Only cancel if losing focus to something else than the contents of the row container. if (!event.relatedTarget || !containerRef.current?.contains(event.relatedTarget)) { - props.cancel(); + cancel(); } }, - [props.cancel], + [containerRef, cancel], ); const onTransitionEnd = useCallback(() => { @@ -168,7 +176,7 @@ function AddListForm(props: AddListFormProps) { if (props.visible) { inputRef.current?.focus(); } - }, [props.visible]); + }, [inputRef, props.visible]); return ( @@ -211,6 +219,8 @@ interface CustomListsImplProps { } function CustomListsImpl(props: CustomListsImplProps) { + const { onSelect: propsOnSelect } = props; + const { customLists, expandLocation, collapseLocation, onBeforeExpand } = useRelayListContext(); const { resetHeight } = useScrollPositionContext(); @@ -221,9 +231,9 @@ function CustomListsImpl(props: CustomListsImplProps) { // Only the geographical part should be sent to the daemon when setting a location. delete location.customList; } - props.onSelect(location); + propsOnSelect(location); }, - [props.onSelect], + [propsOnSelect], ); return ( diff --git a/gui/src/renderer/components/select-location/LocationRow.tsx b/gui/src/renderer/components/select-location/LocationRow.tsx index 41f3aa71522c..a47c1dd64603 100644 --- a/gui/src/renderer/components/select-location/LocationRow.tsx +++ b/gui/src/renderer/components/select-location/LocationRow.tsx @@ -9,7 +9,7 @@ import { import { messages } from '../../../shared/gettext'; import log from '../../../shared/logging'; import { useAppContext } from '../../context'; -import { useBoolean, useStyledRef } from '../../lib/utilityHooks'; +import { useBoolean, useStyledRef } from '../../lib/utility-hooks'; import { useSelector } from '../../redux/store'; import Accordion from '../Accordion'; import * as Cell from '../cell'; @@ -55,6 +55,8 @@ interface IProps { // Renders the rows and its children for countries, cities and relays function LocationRow(props: IProps) { + const { onSelect, onWillExpand: propsOnWillExpand } = props; + const hasChildren = getLocationChildren(props.source).some((child) => child.visible); const buttonRef = useStyledRef(); const userInvokedExpand = useRef(false); @@ -79,19 +81,19 @@ function LocationRow(props: IProps) { const handleClick = useCallback(() => { if (!props.source.selected) { - props.onSelect(props.source.location); + onSelect(props.source.location); } - }, [props.onSelect, props.source.location, props.source.selected]); + }, [onSelect, props.source.location, props.source.selected]); const onWillExpand = useCallback( (nextHeight: number) => { const buttonRect = buttonRef.current?.getBoundingClientRect(); if (expanded !== undefined && buttonRect) { - props.onWillExpand(buttonRect, nextHeight, userInvokedExpand.current); + propsOnWillExpand(buttonRect, nextHeight, userInvokedExpand.current); userInvokedExpand.current = false; } }, - [props.onWillExpand, expanded], + [buttonRef, expanded, propsOnWillExpand], ); const onRemoveFromList = useCallback(async () => { @@ -116,7 +118,7 @@ function LocationRow(props: IProps) { } } } - }, [customLists, props.source.location]); + }, [customLists, props.source.location, updateCustomList]); // Remove an entire custom list. const confirmRemoveCustomList = useCallback(async () => { @@ -130,7 +132,7 @@ function LocationRow(props: IProps) { ); } } - }, [props.source.location.customList]); + }, [deleteCustomList, props.source.location.customList]); if (!props.source.visible) { return null; diff --git a/gui/src/renderer/components/select-location/RelayListContext.tsx b/gui/src/renderer/components/select-location/RelayListContext.tsx index 86fb8de22b6f..3165a9582488 100644 --- a/gui/src/renderer/components/select-location/RelayListContext.tsx +++ b/gui/src/renderer/components/select-location/RelayListContext.tsx @@ -13,7 +13,8 @@ import { useNormalBridgeSettings, useNormalRelaySettings, useTunnelProtocol, -} from '../../lib/utilityHooks'; +} from '../../lib/relay-settings-hooks'; +import { useEffectEvent } from '../../lib/utility-hooks'; import { IRelayLocationCountryRedux } from '../../redux/settings/reducers'; import { useSelector } from '../../redux/store'; import { useCustomListsRelayList } from './custom-list-helpers'; @@ -83,7 +84,7 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { tunnelProtocol, relaySettings, ); - }, [fullRelayList, locationType, relaySettings?.tunnelProtocol]); + }, [fullRelayList, locationType, relaySettings, tunnelProtocol]); const relayListForDaita = useMemo(() => { return filterLocationsByDaita( @@ -94,7 +95,14 @@ export function RelayListContextProvider(props: RelayListContextProviderProps) { relaySettings?.tunnelProtocol ?? 'any', relaySettings?.wireguard.useMultihop ?? false, ); - }, [daita, directOnly, locationType, relaySettings, relayListForEndpointType]); + }, [ + daita, + directOnly, + locationType, + relayListForEndpointType, + relaySettings?.tunnelProtocol, + relaySettings?.wireguard.useMultihop, + ]); // Filters the relays to only keep the relays matching the currently selected filters, e.g. // ownership and providers @@ -280,7 +288,7 @@ function useExpandedLocations(filteredLocations: Array) => { + if (searchTerm !== '') { + setExpandedLocations((expandedLocations) => ({ + ...expandedLocations, + [locationType]: getLocationsExpandedBySearch(filteredLocations, searchTerm), + })); + } + }, + ); + // Expand locations when filters are changed - useEffect(() => { - if (searchTerm !== '') { - setExpandedLocations((expandedLocations) => ({ - ...expandedLocations, - [locationType]: getLocationsExpandedBySearch(filteredLocations, searchTerm), - })); - } - }, [filteredLocations]); + useEffect(() => expandLocationsForSearch(filteredLocations), [filteredLocations]); return { expandedLocations: expandedLocationsMap[locationType], diff --git a/gui/src/renderer/components/select-location/ScopeBar.tsx b/gui/src/renderer/components/select-location/ScopeBar.tsx index bbe493599344..28312da19e5b 100644 --- a/gui/src/renderer/components/select-location/ScopeBar.tsx +++ b/gui/src/renderer/components/select-location/ScopeBar.tsx @@ -57,11 +57,13 @@ interface IScopeBarItemProps { } export function ScopeBarItem(props: IScopeBarItemProps) { + const { onClick: propOnClick } = props; + const onClick = useCallback(() => { if (props.index !== undefined) { - props.onClick?.(props.index); + propOnClick?.(props.index); } - }, [props.onClick, props.index]); + }, [propOnClick, props.index]); return props.index !== undefined ? ( diff --git a/gui/src/renderer/components/select-location/ScrollPositionContext.tsx b/gui/src/renderer/components/select-location/ScrollPositionContext.tsx index d27e252427dd..55ee8dae972f 100644 --- a/gui/src/renderer/components/select-location/ScrollPositionContext.tsx +++ b/gui/src/renderer/components/select-location/ScrollPositionContext.tsx @@ -2,7 +2,8 @@ import { Action } from 'history'; import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { useHistory } from '../../lib/history'; -import { useNormalRelaySettings, useStyledRef } from '../../lib/utilityHooks'; +import { useNormalRelaySettings } from '../../lib/relay-settings-hooks'; +import { useStyledRef } from '../../lib/utility-hooks'; import { CustomScrollbarsRef } from '../CustomScrollbars'; import { LocationType } from './select-location-types'; import { useSelectLocationContext } from './SelectLocationContainer'; @@ -69,7 +70,10 @@ export function ScrollPositionContextProvider(props: ScrollPositionContextProps) scrollViewRef.current?.scrollIntoView(rect); }, []); - const resetHeight = useCallback(() => spacePreAllocationViewRef.current?.reset(), []); + const resetHeight = useCallback( + () => spacePreAllocationViewRef.current?.reset(), + [spacePreAllocationViewRef], + ); const value = useMemo( () => ({ @@ -82,7 +86,13 @@ export function ScrollPositionContextProvider(props: ScrollPositionContextProps) scrollIntoView, resetHeight, }), - [saveScrollPosition, resetScrollPositions], + [ + spacePreAllocationViewRef, + saveScrollPosition, + resetScrollPositions, + scrollIntoView, + resetHeight, + ], ); // Restore the scroll position when parameters change diff --git a/gui/src/renderer/components/select-location/SelectLocation.tsx b/gui/src/renderer/components/select-location/SelectLocation.tsx index 438fdc430dda..506c836548cd 100644 --- a/gui/src/renderer/components/select-location/SelectLocation.tsx +++ b/gui/src/renderer/components/select-location/SelectLocation.tsx @@ -8,8 +8,8 @@ import { useRelaySettingsUpdater } from '../../lib/constraint-updater'; import { daitaFilterActive, filterSpecialLocations } from '../../lib/filter-locations'; import { useHistory } from '../../lib/history'; import { formatHtml } from '../../lib/html-formatter'; +import { useNormalRelaySettings } from '../../lib/relay-settings-hooks'; import { RoutePath } from '../../lib/routes'; -import { useNormalRelaySettings } from '../../lib/utilityHooks'; import { useSelector } from '../../redux/store'; import * as Cell from '../cell'; import { useFilteredProviders } from '../Filter'; @@ -107,7 +107,7 @@ export default function SelectLocation() { saveScrollPosition(); setLocationType(locationType); }, - [saveScrollPosition], + [saveScrollPosition, setLocationType], ); const updateSearchTerm = useCallback( @@ -122,7 +122,7 @@ export default function SelectLocation() { setSearchTerm(value); } }, - [resetScrollPositions, expandSearchResults], + [expandSearchResults, setSearchTerm, resetScrollPositions], ); const showOwnershipFilter = ownership !== Ownership.any; diff --git a/gui/src/renderer/components/select-location/SelectLocationContainer.tsx b/gui/src/renderer/components/select-location/SelectLocationContainer.tsx index 1d0e5251f7aa..66bebdf1b0e0 100644 --- a/gui/src/renderer/components/select-location/SelectLocationContainer.tsx +++ b/gui/src/renderer/components/select-location/SelectLocationContainer.tsx @@ -29,7 +29,7 @@ export default function SelectLocationContainer() { const value = useMemo( () => ({ locationType, setLocationType: setSelectLocationView, searchTerm, setSearchTerm }), - [locationType, searchTerm], + [locationType, searchTerm, setSelectLocationView], ); return ( diff --git a/gui/src/renderer/components/select-location/SpecialLocationList.tsx b/gui/src/renderer/components/select-location/SpecialLocationList.tsx index f4ccf13ec207..e4d105d63586 100644 --- a/gui/src/renderer/components/select-location/SpecialLocationList.tsx +++ b/gui/src/renderer/components/select-location/SpecialLocationList.tsx @@ -45,11 +45,12 @@ interface SpecialLocationRowProps { } function SpecialLocationRow(props: SpecialLocationRowProps) { + const { onSelect: propsOnSelect } = props; const onSelect = useCallback(() => { if (!props.source.selected) { - props.onSelect(props.source.value); + propsOnSelect(props.source.value); } - }, [props.source.selected, props.onSelect, props.source.value]); + }, [props.source, propsOnSelect]); const innerProps: SpecialLocationRowInnerProps = { ...props, @@ -105,7 +106,7 @@ const StyledInfoButton = styled(StyledHoverInfoButton)({ display: 'block' }); export function CustomBridgeLocationRow( props: SpecialLocationRowInnerProps, ) { - const history = useHistory(); + const { push } = useHistory(); const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); const bridgeConfigured = bridgeSettings.custom !== undefined; @@ -114,7 +115,7 @@ export function CustomBridgeLocationRow( const selectedRef = props.source.selected ? props.selectedElementRef : undefined; const background = getButtonColor(props.source.selected, 0, props.source.disabled); - const navigate = useCallback(() => history.push(RoutePath.editCustomBridge), [history.push]); + const navigate = useCallback(() => push(RoutePath.editCustomBridge), [push]); return ( diff --git a/gui/src/renderer/components/select-location/custom-list-helpers.ts b/gui/src/renderer/components/select-location/custom-list-helpers.ts index 5cb6d695b8d7..3f6149cd7fcc 100644 --- a/gui/src/renderer/components/select-location/custom-list-helpers.ts +++ b/gui/src/renderer/components/select-location/custom-list-helpers.ts @@ -46,7 +46,15 @@ export function useCustomListsRelayList( expandedLocations, ), ), - [customLists, relayList, selectedLocation, disabledLocation, expandedLocations], + [ + customLists, + relayList, + searchTerm, + preventDueToCustomBridgeSelected, + selectedLocation, + disabledLocation, + expandedLocations, + ], ); } diff --git a/gui/src/renderer/components/select-location/select-location-hooks.ts b/gui/src/renderer/components/select-location/select-location-hooks.ts index 027185e416a6..fbecc4db5d5c 100644 --- a/gui/src/renderer/components/select-location/select-location-hooks.ts +++ b/gui/src/renderer/components/select-location/select-location-hooks.ts @@ -30,7 +30,7 @@ export function useOnSelectExitLocation() { await onSelectLocation({ normal: settings }); await connectTunnel(); }, - [history, relaySettingsModifier], + [connectTunnel, history, onSelectLocation, relaySettingsModifier], ); const onSelectSpecial = useCallback((_location: undefined) => { @@ -54,7 +54,7 @@ export function useOnSelectEntryLocation() { }); await onSelectLocation({ normal: settings }); }, - [relaySettingsModifier], + [onSelectLocation, relaySettingsModifier, setLocationType], ); const onSelectSpecial = useCallback( @@ -66,7 +66,7 @@ export function useOnSelectEntryLocation() { }); await onSelectLocation({ normal: settings }); }, - [relaySettingsModifier], + [onSelectLocation, relaySettingsModifier, setLocationType], ); return [onSelectRelay, onSelectSpecial] as const; @@ -75,14 +75,17 @@ export function useOnSelectEntryLocation() { function useOnSelectLocation() { const { setRelaySettings } = useAppContext(); - return useCallback(async (relaySettings: RelaySettings) => { - try { - await setRelaySettings(relaySettings); - } catch (e) { - const error = e as Error; - log.error(`Failed to select the location: ${error.message}`); - } - }, []); + return useCallback( + async (relaySettings: RelaySettings) => { + try { + await setRelaySettings(relaySettings); + } catch (e) { + const error = e as Error; + log.error(`Failed to select the location: ${error.message}`); + } + }, + [setRelaySettings], + ); } export function useOnSelectBridgeLocation() { @@ -90,17 +93,20 @@ export function useOnSelectBridgeLocation() { const { setLocationType } = useSelectLocationContext(); const bridgeSettingsModifier = useBridgeSettingsModifier(); - const setLocation = useCallback(async (bridgeUpdate: BridgeSettings) => { - if (bridgeUpdate) { - setLocationType(LocationType.exit); - try { - await updateBridgeSettings(bridgeUpdate); - } catch (e) { - const error = e as Error; - log.error(`Failed to select the bridge location: ${error.message}`); + const setLocation = useCallback( + async (bridgeUpdate: BridgeSettings) => { + if (bridgeUpdate) { + setLocationType(LocationType.exit); + try { + await updateBridgeSettings(bridgeUpdate); + } catch (e) { + const error = e as Error; + log.error(`Failed to select the bridge location: ${error.message}`); + } } - } - }, []); + }, + [setLocationType, updateBridgeSettings], + ); const onSelectRelay = useCallback( (location: RelayLocation) => { @@ -112,7 +118,7 @@ export function useOnSelectBridgeLocation() { }), ); }, - [bridgeSettingsModifier], + [bridgeSettingsModifier, setLocation], ); const onSelectSpecial = useCallback( @@ -135,7 +141,7 @@ export function useOnSelectBridgeLocation() { ); } }, - [bridgeSettingsModifier], + [bridgeSettingsModifier, setLocation], ); return [onSelectRelay, onSelectSpecial] as const; diff --git a/gui/src/renderer/lib/3dmap.ts b/gui/src/renderer/lib/3dmap.ts index ec4def074497..d0e130e4c84b 100644 --- a/gui/src/renderer/lib/3dmap.ts +++ b/gui/src/renderer/lib/3dmap.ts @@ -666,8 +666,8 @@ function getProjectionMatrix(gl: WebGL2RenderingContext): mat4 { // Create a perspective matrix, a special matrix that is // used to simulate the distortion of perspective in a camera. const fieldOfView = (angleOfView / 180) * Math.PI; // in radians - // @ts-ignore - const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; + const canvas = gl.canvas as HTMLCanvasElement; + const aspect = canvas.clientWidth / canvas.clientHeight; const zNear = 0.1; const zFar = 10; const projectionMatrix = mat4.create(); diff --git a/gui/src/renderer/lib/actionsHook.ts b/gui/src/renderer/lib/actionsHook.ts index 2597aee422fa..fc046d0a6694 100644 --- a/gui/src/renderer/lib/actionsHook.ts +++ b/gui/src/renderer/lib/actionsHook.ts @@ -4,6 +4,9 @@ import { ActionCreatorsMapObject, bindActionCreators } from 'redux'; export default function useActions>(actionCreator: M) { const dispatch = useDispatch(); - const actions = useMemo(() => bindActionCreators(actionCreator, dispatch), [dispatch]); + const actions = useMemo( + () => bindActionCreators(actionCreator, dispatch), + [actionCreator, dispatch], + ); return actions; } diff --git a/gui/src/renderer/lib/api-access-methods.ts b/gui/src/renderer/lib/api-access-methods.ts index fd78d1484a99..90d406cc3d66 100644 --- a/gui/src/renderer/lib/api-access-methods.ts +++ b/gui/src/renderer/lib/api-access-methods.ts @@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react'; import { CustomProxy } from '../../shared/daemon-rpc-types'; import { useScheduler } from '../../shared/scheduler'; import { useAppContext } from '../context'; -import { useBoolean } from './utilityHooks'; +import { useBoolean } from './utility-hooks'; export function useApiAccessMethodTest( autoReset = true, @@ -28,54 +28,66 @@ export function useApiAccessMethodTest( // scheduler is used to clear it. const testResultResetScheduler = useScheduler(); - const testApiAccessMethod = useCallback(async (method: CustomProxy | string) => { - testResultResetScheduler.cancel(); - setTestResult(undefined); + const testApiAccessMethod = useCallback( + async (method: CustomProxy | string) => { + testResultResetScheduler.cancel(); + setTestResult(undefined); - setTesting(); - let reachable; - let testPromise; + setTesting(); + let reachable; + let testPromise; - const submitTimestamp = Date.now(); - try { - testPromise = - typeof method === 'string' - ? testApiAccessMethodById(method) - : testCustomApiAccessMethod(method); + const submitTimestamp = Date.now(); + try { + testPromise = + typeof method === 'string' + ? testApiAccessMethodById(method) + : testCustomApiAccessMethod(method); - lastTestPromise.current = testPromise; - reachable = await testPromise; - } catch { - reachable = false; - } + lastTestPromise.current = testPromise; + reachable = await testPromise; + } catch { + reachable = false; + } - // Make sure the loading text is displayed for at least `minDuration` milliseconds. - const submitDuration = Date.now() - submitTimestamp; - if (submitDuration < minDuration) { - await new Promise((resolve) => - delayScheduler.schedule(resolve, minDuration - submitDuration), - ); - } + // Make sure the loading text is displayed for at least `minDuration` milliseconds. + const submitDuration = Date.now() - submitTimestamp; + if (submitDuration < minDuration) { + await new Promise((resolve) => + delayScheduler.schedule(resolve, minDuration - submitDuration), + ); + } - if (testPromise !== lastTestPromise.current) { - return; - } + if (testPromise !== lastTestPromise.current) { + return; + } - setTestResult(reachable); - unsetTesting(); + setTestResult(reachable); + unsetTesting(); - if (autoReset) { - testResultResetScheduler.schedule(() => setTestResult(undefined), 5000); - } + if (autoReset) { + testResultResetScheduler.schedule(() => setTestResult(undefined), 5000); + } - return reachable; - }, []); + return reachable; + }, + [ + autoReset, + delayScheduler, + minDuration, + setTesting, + testApiAccessMethodById, + testCustomApiAccessMethod, + testResultResetScheduler, + unsetTesting, + ], + ); const resetTestResult = useCallback(() => { lastTestPromise.current = undefined; unsetTesting(); setTestResult(undefined); - }, []); + }, [unsetTesting]); return [testing, testResult, testApiAccessMethod, resetTestResult]; } diff --git a/gui/src/renderer/lib/constraint-updater.ts b/gui/src/renderer/lib/constraint-updater.ts index 73fbdd1336a7..6ea021ece90f 100644 --- a/gui/src/renderer/lib/constraint-updater.ts +++ b/gui/src/renderer/lib/constraint-updater.ts @@ -16,7 +16,7 @@ import { NormalRelaySettingsRedux, } from '../redux/settings/reducers'; import { useSelector } from '../redux/store'; -import { useNormalRelaySettings } from './utilityHooks'; +import { useNormalRelaySettings } from './relay-settings-hooks'; export function wrapRelaySettingsOrDefault( relaySettings?: NormalRelaySettingsRedux, diff --git a/gui/src/renderer/lib/history.tsx b/gui/src/renderer/lib/history.tsx index 741c298da633..6d92a0e88c68 100644 --- a/gui/src/renderer/lib/history.tsx +++ b/gui/src/renderer/lib/history.tsx @@ -82,6 +82,14 @@ export default class History { return history; } + public recordScrollPosition(position: [number, number]) { + this.location.state.scrollPosition = position; + } + + public recordSectionExpandedState(id: string, expanded: boolean) { + this.location.state.expandedSections[id] = expanded; + } + public get location(): Location { return this.entries[this.index]; } diff --git a/gui/src/renderer/lib/relay-settings-hooks.ts b/gui/src/renderer/lib/relay-settings-hooks.ts new file mode 100644 index 000000000000..14fe99849d12 --- /dev/null +++ b/gui/src/renderer/lib/relay-settings-hooks.ts @@ -0,0 +1,25 @@ +import { LiftedConstraint, TunnelProtocol } from '../../shared/daemon-rpc-types'; +import { useSelector } from '../redux/store'; + +export function useNormalRelaySettings() { + const relaySettings = useSelector((state) => state.settings.relaySettings); + return 'normal' in relaySettings ? relaySettings.normal : undefined; +} + +// Some features are considered core privacy features and when enabled prevent OpenVPN from being +// used. This hook returns the tunnelprotocol with the exception that it always returns WireGuard +// when any of those features are enabled. +export function useTunnelProtocol(): LiftedConstraint { + const relaySettings = useNormalRelaySettings(); + const multihop = relaySettings?.wireguard.useMultihop ?? false; + const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); + const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); + const openVpnDisabled = daita || multihop || quantumResistant; + + return openVpnDisabled ? 'wireguard' : (relaySettings?.tunnelProtocol ?? 'any'); +} + +export function useNormalBridgeSettings() { + const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); + return bridgeSettings.normal; +} diff --git a/gui/src/renderer/lib/utility-hooks.ts b/gui/src/renderer/lib/utility-hooks.ts new file mode 100644 index 000000000000..1efc49c80467 --- /dev/null +++ b/gui/src/renderer/lib/utility-hooks.ts @@ -0,0 +1,76 @@ +import React, { useCallback, useEffect, useInsertionEffect, useRef, useState } from 'react'; + +export function useMounted() { + const mountedRef = useRef(false); + const isMounted = useCallback(() => mountedRef.current, []); + + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + + return isMounted; +} + +export function useStyledRef(): React.MutableRefObject { + return useRef() as React.MutableRefObject; +} + +export function useCombinedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback { + return useRefCallback((element: T | null) => refs.forEach((ref) => assignToRef(element, ref))); +} + +export function assignToRef(element: T | null, ref?: React.Ref) { + if (typeof ref === 'function') { + ref(element); + } else if (ref && element) { + (ref as React.MutableRefObject).current = element; + } +} + +export function useBoolean(initialValue = false) { + const [value, setValue] = useState(initialValue); + + const setTrue = useCallback(() => setValue(true), []); + const setFalse = useCallback(() => setValue(false), []); + const toggle = useCallback(() => setValue((value) => !value), []); + + return [value, setTrue, setFalse, toggle] as const; +} + +// This hook returns a function that can be used to force a rerender of a component, and +// additionally also returns a variable that can be used to trigger effects as a result. This is a +// hack and should be avoided unless there are no better ways. +export function useRerenderer(): [() => void, number] { + const [count, setCount] = useState(0); + const rerender = useCallback(() => setCount((count) => count + 1), []); + return [rerender, count]; +} + +type Fn = (...args: T) => R; + +export function useEffectEvent( + fn: Fn>, +): Fn { + const ref = useRef>(fn); + + useInsertionEffect(() => { + ref.current = fn; + }, [fn]); + + return useCallback((...args: Args) => ref.current(...args), []); +} + +// Alias for useEffectEvent, but with another name since the effect event is named after a very +// specific usecase. +export const useRefCallback = useEffectEvent; + +export function useLastDefinedValue(value: T): T { + const [definedValue, setDefinedValue] = useState(value); + + useEffect(() => setDefinedValue((prev) => value ?? prev), [value]); + + return value ?? definedValue; +} diff --git a/gui/src/renderer/lib/utilityHooks.ts b/gui/src/renderer/lib/utilityHooks.ts deleted file mode 100644 index be1688f3f38f..000000000000 --- a/gui/src/renderer/lib/utilityHooks.ts +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; - -import { LiftedConstraint, TunnelProtocol } from '../../shared/daemon-rpc-types'; -import { useSelector } from '../redux/store'; - -export function useMounted() { - const mountedRef = useRef(false); - const isMounted = useCallback(() => mountedRef.current, []); - - useEffect(() => { - mountedRef.current = true; - return () => { - mountedRef.current = false; - }; - }, []); - - return isMounted; -} - -export function useStyledRef(): React.RefObject { - return useRef() as React.RefObject; -} - -export function useCombinedRefs(...refs: (React.Ref | undefined)[]): React.RefCallback { - return useCallback((element: T | null) => refs.forEach((ref) => assignToRef(element, ref)), []); -} - -export function assignToRef(element: T | null, ref?: React.Ref) { - if (typeof ref === 'function') { - ref(element); - } else if (ref && element) { - (ref as React.MutableRefObject).current = element; - } -} - -export function useAsyncEffect( - effect: () => Promise void | Promise)>, - dependencies: unknown[], -): void { - const isMounted = useMounted(); - - useEffect(() => { - const promise = effect(); - return () => { - void promise.then((destructor) => { - if (isMounted() && destructor) { - return destructor(); - } - }); - }; - }, dependencies); -} - -export function useBoolean(initialValue = false) { - const [value, setValue] = useState(initialValue); - - const setTrue = useCallback(() => setValue(true), []); - const setFalse = useCallback(() => setValue(false), []); - const toggle = useCallback(() => setValue((value) => !value), []); - - return [value, setTrue, setFalse, toggle] as const; -} - -export function useNormalRelaySettings() { - const relaySettings = useSelector((state) => state.settings.relaySettings); - return 'normal' in relaySettings ? relaySettings.normal : undefined; -} - -// Some features are considered core privacy features and when enabled prevent OpenVPN from being -// used. This hook returns the tunnelprotocol with the exception that it always returns WireGuard -// when any of those features are enabled. -export function useTunnelProtocol(): LiftedConstraint { - const relaySettings = useNormalRelaySettings(); - const multihop = relaySettings?.wireguard.useMultihop ?? false; - const daita = useSelector((state) => state.settings.wireguard.daita?.enabled ?? false); - const quantumResistant = useSelector((state) => state.settings.wireguard.quantumResistant); - const openVpnDisabled = daita || multihop || quantumResistant; - - return openVpnDisabled ? 'wireguard' : (relaySettings?.tunnelProtocol ?? 'any'); -} - -export function useNormalBridgeSettings() { - const bridgeSettings = useSelector((state) => state.settings.bridgeSettings); - return bridgeSettings.normal; -} - -// This hook returns a function that can be used to force a rerender of a component, and -// additionally also returns a variable that can be used to trigger effects as a result. This is a -// hack and should be avoided unless there are no better ways. -export function useRerenderer(): [() => void, number] { - const [count, setCount] = useState(0); - const rerender = useCallback(() => setCount((count) => count + 1), []); - return [rerender, count]; -} diff --git a/gui/src/renderer/redux/store.ts b/gui/src/renderer/redux/store.ts index 1617acce3d1c..073fe67d3022 100644 --- a/gui/src/renderer/redux/store.ts +++ b/gui/src/renderer/redux/store.ts @@ -84,8 +84,10 @@ export function useSelector(fn: (state: IReduxState) => R): R { const willExit = useWillExit(); if (!willExit) { + // eslint-disable-next-line react-compiler/react-compiler valueBeforeExit.current = value; } + // eslint-disable-next-line react-compiler/react-compiler return valueBeforeExit.current; } diff --git a/gui/src/shared/scheduler.ts b/gui/src/shared/scheduler.ts index 8ae5a2cbf0c8..2716097194eb 100644 --- a/gui/src/shared/scheduler.ts +++ b/gui/src/shared/scheduler.ts @@ -31,7 +31,7 @@ export function useScheduler() { useEffect(() => { return () => closeScheduler.cancel(); - }, []); + }, [closeScheduler]); return closeScheduler; } diff --git a/gui/test/unit/notification-evaluation.spec.ts b/gui/test/unit/notification-evaluation.spec.ts index 723307b3d776..d05e967efc55 100644 --- a/gui/test/unit/notification-evaluation.spec.ts +++ b/gui/test/unit/notification-evaluation.spec.ts @@ -28,7 +28,7 @@ describe('System notifications', () => { before(() => { sandbox = sinon.createSandbox(); - // @ts-ignore + // @ts-expect-error Way too many methods to mock. sandbox.stub(NotificationController.prototype, 'createElectronNotification').returns({ show: () => { /* no-op */ diff --git a/gui/test/unit/tunnel-state.spec.ts b/gui/test/unit/tunnel-state.spec.ts index a6013523b843..e751b50c5793 100644 --- a/gui/test/unit/tunnel-state.spec.ts +++ b/gui/test/unit/tunnel-state.spec.ts @@ -14,7 +14,7 @@ const error: TunnelState = { state: 'error' } as TunnelState; describe('Tunnel state', () => { it('Should allow all updates', () => { const stateUpdateSpy = spy(); - // @ts-ignore + // @ts-expect-error stateUpdateSpy doesn't know what type to accept const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state); const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate }); @@ -33,7 +33,7 @@ describe('Tunnel state', () => { it('Should ignore non-expected state update', () => { const stateUpdateSpy = spy(); - // @ts-ignore + // @ts-expect-error stateUpdateSpy doesn't know what type to accept const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state); const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate }); @@ -49,7 +49,7 @@ describe('Tunnel state', () => { it('Should allow new states after expected state is reached', () => { const stateUpdateSpy = spy(); - // @ts-ignore + // @ts-expect-error stateUpdateSpy doesn't know what type to accept const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state); const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate }); @@ -67,7 +67,7 @@ describe('Tunnel state', () => { it('Should allow error state update', () => { const stateUpdateSpy = spy(); - // @ts-ignore + // @ts-expect-error stateUpdateSpy doesn't know what type to accept const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state); const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate }); @@ -86,7 +86,7 @@ describe('Tunnel state', () => { it('Should time out and use last ignored state', () => { const clock = sinon.useFakeTimers({ shouldAdvanceTime: true }); const stateUpdateSpy = spy(); - // @ts-ignore + // @ts-expect-error stateUpdateSpy doesn't know what type to accept const handleTunnelStateUpdate = (tunnelState: TunnelState) => stateUpdateSpy(tunnelState.state); const tunnelStateHandler = new TunnelStateHandler({ handleTunnelStateUpdate });