summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar <oskar@mullvad.net>2024-10-22 15:32:24 +0200
committerOskar <oskar@mullvad.net>2024-10-22 15:32:24 +0200
commit1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129 (patch)
tree43caf1e9884496f64c2333648c16381eaa1cf18a
parent6533665d1fdf9a97e6ea1e36f01ee2f293b4338c (diff)
parentcf62ffdd17eac7958977895f098742c704aa3047 (diff)
downloadmullvadvpn-1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129.tar.xz
mullvadvpn-1ec8c9d6c4dc799fb0c6479cdc9dad438bf37129.zip
Merge branch 'add-react-linter-rules'
-rw-r--r--.github/workflows/git-commit-message-style.yml2
-rw-r--r--gui/eslint.config.mjs6
-rw-r--r--gui/package-lock.json1396
-rw-r--r--gui/package.json1
-rw-r--r--gui/src/renderer/components/Accordion.tsx3
-rw-r--r--gui/src/renderer/components/Account.tsx8
-rw-r--r--gui/src/renderer/components/ApiAccessMethods.tsx24
-rw-r--r--gui/src/renderer/components/AppButton.tsx8
-rw-r--r--gui/src/renderer/components/AppRouter.tsx12
-rw-r--r--gui/src/renderer/components/AriaGroup.tsx23
-rw-r--r--gui/src/renderer/components/Changelog.tsx4
-rw-r--r--gui/src/renderer/components/ClipboardLabel.tsx2
-rw-r--r--gui/src/renderer/components/ContextMenu.tsx14
-rw-r--r--gui/src/renderer/components/CustomDnsSettings.tsx42
-rw-r--r--gui/src/renderer/components/DaitaSettings.tsx30
-rw-r--r--gui/src/renderer/components/Debug.tsx2
-rw-r--r--gui/src/renderer/components/DeviceInfoButton.tsx2
-rw-r--r--gui/src/renderer/components/EditApiAccessMethod.tsx91
-rw-r--r--gui/src/renderer/components/EditCustomBridge.tsx16
-rw-r--r--gui/src/renderer/components/ExpiredAccountAddTime.tsx6
-rw-r--r--gui/src/renderer/components/ExpiredAccountErrorView.tsx31
-rw-r--r--gui/src/renderer/components/Filter.tsx21
-rw-r--r--gui/src/renderer/components/FormattableTextInput.tsx14
-rw-r--r--gui/src/renderer/components/InfoButton.tsx2
-rw-r--r--gui/src/renderer/components/KeyboardNavigation.tsx23
-rw-r--r--gui/src/renderer/components/Launch.tsx10
-rw-r--r--gui/src/renderer/components/List.tsx33
-rw-r--r--gui/src/renderer/components/Login.tsx10
-rw-r--r--gui/src/renderer/components/MacOsScrollbarDetection.tsx8
-rw-r--r--gui/src/renderer/components/Map.tsx73
-rw-r--r--gui/src/renderer/components/Modal.tsx7
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx50
-rw-r--r--gui/src/renderer/components/NotificationArea.tsx8
-rw-r--r--gui/src/renderer/components/NotificationBanner.tsx16
-rw-r--r--gui/src/renderer/components/OpenVpnSettings.tsx2
-rw-r--r--gui/src/renderer/components/PageSlider.tsx10
-rw-r--r--gui/src/renderer/components/ProblemReport.tsx49
-rw-r--r--gui/src/renderer/components/ProxyForm.tsx99
-rw-r--r--gui/src/renderer/components/RedeemVoucher.tsx5
-rw-r--r--gui/src/renderer/components/SearchBar.tsx18
-rw-r--r--gui/src/renderer/components/SelectLanguage.tsx10
-rw-r--r--gui/src/renderer/components/SettingsImport.tsx35
-rw-r--r--gui/src/renderer/components/SettingsTextImport.tsx16
-rw-r--r--gui/src/renderer/components/SimpleInput.tsx36
-rw-r--r--gui/src/renderer/components/SplitTunnelingSettings.tsx46
-rw-r--r--gui/src/renderer/components/TooManyDevices.tsx20
-rw-r--r--gui/src/renderer/components/VpnSettings.tsx7
-rw-r--r--gui/src/renderer/components/WireguardSettings.tsx6
-rw-r--r--gui/src/renderer/components/cell/Input.tsx73
-rw-r--r--gui/src/renderer/components/cell/Section.tsx10
-rw-r--r--gui/src/renderer/components/cell/Selector.tsx40
-rw-r--r--gui/src/renderer/components/cell/SettingsForm.tsx14
-rw-r--r--gui/src/renderer/components/cell/SettingsGroup.tsx4
-rw-r--r--gui/src/renderer/components/cell/SettingsRadioGroup.tsx19
-rw-r--r--gui/src/renderer/components/cell/SettingsRow.tsx7
-rw-r--r--gui/src/renderer/components/cell/SettingsSelect.tsx27
-rw-r--r--gui/src/renderer/components/cell/SettingsTextInput.tsx15
-rw-r--r--gui/src/renderer/components/main-view/ConnectionActionButton.tsx4
-rw-r--r--gui/src/renderer/components/main-view/ConnectionPanel.tsx2
-rw-r--r--gui/src/renderer/components/main-view/FeatureIndicators.tsx3
-rw-r--r--gui/src/renderer/components/main-view/SelectLocationButton.tsx8
-rw-r--r--gui/src/renderer/components/select-location/CustomListDialogs.tsx37
-rw-r--r--gui/src/renderer/components/select-location/CustomLists.tsx50
-rw-r--r--gui/src/renderer/components/select-location/LocationRow.tsx16
-rw-r--r--gui/src/renderer/components/select-location/RelayListContext.tsx36
-rw-r--r--gui/src/renderer/components/select-location/ScopeBar.tsx6
-rw-r--r--gui/src/renderer/components/select-location/ScrollPositionContext.tsx16
-rw-r--r--gui/src/renderer/components/select-location/SelectLocation.tsx6
-rw-r--r--gui/src/renderer/components/select-location/SelectLocationContainer.tsx2
-rw-r--r--gui/src/renderer/components/select-location/SpecialLocationList.tsx9
-rw-r--r--gui/src/renderer/components/select-location/custom-list-helpers.ts10
-rw-r--r--gui/src/renderer/components/select-location/select-location-hooks.ts52
-rw-r--r--gui/src/renderer/lib/3dmap.ts4
-rw-r--r--gui/src/renderer/lib/actionsHook.ts5
-rw-r--r--gui/src/renderer/lib/api-access-methods.ts84
-rw-r--r--gui/src/renderer/lib/constraint-updater.ts2
-rw-r--r--gui/src/renderer/lib/history.tsx8
-rw-r--r--gui/src/renderer/lib/relay-settings-hooks.ts25
-rw-r--r--gui/src/renderer/lib/utility-hooks.ts76
-rw-r--r--gui/src/renderer/lib/utilityHooks.ts94
-rw-r--r--gui/src/renderer/redux/store.ts2
-rw-r--r--gui/src/shared/scheduler.ts2
-rw-r--r--gui/test/unit/notification-evaluation.spec.ts2
-rw-r--r--gui/test/unit/tunnel-state.spec.ts10
84 files changed, 2413 insertions, 724 deletions
diff --git a/.github/workflows/git-commit-message-style.yml b/.github/workflows/git-commit-message-style.yml
index 7cb2d03765..850a563c81 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 75f28f6091..5aafcb4041 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 3f6bddbe34..e5c9a0294b 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",
@@ -13861,9 +14636,421 @@
"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"
+ }
+ },
+ "@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"
+ }
+ },
+ "@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"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/helper-member-expression-to-functions": "^7.24.8",
+ "@babel/helper-optimise-call-expression": "^7.24.7",
+ "@babel/traverse": "^7.25.0"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/traverse": "^7.24.7",
+ "@babel/types": "^7.24.7"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/template": "^7.25.0",
+ "@babel/types": "^7.25.6"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.24.7",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "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"
+ }
+ }
+ }
+ },
+ "@babel/parser": {
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
+ "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
+ "dev": true,
+ "requires": {
+ "@babel/types": "^7.25.6"
+ }
+ },
+ "@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,
+ "requires": {
+ "@babel/helper-create-class-features-plugin": "^7.18.6",
+ "@babel/helper-plugin-utils": "^7.18.6"
+ }
+ },
"@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 6298c01286..c078ade972 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 70ae35c988..27a5be53bc 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<IProps, IState> {
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 c92e56ab1b..1cc666910e 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 78f0a11e77..57668df787 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<Array<ContextMenuItem>>(() => {
const items: Array<ContextMenuItem> = [
@@ -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 (
<Cell.Row data-testid="access-method">
diff --git a/gui/src/renderer/components/AppButton.tsx b/gui/src/renderer/components/AppButton.tsx
index 753f64745e..f82b997cc4 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 e83b6f55cd..3541c095d6 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<IFocusHandle>();
- 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 (
<Focus ref={focusRef}>
diff --git a/gui/src/renderer/components/AriaGroup.tsx b/gui/src/renderer/components/AriaGroup.tsx
index 8adf3243af..9e58283933 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 (
<AriaControlContext.Provider value={contextValue}>{props.children}</AriaControlContext.Provider>
@@ -45,7 +40,7 @@ const AriaDescriptionContext = React.createContext<IAriaDescriptionContext>({
});
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<IAriaInputContext>({
});
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 716daba75d..ea1b42f5c2 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 e9f760fd07..7f1970ebfc 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 165c284eca..fd84a8f0d9 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 (
<StyledMenuItem onClick={onClick} disabled={props.item.disabled}>
diff --git a/gui/src/renderer/components/CustomDnsSettings.tsx b/gui/src/renderer/components/CustomDnsSettings.tsx
index 35acb88a6c..914f7b8406 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<HTMLDivElement>();
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<HTMLTextAreaElement>) => {
- 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<HTMLTextAreaElement>) => {
+ 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 (
<AriaDescriptionGroup>
diff --git a/gui/src/renderer/components/DaitaSettings.tsx b/gui/src/renderer/components/DaitaSettings.tsx
index 4fd3ae0059..bcfca03316 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 f81fb85402..58e446be47 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 4839866972..090bb9c8df 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 a8602675a0..8a11404b1c 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<NewAccessMethodSetting<CustomProxy> | undefined>(method);
+ const [updatedMethod, setUpdatedMethod] = useState<
+ NewAccessMethodSetting<CustomProxy> | undefined
+ >(method);
- const save = useCallback(() => {
- if (updatedMethod.current !== undefined) {
- resetTestResult();
- if (id === undefined) {
- void addApiAccessMethod(updatedMethod.current);
- } else {
- void updateApiAccessMethod({ ...updatedMethod.current, id });
+ const save = useCallback(
+ (method: NewAccessMethodSetting<CustomProxy>) => {
+ 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 (
- <BackAction action={history.pop}>
+ <BackAction action={pop}>
<Layout>
<SettingsContainer>
<NavigationContainer>
@@ -100,17 +113,17 @@ function AccessMethodForm() {
{id !== undefined && method === undefined ? (
<span>Failed to open method</span>
) : (
- <NamedProxyForm proxy={method} onSave={onSave} onCancel={history.pop} />
+ <NamedProxyForm proxy={method} onSave={onSave} onCancel={pop} />
)}
</StyledSettingsContent>
<TestingDialog
- name={updatedMethod.current?.name ?? ''}
+ name={updatedMethod?.name ?? ''}
newMethod={id === undefined}
testing={testing}
testResult={testResult}
cancel={resetTestResult}
- save={save}
+ save={handleDialogSave}
/>
</StyledContent>
</StyledNavigationScrollbars>
@@ -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<ModalAlertType>(type);
-
- const isOpen = props.testing || props.testResult !== undefined;
- const typeValue = isOpen ? type : prevType.current;
+ 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;
+ }
- useEffect(() => {
- if (isOpen) {
- prevType.current = type;
- }
- }, [type]);
+ const type = useLastDefinedValue(currentType);
+ const displayType = type ?? ModalAlertType.failure;
return (
<ModalAlert
- isOpen={isOpen}
- type={typeValue}
- gridButtons={getTestingDialogButtons(typeValue, props.save, props.cancel)}
+ isOpen={!!currentType}
+ type={type}
+ gridButtons={getTestingDialogButtons(displayType, props.save, props.cancel)}
close={props.cancel}
- title={getTestingDialogTitle(typeValue, props.newMethod)}
- message={getTestingDialogSubTitle(typeValue, props.newMethod, props.name)}
+ title={getTestingDialogTitle(displayType, props.newMethod)}
+ message={getTestingDialogSubTitle(displayType, props.newMethod, props.name)}
/>
);
}
diff --git a/gui/src/renderer/components/EditCustomBridge.tsx b/gui/src/renderer/components/EditCustomBridge.tsx
index 7cc403aa6e..7a0ad6f8d8 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 (
- <BackAction action={history.pop}>
+ <BackAction action={pop}>
<Layout>
<SettingsContainer>
<NavigationContainer>
@@ -83,7 +83,7 @@ function CustomBridgeForm() {
<ProxyForm
proxy={bridgeSettings.custom}
onSave={onSave}
- onCancel={history.pop}
+ onCancel={pop}
onDelete={bridgeSettings.custom === undefined ? undefined : showDeleteDialog}
/>
</StyledSettingsContent>
diff --git a/gui/src/renderer/components/ExpiredAccountAddTime.tsx b/gui/src/renderer/components/ExpiredAccountAddTime.tsx
index 3b54642978..8dd48d44d0 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 b15d74329a..a98d3e441c 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 (
<Layout>
@@ -192,7 +192,7 @@ function ExternalPaymentButton() {
} else {
await openLinkWithAuth(links.purchase);
}
- }, []);
+ }, [openLinkWithAuth, recoveryAction, setShowBlockWhenDisconnectedAlert]);
return (
<AppButton.BlockingButton
@@ -225,16 +225,19 @@ function BlockWhenDisconnectedAlert() {
const onCloseBlockWhenDisconnectedInstructions = useCallback(() => {
setShowBlockWhenDisconnectedAlert(false);
- }, []);
+ }, [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);
- }
- }, []);
+ 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 (
<ModalAlert
diff --git a/gui/src/renderer/components/Filter.tsx b/gui/src/renderer/components/Filter.tsx
index b3c223bc38..a0ed1725b1 100644
--- a/gui/src/renderer/components/Filter.tsx
+++ b/gui/src/renderer/components/Filter.tsx
@@ -11,7 +11,8 @@ import {
filterLocationsByEndPointType,
} from '../lib/filter-locations';
import { useHistory } from '../lib/history';
-import { useBoolean, useNormalRelaySettings, useTunnelProtocol } from '../lib/utilityHooks';
+import { useNormalRelaySettings, useTunnelProtocol } from '../lib/relay-settings-hooks';
+import { useBoolean } from '../lib/utility-hooks';
import { IRelayLocationCountryRedux } from '../redux/settings/reducers';
import { useSelector } from '../redux/store';
import Accordion from './Accordion';
@@ -149,7 +150,7 @@ function useFilteredOwnershipOptions(providers: string[], ownership: Ownership):
}
return ownershipOptions;
- }, [locations, providers, ownership]);
+ }, [locations, endpointType, tunnelProtocol, relaySettings, ownership, providers]);
return availableOwnershipOptions;
}
@@ -172,7 +173,7 @@ export function useFilteredProviders(providers: string[], ownership: Ownership):
);
const relaylistForFilters = filterLocations(relayListForEndpointType, ownership, providers);
return providersFromRelays(relaylistForFilters);
- }, [locations, ownership]);
+ }, [endpointType, locations, ownership, providers, relaySettings, tunnelProtocol]);
return availableProviders;
}
@@ -272,22 +273,24 @@ interface IFilterByProviderProps {
}
function FilterByProvider(props: IFilterByProviderProps) {
+ const { setProviders } = props;
+
const [expanded, , , toggleExpanded] = useBoolean(false);
const onToggle = useCallback(
(provider: string) =>
- 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 (
<StyledRow onClick={onToggle}>
diff --git a/gui/src/renderer/components/FormattableTextInput.tsx b/gui/src/renderer/components/FormattableTextInput.tsx
index f6c259c4c4..aae614ddd7 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<HTMLInputElement> {
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 <input ref={combinedRef} type="text" {...otherProps} />;
}
diff --git a/gui/src/renderer/components/InfoButton.tsx b/gui/src/renderer/components/InfoButton.tsx
index e17e7b87b8..4ef2aa3c6e 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 8ed3caa540..cbde4297bd 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<React.ReactElement>;
@@ -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<BackActionFn>();
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<BackActionFn>();
// 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<BackActionFn>) => {
+ props.registerBackAction(backActions.at(0));
+ });
+
+ useEffect(() => registerBackActionEvent(backActions), [backActions]);
return (
<BackActionContext.Provider value={backActionContext}>
diff --git a/gui/src/renderer/components/Launch.tsx b/gui/src/renderer/components/Launch.tsx
index 05bf6aeda6..7a8f75bc9c 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 (
<StyledFooter>
@@ -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 3d1bd99478..4961855fba 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<T>(props: ListProps<T>) {
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<Record<string, Scheduler>>({});
- useEffect(() => {
+ const itemChangeEvent = useEffectEvent((items: Array<T>) => {
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<T>(props: ListProps<T>) {
setDisplayItems((items) => items.filter((item) => item.key !== key));
}, []);
- useEffect(() => {
+ const handleDisplayItemsChange = useEffectEvent((displayItems: Array<RowDisplayData<T>>) => {
// 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<T>(props: ListProps<T>) {
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<T>(props: ListProps<T>) {
data={displayItem}
onRemoved={onRemoved}
render={props.children}
- skipAddTransition={skipAddTransition.current}
+ skipAddTransition={skipAddTransition}
/>
))}
</>
@@ -105,14 +112,16 @@ interface ListItemProps<T> {
}
function ListItem<T>(props: ListItemProps<T>) {
+ 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 430772c2c7..0d234bd66b 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<HTMLButtonElement>) => {
// 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 aebb144f9b..520b6f3f3b 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<HTMLDivElement>();
- 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 (
<StyledContainer>
diff --git a/gui/src/renderer/components/Map.tsx b/gui/src/renderer/components/Map.tsx
index b002cce5e8..9ea69251d9 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<Coordinate | undefined>(() => {
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<GlMap>();
const canvasRef = useRef<HTMLCanvasElement>();
+
+ // 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 render = useCallback(() => requestAnimationFrame(animationFrameCallback), []);
+ const animationFrameCallback = useEffectEvent((now: number) => {
+ now *= 0.001; // convert to seconds
- 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;
+ }
- // 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);
- mapRef.current?.draw(now);
+ // Stops rendering if pause is true. This happens when there is no ongoing movements
+ if (!pause.current) {
+ render();
+ }
+ });
- // Stops rendering if pause is true. This happens when there is no ongoing movements
- if (!pause.current) {
- render();
- }
- },
- [props.animate],
- );
+ 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 1b83e10597..e339566c24 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 ee316de783..680393a6c7 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<CustomScrollbarsRef>();
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 (
<CustomScrollbars
ref={combinedRefs}
@@ -189,7 +196,10 @@ export function BackBarItem() {
const history = useHistory();
// Compare the transition name with dismiss to infer wheter or not the view will slide
// horizontally or vertically and then use matching button.
- const backIcon = useMemo(() => 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 fff30300d0..dac62db192 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<InAppNotificationTroubleshootInfo>();
@@ -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 f79855f004..924f65ff99 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<HTMLDivElement>();
- // Save last non-undefined children to be able to show them during the hide-transition.
- const prevChildren = useRef<React.ReactNode>();
- 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 (
<Collapsible $height={contentHeight} className={props.className} $alignBottom={alignBottom}>
- <Content ref={contentRef}>{props.children ?? prevChildren.current}</Content>
+ <Content ref={contentRef}>{children}</Content>
</Collapsible>
);
}
diff --git a/gui/src/renderer/components/OpenVpnSettings.tsx b/gui/src/renderer/components/OpenVpnSettings.tsx
index 5013b158f8..571e8e3571 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 56d64954e7..d03c90a0e2 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 (
<StyledTransparentButton onClick={onClick}>
diff --git a/gui/src/renderer/components/ProblemReport.tsx b/gui/src/renderer/components/ProblemReport.tsx
index fd481cde07..36ddc09c45 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<HTMLInputElement>) => {
- setEmail(event.target.value);
- }, []);
+ const onChangeEmail = useCallback(
+ (event: ChangeEvent<HTMLInputElement>) => {
+ setEmail(event.target.value);
+ },
+ [setEmail],
+ );
- const onChangeDescription = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
- setMessage(event.target.value);
- }, []);
+ const onChangeDescription = useCallback(
+ (event: ChangeEvent<HTMLTextAreaElement>) => {
+ 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 (
<StyledContent>
@@ -291,7 +298,7 @@ function NoEmailDialog() {
const onCancelNoEmailDialog = useCallback(() => {
setSendState(SendState.initial);
- }, []);
+ }, [setSendState]);
return (
<ModalAlert
@@ -312,7 +319,7 @@ function NoEmailDialog() {
}
function OutdatedVersionWarningDialog() {
- const history = useHistory();
+ const { pop } = useHistory();
const { openUrl } = useAppContext();
const isOffline = useSelector((state) => 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')}
</AppButton.BlueButton>,
]}
- 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 7e006f65c0..9a163ceebf 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<ProxyFormContextProviderProps>) {
+ const { onSave: propsOnSave } = props;
+
const [proxy, setProxy] = useState<CustomProxy | undefined>(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<ShadowsocksCustomProxy>) {
[],
);
+ 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 (
<SettingsGroup title={messages.pgettext('api-access-methods-view', 'Server details')}>
@@ -346,21 +353,25 @@ function EditSocks5Remote(props: EditProxyProps<Socks5RemoteCustomProxy>) {
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 (
<SettingsGroup title={messages.pgettext('api-access-methods-view', 'Remote Server')}>
@@ -435,17 +446,29 @@ function EditSocks5Local(props: EditProxyProps<Socks5LocalCustomProxy>) {
[],
);
- 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 7f637ab125..c88d68a087 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 (
<RedeemVoucherContext.Provider
diff --git a/gui/src/renderer/components/SearchBar.tsx b/gui/src/renderer/components/SearchBar.tsx
index e21439fbbf..fe1d1936aa 100644
--- a/gui/src/renderer/components/SearchBar.tsx
+++ b/gui/src/renderer/components/SearchBar.tsx
@@ -3,7 +3,7 @@ import styled from 'styled-components';
import { colors } from '../../config.json';
import { messages } from '../../shared/gettext';
-import { useStyledRef } from '../lib/utilityHooks';
+import { useEffectEvent, useStyledRef } from '../lib/utility-hooks';
import { normalText } from './common-styles';
import ImageView from './ImageView';
@@ -74,26 +74,30 @@ interface ISearchBarProps {
}
export default function SearchBar(props: ISearchBarProps) {
+ const { onSearch } = props;
+
const inputRef = useStyledRef<HTMLInputElement>();
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 (
<StyledSearchContainer className={props.className}>
diff --git a/gui/src/renderer/components/SelectLanguage.tsx b/gui/src/renderer/components/SelectLanguage.tsx
index eb0ad038da..9bbc110fc1 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<CustomScrollbarsRef>(null);
const selectedCellRef = useRef<HTMLButtonElement>(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 (
- <BackAction action={history.pop}>
+ <BackAction action={pop}>
<Layout>
<SettingsContainer>
<NavigationContainer>
@@ -97,7 +97,7 @@ function usePreferredLocale() {
const preferredLocalesList: SelectorItem<string>[] = 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 d93064ff2e..8fefbedf20 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<ImportStatus>();
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 (
<BackAction action={history.pop}>
diff --git a/gui/src/renderer/components/SettingsTextImport.tsx b/gui/src/renderer/components/SettingsTextImport.tsx
index 7d51e7ac93..c6cb325b0c 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<HTMLTextAreaElement>();
- 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 (
<BackAction action={back}>
diff --git a/gui/src/renderer/components/SimpleInput.tsx b/gui/src/renderer/components/SimpleInput.tsx
index 3d2a1a63a7..8d4a51d8e9 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<React.InputHTMLAttributes<HTMLInputEleme
}
function SimpleInput(props: SimpleInputProps, ref: React.Ref<HTMLInputElement>) {
- 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<HTMLInputElement>) => {
setValue(event.target.value);
- otherProps.onChange?.(event);
+ propsOnChange?.(event);
onChangeValue?.(event.target.value);
},
- [otherProps.onChange, onChangeValue],
+ [propsOnChange, onChangeValue],
);
const onSubmit = useCallback(
(event: React.FormEvent<HTMLInputElement>) => {
- otherProps.onSubmit?.(event);
+ propsOnSubmit?.(event);
onSubmitValue?.(value);
},
- [otherProps.onSubmit, onSubmitValue, value],
+ [propsOnSubmit, onSubmitValue, value],
);
const onKeyPress = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
- 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 2cee823b3e..ed999ba867 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<ILinuxSplitTunnelingApplication[]>();
const [browseError, setBrowseError] = useState<string>();
- 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<ISplitTunnelingApplication[]>();
- 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 (
<Cell.CellButton>
diff --git a/gui/src/renderer/components/TooManyDevices.tsx b/gui/src/renderer/components/TooManyDevices.tsx
index 1f1fcf9247..3e636bcb44 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 d0253ef480..c23ecdbf47 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 9a79111ad7..b199bd5ee5 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 97fe01a185..de2649e3c6 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<HTMLInputEleme
onSubmitValue,
onInvalidValue,
onChangeValue,
+ onFocus: propsOnFocus,
+ onBlur: propsOnBlur,
+ onChange: propsOnChange,
+ onKeyPress: propsOnKeyPress,
...otherProps
} = props;
@@ -79,26 +83,26 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
onInvalidValue?.(value);
}
},
- [onSubmitValue, onInvalidValue],
+ [validateValue, onSubmitValue, onInvalidValue],
);
const onFocus = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
setFocused();
- props.onFocus?.(event);
+ propsOnFocus?.(event);
},
- [props.onFocus],
+ [propsOnFocus, setFocused],
);
const onBlur = useCallback(
(event: React.FocusEvent<HTMLInputElement>) => {
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<HTMLInputEleme
setInternalValue(value);
}
- props.onChange?.(event);
+ propsOnChange?.(event);
onChangeValue?.(value);
},
- [modifyValue, props.onSubmit],
+ [modifyValue, onChangeValue, props.value, propsOnChange],
);
const onKeyPress = useCallback(
@@ -122,23 +126,27 @@ function InputWithRef(props: IInputProps, forwardedRef: React.Ref<HTMLInputEleme
onSubmit(value);
inputRef.current?.blur();
}
- props.onKeyPress?.(event);
+ propsOnKeyPress?.(event);
},
- [value, onSubmit, inputRef, props.onKeyPress],
+ [value, onSubmit, inputRef, propsOnKeyPress],
);
- // If the the initialValue changes in the uncontrolled mode when the user isn't currently writing,
- // then we want to update the value.
- useEffect(() => {
+ 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<HTMLTextAreaElement>();
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<HTMLTextAreaElement>) => {
const value = event.target.value;
setValue(value);
- props.onChange?.(value);
+ propsOnChange?.(value);
},
- [props.onChange],
+ [propsOnChange],
);
const onKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -329,32 +339,37 @@ export function RowInput(props: IRowInputProps) {
const onFocus = useCallback(
(event: React.FocusEvent<HTMLTextAreaElement>) => {
setFocused();
- props.onFocus?.(event);
+ propsOnFocus?.(event);
},
- [props.onFocus],
+ [propsOnFocus, setFocused],
);
const onBlur = useCallback(
(event: React.FocusEvent<HTMLTextAreaElement>) => {
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 056aa8ceac..b7465ecb93 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 3c1988a502..b1e20a0c41 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<T>(props: SelectorCellProps<T>) {
- 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 (
<StyledSelectorCell>
@@ -270,8 +272,11 @@ export function SelectorWithCustomItem<T, U>(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<T, U>(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<T, U>(props: SelectorWithCustomItemProps<
onSelect(newValue!);
},
- [onSelect],
+ [inputRef, onSelect],
);
const validateCustomValue = useCallback(
@@ -319,7 +324,7 @@ export function SelectorWithCustomItem<T, U>(props: SelectorWithCustomItemProps<
}
}
},
- [parseValue, onSelect],
+ [parseValue, isNonCustomItem, handleSelectItem, onSelect],
);
const handleInvalidCustom = useCallback(
@@ -330,11 +335,14 @@ export function SelectorWithCustomItem<T, U>(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 (
<div onMouseDown={handleMouseDown}>
diff --git a/gui/src/renderer/components/cell/SettingsForm.tsx b/gui/src/renderer/components/cell/SettingsForm.tsx
index 1f2be529c1..9a901d88d8 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 f430d96a32..7ca8d0dff1 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 46f3ccded7..6f4f1a0d90 100644
--- a/gui/src/renderer/components/cell/SettingsRadioGroup.tsx
+++ b/gui/src/renderer/components/cell/SettingsRadioGroup.tsx
@@ -17,13 +17,18 @@ interface SettingsSelectProps<T extends string> {
}
export function SettingsRadioGroup<T extends string>(props: SettingsSelectProps<T>) {
+ const { onUpdate } = props;
+
const [value, setValue] = useState<T>(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 (
<StyledRadioGroup>
@@ -92,11 +97,13 @@ interface RadioButtonProps<T extends string> {
}
function RadioButton<T extends string>(props: RadioButtonProps<T>) {
+ const { onSelect } = props;
+
const onChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
- 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 1211098a70..f242106c92 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<IndentedRowProps>) {
unsetError?.();
}
},
- [reportError, unsetError],
+ [props.errorMessage, reportError, unsetError],
);
- const contextValue = useMemo(() => ({ invalid, setInvalid: setInvalidImpl }), [invalid]);
+ const contextValue = useMemo(
+ () => ({ invalid, setInvalid: setInvalidImpl }),
+ [invalid, setInvalidImpl],
+ );
return (
<settingsRowContext.Provider value={contextValue}>
diff --git a/gui/src/renderer/components/cell/SettingsSelect.tsx b/gui/src/renderer/components/cell/SettingsSelect.tsx
index f0d918ded6..7b5e1d7ab4 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<T extends string>(props: SettingsSelectProps<T>)
// 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<T extends string>(props: SettingsSelectProps<T>)
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<T extends string> {
}
function Item<T extends string>(props: ItemProps<T>) {
+ 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 (
<StyledItem
diff --git a/gui/src/renderer/components/cell/SettingsTextInput.tsx b/gui/src/renderer/components/cell/SettingsTextInput.tsx
index 1727997dc4..44381bc681 100644
--- a/gui/src/renderer/components/cell/SettingsTextInput.tsx
+++ b/gui/src/renderer/components/cell/SettingsTextInput.tsx
@@ -2,6 +2,7 @@ import { useCallback, useEffect } from 'react';
import styled from 'styled-components';
import { colors } from '../../../config.json';
+import { useEffectEvent } from '../../lib/utility-hooks';
import { AriaInput } from '../AriaGroup';
import { smallNormalText } from '../common-styles';
import { useSettingsFormSubmittableReporter } from './SettingsForm';
@@ -49,7 +50,7 @@ export function SettingsNumberInput(props: SettingsNumberInputProps) {
(value: string) => {
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<T extends ValueTypes>(props: InputProps<T>) {
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 ad579da671..437fa93321 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<Parameters<typeof SmallButton>[0]>) {
const error = e as Error;
log.error(`Failed to connect the tunnel: ${error.message}`);
}
- }, []);
+ }, [connectTunnel]);
return (
<StyledConnectionButton color={SmallButtonColor.green} onClick={onConnect} {...props}>
@@ -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 5b4e576f76..34e98abeea 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 2d7914fd87..5b7e60ade0 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 30b132a84e..50508a3192 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 (
<StyledSmallButton
@@ -129,7 +129,7 @@ function ReconnectButton(props: MultiButtonCompatibleProps) {
const error = e as Error;
log.error(`Failed to reconnect the tunnel: ${error.message}`);
}
- }, []);
+ }, [reconnectTunnel]);
return (
<StyledReconnectButton
diff --git a/gui/src/renderer/components/select-location/CustomListDialogs.tsx b/gui/src/renderer/components/select-location/CustomListDialogs.tsx
index 4f97706210..4cc42c0623 100644
--- a/gui/src/renderer/components/select-location/CustomListDialogs.tsx
+++ b/gui/src/renderer/components/select-location/CustomListDialogs.tsx
@@ -13,7 +13,7 @@ import { messages } from '../../../shared/gettext';
import log from '../../../shared/logging';
import { useAppContext } from '../../context';
import { formatHtml } from '../../lib/html-formatter';
-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';
@@ -34,6 +34,8 @@ interface AddToListDialogProps {
// Dialog that displays list of custom lists when adding location to custom list.
export function AddToListDialog(props: AddToListDialogProps) {
+ const { hide } = props;
+
const { updateCustomList } = useAppContext();
const customLists = useSelector((state) => 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 (
<ModalAlert
@@ -215,10 +224,12 @@ interface DeleteConfirmDialogProps {
// Dialog for changing the name of a custom list.
export function DeleteConfirmDialog(props: DeleteConfirmDialogProps) {
+ const { confirm: propsConfirm, hide } = props;
+
const confirm = useCallback(() => {
- props.confirm();
- props.hide();
- }, []);
+ propsConfirm();
+ hide();
+ }, [hide, propsConfirm]);
return (
<ModalAlert
diff --git a/gui/src/renderer/components/select-location/CustomLists.tsx b/gui/src/renderer/components/select-location/CustomLists.tsx
index 1cbb692b1b..1f4c77ed6b 100644
--- a/gui/src/renderer/components/select-location/CustomLists.tsx
+++ b/gui/src/renderer/components/select-location/CustomLists.tsx
@@ -6,7 +6,7 @@ import { CustomListError, CustomLists, RelayLocation } from '../../../shared/dae
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 Accordion from '../Accordion';
import * as Cell from '../cell';
import { measurements } from '../common-styles';
@@ -77,15 +77,18 @@ export default function CustomLists(props: CustomListsProps) {
const { searchTerm } = useSelectLocationContext();
const { customLists } = useRelayListContext();
- const createList = useCallback(async (name: string): Promise<void | CustomListError> => {
- 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<void | CustomListError> => {
+ 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<HTMLInputElement>();
// 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<HTMLInputElement>) => {
// 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 (
<BackAction disabled={!props.visible} action={props.cancel}>
@@ -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 41f3aa7152..a47c1dd646 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<C extends LocationSpecification> {
// Renders the rows and its children for countries, cities and relays
function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
+ const { onSelect, onWillExpand: propsOnWillExpand } = props;
+
const hasChildren = getLocationChildren(props.source).some((child) => child.visible);
const buttonRef = useStyledRef<HTMLButtonElement>();
const userInvokedExpand = useRef(false);
@@ -79,19 +81,19 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
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<C extends LocationSpecification>(props: IProps<C>) {
}
}
}
- }, [customLists, props.source.location]);
+ }, [customLists, props.source.location, updateCustomList]);
// Remove an entire custom list.
const confirmRemoveCustomList = useCallback(async () => {
@@ -130,7 +132,7 @@ function LocationRow<C extends LocationSpecification>(props: IProps<C>) {
);
}
}
- }, [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 86fb8de22b..3165a95824 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<IRelayLocationCountryRedu
scrollIntoView(locationRect);
}
},
- [],
+ [scrollIntoView, spacePreAllocationViewRef],
);
// Expand search results when searching
@@ -298,15 +306,19 @@ function useExpandedLocations(filteredLocations: Array<IRelayLocationCountryRedu
[relaySettings, bridgeSettings, locationType, filteredLocations],
);
+ const expandLocationsForSearch = useEffectEvent(
+ (filteredLocations: Array<IRelayLocationCountryRedux>) => {
+ 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 bbe4935993..28312da19e 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 ? (
<StyledScopeBarItem selected={props.selected} onClick={onClick}>
diff --git a/gui/src/renderer/components/select-location/ScrollPositionContext.tsx b/gui/src/renderer/components/select-location/ScrollPositionContext.tsx
index d27e252427..55ee8dae97 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 438fdc430d..506c836548 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 1d0e5251f7..66bebdf1b0 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 f4ccf13ec2..e4d105d635 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<T> {
}
function SpecialLocationRow<T>(props: SpecialLocationRowProps<T>) {
+ 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<T> = {
...props,
@@ -105,7 +106,7 @@ const StyledInfoButton = styled(StyledHoverInfoButton)({ display: 'block' });
export function CustomBridgeLocationRow(
props: SpecialLocationRowInnerProps<SpecialBridgeLocationType>,
) {
- 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 (
<StyledLocationRowContainerWithMargin ref={selectedRef} disabled={props.source.disabled}>
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 5cb6d695b8..3f6149cd7f 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 027185e416..fbecc4db5d 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 ec4def0744..d0e130e4c8 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 2597aee422..fc046d0a66 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<A, M extends ActionCreatorsMapObject<A>>(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 fd78d1484a..90d406cc3d 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<void>((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<void>((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 73fbdd1336..6ea021ece9 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 741c298da6..6d92a0e88c 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<LocationState> {
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 0000000000..14fe99849d
--- /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<TunnelProtocol> {
+ 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 0000000000..1efc49c804
--- /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<T>(): React.MutableRefObject<T> {
+ return useRef() as React.MutableRefObject<T>;
+}
+
+export function useCombinedRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
+ return useRefCallback((element: T | null) => refs.forEach((ref) => assignToRef(element, ref)));
+}
+
+export function assignToRef<T>(element: T | null, ref?: React.Ref<T>) {
+ if (typeof ref === 'function') {
+ ref(element);
+ } else if (ref && element) {
+ (ref as React.MutableRefObject<T>).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<T extends unknown[], R> = (...args: T) => R;
+
+export function useEffectEvent<Args extends unknown[]>(
+ fn: Fn<Args, void | undefined | Promise<void | undefined>>,
+): Fn<Args, void> {
+ const ref = useRef<Fn<Args, void>>(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<T>(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 be1688f3f3..0000000000
--- 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<T>(): React.RefObject<T> {
- return useRef() as React.RefObject<T>;
-}
-
-export function useCombinedRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
- return useCallback((element: T | null) => refs.forEach((ref) => assignToRef(element, ref)), []);
-}
-
-export function assignToRef<T>(element: T | null, ref?: React.Ref<T>) {
- if (typeof ref === 'function') {
- ref(element);
- } else if (ref && element) {
- (ref as React.MutableRefObject<T>).current = element;
- }
-}
-
-export function useAsyncEffect(
- effect: () => Promise<void | (() => void | Promise<void>)>,
- 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<TunnelProtocol> {
- 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 1617acce3d..073fe67d30 100644
--- a/gui/src/renderer/redux/store.ts
+++ b/gui/src/renderer/redux/store.ts
@@ -84,8 +84,10 @@ export function useSelector<R>(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 8ae5a2cbf0..2716097194 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 723307b3d7..d05e967efc 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 a6013523b8..e751b50c57 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 });