diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index a6f34fea..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules -dist -out -.gitignore diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index f08101e5..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,51 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - jest: true - }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - '@electron-toolkit/eslint-config-ts/recommended', - '@electron-toolkit/eslint-config-prettier', - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:@typescript-eslint/recommended', - 'prettier', - 'react-app', - 'react-app/jest' - ], - rules: { - // disable the rule for all files - 'no-prototype-builtins': 'off', - 'no-case-declarations': 'off', - 'no-useless-escape': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-this-alias': 'warn', - 'react/react-in-jsx-scope': 'off', - camelcase: 'error', - 'spaced-comment': 'error', - quotes: ['error', 'single'], - 'no-duplicate-imports': 'error', - 'react/display-name': false, - 'import/no-anonymous-default-export': 'off' - }, - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaFeatures: { - jsx: true - }, - ecmaVersion: 'latest', - sourceType: 'module' - }, - plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], - settings: { - 'import/resolver': { - typescript: {} - } - } -} diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 6110c58a..4e1ce05c 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -10,16 +10,16 @@ export default defineConfig({ '@/lib': resolve('src/main/lib'), '@/classes': resolve('src/main/classes'), '@shared': resolve('src/shared'), - } - } + }, + }, }, preload: { plugins: [externalizeDepsPlugin()], resolve: { alias: { - '@shared': resolve('src/shared') - } - } + '@shared': resolve('src/shared'), + }, + }, }, renderer: { assetsInclude: ['src/renderer/public/**/*'], @@ -31,9 +31,9 @@ export default defineConfig({ '@/assets': resolve('src/renderer/src/assets'), '@/pages': resolve('src/renderer/src/pages'), '@/components': resolve('src/renderer/src/components'), - '@/utils': resolve('src/renderer/src/utils') - } + '@/utils': resolve('src/renderer/src/utils'), + }, }, - plugins: [react()] - } + plugins: [react()], + }, }) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..600766e6 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,69 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from '@electron-toolkit/eslint-config-ts' +import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' + +export default tseslint.config( + { + ignores: ['node_modules/**', 'dist/**', 'out/**'], + }, + js.configs.recommended, + tseslint.configs.recommended, + { + files: ['**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2021, + ...globals.jest, + }, + }, + rules: { + 'no-prototype-builtins': 'off', + 'no-case-declarations': 'off', + 'no-useless-escape': 'off', + // Backend payloads and theme tokens are snake_case by contract, so this + // rule only ever fires on identifiers we are not free to rename. + camelcase: 'off', + // The `/` marker keeps `--fix` from mangling TypeScript triple-slash + // directives (`/// `) into `// / `. + 'spaced-comment': ['error', 'always', { markers: ['/'] }], + 'no-duplicate-imports': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-this-alias': 'warn', + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/no-require-imports': 'warn', + }, + }, + // React rules apply to the renderer only. The main process has helpers named + // `use*` (useNethVoiceAPI, useLogin) that are plain functions, not hooks, and + // rules-of-hooks flags them when it is allowed to see them. + { + files: ['src/renderer/**/*.{js,jsx,ts,tsx}'], + ...react.configs.flat.recommended, + settings: { react: { version: 'detect' } }, + }, + { + files: ['src/renderer/**/*.{js,jsx,ts,tsx}'], + ...react.configs.flat['jsx-runtime'], + }, + { + files: ['src/renderer/**/*.{js,jsx,ts,tsx}'], + plugins: { 'react-hooks': reactHooks }, + rules: { + ...reactHooks.configs.recommended.rules, + // TypeScript already checks component props. + 'react/prop-types': 'off', + 'react/react-in-jsx-scope': 'off', + 'react/display-name': 'off', + }, + }, + eslintConfigPrettier, +) diff --git a/package-lock.json b/package-lock.json index 6be1245e..5139ba37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,8 +17,8 @@ }, "devDependencies": { "@date-fns/utc": "^2.0.0", - "@electron-toolkit/eslint-config-prettier": "^2.0.0", - "@electron-toolkit/eslint-config-ts": "^1.0.1", + "@electron-toolkit/eslint-config-prettier": "^3.0.0", + "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/tsconfig": "^2.0.0", "@electron-toolkit/utils": "^3.0.0", @@ -51,8 +51,9 @@ "electron-builder": "^26.16.1", "electron-builder-notarize": "github:therockerline/electron-builder-notarize#customizations", "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-react": "^7.33.2", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "framer-motion": "^11.11.8", "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.0.1", @@ -470,32 +471,33 @@ "license": "MIT" }, "node_modules/@electron-toolkit/eslint-config-prettier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-2.0.0.tgz", - "integrity": "sha512-L+uG1FvJcAZkPZpSi6B1pmdpyJFyOxWDTjr1Vs47vSryxv/EX1Ch6o4HVsachlDq3fMEkDgojuP2F3ZvVZMoLw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-prettier/-/eslint-config-prettier-3.0.0.tgz", + "integrity": "sha512-YapmIOVkbYdHLuTa+ad1SAVtcqYL9A/SJsc7cxQokmhcwAwonGevNom37jBf9slXegcZ/Slh01I/JARG1yhNFw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-prettier": "^5.0.1" + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3" }, "peerDependencies": { - "eslint": ">= 8.0.0", + "eslint": ">= 9.0.0", "prettier": ">= 3.0.0" } }, "node_modules/@electron-toolkit/eslint-config-ts": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-ts/-/eslint-config-ts-1.0.1.tgz", - "integrity": "sha512-6RkoGE/W/WVlDozy/u6/lo5THNP5CCAPxdKpdQuYaopUfObs3o+JyxdlSb45imh+nL8nv875970EoyplntXaug==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@electron-toolkit/eslint-config-ts/-/eslint-config-ts-3.1.0.tgz", + "integrity": "sha512-MowZQKd3yxXSDLack5QvjQwYHhpOJFoWBGBwJ/k+DCd7NUSendplECbQGFp86tPQYPUrPBPceR/hdsSAnaY5ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "^6.14.0", - "@typescript-eslint/parser": "^6.14.0" + "@eslint/js": "^9.24.0", + "globals": "^16.0.0", + "typescript-eslint": "^8.29.1" }, "peerDependencies": { - "eslint": ">=8.0.0", + "eslint": ">=9.0.0", "typescript": "*" }, "peerDependenciesMeta": { @@ -1356,9 +1358,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, "license": "MIT", "dependencies": { @@ -1374,44 +1376,122 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.7.tgz", + "integrity": "sha512-F42g89Qd5oAWtp0k0nnSrjziAKza7w8SVT4mStc18LZMaRb4J1HQAHLCalEtDCxrTuksx7NU9qsmeLwpOfPqWw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", + "js-yaml": "^4.3.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -1420,16 +1500,13 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1448,27 +1525,41 @@ "node": "*" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://eslint.org/donate" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@floating-ui/core": { @@ -1634,44 +1725,42 @@ "react-hook-form": "^7.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, + "license": "Apache-2.0", "engines": { - "node": "*" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1688,13 +1777,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2859,16 +2954,16 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@react-aria/focus": { @@ -3917,13 +4012,6 @@ "@types/node": "*" } }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -3984,124 +4072,213 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, - "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", "dev": true, "license": "MIT", "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4109,84 +4286,144 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "ISC" + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", @@ -4718,16 +4955,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -6413,19 +6640,6 @@ "node": "*" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -6510,19 +6724,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -7250,84 +7451,91 @@ } }, "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, "peerDependencies": { "eslint": ">=7.0.0" } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", "dev": true, "license": "MIT", "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -7338,7 +7546,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -7383,10 +7591,23 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7431,9 +7652,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7441,29 +7662,29 @@ "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7471,22 +7692,6 @@ "concat-map": "0.0.1" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -7500,32 +7705,19 @@ "node": "*" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -7776,16 +7968,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-type": { @@ -7865,24 +8057,23 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -8290,6 +8481,19 @@ "node": ">=10.0" } }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -8307,27 +8511,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8374,13 +8557,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -9139,16 +9315,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -9608,10 +9774,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10849,16 +11025,6 @@ "dev": true, "license": "ISC" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/pbkdf2": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", @@ -11288,9 +11454,9 @@ } }, "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, "license": "MIT", "dependencies": { @@ -12142,23 +12308,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", @@ -12404,9 +12553,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -13258,20 +13407,19 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.3.6" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/tabbable": { @@ -13483,13 +13631,6 @@ "rimraf": "bin.js" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -13688,16 +13829,16 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", - "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-interface-checker": { @@ -13829,6 +13970,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/uiohook-napi": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/uiohook-napi/-/uiohook-napi-1.5.5.tgz", diff --git a/package.json b/package.json index a188c6af..d5b35db2 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "homepage": "https://electron-vite.org", "scripts": { "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "lint": "eslint . --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", @@ -35,8 +35,8 @@ }, "devDependencies": { "@date-fns/utc": "^2.0.0", - "@electron-toolkit/eslint-config-prettier": "^2.0.0", - "@electron-toolkit/eslint-config-ts": "^1.0.1", + "@electron-toolkit/eslint-config-prettier": "^3.0.0", + "@electron-toolkit/eslint-config-ts": "^3.1.0", "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/tsconfig": "^2.0.0", "@electron-toolkit/utils": "^3.0.0", @@ -69,8 +69,9 @@ "electron-builder": "^26.16.1", "electron-builder-notarize": "github:therockerline/electron-builder-notarize#customizations", "electron-vite": "^2.0.0", - "eslint": "^8.56.0", - "eslint-plugin-react": "^7.33.2", + "eslint": "^9.39.5", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^5.2.0", "framer-motion": "^11.11.8", "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.0.1", diff --git a/postcss.config.js b/postcss.config.js index 311ffbe5..825cb186 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -3,6 +3,6 @@ module.exports = { 'postcss-import': {}, 'tailwindcss/nesting': {}, tailwindcss: {}, - autoprefixer: {} - } + autoprefixer: {}, + }, } diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index 82b730ac..5fb1604a 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -1,4 +1,10 @@ -import { Account, AuthAppData, AvailableDevices, ConfigFile, PhoneIslandPosition } from '@shared/types' +import { + Account, + AuthAppData, + AvailableDevices, + ConfigFile, + PhoneIslandPosition, +} from '@shared/types' import { Log } from '@shared/utils/logger' import { safeStorage } from 'electron' import { store } from '@/lib/mainStore' @@ -10,7 +16,7 @@ import { requires2FA, isJWTExpired } from '@shared/utils/jwt' const defaultConfig: ConfigFile = { lastUser: undefined, - accounts: {} + accounts: {}, } type EventCallback = (...args: any[]) => void | Promise @@ -20,7 +26,7 @@ type EventListenerCallback = { } export enum AccountEvents { LOGIN = 'LOGIN', - LOGOUT = 'LOGOUT' + LOGOUT = 'LOGOUT', } export class AccountController { @@ -39,11 +45,16 @@ export class AccountController { * @param delayMs Delay between retries in milliseconds (default 500) * @returns true if encryption became available, false if timed out */ - private async waitForSafeStorage(maxRetries: number = 20, delayMs: number = 500): Promise { + private async waitForSafeStorage( + maxRetries: number = 20, + delayMs: number = 500, + ): Promise { let retries = 0 while (!safeStorage.isEncryptionAvailable() && retries < maxRetries) { - Log.info(`waiting for safeStorage to become available (attempt ${retries + 1}/${maxRetries})`) - await new Promise(resolve => setTimeout(resolve, delayMs)) + Log.info( + `waiting for safeStorage to become available (attempt ${retries + 1}/${maxRetries})`, + ) + await new Promise((resolve) => setTimeout(resolve, delayMs)) retries++ } return safeStorage.isEncryptionAvailable() @@ -63,25 +74,30 @@ export class AccountController { const { NethVoiceAPI } = useNethVoiceAPI(account) await NethVoiceAPI.Authentication.phoneIslandTokenLogout() await NethVoiceAPI.Authentication.logout() - Log.info('AccountController.logout() - logout API call completed successfully') + Log.info( + 'AccountController.logout() - logout API call completed successfully', + ) } catch (e) { Log.warning('Error calling logout API:', e) } } - store.updateStore({ - auth: { - ...store.store.auth!, - lastUser: undefined, - lastUserCryptPsw: undefined + store.updateStore( + { + auth: { + ...store.store.auth!, + lastUser: undefined, + lastUserCryptPsw: undefined, + }, + account: undefined, + theme: store.store.theme, + connection: store.store.connection || false, + accountStatus: store.store.accountStatus || 'offline', + isCallsEnabled: store.store.isCallsEnabled || false, + lastDevice: store.store.lastDevice || undefined, }, - account: undefined, - theme: store.store.theme, - connection: store.store.connection || false, - accountStatus: store.store.accountStatus || 'offline', - isCallsEnabled: store.store.isCallsEnabled || false, - lastDevice: store.store.lastDevice || undefined - }, 'logout') + 'logout', + ) store.saveToDisk() } @@ -89,16 +105,19 @@ export class AccountController { // const authAppData = store.store.auth if (authAppData?.lastUser) { - const lastLoggedAccount = authAppData.availableAccounts[authAppData.lastUser] + const lastLoggedAccount = + authAppData.availableAccounts[authAppData.lastUser] if (lastLoggedAccount && authAppData.lastUserCryptPsw) { try { // Wait for encryption to become available (max 10 seconds with 500ms intervals) - if (!await this.waitForSafeStorage()) { - Log.warning('auto login failed: safeStorage encryption not available after waiting') + if (!(await this.waitForSafeStorage())) { + Log.warning( + 'auto login failed: safeStorage encryption not available after waiting', + ) return false } try { - const bfs = Object.values(authAppData.lastUserCryptPsw) as any; + const bfs = Object.values(authAppData.lastUserCryptPsw) as any if (bfs[0] === 'Buffer') { authAppData.lastUserCryptPsw = bfs[1] } else { @@ -107,7 +126,9 @@ export class AccountController { } catch (e) { Log.warning('auto login failed decrypt user:', e) } - const psw: Buffer = Buffer.from((authAppData.lastUserCryptPsw as Uint8Array)) + const psw: Buffer = Buffer.from( + authAppData.lastUserCryptPsw as Uint8Array, + ) const decryptString = safeStorage.decryptString(psw) const _accountData = JSON.parse(decryptString) const password = _accountData.password @@ -117,7 +138,9 @@ export class AccountController { if (!isJWTExpired(lastLoggedAccount.jwtToken)) { // Token is still valid locally, check if it requires 2FA if (requires2FA(lastLoggedAccount.jwtToken)) { - Log.info('auto login failed: 2FA required, user interaction needed') + Log.info( + 'auto login failed: 2FA required, user interaction needed', + ) return false } @@ -132,32 +155,41 @@ export class AccountController { await NethVoiceAPI.User.me() // If we get here, the token is valid on the server - Log.info('auto login: token validated with server, using saved token') + Log.info( + 'auto login: token validated with server, using saved token', + ) } catch (error: any) { // Token was rejected by server (401/403) or network error - Log.info('auto login failed: saved token rejected by server', error?.response?.status || error?.message) + Log.info( + 'auto login failed: saved token rejected by server', + error?.response?.status || error?.message, + ) return false } // The saved SIP settings may be stale (e.g. NethVoice updated while NethLink was closed): // refresh them from the server before the PhoneIsland is built with them - const sipChanged = await this.refreshServerConfig(lastLoggedAccount) + const sipChanged = + await this.refreshServerConfig(lastLoggedAccount) // Update store with the saved account (don't do a new login!) // IMPORTANT: Preserve auth.lastUser and auth.lastUserCryptPsw so they are saved to disk // IMPORTANT: Set connection: true to prevent "No internet connection" banner - store.updateStore({ - account: lastLoggedAccount, - theme: lastLoggedAccount.theme, - connection: true, - accountStatus: store.store.accountStatus || 'offline', - isCallsEnabled: store.store.isCallsEnabled || false, - auth: { - ...authAppData, - lastUser: authAppData.lastUser, - lastUserCryptPsw: authAppData.lastUserCryptPsw - } - }, 'autoLogin') + store.updateStore( + { + account: lastLoggedAccount, + theme: lastLoggedAccount.theme, + connection: true, + accountStatus: store.store.accountStatus || 'offline', + isCallsEnabled: store.store.isCallsEnabled || false, + auth: { + ...authAppData, + lastUser: authAppData.lastUser, + lastUserCryptPsw: authAppData.lastUserCryptPsw, + }, + }, + 'autoLogin', + ) if (sipChanged) { store.saveToDisk() } @@ -169,10 +201,18 @@ export class AccountController { } // Token is expired or doesn't exist, do a new login - const tempLoggedAccount = await this.NethVoiceAPI.Authentication.login(lastLoggedAccount.host, lastLoggedAccount.username, password) + const tempLoggedAccount = + await this.NethVoiceAPI.Authentication.login( + lastLoggedAccount.host, + lastLoggedAccount.username, + password, + ) // Check if 2FA is required - auto-login should fail in this case - if (tempLoggedAccount.jwtToken && requires2FA(tempLoggedAccount.jwtToken)) { + if ( + tempLoggedAccount.jwtToken && + requires2FA(tempLoggedAccount.jwtToken) + ) { Log.info('auto login failed: 2FA required, user interaction needed') return false } @@ -209,16 +249,26 @@ export class AccountController { async refreshServerConfig(account: Account): Promise { try { const { parseConfig } = useLogin() - const config: string = await NetworkController.instance.get(`https://${account.host}/config/config.production.js`) + const config: string = await NetworkController.instance.get( + `https://${account.host}/config/config.production.js`, + ) const previous = { sipHost: account.sipHost, sipPort: account.sipPort } parseConfig(account, config) - const changed = previous.sipHost !== account.sipHost || previous.sipPort !== account.sipPort + const changed = + previous.sipHost !== account.sipHost || + previous.sipPort !== account.sipPort if (changed) { - Log.info('server SIP config changed', { previous, current: { sipHost: account.sipHost, sipPort: account.sipPort } }) + Log.info('server SIP config changed', { + previous, + current: { sipHost: account.sipHost, sipPort: account.sipPort }, + }) } return changed } catch (e) { - Log.warning('unable to refresh server config, keeping cached SIP settings:', e) + Log.warning( + 'unable to refresh server config, keeping cached SIP settings:', + e, + ) return false } } @@ -243,14 +293,23 @@ export class AccountController { return changed } - async saveLoggedAccount(account: Account, password: string): Promise { + async saveLoggedAccount( + account: Account, + password: string, + ): Promise { try { // Wait for encryption to become available (max 10 seconds with 500ms intervals) - if (!await this.waitForSafeStorage()) { - Log.error('saveLoggedAccount: safeStorage encryption not available after waiting') + if (!(await this.waitForSafeStorage())) { + Log.error( + 'saveLoggedAccount: safeStorage encryption not available after waiting', + ) throw new Error('Encryption not available') } - const clearString = JSON.stringify({ host: account.host, username: account.username, password: password }) + const clearString = JSON.stringify({ + host: account.host, + username: account.username, + password: password, + }) const cryptString = safeStorage.encryptString(clearString) const accountUID = getAccountUID(account) const authAppData = store.store.auth @@ -259,31 +318,36 @@ export class AccountController { if (accountPreviousData) { account = { ...accountPreviousData, - ...account + ...account, } } } - store.updateStore({ - account, - theme: account.theme, - auth: { - availableAccounts: { - ...store.store.auth?.availableAccounts, - [accountUID]: account + store.updateStore( + { + account, + theme: account.theme, + auth: { + availableAccounts: { + ...store.store.auth?.availableAccounts, + [accountUID]: account, + }, + isFirstStart: false, + lastUser: accountUID, + lastUserCryptPsw: cryptString, }, - isFirstStart: false, - lastUser: accountUID, - lastUserCryptPsw: cryptString + device: account.data?.default_device + ? { + type: account.data.default_device.type as AvailableDevices, + id: account.data.default_device.id, + } + : undefined, + connection: store.store.connection || false, + accountStatus: store.store.accountStatus || 'offline', + isCallsEnabled: store.store.isCallsEnabled || false, + lastDevice: store.store.lastDevice || undefined, }, - device: account.data?.default_device ? { - type: account.data.default_device.type as AvailableDevices, - id: account.data.default_device.id, - } : undefined, - connection: store.store.connection || false, - accountStatus: store.store.accountStatus || 'offline', - isCallsEnabled: store.store.isCallsEnabled || false, - lastDevice: store.store.lastDevice || undefined - }, 'saveLoggedAccount') + 'saveLoggedAccount', + ) store.saveToDisk() return account } catch (e) { @@ -293,8 +357,6 @@ export class AccountController { } } - - async updateTheme(theme: any) { if (store.store) { const account = store.store.account @@ -358,12 +420,13 @@ export class AccountController { } } - getAccountPhoneIslandPosition(): PhoneIslandPosition | undefined { return store.store.account?.phoneIslandPosition } - setAccountPhoneIslandPosition(phoneIslandPosition: PhoneIslandPosition): void { + setAccountPhoneIslandPosition( + phoneIslandPosition: PhoneIslandPosition, + ): void { const account = store.store.account const auth = store.store.auth if (account) { @@ -373,8 +436,8 @@ export class AccountController { ...auth, availableAccounts: { ...auth?.availableAccounts, - [getAccountUID(account)]: account - } + [getAccountUID(account)]: account, + }, } store.set('auth', _auth, true) store.saveToDisk() @@ -385,7 +448,9 @@ export class AccountController { return store.store.account?.nethlinkBounds } - setAccountNethLinkBounds(nethlinkBounds: Electron.Rectangle | undefined): void { + setAccountNethLinkBounds( + nethlinkBounds: Electron.Rectangle | undefined, + ): void { const account = store.store.account Log.debug('MAIN PRESENCE BACK', account?.data?.mainPresence) const auth = store.store.auth @@ -396,8 +461,8 @@ export class AccountController { ...auth, availableAccounts: { ...auth?.availableAccounts, - [getAccountUID(account)]: account - } + [getAccountUID(account)]: account, + }, } store.set('auth', _auth, true) store.saveToDisk() diff --git a/src/main/classes/controllers/AppController.ts b/src/main/classes/controllers/AppController.ts index c51ba7d5..83756f98 100644 --- a/src/main/classes/controllers/AppController.ts +++ b/src/main/classes/controllers/AppController.ts @@ -1,11 +1,11 @@ -import { LoginController } from "./LoginController" -import { NethLinkController } from "./NethLinkController" -import { PhoneIslandController } from "./PhoneIslandController" -import { TrayController } from "./TrayController" -import { Log } from "@shared/utils/logger" -import { DevToolsController } from "./DevToolsController" -import { store } from "@/lib/mainStore" -import { delay } from "@shared/utils/utils" +import { LoginController } from './LoginController' +import { NethLinkController } from './NethLinkController' +import { PhoneIslandController } from './PhoneIslandController' +import { TrayController } from './TrayController' +import { Log } from '@shared/utils/logger' +import { DevToolsController } from './DevToolsController' +import { store } from '@/lib/mainStore' +import { delay } from '@shared/utils/utils' export class AppController { static _app: Electron.App @@ -14,7 +14,6 @@ export class AppController { AppController._app = app } - static async safeQuit() { if (!AppController.onQuit) { AppController.onQuit = true diff --git a/src/main/classes/controllers/CommandBarController.ts b/src/main/classes/controllers/CommandBarController.ts index b66f18d8..87c0fb84 100644 --- a/src/main/classes/controllers/CommandBarController.ts +++ b/src/main/classes/controllers/CommandBarController.ts @@ -52,7 +52,7 @@ export class CommandBarController { x: centerX, y: centerY, width: this.originalSize.width, - height: this.originalSize.height + height: this.originalSize.height, }) const isWindows = process.platform === 'win32' @@ -112,7 +112,7 @@ export class CommandBarController { window.hide() this.window.emit(IPC_EVENTS.HIDE_COMMAND_BAR) }, - 100 + 100, ) } } @@ -121,7 +121,7 @@ export class CommandBarController { } } - resize(size: { width: number, height: number }) { + resize(size: { width: number; height: number }) { try { const window = this.window.getWindow() if (window && this.isVisible) { @@ -131,7 +131,7 @@ export class CommandBarController { x: bounds.x, y: bounds.y, width: size.width, - height: size.height + height: size.height, }) } } catch (e) { diff --git a/src/main/classes/controllers/DevToolsController.ts b/src/main/classes/controllers/DevToolsController.ts index a9b080b4..8e0f60dd 100644 --- a/src/main/classes/controllers/DevToolsController.ts +++ b/src/main/classes/controllers/DevToolsController.ts @@ -1,7 +1,6 @@ import { DevToolsWindow } from '../windows' export class DevToolsController { - static instance: DevToolsController window: DevToolsWindow constructor() { diff --git a/src/main/classes/controllers/LoginController.ts b/src/main/classes/controllers/LoginController.ts index ab8acaaf..d8036936 100644 --- a/src/main/classes/controllers/LoginController.ts +++ b/src/main/classes/controllers/LoginController.ts @@ -2,7 +2,6 @@ import { LOGIN_WINDOW_WIDTH, LoginWindow } from '../windows' import { Log } from '@shared/utils/logger' export class LoginController { - static instance: LoginController window: LoginWindow @@ -18,27 +17,36 @@ export class LoginController { if (loginPage) { const bounds = loginPage.getBounds() const height = h + 32 - loginPage.setBounds({ - ...bounds, - width: LOGIN_WINDOW_WIDTH, - height - }, true) + loginPage.setBounds( + { + ...bounds, + width: LOGIN_WINDOW_WIDTH, + height, + }, + true, + ) if (bounds.height === 0) { loginPage.center() } } - } catch (e) { Log.warning('error during resize LoginWindow: ', e) } + } catch (e) { + Log.warning('error during resize LoginWindow: ', e) + } } show() { try { this.window.show() - } catch (e) { Log.warning('error during showing LoginWindow: ', e) } + } catch (e) { + Log.warning('error during showing LoginWindow: ', e) + } } hide() { try { this.window!.hide() - } catch (e) { Log.warning('error during hiding LoginWindow: ', e) } + } catch (e) { + Log.warning('error during hiding LoginWindow: ', e) + } } async quit() { @@ -52,5 +60,4 @@ export class LoginController { async safeQuit() { await this.quit() } - } diff --git a/src/main/classes/controllers/NethLinkController.ts b/src/main/classes/controllers/NethLinkController.ts index c8f95621..03c57809 100644 --- a/src/main/classes/controllers/NethLinkController.ts +++ b/src/main/classes/controllers/NethLinkController.ts @@ -14,25 +14,36 @@ export class NethLinkController { init() { try { this.show() - } catch (e) { Log.warning('error during initializing NethLinkWindow: ', e) } + } catch (e) { + Log.warning('error during initializing NethLinkWindow: ', e) + } } show() { try { this.window.show() - } catch (e) { Log.warning('error during showing NethLinkWindow: ', e) } + } catch (e) { + Log.warning('error during showing NethLinkWindow: ', e) + } } hide() { try { this.window.hide() - } catch (e) { Log.warning('error during hiding NethLinkWindow: ', e) } + } catch (e) { + Log.warning('error during hiding NethLinkWindow: ', e) + } } sendUpdateNotification() { try { this.window.emit(IPC_EVENTS.UPDATE_APP_NOTIFICATION) - } catch (e) { Log.warning('error during send update notification to the NethLinkWindow: ', e) } + } catch (e) { + Log.warning( + 'error during send update notification to the NethLinkWindow: ', + e, + ) + } } async logout() { diff --git a/src/main/classes/controllers/NetworkController.ts b/src/main/classes/controllers/NetworkController.ts index 690994bf..81487ae7 100644 --- a/src/main/classes/controllers/NetworkController.ts +++ b/src/main/classes/controllers/NetworkController.ts @@ -1,4 +1,4 @@ -import { Log } from '@shared/utils/logger'; +import { Log } from '@shared/utils/logger' import axios, { AxiosError } from 'axios' export class NetworkController { static instance: NetworkController @@ -6,30 +6,66 @@ export class NetworkController { NetworkController.instance = this } - async post(path: string, data: object | undefined, config: { headers: { Authorization?: string | undefined; 'Content-Type': string; }; } | undefined = { headers: { 'Content-Type': 'application/json' } }): Promise { + async post( + path: string, + data: object | undefined, + config: + | { + headers: { + Authorization?: string | undefined + 'Content-Type': string + } + } + | undefined = { headers: { 'Content-Type': 'application/json' } }, + ): Promise { try { const response = await axios.post(path, data, { timeout: 5000, - ...config + ...config, }) return response.data } catch (e: any) { const err: AxiosError = e - Log.error('during fetch POST', err.name, err.code, err.message, path, config, data) + Log.error( + 'during fetch POST', + err.name, + err.code, + err.message, + path, + config, + data, + ) throw e } } - async get(path: string, config: { headers: { Authorization?: string | undefined; 'Content-Type': string } } | undefined = { headers: { 'Content-Type': 'application/json' } }): Promise { + async get( + path: string, + config: + | { + headers: { + Authorization?: string | undefined + 'Content-Type': string + } + } + | undefined = { headers: { 'Content-Type': 'application/json' } }, + ): Promise { try { const response = await axios.get(path, { timeout: 5000, - ...config + ...config, }) return response.data } catch (e: any) { const err: AxiosError = e - Log.error('during fetch GET', err.name, err.code, err.message, path, config) + Log.error( + 'during fetch GET', + err.name, + err.code, + err.message, + path, + config, + ) throw e } } @@ -37,7 +73,7 @@ export class NetworkController { async head(path: string, timeoutMs: number = 5000): Promise { try { await axios.head(path, { - timeout: timeoutMs + timeout: timeoutMs, }) return true } catch (e: any) { @@ -46,5 +82,4 @@ export class NetworkController { return false } } - } diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 659268c9..fd7e819d 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -13,7 +13,9 @@ export class PhoneIslandController { private isWarmingUp: boolean = false private lastVisibleBounds: Electron.Rectangle | undefined - private isValidVisibleBounds(bounds: Electron.Rectangle | undefined): bounds is Electron.Rectangle { + private isValidVisibleBounds( + bounds: Electron.Rectangle | undefined, + ): bounds is Electron.Rectangle { return !!bounds && bounds.width > 1 && bounds.height > 1 } @@ -23,7 +25,9 @@ export class PhoneIslandController { } } - private getPersistableBounds(bounds: Electron.Rectangle | undefined): Electron.Rectangle | undefined { + private getPersistableBounds( + bounds: Electron.Rectangle | undefined, + ): Electron.Rectangle | undefined { if (this.isValidVisibleBounds(bounds)) { return bounds } @@ -31,17 +35,28 @@ export class PhoneIslandController { return this.lastVisibleBounds ? { ...this.lastVisibleBounds } : undefined } - private getBoundsForSize(bounds: Electron.Rectangle, size: Size): Electron.Rectangle { + private getBoundsForSize( + bounds: Electron.Rectangle, + size: Size, + ): Electron.Rectangle { return { x: bounds.x, y: bounds.y, width: size.w, - height: size.h + height: size.h, } } - private areBoundsEqual(left: Electron.Rectangle, right: Electron.Rectangle): boolean { - return left.x === right.x && left.y === right.y && left.width === right.width && left.height === right.height + private areBoundsEqual( + left: Electron.Rectangle, + right: Electron.Rectangle, + ): boolean { + return ( + left.x === right.x && + left.y === right.y && + left.width === right.width && + left.height === right.height + ) } private buildSavedPosition(bounds: Electron.Rectangle): PhoneIslandPosition { @@ -58,23 +73,28 @@ export class PhoneIslandController { x: display.bounds.x, y: display.bounds.y, width: display.bounds.width, - height: display.bounds.height + height: display.bounds.height, }, workArea: { x: display.workArea.x, y: display.workArea.y, width: display.workArea.width, - height: display.workArea.height - } + height: display.workArea.height, + }, } } private savePhoneIslandPosition(bounds: Electron.Rectangle) { this.rememberVisibleBounds(bounds) - AccountController.instance.setAccountPhoneIslandPosition(this.buildSavedPosition(bounds)) + AccountController.instance.setAccountPhoneIslandPosition( + this.buildSavedPosition(bounds), + ) } - private isRectInsideWorkArea(bounds: Electron.Rectangle, workArea: Electron.Rectangle): boolean { + private isRectInsideWorkArea( + bounds: Electron.Rectangle, + workArea: Electron.Rectangle, + ): boolean { return ( bounds.x >= workArea.x && bounds.y >= workArea.y && @@ -83,7 +103,10 @@ export class PhoneIslandController { ) } - private clampBoundsToDisplay(bounds: Electron.Rectangle, display: Electron.Display): Electron.Rectangle { + private clampBoundsToDisplay( + bounds: Electron.Rectangle, + display: Electron.Display, + ): Electron.Rectangle { const { workArea } = display const width = Math.min(bounds.width, workArea.width) const height = Math.min(bounds.height, workArea.height) @@ -94,7 +117,7 @@ export class PhoneIslandController { x: Math.min(Math.max(bounds.x, workArea.x), maxX), y: Math.min(Math.max(bounds.y, workArea.y), maxY), width, - height + height, } } @@ -104,15 +127,25 @@ export class PhoneIslandController { const height = Math.min(size.h, primaryDisplay.workArea.height) return { - x: primaryDisplay.workArea.x + Math.round((primaryDisplay.workArea.width - width) / 2), - y: primaryDisplay.workArea.y + Math.round((primaryDisplay.workArea.height - height) / 2), + x: + primaryDisplay.workArea.x + + Math.round((primaryDisplay.workArea.width - width) / 2), + y: + primaryDisplay.workArea.y + + Math.round((primaryDisplay.workArea.height - height) / 2), width, - height + height, } } - private hasDisplayContextChanged(savedPosition: PhoneIslandPosition, display: Electron.Display): boolean { - if (savedPosition.displayScaleFactor !== undefined && Math.abs(savedPosition.displayScaleFactor - display.scaleFactor) > 0.01) { + private hasDisplayContextChanged( + savedPosition: PhoneIslandPosition, + display: Electron.Display, + ): boolean { + if ( + savedPosition.displayScaleFactor !== undefined && + Math.abs(savedPosition.displayScaleFactor - display.scaleFactor) > 0.01 + ) { return true } @@ -130,11 +163,14 @@ export class PhoneIslandController { } private resolveWindowsBounds(size: Size): Electron.Rectangle { - const savedPosition = AccountController.instance.getAccountPhoneIslandPosition() + const savedPosition = + AccountController.instance.getAccountPhoneIslandPosition() if (!savedPosition) { const primaryBounds = this.getPrimaryDisplayBounds(size) - Log.info(`PhoneIsland no saved Windows position, fallback to primary display (${primaryBounds.x}, ${primaryBounds.y})`) + Log.info( + `PhoneIsland no saved Windows position, fallback to primary display (${primaryBounds.x}, ${primaryBounds.y})`, + ) return primaryBounds } @@ -143,56 +179,71 @@ export class PhoneIslandController { x: savedPosition.x, y: savedPosition.y, width: size.w, - height: size.h + height: size.h, } - const displayFromId = savedPosition.displayId !== undefined - ? displays.find((display) => display.id === savedPosition.displayId) - : undefined + const displayFromId = + savedPosition.displayId !== undefined + ? displays.find((display) => display.id === savedPosition.displayId) + : undefined if (displayFromId) { if (this.hasDisplayContextChanged(savedPosition, displayFromId)) { const primaryBounds = this.getPrimaryDisplayBounds(size) - Log.info('PhoneIsland saved Windows display context changed, fallback to primary display', { - savedPosition, - displayId: displayFromId.id, - currentScaleFactor: displayFromId.scaleFactor, - currentWorkArea: displayFromId.workArea, - fallbackBounds: primaryBounds - }) + Log.info( + 'PhoneIsland saved Windows display context changed, fallback to primary display', + { + savedPosition, + displayId: displayFromId.id, + currentScaleFactor: displayFromId.scaleFactor, + currentWorkArea: displayFromId.workArea, + fallbackBounds: primaryBounds, + }, + ) return primaryBounds } - const clampedBounds = this.clampBoundsToDisplay(savedBounds, displayFromId) + const clampedBounds = this.clampBoundsToDisplay( + savedBounds, + displayFromId, + ) Log.info('PhoneIsland restored on saved Windows display', { savedPosition, displayId: displayFromId.id, - restoredBounds: clampedBounds + restoredBounds: clampedBounds, }) return clampedBounds } - const displayMatchingBounds = displays.find((display) => this.isRectInsideWorkArea(savedBounds, display.workArea)) + const displayMatchingBounds = displays.find((display) => + this.isRectInsideWorkArea(savedBounds, display.workArea), + ) if (displayMatchingBounds) { - const clampedBounds = this.clampBoundsToDisplay(savedBounds, displayMatchingBounds) + const clampedBounds = this.clampBoundsToDisplay( + savedBounds, + displayMatchingBounds, + ) Log.info('PhoneIsland restored from legacy Windows position', { savedPosition, displayId: displayMatchingBounds.id, - restoredBounds: clampedBounds + restoredBounds: clampedBounds, }) return clampedBounds } const primaryBounds = this.getPrimaryDisplayBounds(size) - Log.info('PhoneIsland saved Windows position is invalid, fallback to primary display', { - savedPosition, - displays: displays.map((display) => ({ - id: display.id, - scaleFactor: display.scaleFactor, - workArea: display.workArea - })), - fallbackBounds: primaryBounds - }) + Log.info( + 'PhoneIsland saved Windows position is invalid, fallback to primary display', + { + savedPosition, + displays: displays.map((display) => ({ + id: display.id, + scaleFactor: display.scaleFactor, + workArea: display.workArea, + })), + fallbackBounds: primaryBounds, + }, + ) return primaryBounds } @@ -217,7 +268,7 @@ export class PhoneIslandController { this.rememberVisibleBounds(window.getBounds()) } } - //make sure the size is equal to [0,0] when you want to close the phone island, otherwise the size will not close and will generate slowness problems. + // make sure the size is equal to [0,0] when you want to close the phone island, otherwise the size will not close and will generate slowness problems. if (h === 0 && w === 0) { const persistableBounds = this.getPersistableBounds(bounds) if (persistableBounds) { @@ -225,7 +276,9 @@ export class PhoneIslandController { } window.hide() const hiddenFrom = persistableBounds || bounds - Log.info(`PhoneIsland resize to 0x0 -> hidden from (${hiddenFrom.x}, ${hiddenFrom.y})`) + Log.info( + `PhoneIsland resize to 0x0 -> hidden from (${hiddenFrom.x}, ${hiddenFrom.y})`, + ) } else { // Don't show window during warm-up if (!window.isVisible() && !this.isWarmingUp) { @@ -238,21 +291,24 @@ export class PhoneIslandController { window.setAlwaysOnTop(true, 'screen-saver') const finalBounds = window.getBounds() this.rememberVisibleBounds(finalBounds) - Log.info(`PhoneIsland shown via resize at position (${finalBounds.x}, ${finalBounds.y}) size ${finalBounds.width}x${finalBounds.height}`) + Log.info( + `PhoneIsland shown via resize at position (${finalBounds.x}, ${finalBounds.y}) size ${finalBounds.width}x${finalBounds.height}`, + ) } } } } catch (e) { Log.warning('error during resizing PhoneIslandWindow:', e) } - } showPhoneIsland(size: Size) { try { const window = this.window.getWindow() if (window) { - Log.info(`PhoneIsland showPhoneIsland called with size ${size.w}x${size.h}`) + Log.info( + `PhoneIsland showPhoneIsland called with size ${size.w}x${size.h}`, + ) this.resize(size) if (process.platform === 'win32') { const restoredBounds = this.resolveWindowsBounds(size) @@ -263,29 +319,41 @@ export class PhoneIslandController { window.setAlwaysOnTop(true, 'screen-saver') } } else if (process.platform !== 'linux') { - const phoneIslandPosition = AccountController.instance.getAccountPhoneIslandPosition() - Log.info(`PhoneIsland saved position: ${phoneIslandPosition ? `(${phoneIslandPosition.x}, ${phoneIslandPosition.y})` : 'none'}`) + const phoneIslandPosition = + AccountController.instance.getAccountPhoneIslandPosition() + Log.info( + `PhoneIsland saved position: ${phoneIslandPosition ? `(${phoneIslandPosition.x}, ${phoneIslandPosition.y})` : 'none'}`, + ) if (phoneIslandPosition) { const displays = screen.getAllDisplays() - const isPhoneIslandOnDisplay = displays.reduce((result, display) => { - const area = display.workArea - return ( - result || - (phoneIslandPosition.x >= area.x && - phoneIslandPosition.y >= area.y && - (phoneIslandPosition.x + size.w) < (area.x + area.width) && - (phoneIslandPosition.y + size.h) < (area.y + area.height)) - ) - }, false) + const isPhoneIslandOnDisplay = displays.reduce( + (result, display) => { + const area = display.workArea + return ( + result || + (phoneIslandPosition.x >= area.x && + phoneIslandPosition.y >= area.y && + phoneIslandPosition.x + size.w < area.x + area.width && + phoneIslandPosition.y + size.h < area.y + area.height) + ) + }, + false, + ) if (isPhoneIslandOnDisplay) { - window?.setBounds({ x: phoneIslandPosition.x, y: phoneIslandPosition.y }, false) - Log.info(`PhoneIsland positioned at saved location (${phoneIslandPosition.x}, ${phoneIslandPosition.y})`) + window?.setBounds( + { x: phoneIslandPosition.x, y: phoneIslandPosition.y }, + false, + ) + Log.info( + `PhoneIsland positioned at saved location (${phoneIslandPosition.x}, ${phoneIslandPosition.y})`, + ) } else { window?.center() - Log.info(`PhoneIsland saved position is off-screen, centered instead. Displays: ${JSON.stringify(displays.map(d => d.workArea))}`) + Log.info( + `PhoneIsland saved position is off-screen, centered instead. Displays: ${JSON.stringify(displays.map((d) => d.workArea))}`, + ) } - } - else { + } else { window?.center() Log.info('PhoneIsland no saved position, centered') } @@ -293,7 +361,9 @@ export class PhoneIslandController { window?.center() } const finalBounds = window.getBounds() - Log.info(`PhoneIsland final bounds after showPhoneIsland: (${finalBounds.x}, ${finalBounds.y}) ${finalBounds.width}x${finalBounds.height}`) + Log.info( + `PhoneIsland final bounds after showPhoneIsland: (${finalBounds.x}, ${finalBounds.y}) ${finalBounds.width}x${finalBounds.height}`, + ) } } catch (e) { Log.warning('error during showing PhoneIslandWindow:', e) @@ -306,7 +376,9 @@ export class PhoneIslandController { const phoneIslandBounds = window?.getBounds() const persistableBounds = this.getPersistableBounds(phoneIslandBounds) if (persistableBounds) { - Log.info(`PhoneIsland hiding, saving position (${persistableBounds.x}, ${persistableBounds.y}) size ${persistableBounds.width}x${persistableBounds.height}`) + Log.info( + `PhoneIsland hiding, saving position (${persistableBounds.x}, ${persistableBounds.y}) size ${persistableBounds.width}x${persistableBounds.height}`, + ) this.savePhoneIslandPosition(persistableBounds) } debouncer('hide', () => window?.hide(), 250) @@ -339,29 +411,31 @@ export class PhoneIslandController { } callTransfer(to: string) { - Log.info("Tranfer to", to) + Log.info('Tranfer to', to) this.window.emit(IPC_EVENTS.TRANSFER_CALL, to) } intrudeCall(to: string) { - Log.info("Intrude to", to) + Log.info('Intrude to', to) this.window.emit(IPC_EVENTS.INTRUDE_CALL, to) } listenCall(to: string) { - Log.info("Listen to", to) + Log.info('Listen to', to) this.window.emit(IPC_EVENTS.LISTEN_CALL, to) } updateDefaultDevice(ext: Extension, force: boolean) { try { - - //const { NethVoiceAPI } = useNethVoiceAPI(store.store.account) - //NethVoiceAPI.User.me().then((me) => { + // const { NethVoiceAPI } = useNethVoiceAPI(store.store.account) + // NethVoiceAPI.User.me().then((me) => { this.window.emit(IPC_EVENTS.CHANGE_DEFAULT_DEVICE, ext, force) - //}) + // }) } catch (e) { - Log.warning('error during emitting updateDefaultDevice event to the PhoneIslandWindow:', e) + Log.warning( + 'error during emitting updateDefaultDevice event to the PhoneIslandWindow:', + e, + ) } } @@ -415,7 +489,10 @@ export class PhoneIslandController { const bounds = window.getBounds() if (bounds.width > 0 && bounds.height > 0) { if (process.platform === 'win32') { - const restoredBounds = this.resolveWindowsBounds({ w: bounds.width, h: bounds.height }) + const restoredBounds = this.resolveWindowsBounds({ + w: bounds.width, + h: bounds.height, + }) window.setBounds(restoredBounds, false) this.rememberVisibleBounds(restoredBounds) } diff --git a/src/main/classes/controllers/TrayController.ts b/src/main/classes/controllers/TrayController.ts index 27595780..843189fa 100644 --- a/src/main/classes/controllers/TrayController.ts +++ b/src/main/classes/controllers/TrayController.ts @@ -1,4 +1,11 @@ -import { Menu, MenuItem, MenuItemConstructorOptions, Tray, nativeImage, nativeTheme } from 'electron' +import { + Menu, + MenuItem, + MenuItemConstructorOptions, + Tray, + nativeImage, + nativeTheme, +} from 'electron' import path from 'path' import { LoginController } from './LoginController' import { NethLinkController } from './NethLinkController' @@ -10,7 +17,7 @@ import { Log } from '@shared/utils/logger' import { t } from 'i18next' export type TrayUpdaterProps = { - enableShowButton?: boolean, + enableShowButton?: boolean isShowButtonVisible?: boolean } export class TrayController { @@ -18,9 +25,7 @@ export class TrayController { static instance: TrayController constructor() { TrayController.instance = this - const theme = nativeTheme.shouldUseDarkColors - ? 'dark' - : 'light' + const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light' const image = this.getImage(theme) this.tray = new Tray(image) this.updateTray() @@ -36,16 +41,18 @@ export class TrayController { getImage(theme: 'light' | 'dark') { let pathImage = '../../public/TrayToolbarIconWhite.png' if (process.platform === 'win32') { - pathImage = theme === 'light' ? '../../public/TrayToolbarIconBlack.png' : '../../public/TrayToolbarIconWhite.png' + pathImage = + theme === 'light' + ? '../../public/TrayToolbarIconBlack.png' + : '../../public/TrayToolbarIconWhite.png' } - const image = nativeImage.createFromPath( - path.join(__dirname, pathImage) - ).resize({ height: 18, width: 18 }) - image.setTemplateImage(true); + const image = nativeImage + .createFromPath(path.join(__dirname, pathImage)) + .resize({ height: 18, width: 18 }) + image.setTemplateImage(true) return image } - changeIconByTheme(theme: 'light' | 'dark') { const image = this.getImage(theme) this.tray.setImage(image) @@ -55,35 +62,46 @@ export class TrayController { try { if (enableShowButton) { if (store.store.account) { - if (NethLinkController.instance && NethLinkController.instance.window?.isOpen()) + if ( + NethLinkController.instance && + NethLinkController.instance.window?.isOpen() + ) NethLinkController.instance.hide() - else - NethLinkController.instance.show() + else NethLinkController.instance.show() } else { - if (LoginController.instance && LoginController.instance.window?.isOpen()) + if ( + LoginController.instance && + LoginController.instance.window?.isOpen() + ) LoginController.instance.hide() - else - LoginController.instance.show() + else LoginController.instance.show() } - } } catch (e) { - Log.warning('error during toggling a NethLink window from the TrayIcon Button:', e) + Log.warning( + 'error during toggling a NethLink window from the TrayIcon Button:', + e, + ) } } - updateTray({ - enableShowButton, - isShowButtonVisible - }: TrayUpdaterProps = { - isShowButtonVisible: true - }) { + updateTray( + { enableShowButton, isShowButtonVisible }: TrayUpdaterProps = { + isShowButtonVisible: true, + }, + ) { try { - const _isShowButtonVisible = isShowButtonVisible === undefined ? true : isShowButtonVisible - //TODO: add check if window is focused and add the focus option + const _isShowButtonVisible = + isShowButtonVisible === undefined ? true : isShowButtonVisible + // TODO: add check if window is focused and add the focus option const label = store.store.account - ? ((NethLinkController.instance && NethLinkController.instance.window?.isOpen()) ? `${t('Tray.Hide')} NethLink` : `${t('Tray.Show')} NethLink`) - : ((LoginController.instance && LoginController.instance.window?.isOpen()) ? `${t('Tray.Hide')} Login` : `${t('Tray.Show')} Login`) + ? NethLinkController.instance && + NethLinkController.instance.window?.isOpen() + ? `${t('Tray.Hide')} NethLink` + : `${t('Tray.Show')} NethLink` + : LoginController.instance && LoginController.instance.window?.isOpen() + ? `${t('Tray.Hide')} Login` + : `${t('Tray.Show')} Login` const menu: (MenuItemConstructorOptions | MenuItem)[] = [ { role: 'window', @@ -93,7 +111,7 @@ export class TrayController { visible: _isShowButtonVisible, click: (_menuItem, _window, _event) => { this.toggleWindow(enableShowButton ?? false) - } + }, }, { role: process.platform === 'win32' ? 'close' : 'window', @@ -102,8 +120,8 @@ export class TrayController { enabled: enableShowButton ?? false, click: (_menuItem, _window, _event) => { AppController.safeQuit() - } - } + }, + }, ] if (isDev()) { @@ -117,14 +135,15 @@ export class TrayController { new DevToolsController() } DevToolsController.instance.toggle() - } + }, }) } this.tray.setContextMenu(Menu.buildFromTemplate(menu)) } catch (e) { - Log.warning('error during updating the Tray Icon menu context contents:', e) + Log.warning( + 'error during updating the Tray Icon menu context contents:', + e, + ) } - } - } diff --git a/src/main/classes/windows/BaseWindow.ts b/src/main/classes/windows/BaseWindow.ts index b1d9c522..1f1300e6 100644 --- a/src/main/classes/windows/BaseWindow.ts +++ b/src/main/classes/windows/BaseWindow.ts @@ -12,7 +12,11 @@ export class BaseWindow { private _params: Record private _config: WindowOptions - constructor(id: string, config?: WindowOptions, params?: Record) { + constructor( + id: string, + config?: WindowOptions, + params?: Record, + ) { this._id = id this._params = Object.assign({}, params) this._config = Object.assign({}, config) @@ -28,9 +32,9 @@ export class BaseWindow { target.webContents.isDevToolsOpened() ? target.webContents.closeDevTools() : target.webContents.openDevTools({ - title: target.title, - mode: 'detach' - }) + title: target.title, + mode: 'detach', + }) } } @@ -43,7 +47,7 @@ export class BaseWindow { this._window?.webContents.send(event, ...args) } catch (e) { Log.error('on window.emit', e, { event, args }) - throw (e) + throw e } } @@ -103,7 +107,7 @@ export class BaseWindow { const onReady = (_e) => { this._callbacks.forEach((c) => c()) - //once called I remove them + // once called I remove them this._callbacks = [] } @@ -112,9 +116,9 @@ export class BaseWindow { } window.once('ready-to-show', onReady) - isDev() && window.webContents.ipc.on(IPC_EVENTS.OPEN_DEV_TOOLS, onOpenDevTools) + isDev() && + window.webContents.ipc.on(IPC_EVENTS.OPEN_DEV_TOOLS, onOpenDevTools) !isDev() && window.removeMenu() this._window = window } } - diff --git a/src/main/classes/windows/CommandBarWindow.ts b/src/main/classes/windows/CommandBarWindow.ts index e7ab0453..0c4746e3 100644 --- a/src/main/classes/windows/CommandBarWindow.ts +++ b/src/main/classes/windows/CommandBarWindow.ts @@ -29,8 +29,8 @@ export class CommandBarWindow extends BaseWindow { thickFrame: false, trafficLightPosition: { x: 0, y: 0 }, webPreferences: { - nodeIntegration: true - } + nodeIntegration: true, + }, }) this.addOnBuildListener(() => { diff --git a/src/main/classes/windows/DevToolsWindow.ts b/src/main/classes/windows/DevToolsWindow.ts index 1f1979b8..4d4407ec 100644 --- a/src/main/classes/windows/DevToolsWindow.ts +++ b/src/main/classes/windows/DevToolsWindow.ts @@ -18,7 +18,6 @@ export class DevToolsWindow extends BaseWindow { maximizable: false, titleBarStyle: 'default', title: 'NethLink DevTools', - }) } } diff --git a/src/main/classes/windows/LoginWindow.ts b/src/main/classes/windows/LoginWindow.ts index 9a0e2b1b..8331dc6c 100644 --- a/src/main/classes/windows/LoginWindow.ts +++ b/src/main/classes/windows/LoginWindow.ts @@ -32,7 +32,6 @@ export class LoginWindow extends BaseWindow { thickFrame: true, icon: '../../public/LogoBlueSimpleDark.svg', titleBarOverlay: true, - }) } @@ -40,13 +39,12 @@ export class LoginWindow extends BaseWindow { try { super.show() const bounds = this._window?.getBounds() - if (bounds && (bounds?.height === LoginPageSize.h)) { - bounds.height = 500; + if (bounds && bounds?.height === LoginPageSize.h) { + bounds.height = 500 this._window?.setBounds(bounds) this._window?.center() } - } - catch (e: any) { + } catch (e: any) { if (e.message === 'Object has been destroyed') { this.buildWindow() return this.show() @@ -78,7 +76,7 @@ export class LoginWindow extends BaseWindow { async toggleVisibility() { debouncer('loginToggleVisibility', async () => { TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) }) } diff --git a/src/main/classes/windows/NethLinkWindow.ts b/src/main/classes/windows/NethLinkWindow.ts index e78fe33e..8310afb3 100644 --- a/src/main/classes/windows/NethLinkWindow.ts +++ b/src/main/classes/windows/NethLinkWindow.ts @@ -35,8 +35,7 @@ export class NethLinkWindow extends BaseWindow { fullscreen: false, thickFrame: true, icon: '../../public/LogoBlueSimpleDark.svg', - titleBarOverlay: true - + titleBarOverlay: true, }) this.size = NethLinkPageSize NethLinkWindow.instance = this @@ -76,18 +75,21 @@ export class NethLinkWindow extends BaseWindow { show(): void { try { - const accountBounds = AccountController.instance.getAccountNethLinkBounds() + const accountBounds = + AccountController.instance.getAccountNethLinkBounds() if (accountBounds) { - const isAccountBoundsOnDisplay = screen.getAllDisplays().reduce((result, display) => { - const area = display.workArea - return ( - result || - (accountBounds.x >= area.x && - accountBounds.y >= area.y && - (accountBounds.x + accountBounds.width) <= (area.x + area.width) && - (accountBounds.y + accountBounds.height) <= (area.y + area.height)) - ) - }, false) + const isAccountBoundsOnDisplay = screen + .getAllDisplays() + .reduce((result, display) => { + const area = display.workArea + return ( + result || + (accountBounds.x >= area.x && + accountBounds.y >= area.y && + accountBounds.x + accountBounds.width <= area.x + area.width && + accountBounds.y + accountBounds.height <= area.y + area.height) + ) + }, false) if (isAccountBoundsOnDisplay) { this._applyBounds(accountBounds) } else { @@ -100,8 +102,7 @@ export class NethLinkWindow extends BaseWindow { this._window?.setVisibleOnAllWorkspaces(true) this._window?.focus() this._window?.setVisibleOnAllWorkspaces(false) - } - catch (e: any) { + } catch (e: any) { if (e.message === 'Object has been destroyed') { this.buildWindow() return this.show() @@ -138,16 +139,28 @@ export class NethLinkWindow extends BaseWindow { if (Date.now() < this._programmaticBoundsUntil) { return } - debouncer('onMoveNethLinkWindow', () => { - if (!this._window || this._window.isDestroyed() || this._window.isMinimized() || !this._window.isVisible()) { - return - } - const bounds = this._window.getBounds() - if (bounds.width < NethLinkPageSize.w || bounds.height < NethLinkPageSize.h) { - return - } - this.saveBounds(bounds) - }, 1000) + debouncer( + 'onMoveNethLinkWindow', + () => { + if ( + !this._window || + this._window.isDestroyed() || + this._window.isMinimized() || + !this._window.isVisible() + ) { + return + } + const bounds = this._window.getBounds() + if ( + bounds.width < NethLinkPageSize.w || + bounds.height < NethLinkPageSize.h + ) { + return + } + this.saveBounds(bounds) + }, + 1000, + ) }) this._window?.on('show', this.toggleVisibility) this._window?.on('closed', this.toggleVisibility) @@ -161,7 +174,7 @@ export class NethLinkWindow extends BaseWindow { debouncer('nethlinkToggleVisibility', async () => { await delay(250) TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) }) } diff --git a/src/main/classes/windows/PhoneIslandWindow.ts b/src/main/classes/windows/PhoneIslandWindow.ts index 634ce53b..4be21769 100644 --- a/src/main/classes/windows/PhoneIslandWindow.ts +++ b/src/main/classes/windows/PhoneIslandWindow.ts @@ -3,7 +3,6 @@ import { BaseWindow } from './BaseWindow' import { ipcMain } from 'electron' export class PhoneIslandWindow extends BaseWindow { - public static currentSize: Partial = {} constructor() { super(PAGES.PHONEISLAND, { @@ -32,8 +31,8 @@ export class PhoneIslandWindow extends BaseWindow { trafficLightPosition: { x: 0, y: 0 }, webPreferences: { nodeIntegration: true, - backgroundThrottling: false - } + backgroundThrottling: false, + }, }) this.addOnBuildListener(() => { @@ -66,13 +65,23 @@ export class PhoneIslandWindow extends BaseWindow { // Forward all console messages from PhoneIsland renderer to main process log file // Uses 'phone-island-log' channel which always writes to file (not gated by isDev) - window.webContents.on('console-message', (_event, level, message, line, sourceId) => { - const levelMap = ['DEBUG', 'INFO', 'WARNING', 'ERROR'] - const levelStr = levelMap[level] || 'INFO' - const source = sourceId ? sourceId.split('/').pop() : 'unknown' - const timestamp = new Date().toISOString().replace('T', ' ').replace('Z', '') - ipcMain.emit('phone-island-log', {}, `${timestamp} [PhoneIsland] [${levelStr}] ${message} (${source}:${line})`) - }) + window.webContents.on( + 'console-message', + (_event, level, message, line, sourceId) => { + const levelMap = ['DEBUG', 'INFO', 'WARNING', 'ERROR'] + const levelStr = levelMap[level] || 'INFO' + const source = sourceId ? sourceId.split('/').pop() : 'unknown' + const timestamp = new Date() + .toISOString() + .replace('T', ' ') + .replace('Z', '') + ipcMain.emit( + 'phone-island-log', + {}, + `${timestamp} [PhoneIsland] [${levelStr}] ${message} (${source}:${line})`, + ) + }, + ) } }) } diff --git a/src/main/classes/windows/SplashScreenWindow.ts b/src/main/classes/windows/SplashScreenWindow.ts index 2e76b6a9..356b3be6 100644 --- a/src/main/classes/windows/SplashScreenWindow.ts +++ b/src/main/classes/windows/SplashScreenWindow.ts @@ -28,7 +28,7 @@ export class SplashScreenWindow extends BaseWindow { acceptFirstMouse: false, frame: false, thickFrame: false, - trafficLightPosition: { x: 0, y: 0 } + trafficLightPosition: { x: 0, y: 0 }, }) } @@ -37,10 +37,9 @@ export class SplashScreenWindow extends BaseWindow { super.show() TrayController.instance.updateTray({ enableShowButton: true, - isShowButtonVisible: false + isShowButtonVisible: false, }) - } - catch (e) { + } catch (e) { Log.warning('during showing the SplashScreenWindow:', e) } } @@ -49,7 +48,7 @@ export class SplashScreenWindow extends BaseWindow { try { this._window?.hide() TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) } catch (e) { Log.warning('during hiding the SplashScreenWindow:', e) diff --git a/src/main/lib/commandBarShortcut.ts b/src/main/lib/commandBarShortcut.ts index 8971707f..e8213b0e 100644 --- a/src/main/lib/commandBarShortcut.ts +++ b/src/main/lib/commandBarShortcut.ts @@ -11,7 +11,11 @@ let currentModifier: CommandBarDoubleTapModifier | undefined let keydownHandler: ((e: any) => void) | undefined let keyupHandler: ((e: any) => void) | undefined -function isModifierKeyEvent(e: any, UiohookKey: any, modifier: CommandBarDoubleTapModifier) { +function isModifierKeyEvent( + e: any, + UiohookKey: any, + modifier: CommandBarDoubleTapModifier, +) { switch (modifier) { case 'Cmd': return e.keycode === UiohookKey.Meta || e.keycode === UiohookKey.MetaRight @@ -23,8 +27,7 @@ function isModifierKeyEvent(e: any, UiohookKey: any, modifier: CommandBarDoubleT // On most Linux layouts AltGr is the right Alt key, but it can also emit CtrlRight. // Be permissive so the configured shortcut actually triggers. return ( - e.keycode === UiohookKey.AltRight || - e.keycode === UiohookKey.CtrlRight + e.keycode === UiohookKey.AltRight || e.keycode === UiohookKey.CtrlRight ) } } diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 30c60f71..dffaeaa3 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -4,12 +4,27 @@ import { PhoneIslandController } from '@/classes/controllers/PhoneIslandControll import { CommandBarController } from '@/classes/controllers/CommandBarController' import { IPC_EVENTS } from '@shared/constants' import { Account, OnDraggingWindow, PAGES } from '@shared/types' -import { BrowserWindow, app, ipcMain, screen, shell, desktopCapturer, globalShortcut, clipboard, Notification } from 'electron' +import { + BrowserWindow, + app, + ipcMain, + screen, + shell, + desktopCapturer, + globalShortcut, + clipboard, + Notification, +} from 'electron' import { Log } from '@shared/utils/logger' import { NethLinkController } from '@/classes/controllers/NethLinkController' import { AppController } from '@/classes/controllers/AppController' import { store } from './mainStore' -import { debouncer, getAccountUID, getPageFromQuery, isDev } from '@shared/utils/utils' +import { + debouncer, + getAccountUID, + getPageFromQuery, + isDev, +} from '@shared/utils/utils' import { NetworkController } from '@/classes/controllers/NetworkController' import { useLogin } from '@shared/useLogin' import { PhoneIslandWindow } from '@/classes/windows' @@ -22,7 +37,7 @@ import { stopCommandBarDoubleTapShortcut, } from './commandBarShortcut' -const { keyboard, Key } = require("@nut-tree-fork/nut-js"); +const { keyboard, Key } = require('@nut-tree-fork/nut-js') // Global flag to ensure audio warm-up runs only once per app session let hasRunAudioWarmup = false @@ -48,7 +63,9 @@ let lastServerConfigCheck = 0 */ function reloadPhoneIslandForSipChange(trigger: string) { if (isCallActive()) { - Log.info(`SIP config changed (${trigger}) but a call is active - PhoneIsland reload deferred to call end`) + Log.info( + `SIP config changed (${trigger}) but a call is active - PhoneIsland reload deferred to call end`, + ) isPhoneIslandReloadPending = true return } @@ -59,10 +76,13 @@ function reloadPhoneIslandForSipChange(trigger: string) { function onSyncEmitter( channel: IPC_EVENTS, - asyncCallback: (...args: any[]) => Promise + asyncCallback: (...args: any[]) => Promise, ): void { ipcMain.on(channel, async (event, ...args) => { - let syncResponse = [undefined, undefined] as [T | undefined, Error | undefined] + let syncResponse = [undefined, undefined] as [ + T | undefined, + Error | undefined, + ] try { const response = await asyncCallback(...args) syncResponse = [response, undefined] @@ -73,7 +93,7 @@ function onSyncEmitter( } else if (typeof e === 'string') { error.message = e } else { - error.message = "Unknown error" + error.message = 'Unknown error' } syncResponse = [undefined, error] } @@ -114,7 +134,6 @@ export function disableCommandBarShortcuts() { } export function registerIpcEvents() { - let draggingWindows: OnDraggingWindow = {} onSyncEmitter(IPC_EVENTS.GET_LOCALE, async () => { @@ -130,7 +149,10 @@ export function registerIpcEvents() { Log.error(e) try { PhoneIslandController.instance.window.emit(IPC_EVENTS.END_CALL) - NethLinkController.instance.window.emit(IPC_EVENTS.RESPONSE_START_CALL_BY_URL, false) + NethLinkController.instance.window.emit( + IPC_EVENTS.RESPONSE_START_CALL_BY_URL, + false, + ) } catch (e) { Log.error(e) } finally { @@ -141,19 +163,23 @@ export function registerIpcEvents() { const request = get( url, { - timeout: 3000 + timeout: 3000, }, (res) => { const statusCode = res.statusCode ?? 0 - const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 302 + const isSuccess = + (statusCode >= 200 && statusCode < 300) || statusCode === 302 if (!isSuccess) { triggerError(new Error(`status error: ${statusCode}`), request) return } - NethLinkController.instance.window.emit(IPC_EVENTS.RESPONSE_START_CALL_BY_URL, true) + NethLinkController.instance.window.emit( + IPC_EVENTS.RESPONSE_START_CALL_BY_URL, + true, + ) PhoneIslandController.instance.window.show() Log.debug('START_CALL_BY_URL', url, statusCode) - } + }, ) request.on('error', (e) => { @@ -165,103 +191,113 @@ export function registerIpcEvents() { }) ipcMain.on(IPC_EVENTS.UPDATE_SHARED_STATE, (_, newState, page, selector) => { - const windows = BrowserWindow.getAllWindows(); + const windows = BrowserWindow.getAllWindows() store.updateStore(newState, `${page}[${selector}]`) - windows.forEach(win => { + windows.forEach((win) => { const targetPage = win.webContents.getTitle() try { if (page !== targetPage) { - win.webContents.send(IPC_EVENTS.SHARED_STATE_UPDATED, newState, page); + win.webContents.send(IPC_EVENTS.SHARED_STATE_UPDATED, newState, page) } } catch (e) { Log.error(`Data origin: ${page}, target: ${targetPage}`, e) } - }); - }); + }) + }) ipcMain.on(IPC_EVENTS.START_DRAG, (event) => { - const window = BrowserWindow.fromWebContents(event.sender); + const window = BrowserWindow.fromWebContents(event.sender) if (window) { - const cursorPosition = screen.getCursorScreenPoint(); - const startMousePosition = { x: cursorPosition.x, y: cursorPosition.y }; + const cursorPosition = screen.getCursorScreenPoint() + const startMousePosition = { x: cursorPosition.x, y: cursorPosition.y } const [x, y] = window.getPosition() const startWindowPosition = { - x, y + x, + y, } if (!draggingWindows?.hasOwnProperty(window.title)) { const interval: number = setInterval(() => { updateWindowPosition(window) - }, 1000 / 300) as unknown as number; // => 300 frames per seconds + }, 1000 / 300) as unknown as number // => 300 frames per seconds draggingWindows = { ...draggingWindows, [window.title]: { interval, startMousePosition, - startWindowPosition - } + startWindowPosition, + }, } } } - }); + }) ipcMain.on(IPC_EVENTS.STOP_DRAG, (event) => { - const window = BrowserWindow.fromWebContents(event.sender); + const window = BrowserWindow.fromWebContents(event.sender) if (window && draggingWindows?.hasOwnProperty(window.title)) { const draggingWindow = draggingWindows[window.title] clearInterval(draggingWindow.interval) delete draggingWindows[window.title] - const cursorPosition = screen.getCursorScreenPoint(); - const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x; - const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y; + const cursorPosition = screen.getCursorScreenPoint() + const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x + const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y if (window.title === PAGES.PHONEISLAND) { const bounds = window.getBounds() - Log.info(`PhoneIsland dragged from (${draggingWindow.startWindowPosition.x}, ${draggingWindow.startWindowPosition.y}) to (${bounds.x}, ${bounds.y})`) + Log.info( + `PhoneIsland dragged from (${draggingWindow.startWindowPosition.x}, ${draggingWindow.startWindowPosition.y}) to (${bounds.x}, ${bounds.y})`, + ) } if (Math.abs(deltaX) <= 3 && Math.abs(deltaY) <= 3) { - debouncer(IPC_EVENTS.ENABLE_CLICK, () => { - event.sender.send(IPC_EVENTS.ENABLE_CLICK) - }, 100) + debouncer( + IPC_EVENTS.ENABLE_CLICK, + () => { + event.sender.send(IPC_EVENTS.ENABLE_CLICK) + }, + 100, + ) } } - }); - + }) function updateWindowPosition(window: Electron.BrowserWindow) { try { const draggingWindow = draggingWindows[window.title] if (draggingWindow) { - const cursorPosition = screen.getCursorScreenPoint(); - const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x; - const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y; + const cursorPosition = screen.getCursorScreenPoint() + const deltaX = cursorPosition.x - draggingWindow.startMousePosition.x + const deltaY = cursorPosition.y - draggingWindow.startMousePosition.y if (deltaX !== 0 || deltaY !== 0) { - const newX = draggingWindow.startWindowPosition.x + deltaX; - const newY = draggingWindow.startWindowPosition.y + deltaY; + const newX = draggingWindow.startWindowPosition.x + deltaX + const newY = draggingWindow.startWindowPosition.y + deltaY if (window.title === PAGES.PHONEISLAND) { const { width, height } = PhoneIslandWindow.currentSize - window.setBounds({ - x: newX, - y: newY, - width, - height - }, false) + window.setBounds( + { + x: newX, + y: newY, + width, + height, + }, + false, + ) } else { const [w, h] = window.getContentSize() - window.setBounds({ - x: newX, - y: newY, - width: w, - height: h - }, false) + window.setBounds( + { + x: newX, + y: newY, + width: w, + height: h, + }, + false, + ) } } } } catch (e) { - + // Runs on every drag frame: swallow silently rather than flood the log. } } - - ipcMain.on(IPC_EVENTS.UPDATE_CONNECTION_STATE, (_, isOnline) => { if (store.store) { Log.info('Update connection state:', isOnline) @@ -270,12 +306,12 @@ export function registerIpcEvents() { store.saveToDisk() } } - }); + }) ipcMain.on(IPC_EVENTS.REQUEST_SHARED_STATE, (event) => { const page = getPageFromQuery(event?.sender?.getTitle()) - event.sender.send(IPC_EVENTS.SHARED_STATE_UPDATED, store.store, page); - }); + event.sender.send(IPC_EVENTS.SHARED_STATE_UPDATED, store.store, page) + }) ipcMain.on(IPC_EVENTS.CLOSE_NETH_LINK, async (event) => { AppController.safeQuit() @@ -290,21 +326,24 @@ export function registerIpcEvents() { shell.openExternal(path) }) - ipcMain.on(IPC_EVENTS.SEND_NOTIFICATION, async (_, title, options, openPath) => { - const notification = new Notification({ - title, - ...options, - }) + ipcMain.on( + IPC_EVENTS.SEND_NOTIFICATION, + async (_, title, options, openPath) => { + const notification = new Notification({ + title, + ...options, + }) - notification.on('click', () => { - const account = store.store.account - if (openPath && account?.host) { - shell.openExternal(buildHostUrl(account.host, openPath)) - } - }) + notification.on('click', () => { + const account = store.store.account + if (openPath && account?.host) { + shell.openExternal(buildHostUrl(account.host, openPath)) + } + }) - notification.show() - }) + notification.show() + }, + ) ipcMain.on(IPC_EVENTS.COPY_TO_CLIPBOARD, async (_, text) => { clipboard.writeText(text) @@ -343,12 +382,18 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.CHANGE_THEME, (_, theme) => { AccountController.instance.updateTheme(theme) try { - NethLinkController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) + NethLinkController.instance?.window?.emit( + IPC_EVENTS.ON_CHANGE_THEME, + theme, + ) } catch (e) { Log.error(e) } try { - DevToolsController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) + DevToolsController.instance?.window?.emit( + IPC_EVENTS.ON_CHANGE_THEME, + theme, + ) } catch (e) { Log.error(e) } @@ -370,29 +415,41 @@ export function registerIpcEvents() { Log.info('Send CHANGE_PREFERRED_DEVICES event with', { preferredDevices: account.preferredDevices, shouldRunWarmup, - deviceType + deviceType, }) AccountController.instance.updatePreferredDevice(account.preferredDevices) - PhoneIslandController.instance.window.emit(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, { - ...account.preferredDevices, - shouldRunWarmup - }) + PhoneIslandController.instance.window.emit( + IPC_EVENTS.CHANGE_PREFERRED_DEVICES, + { + ...account.preferredDevices, + shouldRunWarmup, + }, + ) }, 250) }) ipcMain.on(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, (_, devices) => { Log.info('Received CHANGE_PREFERRED_DEVICES in ipcEvents:', devices) AccountController.instance.updatePreferredDevice(devices) - PhoneIslandController.instance.window.emit(IPC_EVENTS.CHANGE_PREFERRED_DEVICES, devices) + PhoneIslandController.instance.window.emit( + IPC_EVENTS.CHANGE_PREFERRED_DEVICES, + devices, + ) }) ipcMain.on(IPC_EVENTS.CHANGE_RINGTONE_SETTINGS, (_, settings) => { Log.info('Received CHANGE_RINGTONE_SETTINGS in ipcEvents:', settings) - PhoneIslandController.instance.window.emit(IPC_EVENTS.CHANGE_RINGTONE_SETTINGS, settings) + PhoneIslandController.instance.window.emit( + IPC_EVENTS.CHANGE_RINGTONE_SETTINGS, + settings, + ) }) ipcMain.on(IPC_EVENTS.PLAY_RINGTONE_PREVIEW, (_, audioData) => { - PhoneIslandController.instance.window.emit(IPC_EVENTS.PLAY_RINGTONE_PREVIEW, audioData) + PhoneIslandController.instance.window.emit( + IPC_EVENTS.PLAY_RINGTONE_PREVIEW, + audioData, + ) }) ipcMain.on(IPC_EVENTS.STOP_RINGTONE_PREVIEW, () => { @@ -401,7 +458,7 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.AUDIO_PLAYER_CLOSED, () => { // Broadcast to all windows - BrowserWindow.getAllWindows().forEach(window => { + BrowserWindow.getAllWindows().forEach((window) => { window.webContents.send(IPC_EVENTS.AUDIO_PLAYER_CLOSED) }) }) @@ -412,7 +469,8 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.CHANGE_SHORTCUT, async (_, combo) => { // Use tracked shortcut if available, otherwise fall back to store - const previousCombo = registeredCallShortcut || store.store.account?.shortcut + const previousCombo = + registeredCallShortcut || store.store.account?.shortcut if (previousCombo) { try { globalShortcut.unregister(previousCombo) @@ -432,36 +490,38 @@ export function registerIpcEvents() { try { const registered = globalShortcut.register(combo, async () => { - // get selected text content - const isMac = os.platform() === 'darwin' - const isLinux = os.platform() === 'linux'; - const modifierKey = isMac ? Key.LeftSuper : Key.LeftControl - keyboard.config.autoDelayMs = 50; - await keyboard.pressKey(modifierKey); - await keyboard.pressKey(Key.C); - await keyboard.releaseKey(Key.C); - await keyboard.releaseKey(modifierKey); - await new Promise(resolve => setTimeout(resolve, 100)); - - // trim spaces - let selectedText = await clipboard.readText(isLinux ? 'selection' : 'clipboard'); - if (typeof selectedText !== 'string') return - selectedText = selectedText.trim() - - // remove spaces between text - const prefixMatch = selectedText.match(/^[*#+]+/) - const prefix = prefixMatch ? prefixMatch[0] : '' - let sanitized = selectedText.replace(/[^\d]/g, '') - let number = prefix + sanitized - - // check is a valid number - const isValidNumber = /^([*#+]?)(\d{2,})$/.test(number) - if (isValidNumber) { - Log.info('Shortcut call to:', number) - PhoneIslandController.instance.call(number) - } else { - Log.info('Selected text is not a valid number:', selectedText) - } + // get selected text content + const isMac = os.platform() === 'darwin' + const isLinux = os.platform() === 'linux' + const modifierKey = isMac ? Key.LeftSuper : Key.LeftControl + keyboard.config.autoDelayMs = 50 + await keyboard.pressKey(modifierKey) + await keyboard.pressKey(Key.C) + await keyboard.releaseKey(Key.C) + await keyboard.releaseKey(modifierKey) + await new Promise((resolve) => setTimeout(resolve, 100)) + + // trim spaces + let selectedText = await clipboard.readText( + isLinux ? 'selection' : 'clipboard', + ) + if (typeof selectedText !== 'string') return + selectedText = selectedText.trim() + + // remove spaces between text + const prefixMatch = selectedText.match(/^[*#+]+/) + const prefix = prefixMatch ? prefixMatch[0] : '' + const sanitized = selectedText.replace(/[^\d]/g, '') + const number = prefix + sanitized + + // check is a valid number + const isValidNumber = /^([*#+]?)(\d{2,})$/.test(number) + if (isValidNumber) { + Log.info('Shortcut call to:', number) + PhoneIslandController.instance.call(number) + } else { + Log.info('Selected text is not a valid number:', selectedText) + } }) if (registered) { registeredCallShortcut = combo @@ -475,16 +535,21 @@ export function registerIpcEvents() { }) ipcMain.on(IPC_EVENTS.GET_NETHVOICE_CONFIG, async (e, account) => { - //I import the config file of this host to take the information about SIP_host and port only if I am on demo-leopard I have to take them static + // I import the config file of this host to take the information about SIP_host and port only if I am on demo-leopard I have to take them static const { parseConfig } = useLogin() - const config: string = await NetworkController.instance.get(`https://${account.host}/config/config.production.js`) + const config: string = await NetworkController.instance.get( + `https://${account.host}/config/config.production.js`, + ) account = parseConfig(account, config) e.reply(IPC_EVENTS.SET_NETHVOICE_CONFIG, account) }) ipcMain.on(IPC_EVENTS.EMIT_QUEUE_UPDATE, (_, queue) => { try { - NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_QUEUE_UPDATE, queue) + NethLinkController.instance.window.emit( + IPC_EVENTS.EMIT_QUEUE_UPDATE, + queue, + ) } catch (e) { Log.error(e) } @@ -492,7 +557,9 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.EMIT_CALL_ACTIVE, (_) => { if (!hasActiveCall) { - Log.info('Call active (started or answered) - setting hasActiveCall = true') + Log.info( + 'Call active (started or answered) - setting hasActiveCall = true', + ) hasActiveCall = true } }) @@ -517,7 +584,10 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE, (_, mainPresence) => { try { - NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE, mainPresence) + NethLinkController.instance.window.emit( + IPC_EVENTS.EMIT_MAIN_PRESENCE_UPDATE, + mainPresence, + ) } catch (e) { Log.error(e) } @@ -541,12 +611,19 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.RECONNECT_SOCKET, async () => { try { - const previous = { sipHost: store.store.account?.sipHost, sipPort: store.store.account?.sipPort } + const previous = { + sipHost: store.store.account?.sipHost, + sipPort: store.store.account?.sipPort, + } // autoLogin refreshes the SIP settings from the server config too await AccountController.instance.autoLogin() NethLinkController.instance.window.emit(IPC_EVENTS.RECONNECT_SOCKET) const account = store.store.account - if (account && (account.sipHost !== previous.sipHost || account.sipPort !== previous.sipPort)) { + if ( + account && + (account.sipHost !== previous.sipHost || + account.sipPort !== previous.sipPort) + ) { reloadPhoneIslandForSipChange('socket reconnected') } } catch (e) { @@ -563,7 +640,8 @@ export function registerIpcEvents() { } lastServerConfigCheck = now try { - const changed = await AccountController.instance.syncLoggedAccountServerConfig() + const changed = + await AccountController.instance.syncLoggedAccountServerConfig() if (changed) { reloadPhoneIslandForSipChange('webrtc down') } @@ -574,27 +652,31 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.FULLSCREEN_ENTER, () => { try { - PhoneIslandController.instance.window.getWindow()?.setFullScreen(true); + PhoneIslandController.instance.window.getWindow()?.setFullScreen(true) } catch (e) { Log.error('ENTER FULLSCREEN error ', e) } }) ipcMain.on(IPC_EVENTS.FULLSCREEN_EXIT, () => { try { - PhoneIslandController.instance.window.getWindow()?.setFullScreen(false); + PhoneIslandController.instance.window.getWindow()?.setFullScreen(false) } catch (e) { Log.error('EXIT FULLSCREEN error ', e) } }) ipcMain.on(IPC_EVENTS.SCREEN_SHARE_INIT, () => { - desktopCapturer.getSources({ types: ['screen'] }) // allow only entire screen sharing - .then(sources => { - PhoneIslandController.instance?.window?.emit(IPC_EVENTS.SCREEN_SHARE_SOURCES, sources) + desktopCapturer + .getSources({ types: ['screen'] }) // allow only entire screen sharing + .then((sources) => { + PhoneIslandController.instance?.window?.emit( + IPC_EVENTS.SCREEN_SHARE_SOURCES, + sources, + ) }) - .catch(e => { + .catch((e) => { Log.error('ENTER SCREEN_SHARE_INIT error ', e) - }); - }); + }) + }) ipcMain.on(IPC_EVENTS.URL_OPEN, (_, data) => { try { @@ -630,13 +712,16 @@ export function registerIpcEvents() { } }) - ipcMain.on(IPC_EVENTS.COMMAND_BAR_RESIZE, (_, size: { width: number, height: number }) => { - try { - CommandBarController.instance?.resize(size) - } catch (e) { - Log.error('COMMAND_BAR_RESIZE error', e) - } - }) + ipcMain.on( + IPC_EVENTS.COMMAND_BAR_RESIZE, + (_, size: { width: number; height: number }) => { + try { + CommandBarController.instance?.resize(size) + } catch (e) { + Log.error('COMMAND_BAR_RESIZE error', e) + } + }, + ) ipcMain.on(IPC_EVENTS.CHANGE_COMMAND_BAR_SHORTCUT, async (_, combo) => { if (!isUserLoggedIn()) { @@ -656,12 +741,22 @@ export function registerIpcEvents() { } } - const allowedSoloModifiers: CommandBarDoubleTapModifier[] = ['Ctrl', 'Alt', 'AltGr', 'Cmd'] - const isSoloModifier = (value: string): value is CommandBarDoubleTapModifier => + const allowedSoloModifiers: CommandBarDoubleTapModifier[] = [ + 'Ctrl', + 'Alt', + 'AltGr', + 'Cmd', + ] + const isSoloModifier = ( + value: string, + ): value is CommandBarDoubleTapModifier => allowedSoloModifiers.includes(value as CommandBarDoubleTapModifier) const isOnlyModifiersButMultiple = (value: string) => { - const parts = value.split('+').map((p) => p.trim()).filter(Boolean) + const parts = value + .split('+') + .map((p) => p.trim()) + .filter(Boolean) return parts.length > 1 && parts.every((p) => isSoloModifier(p)) } @@ -689,7 +784,10 @@ export function registerIpcEvents() { // Reject modifier-only combos with multiple modifiers (e.g. Ctrl+Alt) if (isOnlyModifiersButMultiple(normalizedCombo)) { - Log.warning('Invalid Command Bar shortcut (multiple modifiers only):', normalizedCombo) + Log.warning( + 'Invalid Command Bar shortcut (multiple modifiers only):', + normalizedCombo, + ) AccountController.instance.updateCommandBarShortcut('') applyDefault() return diff --git a/src/main/lib/mainStore.ts b/src/main/lib/mainStore.ts index 0ccfc215..ae2b603f 100644 --- a/src/main/lib/mainStore.ts +++ b/src/main/lib/mainStore.ts @@ -1,12 +1,15 @@ -import { Account, AuthAppData, LocalStorageData } from '@shared/types'; -import { app, ipcMain } from 'electron'; -import path from 'path'; -import fs from 'fs'; -import { IPC_EVENTS } from '@shared/constants'; -import { Log } from '@shared/utils/logger'; -import { difference } from 'lodash'; - -const AVAILABLE_USER_DATA_PATH = path.join(app.getPath("userData"), `available_user_data.json`); +import { Account, AuthAppData, LocalStorageData } from '@shared/types' +import { app, ipcMain } from 'electron' +import path from 'path' +import fs from 'fs' +import { IPC_EVENTS } from '@shared/constants' +import { Log } from '@shared/utils/logger' +import { difference } from 'lodash' + +const AVAILABLE_USER_DATA_PATH = path.join( + app.getPath('userData'), + `available_user_data.json`, +) // on Windows the rename can transiently fail while an antivirus keeps the target open const RENAME_RETRY_DELAYS_MS = [10, 30, 90] @@ -78,7 +81,11 @@ function parseJsonFile(filePath: string): any | undefined { return undefined } if (isAllZeroes(buffer)) { - Log.warning('STORE', filePath, 'contains only NUL bytes - corrupted by an unclean shutdown') + Log.warning( + 'STORE', + filePath, + 'contains only NUL bytes - corrupted by an unclean shutdown', + ) return undefined } return JSON.parse(buffer.toString('utf-8')) @@ -103,15 +110,19 @@ function readJsonSafe(filePath: string, fallback: T): T { return backup as T } } - Log.warning('STORE no usable data for', filePath, '- falling back to defaults') + Log.warning( + 'STORE no usable data for', + filePath, + '- falling back to defaults', + ) return fallback } class Store { - assignedInstanceID; + assignedInstanceID USER_DATA_PATH constructor() { - const instance = process.argv.find(p => p.includes('INSTANCE=')) + const instance = process.argv.find((p) => p.includes('INSTANCE=')) if (instance) { const i = instance?.split('=') this.assignedInstanceID = i[1] @@ -119,14 +130,23 @@ class Store { this.assignedInstanceID = 0 } Log.info({ assignedInstanceID: this.assignedInstanceID }) - this.USER_DATA_PATH = path.join(app.getPath("userData"), `user_data${this.assignedInstanceID || ''}.json`); + this.USER_DATA_PATH = path.join( + app.getPath('userData'), + `user_data${this.assignedInstanceID || ''}.json`, + ) const hasUserData = fs.existsSync(this.USER_DATA_PATH) const hasAvailableUserData = fs.existsSync(AVAILABLE_USER_DATA_PATH) if (!hasUserData || !hasAvailableUserData) { if (!hasAvailableUserData) { // migration path for the installations where the accounts lived only in user_data.json - const userData = readJsonSafe>(this.USER_DATA_PATH, {}) - writeJsonAtomic(AVAILABLE_USER_DATA_PATH, JSON.stringify(userData?.auth?.availableAccounts ?? {})) + const userData = readJsonSafe>( + this.USER_DATA_PATH, + {}, + ) + writeJsonAtomic( + AVAILABLE_USER_DATA_PATH, + JSON.stringify(userData?.auth?.availableAccounts ?? {}), + ) } if (!hasUserData) { writeJsonAtomic(this.USER_DATA_PATH, JSON.stringify({})) @@ -148,36 +168,55 @@ class Store { const diff = difference(Object.values(o), Object.values(this.store as any)) if (diff.length > 0 || force) { this.store = o - ipcMain.emit(IPC_EVENTS.UPDATE_SHARED_STATE, undefined, this.store, 'main', selector) + ipcMain.emit( + IPC_EVENTS.UPDATE_SHARED_STATE, + undefined, + this.store, + 'main', + selector, + ) } } updateStore(newState: T | null | undefined, from: string) { - const diff = difference(Object.values(newState as any || {}), Object.values(this.store as any || {})) - Log.debug('STORE update shared store from', from, Object.keys(newState as any || {})) + const diff = difference( + Object.values((newState as any) || {}), + Object.values((this.store as any) || {}), + ) + Log.debug( + 'STORE update shared store from', + from, + Object.keys((newState as any) || {}), + ) if (diff.length > 0 || this.store === undefined) { this.store = Object.assign({}, newState ?? ({} as T)) } } saveToDisk(forceSave: boolean = false) { - const availableUserData = (this.store as LocalStorageData)?.auth?.availableAccounts + const availableUserData = (this.store as LocalStorageData)?.auth + ?.availableAccounts try { - writeJsonAtomic(this.USER_DATA_PATH, JSON.stringify(this.store ?? {})); + writeJsonAtomic(this.USER_DATA_PATH, JSON.stringify(this.store ?? {})) } catch (e) { Log.error('STORE unable to persist', this.USER_DATA_PATH, e) } if (Object.keys(availableUserData || {}).length > 0 || forceSave) { try { - writeJsonAtomic(AVAILABLE_USER_DATA_PATH, JSON.stringify(availableUserData ?? {})); + writeJsonAtomic( + AVAILABLE_USER_DATA_PATH, + JSON.stringify(availableUserData ?? {}), + ) } catch (e) { Log.error('STORE unable to persist', AVAILABLE_USER_DATA_PATH, e) } } } - getAvailableFromDisk(): { [accountUID: string]: Account; } { - const availableUserData = readJsonSafe<{ [accountUID: string]: Account } | null>(AVAILABLE_USER_DATA_PATH, null) + getAvailableFromDisk(): { [accountUID: string]: Account } { + const availableUserData = readJsonSafe<{ + [accountUID: string]: Account + } | null>(AVAILABLE_USER_DATA_PATH, null) if (!availableUserData || typeof availableUserData !== 'object') { return {} } @@ -187,13 +226,16 @@ class Store { getFromDisk(): T | null { const data = readJsonSafe(this.USER_DATA_PATH, null) if (!data || typeof data !== 'object') { - Log.error('retrieving user data: no usable content in', this.USER_DATA_PATH) + Log.error( + 'retrieving user data: no usable content in', + this.USER_DATA_PATH, + ) return null } const retrivedStore = data as LocalStorageData retrivedStore.auth = { ...(retrivedStore.auth ?? {}), - availableAccounts: this.getAvailableFromDisk() + availableAccounts: this.getAvailableFromDisk(), } as AuthAppData return retrivedStore as T } diff --git a/src/main/lib/windowConstructor.ts b/src/main/lib/windowConstructor.ts index 345cd408..a8e1a262 100644 --- a/src/main/lib/windowConstructor.ts +++ b/src/main/lib/windowConstructor.ts @@ -17,16 +17,22 @@ export function createWindow( height: 300, show: false, fullscreenable: false, - devPath: undefined + devPath: undefined, }, - params?: Record + params?: Record, ): BrowserWindow { const mainWindow = new BrowserWindow({ parent: undefined, title: id, ...config, - titleBarStyle: config.titleBarStyle ?? (process.platform === 'darwin' ? 'customButtonsOnHover' : 'hidden'), - ...(process.platform === 'linux' ? (config.icon ? { icon: config.icon } : {}) : {}), + titleBarStyle: + config.titleBarStyle ?? + (process.platform === 'darwin' ? 'customButtonsOnHover' : 'hidden'), + ...(process.platform === 'linux' + ? config.icon + ? { icon: config.icon } + : {} + : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, @@ -36,13 +42,16 @@ export function createWindow( }, hiddenInMissionControl: false, }) - params = ({ + params = { page: id, - ...params - }) + ...params, + } if (is.dev && process.env['ELECTRON_RENDERER_URL']) { const propsUrl = params - ? Object.entries(params).reduce((p, c) => `${p}${p === '?' ? '' : '&'}${c[0]}=${c[1]}`, '?') + ? Object.entries(params).reduce( + (p, c) => `${p}${p === '?' ? '' : '&'}${c[0]}=${c[1]}`, + '?', + ) : '' const devServerURL = `${process.env['ELECTRON_RENDERER_URL']!}/#/${id}${propsUrl}` mainWindow.loadURL(devServerURL, {}) @@ -50,18 +59,19 @@ export function createWindow( const fileRoute = join(__dirname, '../renderer/index.html') mainWindow.loadFile(fileRoute, { hash: id, - query: params + query: params, }) } - mainWindow.on('hide', () => { }) + mainWindow.on('hide', () => {}) - mainWindow.on('close', () => { }) + mainWindow.on('close', () => {}) mainWindow.on('ready-to-show', () => { - isDevTools() && mainWindow.webContents.openDevTools({ - mode: 'detach' - }) + isDevTools() && + mainWindow.webContents.openDevTools({ + mode: 'detach', + }) }) mainBindings(ipcMain, mainWindow, fs) diff --git a/src/main/lib/windowsFocus.ts b/src/main/lib/windowsFocus.ts index 20a27139..ea6c37c1 100644 --- a/src/main/lib/windowsFocus.ts +++ b/src/main/lib/windowsFocus.ts @@ -45,14 +45,22 @@ export function forceWindowFocus(window: BrowserWindow): boolean { // Define the functions we need const GetCurrentThreadId = kernel32.func('uint32_t GetCurrentThreadId()') - const GetWindowThreadProcessId = user32.func('uint32_t GetWindowThreadProcessId(void* hwnd, uint32_t* lpdwProcessId)') - const AttachThreadInput = user32.func('bool AttachThreadInput(uint32_t idAttach, uint32_t idAttachTo, bool fAttach)') - const SetForegroundWindow = user32.func('bool SetForegroundWindow(void* hwnd)') + const GetWindowThreadProcessId = user32.func( + 'uint32_t GetWindowThreadProcessId(void* hwnd, uint32_t* lpdwProcessId)', + ) + const AttachThreadInput = user32.func( + 'bool AttachThreadInput(uint32_t idAttach, uint32_t idAttachTo, bool fAttach)', + ) + const SetForegroundWindow = user32.func( + 'bool SetForegroundWindow(void* hwnd)', + ) const BringWindowToTop = user32.func('bool BringWindowToTop(void* hwnd)') const SetFocus = user32.func('void* SetFocus(void* hwnd)') const GetForegroundWindow = user32.func('void* GetForegroundWindow()') const ShowWindow = user32.func('bool ShowWindow(void* hwnd, int nCmdShow)') - const keybd_event = user32.func('void keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo)') + const keybd_event = user32.func( + 'void keybd_event(uint8_t bVk, uint8_t bScan, uint32_t dwFlags, uintptr_t dwExtraInfo)', + ) // Get the native window handle const hwnd = window.getNativeWindowHandle() diff --git a/src/main/main.ts b/src/main/main.ts index 6d57e23b..d8827f12 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,15 +1,35 @@ -import { app, ipcMain, nativeTheme, powerMonitor, protocol, systemPreferences, dialog, shell, globalShortcut, net } from 'electron' +import { + app, + ipcMain, + nativeTheme, + powerMonitor, + protocol, + systemPreferences, + dialog, + shell, + globalShortcut, + net, +} from 'electron' if (process.platform === 'win32') { require('win-ca/api')({ inject: '+', save: false }) } -import { registerIpcEvents, isCallActive, disableCommandBarShortcuts } from '@/lib/ipcEvents' +import { + registerIpcEvents, + isCallActive, + disableCommandBarShortcuts, +} from '@/lib/ipcEvents' import { AccountController } from './classes/controllers' import { PhoneIslandController } from './classes/controllers/PhoneIslandController' import { CommandBarController } from './classes/controllers/CommandBarController' -import { Account, AuthAppData, AvailableThemes, LocalStorageData } from '@shared/types' +import { + Account, + AuthAppData, + AvailableThemes, + LocalStorageData, +} from '@shared/types' import { TrayController } from './classes/controllers/TrayController' import { LoginController } from './classes/controllers/LoginController' -import { join, resolve } from 'path' +import path, { join, resolve } from 'path' import { Log } from '@shared/utils/logger' import { NethLinkController } from './classes/controllers/NethLinkController' import { SplashScreenController } from './classes/controllers/SplashScreenController' @@ -24,15 +44,14 @@ import { } from './lib/commandBarShortcut' import { store } from './lib/mainStore' import fs from 'fs' -import path from 'path' import i18next from 'i18next' import Backend from 'i18next-fs-backend/cjs' import { uniq } from 'lodash' -import { Registry } from 'rage-edit'; +import { Registry } from 'rage-edit' import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { URL } from 'url' -//get app parameter +// get app parameter const params = process.argv for (const arg of params) { if (arg.includes('=')) { @@ -43,8 +62,7 @@ for (const arg of params) { // } else { // kv[1] = undefined // } - if (kv[1]) - process.env[kv[0]] = kv[1] + if (kv[1]) process.env[kv[0]] = kv[1] } } const multipleInstances = !!process.env['INSTANCE'] @@ -53,44 +71,56 @@ Log.debug('ENV:', process.env) function startup() { app.setName('NethLink') - app.setAppUserModelId('com.nethesis.nethlink.app') //must match electron-builder appId so Windows notification permissions and taskbar pinning work correctly - ///LOGGER + app.setAppUserModelId('com.nethesis.nethlink.app') // must match electron-builder appId so Windows notification permissions and taskbar pinning work correctly + /// LOGGER startLogger() attachGlobalErrorHandlers() - //windows + // windows const gotTheLock = multipleInstances || app.requestSingleInstanceLock() Log.info('gotTheLock', gotTheLock) if (!gotTheLock) { Log.info('Block second instance') app.quit() - return; + return } else { - - //I set the app to open at operating system startup + // I set the app to open at operating system startup if (process.env.node_env !== 'development' && !isDev()) { app.setLoginItemSettings({ - openAtLogin: true + openAtLogin: true, }) } - ipcMain.on(IPC_EVENTS.LOGIN, async (e, props?: { account?: Account, password?: string, showNethlink: boolean, }) => { - const { password, showNethlink, account } = props || { showNethlink: true } - if (LoginController.instance && LoginController.instance.window.isOpen() && password && account) { - Log.info("LOGIN SUCCESS") - await LoginController.instance.quit() - await AccountController.instance.saveLoggedAccount(account, password) - } - store.saveToDisk() + ipcMain.on( + IPC_EVENTS.LOGIN, + async ( + e, + props?: { account?: Account; password?: string; showNethlink: boolean }, + ) => { + const { password, showNethlink, account } = props || { + showNethlink: true, + } + if ( + LoginController.instance && + LoginController.instance.window.isOpen() && + password && + account + ) { + Log.info('LOGIN SUCCESS') + await LoginController.instance.quit() + await AccountController.instance.saveLoggedAccount(account, password) + } + store.saveToDisk() - // Create app windows only if we actually have a logged account. - if (store.store.account) { - createNethLink(showNethlink) - } else { - Log.info('LOGIN event ignored: no logged account in store') - } - }) + // Create app windows only if we actually have a logged account. + if (store.store.account) { + createNethLink(showNethlink) + } else { + Log.info('LOGIN event ignored: no logged account in store') + } + }, + ) ipcMain.on(IPC_EVENTS.LOGOUT, async (_event) => { Log.info('logout from event') @@ -109,7 +139,7 @@ function startup() { AccountController.instance.logout() await delay(1000) TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) showLogin() }) @@ -127,13 +157,10 @@ function startup() { attachPowerMonitor() app.dock?.hide() - } - } async function startLocalization() { - const convertPath = (filename): string => { const dir = __dirname let loadPath = join(`./public/locales/{{lng}}/${filename}.json`) @@ -164,52 +191,57 @@ async function startLocalization() { const locales = uniq([locale!.split('-')[0], ...fallbackLng]) return locales }, - cacheUserLanguage: Function.prototype + cacheUserLanguage: Function.prototype, } Log.info(config) await i18next.use(Backend).use(electronDetector).init(config) } function startLogger() { - const today = new Date().toISOString().split('T')[0]; - const logFilePath = path.join(app.getPath("userData"), `./logs/app_${today}_${store.assignedInstanceID}.log`); + const today = new Date().toISOString().split('T')[0] + const logFilePath = path.join( + app.getPath('userData'), + `./logs/app_${today}_${store.assignedInstanceID}.log`, + ) const logOnFile = async (message) => { - const logDir = path.dirname(logFilePath); + const logDir = path.dirname(logFilePath) if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - fs.appendFile(logFilePath, `App Version: ${app.getVersion()}\n`, (err) => { - if (err) throw err; - }); + fs.mkdirSync(logDir, { recursive: true }) + fs.appendFile( + logFilePath, + `App Version: ${app.getVersion()}\n`, + (err) => { + if (err) throw err + }, + ) } if (!message) { // Create new object error to get stack trace - const error = new Error(); + const error = new Error() // Get stack trace as string - const stack = error.stack; + const stack = error.stack // Divide stack trace in lines - const stackLines = stack?.split('\n'); + const stackLines = stack?.split('\n') // Get the line contains function call (the third one) - const callerLine = stackLines?.[2].split('at ')[1]; + const callerLine = stackLines?.[2].split('at ')[1] message = callerLine } fs.appendFile(logFilePath, message + '\n', (err) => { - if (err) throw err; - }); + if (err) throw err + }) isDev() && console.log(message) } ipcMain.on('log-message', (e, message) => { - if (message && isDev()) - logOnFile(message) + if (message && isDev()) logOnFile(message) }) // Always log PhoneIsland renderer messages (for audio diagnostics in production) ipcMain.on('phone-island-log', (_e, message) => { - if (message) - logOnFile(message) + if (message) logOnFile(message) }) function deleteLogFile() { if (fs.existsSync(logFilePath)) { - fs.rmSync(logFilePath); + fs.rmSync(logFilePath) } } deleteLogFile() @@ -240,21 +272,23 @@ function attachOnReadyProcess() { let retryAppStart: NodeJS.Timeout | undefined = undefined app.whenReady().then(async () => { - let isGone = false + const isGone = false Log.info('APP READY') await startLocalization() - //I create the Tray controller instance - I define to it the function it should execute upon clicking on the icon + // I create the Tray controller instance - I define to it the function it should execute upon clicking on the icon new SplashScreenController() new TrayController() - //I display the splashscreen when the splashscreen component is correctly loaded. + // I display the splashscreen when the splashscreen component is correctly loaded. SplashScreenController.instance.window.addOnBuildListener(() => { // On Windows, check if system is locked before starting if (process.platform === 'win32') { const idleState = powerMonitor.getSystemIdleState(1) if (idleState === 'locked') { - Log.info('Windows is locked, waiting for unlock before starting app...') + Log.info( + 'Windows is locked, waiting for unlock before starting app...', + ) // Wait for unlock-screen event before starting powerMonitor.once('unlock-screen', () => { Log.info('Windows unlocked, starting app now...') @@ -302,7 +336,7 @@ function attachOnReadyProcess() { 'web-contents-created', 'will-continue-activity', 'will-finish-launching', - 'will-quit' + 'will-quit', ] events.forEach((e: any) => { app.on(e, (...args) => { @@ -322,17 +356,16 @@ function attachOnReadyProcess() { data = store.getFromDisk() store.updateStore(data, 'startApp') safeStartApp(++attempt) - return; + return } else { await resetApp() showLogin() SplashScreenController.instance.window.quit(true) - //once the loading is complete I enable the ability to click on the icon in the tray + // once the loading is complete I enable the ability to click on the icon in the tray TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) } - } else { const isOnline = await checkConnection() Log.info('START - START APP, retry:', attempt) @@ -340,11 +373,15 @@ function attachOnReadyProcess() { Log.info('START - NO CONNECTION', attempt) if (attempt >= 3) { // Stop retrying and show the no connection dialog - Log.info('START - showing no connection dialog, stopping automatic retries') + Log.info( + 'START - showing no connection dialog, stopping automatic retries', + ) waitingForConnection = true startConnectionPolling() try { - SplashScreenController.instance.window.emit(IPC_EVENTS.SHOW_NO_CONNECTION) + SplashScreenController.instance.window.emit( + IPC_EVENTS.SHOW_NO_CONNECTION, + ) } catch (e) { Log.error(e) } @@ -370,19 +407,22 @@ function attachOnReadyProcess() { ipcMain.emit(IPC_EVENTS.LOGIN, undefined, { showNethlink: true }) } else { Log.info('START - autologin failed') - store.updateStore({ - auth: { - ...store.store.auth!, - lastUser: undefined, - lastUserCryptPsw: undefined + store.updateStore( + { + auth: { + ...store.store.auth!, + lastUser: undefined, + lastUserCryptPsw: undefined, + }, + account: undefined, + theme: 'system', + connection: store.store.connection || false, + accountStatus: 'offline', + isCallsEnabled: false, + lastDevice: undefined, }, - account: undefined, - theme: 'system', - connection: store.store.connection || false, - accountStatus: 'offline', - isCallsEnabled: false, - lastDevice: undefined - }, 'showLogin') + 'showLogin', + ) showLogin() } } else { @@ -390,9 +430,9 @@ function attachOnReadyProcess() { showLogin() } SplashScreenController.instance.window.quit(true) - //once the loading is complete I enable the ability to click on the icon in the tray + // once the loading is complete I enable the ability to click on the icon in the tray TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) } } @@ -420,7 +460,7 @@ function attachOnReadyProcess() { } try { TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) } catch (e) { Log.error('START - unable to update the tray', e) @@ -446,7 +486,9 @@ function attachOnReadyProcess() { waitingForConnection = false stopConnectionPolling() try { - SplashScreenController.instance.window.emit(IPC_EVENTS.HIDE_NO_CONNECTION) + SplashScreenController.instance.window.emit( + IPC_EVENTS.HIDE_NO_CONNECTION, + ) } catch (e) { // Splash screen might be closed } @@ -474,8 +516,7 @@ function attachOnReadyProcess() { app.dock?.hide() }) app.on('before-quit', async (e) => { - - e.preventDefault(); + e.preventDefault() if (retryAppStart) { clearTimeout(retryAppStart) } @@ -483,7 +524,9 @@ function attachOnReadyProcess() { const account: Account = store.get('account') as Account if (account?.data) { - const ext = account.data.endpoints.extension.find((e) => e.type === "webrtc") + const ext = account.data.endpoints.extension.find( + (e) => e.type === 'webrtc', + ) if (ext && account.data.default_device?.type !== 'physical') { const { NethVoiceAPI } = useNethVoiceAPI(account) @@ -497,7 +540,7 @@ function attachOnReadyProcess() { } // read shortcut from config and unregister - Log.info("Unregister all shortcuts") + Log.info('Unregister all shortcuts') await globalShortcut.unregisterAll() // Stop uiohook for command bar @@ -509,35 +552,57 @@ function attachOnReadyProcess() { } Log.info('APP QUIT CORRECTLY') - app.exit(); + app.exit() }) } async function registryProtocol(protocol: string) { - const AppName = app.getName(); - - await Registry.set(`HKCU\\Software\\${AppName}\\Capabilities`, 'ApplicationName', AppName); - await Registry.set(`HKCU\\Software\\${AppName}\\Capabilities`, 'ApplicationDescription', AppName); - - await Registry.set(`HKCU\\Software\\${AppName}\\Capabilities\\URLAssociations`, protocol, `${AppName}.${protocol}`); - - await Registry.set(`HKCU\\Software\\Classes\\${AppName}.${protocol}\\DefaultIcon`, '', process.execPath); - - await Registry.set(`HKCU\\Software\\Classes\\${AppName}.${protocol}\\shell\\open\\command`, '', `"${process.execPath}" "%1"`); - - await Registry.set(`HKCU\\Software\\RegisteredApplications`, AppName, `Software\\${AppName}\\Capabilities`); - + const AppName = app.getName() + + await Registry.set( + `HKCU\\Software\\${AppName}\\Capabilities`, + 'ApplicationName', + AppName, + ) + await Registry.set( + `HKCU\\Software\\${AppName}\\Capabilities`, + 'ApplicationDescription', + AppName, + ) + + await Registry.set( + `HKCU\\Software\\${AppName}\\Capabilities\\URLAssociations`, + protocol, + `${AppName}.${protocol}`, + ) + + await Registry.set( + `HKCU\\Software\\Classes\\${AppName}.${protocol}\\DefaultIcon`, + '', + process.execPath, + ) + + await Registry.set( + `HKCU\\Software\\Classes\\${AppName}.${protocol}\\shell\\open\\command`, + '', + `"${process.execPath}" "%1"`, + ) + + await Registry.set( + `HKCU\\Software\\RegisteredApplications`, + AppName, + `Software\\${AppName}\\Capabilities`, + ) } async function removeRegistryProtocol(protocol: string) { - const AppName = app.getName(); - - await Registry.delete(`HKCU\\Software\\${AppName}`); + const AppName = app.getName() - await Registry.delete(`HKCU\\Software\\Classes\\${AppName}.${protocol}`); + await Registry.delete(`HKCU\\Software\\${AppName}`) - await Registry.delete(`HKCU\\Software\\RegisteredApplications`, AppName); + await Registry.delete(`HKCU\\Software\\Classes\\${AppName}.${protocol}`) + await Registry.delete(`HKCU\\Software\\RegisteredApplications`, AppName) } async function attachProtocolListeners() { @@ -551,7 +616,6 @@ async function attachProtocolListeners() { app.removeAsDefaultProtocolClient('tel') app.removeAsDefaultProtocolClient('callto') app.removeAsDefaultProtocolClient('nethlink') - } } if (process.platform === 'win32') { @@ -566,46 +630,62 @@ async function attachProtocolListeners() { Log.info('attachProtocolListeners:', process.argv.join('; ')) // set the path of electron.exe and your app. // these two additional parameters are only available on windows. - res['tel'] = app.setAsDefaultProtocolClient('tel', process.execPath, [resolve(process.argv[1])]) - res['callto'] = app.setAsDefaultProtocolClient('callto', process.execPath, [resolve(process.argv[1])]) - res['nethlink'] = app.setAsDefaultProtocolClient('nethlink', process.execPath, [resolve(process.argv[1])]) + res['tel'] = app.setAsDefaultProtocolClient('tel', process.execPath, [ + resolve(process.argv[1]), + ]) + res['callto'] = app.setAsDefaultProtocolClient('callto', process.execPath, [ + resolve(process.argv[1]), + ]) + res['nethlink'] = app.setAsDefaultProtocolClient( + 'nethlink', + process.execPath, + [resolve(process.argv[1])], + ) } else { res['tel'] = app.setAsDefaultProtocolClient('tel') res['callto'] = app.setAsDefaultProtocolClient('callto') res['nethlink'] = app.setAsDefaultProtocolClient('nethlink') - } Log.info('associated protocols:', res) - app.on('second-instance', (event, commandLine, workingDirectory, additionalData) => { - // Print out data received from the second instance. - const cmd = commandLine.pop() - Log.info('SECOND INSTANCE', { event, commandLine, workingDirectory, additionalData, cmd }) - if (!multipleInstances) { - if (cmd) { - const regex = /(\w+):(?:\/\/?)?([^\/?]+(?:\/[^?]*)?(?:\?.*)?)/; - const match = cmd.match(regex) - if (match) { - const [protocol, data] = [match[1], match[2]] - switch (protocol) { - case 'nethlink': - handleNethLinkProtocol(data); - break; - case 'tel': - case 'callto': - handleStartCallProtocols(data); - break; + app.on( + 'second-instance', + (event, commandLine, workingDirectory, additionalData) => { + // Print out data received from the second instance. + const cmd = commandLine.pop() + Log.info('SECOND INSTANCE', { + event, + commandLine, + workingDirectory, + additionalData, + cmd, + }) + if (!multipleInstances) { + if (cmd) { + const regex = /(\w+):(?:\/\/?)?([^\/?]+(?:\/[^?]*)?(?:\?.*)?)/ + const match = cmd.match(regex) + if (match) { + const [protocol, data] = [match[1], match[2]] + switch (protocol) { + case 'nethlink': + handleNethLinkProtocol(data) + break + case 'tel': + case 'callto': + handleStartCallProtocols(data) + break + } } } } - } - }) + }, + ) app.on('open-url', (ev, origin) => { handleOpenUrlProtocols(origin) }) - //I assign the app as usable for tel and callto protocol response + // I assign the app as usable for tel and callto protocol response protocol.handle('tel', (req) => { return handleStartCallProtocols(req.url) }) @@ -618,24 +698,24 @@ async function attachProtocolListeners() { }) function handleOpenUrlProtocols(url: string) { - const regex = /(\w+):(?:\/\/?)?([^\/?]+(?:\/[^?]*)?(?:\?.*)?)/; + const regex = /(\w+):(?:\/\/?)?([^\/?]+(?:\/[^?]*)?(?:\?.*)?)/ const match = url.match(regex) if (match) { const [protocol, data] = [match[1], match[2]] switch (protocol) { case 'nethlink': - handleNethLinkProtocol(data); - break; + handleNethLinkProtocol(data) + break case 'tel': case 'callto': - handleStartCallProtocols(data); - break; + handleStartCallProtocols(data) + break } } } function handleStartCallProtocols(url: string): Promise { - const regex = /(\+?\*?\d+)/; + const regex = /(\+?\*?\d+)/ const match = url.match(regex) if (match) { Log.info('HandleProtocol TEL/CALLTO:', match[0]) @@ -645,7 +725,7 @@ async function attachProtocolListeners() { } function handleNethLinkProtocol(url: string): Promise { - //we have to define the purpose of the nethlink custom protocol + // we have to define the purpose of the nethlink custom protocol Log.info('HandleProtocol Nethlink:', url) const cleanUrl = url.replace(/^nethlink:\/?\/?/, '') @@ -663,21 +743,21 @@ async function attachProtocolListeners() { Log.info('Transfer call to:', to) PhoneIslandController.instance.callTransfer(to) } - break; + break } case 'listen': { if (to) { Log.info('Listen call to:', to) PhoneIslandController.instance.listenCall(to) } - break; + break } case 'intrude': { if (to) { Log.info('Intrude call to:', to) PhoneIslandController.instance.intrudeCall(to) } - break; + break } default: Log.warning('Unknown nethlink action:', action) @@ -691,10 +771,10 @@ async function attachProtocolListeners() { } function attachPowerMonitor() { - //Define how the nethlink have to manage the power suspend and after the power resume events - powerMonitor.on('suspend', onAppSuspend); - powerMonitor.on('resume', onAppResume); - powerMonitor.on('unlock-screen', onAppResume); + // Define how the nethlink have to manage the power suspend and after the power resume events + powerMonitor.on('suspend', onAppSuspend) + powerMonitor.on('resume', onAppResume) + powerMonitor.on('unlock-screen', onAppResume) powerMonitor.on('shutdown', onAppShutdown) } @@ -716,7 +796,7 @@ async function onAppResume() { const data = store.getFromDisk() store.updateStore(data, 'onAppResume') Log.info('APP POWER RESUME') - let showNethlink = true + const showNethlink = true if (store.store.account && NethLinkController.instance) { const autoLoginResult = await AccountController.instance.autoLogin() if (autoLoginResult) { @@ -725,7 +805,9 @@ async function onAppResume() { // Don't reload PhoneIsland if there's an active call - this would destroy the call const activeCall = isCallActive() if (activeCall) { - Log.info('APP POWER RESUME - Skipping PhoneIsland reload due to active call') + Log.info( + 'APP POWER RESUME - Skipping PhoneIsland reload due to active call', + ) } else { Log.info('APP POWER RESUME - Reloading PhoneIsland (no active call)') PhoneIslandController.instance.window.getWindow()?.reload() @@ -737,37 +819,44 @@ async function onAppResume() { } function changeNethlinkTheme() { - let updatedSystemTheme: AvailableThemes = nativeTheme.shouldUseDarkColors + const updatedSystemTheme: AvailableThemes = nativeTheme.shouldUseDarkColors ? 'dark' : 'light' - //set nethlink pages theme + // set nethlink pages theme if (store.store) { - if (store.store.account?.theme === 'dark' || store.store.account?.theme === 'light') { + if ( + store.store.account?.theme === 'dark' || + store.store.account?.theme === 'light' + ) { store.set('theme', store.store.account?.theme) } else { store.set('theme', updatedSystemTheme) } } - //se tray icon theme based on system settings + // se tray icon theme based on system settings if (process.platform === 'win32') { - Registry.get(`HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize`, 'SystemUsesLightTheme').then((system) => { - Log.info('THEME CHANGE SYSTEM', system) - const theme = system === 1 ? 'light' : 'dark' - TrayController.instance?.changeIconByTheme(theme) - }).catch((e) => { - Log.error(e) - TrayController.instance?.changeIconByTheme('dark') - }); + Registry.get( + `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize`, + 'SystemUsesLightTheme', + ) + .then((system) => { + Log.info('THEME CHANGE SYSTEM', system) + const theme = system === 1 ? 'light' : 'dark' + TrayController.instance?.changeIconByTheme(theme) + }) + .catch((e) => { + Log.error(e) + TrayController.instance?.changeIconByTheme('dark') + }) } else { TrayController.instance?.changeIconByTheme(updatedSystemTheme) } } function attachThemeChangeListener() { - changeNethlinkTheme() nativeTheme.on('updated', () => { changeNethlinkTheme() @@ -778,20 +867,23 @@ function attachThemeChangeListener() { */ async function resetApp() { const availableAccounts = store.getAvailableFromDisk() - store.updateStore({ - account: undefined, - auth: { - availableAccounts: availableAccounts, - isFirstStart: Object.keys(availableAccounts).length === 0, - lastUser: undefined, - lastUserCryptPsw: undefined + store.updateStore( + { + account: undefined, + auth: { + availableAccounts: availableAccounts, + isFirstStart: Object.keys(availableAccounts).length === 0, + lastUser: undefined, + lastUserCryptPsw: undefined, + }, + theme: 'system', + connection: true, + lastDevice: undefined, + accountStatus: 'offline', + isCallsEnabled: false, }, - theme: 'system', - connection: true, - lastDevice: undefined, - accountStatus: 'offline', - isCallsEnabled: false - }, 'resetApp') + 'resetApp', + ) await delay(100) store.saveToDisk() await delay(100) @@ -799,51 +891,57 @@ async function resetApp() { async function getPermissions() { if (process.platform === 'darwin') { - //if permission has already been granted I disabled another request to improve performance + // if permission has already been granted I disabled another request to improve performance let cameraPermission = true - const cameraPermissionState = systemPreferences.getMediaAccessStatus('camera') + const cameraPermissionState = + systemPreferences.getMediaAccessStatus('camera') if (cameraPermissionState !== 'granted') { cameraPermission = await systemPreferences.askForMediaAccess('camera') } let microphonePermission = true - const microphonePermissionState = systemPreferences.getMediaAccessStatus('microphone') + const microphonePermissionState = + systemPreferences.getMediaAccessStatus('microphone') if (microphonePermissionState !== 'granted') { - microphonePermission = await systemPreferences.askForMediaAccess('microphone') + microphonePermission = + await systemPreferences.askForMediaAccess('microphone') } let recordScreenPermission = true - const recordScreenPermissionState = systemPreferences.getMediaAccessStatus('screen') + const recordScreenPermissionState = + systemPreferences.getMediaAccessStatus('screen') if (recordScreenPermissionState !== 'granted') { recordScreenPermission = false - dialog.showMessageBox({ - type: 'warning', - title: i18next.t('Common.Screen share dialog title') || 'Screen share', - message: i18next.t('Common.Screen share dialog description'), - buttons: [ - i18next.t('Common.Cancel button') || 'Cancel', - i18next.t('Common.Open settings button') || 'Open Settings' - ], - defaultId: 1, - cancelId: 0 - }).then(result => { - if (result.response === 1) { - shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording'); - } - }); + dialog + .showMessageBox({ + type: 'warning', + title: + i18next.t('Common.Screen share dialog title') || 'Screen share', + message: i18next.t('Common.Screen share dialog description'), + buttons: [ + i18next.t('Common.Cancel button') || 'Cancel', + i18next.t('Common.Open settings button') || 'Open Settings', + ], + defaultId: 1, + cancelId: 0, + }) + .then((result) => { + if (result.response === 1) { + shell.openExternal( + 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording', + ) + } + }) } - Log.info( - 'START - acquired permissions:', - { - cameraPermissionState, - cameraPermission, - microphonePermissionState, - microphonePermission, - recordScreenPermission, - recordScreenPermissionState - } - ) + Log.info('START - acquired permissions:', { + cameraPermissionState, + cameraPermission, + microphonePermissionState, + microphonePermission, + recordScreenPermission, + recordScreenPermissionState, + }) } } @@ -859,8 +957,7 @@ async function createNethLink(show: boolean = true) { await delay(500) new NethLinkController() await delay(250) - if (show) - NethLinkController.instance.show() + if (show) NethLinkController.instance.show() await delay(1000) new PhoneIslandController() checkForUpdate() @@ -886,7 +983,7 @@ async function createNethLink(show: boolean = true) { // If commandBarShortcut is '', user explicitly cleared it - don't apply any shortcut // read shortcut from config and set it to app - Log.info("Shortcut readed:", account.shortcut) + Log.info('Shortcut readed:', account.shortcut) if (account.shortcut && account.shortcut?.length > 0) { ipcMain.emit(IPC_EVENTS.CHANGE_SHORTCUT, undefined, account.shortcut) } @@ -896,37 +993,41 @@ async function createNethLink(show: boolean = true) { async function checkForUpdate() { try { Log.info('Current app version:', app.getVersion(), 'check for updates...') - const latestVersionData = await NetworkController.instance.get(GIT_RELEASES_URL) + const latestVersionData = + await NetworkController.instance.get(GIT_RELEASES_URL) Log.info('Head add version:', latestVersionData.name) - if (latestVersionData.name !== ("v" + app.getVersion()) || isDev()) { + if (latestVersionData.name !== 'v' + app.getVersion() || isDev()) { NethLinkController.instance.sendUpdateNotification() } } catch (error: any) { - Log.warning('Skipping update check after network error', error?.message || error) + Log.warning( + 'Skipping update check after network error', + error?.message || error, + ) } } function checkData(data: any): boolean { - const isValid = data?.hasOwnProperty('auth') && + const isValid = + data?.hasOwnProperty('auth') && data?.hasOwnProperty('theme') && data?.hasOwnProperty('connection') Log.info('Check if app data is valid:', isValid) return isValid - } const CONNECTIVITY_CHECK_ENDPOINTS = [ 'https://connectivitycheck.gstatic.com/generate_204', // Google's connectivity check 'https://1.1.1.1/cdn-cgi/trace', // Cloudflare - 'https://cloudflare.com/cdn-cgi/trace' // Cloudflare alternative + 'https://cloudflare.com/cdn-cgi/trace', // Cloudflare alternative ] async function checkConnection() { // Quick check using Electron's built-in net.isOnline() if (!net.isOnline()) { - Log.debug("checkConnection: net.isOnline() returned false") + Log.debug('checkConnection: net.isOnline() returned false') if (store.store.connection !== false) { - ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, false); + ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, false) } return false } @@ -936,18 +1037,21 @@ async function checkConnection() { for (const endpoint of CONNECTIVITY_CHECK_ENDPOINTS) { connected = await NetworkController.instance.head(endpoint, 3000) if (connected) { - Log.debug("checkConnection: succeeded with", endpoint) + Log.debug('checkConnection: succeeded with', endpoint) break } - Log.debug("checkConnection: failed with", endpoint, "trying next...") + Log.debug('checkConnection: failed with', endpoint, 'trying next...') } - Log.debug("checkConnection:", { connected, connection: store.store.connection }) + Log.debug('checkConnection:', { + connected, + connection: store.store.connection, + }) if (connected !== store.store.connection) { - ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, connected); + ipcMain.emit(IPC_EVENTS.UPDATE_CONNECTION_STATE, undefined, connected) } return connected } -//BEGIN APP +// BEGIN APP startup() diff --git a/src/preload/index.ts b/src/preload/index.ts index a4b8a061..692e1aa2 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,36 +1,39 @@ -import { NotificationConstructorOptions, contextBridge, ipcRenderer } from 'electron' +import { + NotificationConstructorOptions, + contextBridge, + ipcRenderer, +} from 'electron' import os from 'os' import { electronAPI } from '@electron-toolkit/preload' import { IPC_EVENTS } from '@shared/constants' -import { - Account, - ContactType, - Size, -} from '@shared/types' +import { Account, ContactType, Size } from '@shared/types' import { preloadBindings } from 'i18next-electron-fs-backend' export interface IElectronAPI { - env: NodeJS.ProcessEnv, - appVersion: string, - platform: string, - osRelease: string, - arch: string, + env: NodeJS.ProcessEnv + appVersion: string + platform: string + osRelease: string + arch: string // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. - //TRANSLATIONS + // TRANSLATIONS i18nextElectronBackend: any - //SYNC EMITTERS - expect response - login: (host: string, username: string, password: string) => Promise + // SYNC EMITTERS - expect response + login: ( + host: string, + username: string, + password: string, + ) => Promise deleteSpeedDial(contact: ContactType): Promise getLocale(): Promise onStartCall(callback: (number: string | number) => void): void onUpdateAppNotification(showUpdateAppNotification: () => void): void - - //EMITTER - only emit, no response + // EMITTER - only emit, no response openDevTool(hash: string): unknown sendNotification( title: string, @@ -49,14 +52,14 @@ export interface IElectronAPI { hidePhoneIsland(): void showPhoneIsland(size: Size): void copyToClipboard(text: string): void - } const customElectronAPI = { ...electronAPI, send: (channel, ...data) => ipcRenderer.send(channel, ...data), - receive: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)), - removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel) + receive: (channel, func) => + ipcRenderer.on(channel, (event, ...args) => func(...args)), + removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel), } function addListener(channel) { @@ -70,12 +73,15 @@ function addListener(channel) { function setEmitterSync(event): () => Promise { return async (...args): Promise => { return await new Promise((resolve, reject) => { - //this timout is used to execute the react setters before sendSync freezes the UI + // this timout is used to execute the react setters before sendSync freezes the UI setTimeout(() => { - const [returnValue, err] = ipcRenderer.sendSync(event, ...args) as [T, Error | undefined] + const [returnValue, err] = ipcRenderer.sendSync(event, ...args) as [ + T, + Error | undefined, + ] if (err) reject(err) else resolve(returnValue) - }, 100); + }, 100) }) } } @@ -94,11 +100,11 @@ const api: IElectronAPI = { osRelease: os.release(), arch: process.arch, i18nextElectronBackend: preloadBindings(ipcRenderer, process), - //SYNC EMITTERS - expect response + // SYNC EMITTERS - expect response login: setEmitterSync(IPC_EVENTS.LOGIN), getLocale: setEmitterSync(IPC_EVENTS.GET_LOCALE), - //EMITTER - only emit, no response + // EMITTER - only emit, no response sendInitializationCompleted: setEmitter(IPC_EVENTS.INITIALIZATION_COMPELTED), openDevTool: setEmitter(IPC_EVENTS.OPEN_DEV_TOOLS), hideLoginWindow: setEmitter(IPC_EVENTS.HIDE_LOGIN_WINDOW), @@ -113,9 +119,8 @@ const api: IElectronAPI = { copyToClipboard: setEmitter(IPC_EVENTS.COPY_TO_CLIPBOARD), sendNotification: setEmitter(IPC_EVENTS.SEND_NOTIFICATION), - //LISTENERS - receive data async + // LISTENERS - receive data async onUpdateAppNotification: addListener(IPC_EVENTS.UPDATE_APP_NOTIFICATION), - } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index cd9abbfa..5140e346 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,6 +1,12 @@ import { Outlet, RouterProvider, createHashRouter } from 'react-router-dom' import { useInitialize } from '@/hooks/useInitialize' -import { LoginPage, PhoneIslandPage, SplashScreenPage, NethLinkPage, CommandBarPage } from '@/pages' +import { + LoginPage, + PhoneIslandPage, + SplashScreenPage, + NethLinkPage, + CommandBarPage, +} from '@/pages' import { loadI18n } from './lib/i18n' import { Log } from '@shared/utils/logger' import { useEffect, useState } from 'react' @@ -9,7 +15,7 @@ import { delay } from '@shared/utils/utils' import i18next from 'i18next' import { DevToolsPage } from './pages/DevToolsPage' import { parseThemeToClassName } from './utils' -import { useRegisterStoreHook, useSharedState } from "@renderer/store"; +import { useRegisterStoreHook, useSharedState } from '@renderer/store' import { PageContext, usePageCtx } from './contexts/pageContext' import { IPC_EVENTS } from '@shared/constants' import { useNetwork } from '@shared/useNetwork' @@ -18,16 +24,16 @@ import './index.css' const RequestStateComponent = () => { const pageData = usePageCtx() useRegisterStoreHook() - const [theme,] = useSharedState('theme') - const [account,] = useSharedState('account') - const [connection,] = useSharedState('connection') + const [theme] = useSharedState('theme') + const [account] = useSharedState('account') + const [connection] = useSharedState('connection') const [hasWindowConfig, setHasWindowConfig] = useState(false) const { HEAD } = useNetwork() const CONNECTIVITY_CHECK_ENDPOINTS = [ 'https://connectivitycheck.gstatic.com/generate_204', // Google's connectivity check 'https://1.1.1.1/cdn-cgi/trace', // Cloudflare - 'https://cloudflare.com/cdn-cgi/trace' // Cloudflare alternative + 'https://cloudflare.com/cdn-cgi/trace', // Cloudflare alternative ] async function checkConnection() { @@ -35,7 +41,7 @@ const RequestStateComponent = () => { if (!navigator.onLine) { Log.debug('check connection: navigator.onLine returned false') if (connection !== false) { - window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, false); + window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, false) } return } @@ -53,7 +59,7 @@ const RequestStateComponent = () => { Log.debug('check connection', { connected, connection: connection }) if (connected !== connection) { - window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, connected); + window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, connected) } } @@ -73,7 +79,7 @@ const RequestStateComponent = () => { SIP_HOST: account.sipHost, SIP_PORT: account.sipPort, TIMEZONE: account.timezone, - VOICE_ENDPOINT: account.voiceEndpoint + VOICE_ENDPOINT: account.voiceEndpoint, } Log.info('WINDOW CONFIG', pageData?.page, window['CONFIG']) setHasWindowConfig(true) @@ -87,7 +93,7 @@ const RequestStateComponent = () => { const loader = async () => { Log.debug('check i18n initialization') let time = 0 - //I wait for the language to load or 200 milliseconds + // I wait for the language to load or 200 milliseconds while (time < 20 && !i18next.isInitialized) { await delay(10) time++ @@ -98,63 +104,74 @@ const RequestStateComponent = () => { const router = createHashRouter([ { path: '/', - element: , + element: ( + + ), loader: loader, children: [ { path: PAGES.SPLASHSCREEN, - element: + element: ( + + ), }, { path: PAGES.LOGIN, - element: + element: ( + + ), }, { path: PAGES.PHONEISLAND, - element: hasWindowConfig && + element: hasWindowConfig && , }, { path: PAGES.NETHLINK, - element: + element: , }, { path: PAGES.DEVTOOLS, - element: + element: , }, { path: PAGES.COMMANDBAR, - element: - } - ] - } + element: , + }, + ], + }, ]) return } -const Layout = ({ theme, page }: { theme?: AvailableThemes, page?: PAGES }) => { - - const [isCSSLoaded, setIsCSSLoaded] = useState(false); +const Layout = ({ theme, page }: { theme?: AvailableThemes; page?: PAGES }) => { + const [isCSSLoaded, setIsCSSLoaded] = useState(false) useEffect(() => { if (page) { importStyle() } - }, [page]); + }, [page]) const importStyle = async () => { // Importing CSS dynamically when the page is not 'PHONEISLAND' if (page === PAGES.PHONEISLAND) { await import('@nethesis/phone-island/dist/index.css') } - setIsCSSLoaded(true); + setIsCSSLoaded(true) } if (!isCSSLoaded) { - return ; + return } return ( <> - + > @@ -162,7 +179,6 @@ const Layout = ({ theme, page }: { theme?: AvailableThemes, page?: PAGES }) => { } export default function App() { - useInitialize(() => { Log.debug('initialize i18n') loadI18n() diff --git a/src/renderer/src/components/ConnectionErrorDialog.tsx b/src/renderer/src/components/ConnectionErrorDialog.tsx index 6f2f77c0..9441ff81 100644 --- a/src/renderer/src/components/ConnectionErrorDialog.tsx +++ b/src/renderer/src/components/ConnectionErrorDialog.tsx @@ -1,42 +1,58 @@ -import { faWarning } from "@fortawesome/free-solid-svg-icons" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { t } from "i18next" -import { Button } from "./Nethesis" -import classNames from "classnames" -import { useTheme } from "@renderer/theme/Context" - +import { faWarning } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { t } from 'i18next' +import { Button } from './Nethesis' +import classNames from 'classnames' +import { useTheme } from '@renderer/theme/Context' interface ConnectionErrorDialogProps { - onButtonClick: () => void, - buttonText: string, + onButtonClick: () => void + buttonText: string variant: 'splashscreen' | 'nethlink' className?: string } -export const ConnectionErrorDialog = ({ buttonText, onButtonClick, variant, className }: ConnectionErrorDialogProps) => { - +export const ConnectionErrorDialog = ({ + buttonText, + onButtonClick, + variant, + className, +}: ConnectionErrorDialogProps) => { const { theme } = useTheme() return ( <> - - + + - + - {t('Common.No internet connection title')} - {t('Common.No internet connection description')} + + {t('Common.No internet connection title')} + + + {t('Common.No internet connection description')} + - {buttonText} + + {buttonText} + > diff --git a/src/renderer/src/components/ElectronDraggableWindow.tsx b/src/renderer/src/components/ElectronDraggableWindow.tsx index 7faf71c9..127c2a4e 100644 --- a/src/renderer/src/components/ElectronDraggableWindow.tsx +++ b/src/renderer/src/components/ElectronDraggableWindow.tsx @@ -1,10 +1,9 @@ -import { useRef, useEffect } from 'react'; -import { IPC_EVENTS } from '@shared/constants'; -import classNames from 'classnames'; -import { isDev } from '@shared/utils/utils'; +import { useRef, useEffect } from 'react' +import { IPC_EVENTS } from '@shared/constants' +import classNames from 'classnames' +import { isDev } from '@shared/utils/utils' export const ElectronDraggableWindow = ({ children }) => { - const isDrag = useRef(false) const passthroughEvent = useRef(false) const mouseDownEvent = useRef(null) @@ -26,54 +25,54 @@ export const ElectronDraggableWindow = ({ children }) => { target.current = null passthroughEvent.current = false } - }; + } const handleMouseDown = (e: MouseEvent) => { - const now = Date.now(); + const now = Date.now() if (now - lastMouseDownTime.current < 100) { - return; + return } - lastMouseDownTime.current = now; + lastMouseDownTime.current = now if (!isDrag.current && !mouseDownEvent.current) { target.current = e.target passthroughEvent.current = false - window.electron.send(IPC_EVENTS.START_DRAG); + window.electron.send(IPC_EVENTS.START_DRAG) mouseDownEvent.current = e mouseUpEvent.current = null isDrag.current = true } - }; + } const handleMouseUp = (e: MouseEvent) => { mouseDownEvent.current = null - window.electron.send(IPC_EVENTS.STOP_DRAG); + window.electron.send(IPC_EVENTS.STOP_DRAG) isDrag.current = false mouseUpEvent.current = e - }; + } const handleMouseLeave = () => { if (isDrag.current) { - window.electron.send(IPC_EVENTS.STOP_DRAG); - isDrag.current = false; - mouseDownEvent.current = null; + window.electron.send(IPC_EVENTS.STOP_DRAG) + isDrag.current = false + mouseDownEvent.current = null } - }; + } const handleWindowBlur = () => { if (isDrag.current) { - isDrag.current = false; - mouseDownEvent.current = null; - window.electron.send(IPC_EVENTS.STOP_DRAG); + isDrag.current = false + mouseDownEvent.current = null + window.electron.send(IPC_EVENTS.STOP_DRAG) } - }; + } useEffect(() => { - document.addEventListener('click', handleMouseClick, { capture: true }); - document.addEventListener('mousedown', handleMouseDown, { capture: true }); - document.addEventListener('mouseup', handleMouseUp, { capture: true }); - window.addEventListener('mouseleave', handleMouseLeave); - window.addEventListener('blur', handleWindowBlur); + document.addEventListener('click', handleMouseClick, { capture: true }) + document.addEventListener('mousedown', handleMouseDown, { capture: true }) + document.addEventListener('mouseup', handleMouseUp, { capture: true }) + window.addEventListener('mouseleave', handleMouseLeave) + window.addEventListener('blur', handleWindowBlur) window.electron.receive(IPC_EVENTS.ENABLE_CLICK, () => { if (target.current && !passthroughEvent.current) { @@ -82,26 +81,29 @@ export const ElectronDraggableWindow = ({ children }) => { bubbles: true, cancelable: true, view: window, - }); + }) newEvent['nethlink'] = true - target.current.dispatchEvent(newEvent); + target.current.dispatchEvent(newEvent) } - }); + }) return () => { - document.removeEventListener('click', handleMouseClick); - document.removeEventListener('mousedown', handleMouseDown); - document.removeEventListener('mouseup', handleMouseUp); - window.removeEventListener('mouseleave', handleMouseLeave); - window.removeEventListener('blur', handleWindowBlur); - }; - }, []); + document.removeEventListener('click', handleMouseClick) + document.removeEventListener('mousedown', handleMouseDown) + document.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('mouseleave', handleMouseLeave) + window.removeEventListener('blur', handleWindowBlur) + } + }, []) return ( {children} - ); -}; \ No newline at end of file + ) +} diff --git a/src/renderer/src/components/EmptyList.tsx b/src/renderer/src/components/EmptyList.tsx index cd44f1dd..6b8197b8 100644 --- a/src/renderer/src/components/EmptyList.tsx +++ b/src/renderer/src/components/EmptyList.tsx @@ -1,25 +1,32 @@ -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import classNames from "classnames" -import { TimedComponent } from "./TimedComponent" - +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import classNames from 'classnames' +import { TimedComponent } from './TimedComponent' export interface EmptyListProps { - icon: IconProp, - text: string, + icon: IconProp + text: string subtitle?: string } export const EmptyList = ({ icon, text, subtitle }: EmptyListProps) => { return ( - - - + + + - - {text} + + + {text} + {subtitle && ( - {subtitle} + + {subtitle} + )} diff --git a/src/renderer/src/components/Modal/ModalActions.tsx b/src/renderer/src/components/Modal/ModalActions.tsx index 67d8af12..5e377702 100644 --- a/src/renderer/src/components/Modal/ModalActions.tsx +++ b/src/renderer/src/components/Modal/ModalActions.tsx @@ -13,7 +13,9 @@ import { useTheme } from '@renderer/theme/Context' import { cleanClassName } from '@renderer/utils' import type { FC, PropsWithChildren, ComponentProps } from 'react' -export type ModalActionsProps = PropsWithChildren, 'className'>> +export type ModalActionsProps = PropsWithChildren< + Omit, 'className'> +> export const ModalActions: FC = ({ children, ...props }) => { const { modal: theme } = useTheme().theme diff --git a/src/renderer/src/components/Modal/ModalContent.tsx b/src/renderer/src/components/Modal/ModalContent.tsx index 119426dd..bf15c4f9 100644 --- a/src/renderer/src/components/Modal/ModalContent.tsx +++ b/src/renderer/src/components/Modal/ModalContent.tsx @@ -13,7 +13,9 @@ import { useTheme } from '@renderer/theme/Context' import { cleanClassName } from '@renderer/utils' import type { FC, PropsWithChildren, ComponentProps } from 'react' -export type ModalContentProps = PropsWithChildren, 'className'>> +export type ModalContentProps = PropsWithChildren< + Omit, 'className'> +> export const ModalContent: FC = ({ children, ...props }) => { const { modal: theme } = useTheme().theme diff --git a/src/renderer/src/components/Modal/index.tsx b/src/renderer/src/components/Modal/index.tsx index 80a50315..2175c3be 100644 --- a/src/renderer/src/components/Modal/index.tsx +++ b/src/renderer/src/components/Modal/index.tsx @@ -13,8 +13,14 @@ * */ -import { FC, ComponentProps, PropsWithChildren, RefObject } from 'react' -import { Fragment, createRef } from 'react' +import { + FC, + ComponentProps, + PropsWithChildren, + RefObject, + Fragment, + createRef, +} from 'react' import { ModalContent } from './ModalContent' import { ModalActions } from './ModalActions' import { Transition, Dialog } from '@headlessui/react' @@ -46,25 +52,38 @@ const ModalComponent: FC = ({ const cleanProps = cleanClassName(props) return ( - afterLeave && afterLeave()}> + afterLeave && afterLeave()} + > onClose()} initialFocus={focus && focus} {...cleanProps} > - + - - + ' + > + - {children} + + {children} + @@ -79,5 +98,5 @@ ModalActions.displayName = 'Modal.Actions' export const Modal = Object.assign(ModalComponent, { Content: ModalContent, - Actions: ModalActions + Actions: ModalActions, }) diff --git a/src/renderer/src/components/ModuleTitle.tsx b/src/renderer/src/components/ModuleTitle.tsx index f4c69f3e..6a6bdc26 100644 --- a/src/renderer/src/components/ModuleTitle.tsx +++ b/src/renderer/src/components/ModuleTitle.tsx @@ -1,20 +1,19 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { Button } from "./Nethesis" -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import { DefaultTFuncReturn } from "i18next" -import { ReactNode, useId } from "react" -import { ClassNames } from "@renderer/utils" -import { useIsTruncated } from "@renderer/hooks/useIsTruncated" -import { CustomThemedTooltip } from "./Nethesis/CustomThemedTooltip" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Button } from './Nethesis' +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { DefaultTFuncReturn } from 'i18next' +import { ReactNode, useId } from 'react' +import { ClassNames } from '@renderer/utils' +import { useIsTruncated } from '@renderer/hooks/useIsTruncated' +import { CustomThemedTooltip } from './Nethesis/CustomThemedTooltip' export interface ModuleTitleProps { - title?: string | DefaultTFuncReturn | ReactNode, - action?: () => void, - actionText?: string | DefaultTFuncReturn, + title?: string | DefaultTFuncReturn | ReactNode + action?: () => void + actionText?: string | DefaultTFuncReturn actionIcon?: IconProp - actionComponent?: ReactNode, + actionComponent?: ReactNode className?: string - } export const ModuleTitle = ({ title, @@ -22,40 +21,53 @@ export const ModuleTitle = ({ actionText, actionIcon, actionComponent, - className + className, }: ModuleTitleProps) => { - const tooltipId = useId() - const [titleRef, isTitleTruncated] = useIsTruncated([title]) + const [titleRef, isTitleTruncated] = useIsTruncated([ + title, + ]) const showTitleTooltip = typeof title === 'string' && isTitleTruncated return ( - - - {title && - {title} - {showTitleTooltip && } - } + + + {title && ( + + {title} + {showTitleTooltip && ( + + )} + + )} {action && ( - {actionIcon && } - {actionText && - {actionText} - - } + {actionIcon && ( + + )} + {actionText && ( + + {actionText} + + )} )} {actionComponent} diff --git a/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx b/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx index 87eb69c1..32c8db70 100644 --- a/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx @@ -10,9 +10,9 @@ import { Scrollable } from '@renderer/components/Scrollable' import { ModuleTitle } from '@renderer/components/ModuleTitle' import { parseThemeToClassName } from '@renderer/utils' -export interface AboutBoxProps { } +export interface AboutBoxProps {} -export function AboutModule({ }: AboutBoxProps) { +export function AboutModule() { const page = usePageCtx() const [theme] = useSharedState('theme') const [notifications] = useSharedState('notifications') @@ -35,38 +35,44 @@ export function AboutModule({ }: AboutBoxProps) { <> - - + + - + NethLink by{' '} {account?.companyName} - + {t('About.current_version', { version: window.api.appVersion })} {!!notifications?.system?.update && ( - - + + {t('About.update_available')} - - - - {t('About.download')} + + + + + {t('About.download')} + diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx index c77ca856..2ad59f5a 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx @@ -1,12 +1,15 @@ -import classNames from "classnames" +import classNames from 'classnames' interface BackdropProps { - onBackdropClick: () => void, + onBackdropClick: () => void className?: string } export function Backdrop({ onBackdropClick, className }: BackdropProps) { return ( { e.preventDefault() e.stopPropagation() diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx index d73df74e..343858fd 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx @@ -9,7 +9,10 @@ import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventH import { useIsTruncated } from '@renderer/hooks/useIsTruncated' import { Avatar } from '../../../Nethesis' import { CustomThemedTooltip } from '../../../Nethesis/CustomThemedTooltip' -import { faPhone as CallIcon, faBriefcase as CompanyIcon } from '@fortawesome/free-solid-svg-icons' +import { + faPhone as CallIcon, + faBriefcase as CompanyIcon, +} from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { ReactNode } from 'react' import classNames from 'classnames' @@ -89,7 +92,9 @@ export const ContactNameAndActions = ({ contact.company !== displayName // Attach tooltips only when the text is actually truncated - const [nameRef, isNameTruncated] = useIsTruncated([displayName]) + const [nameRef, isNameTruncated] = useIsTruncated([ + displayName, + ]) const [companyRef, isCompanyTruncated] = useIsTruncated([ contact.company, ]) @@ -138,8 +143,18 @@ export const ContactNameAndActions = ({ )} onClick={onClick} /> - - + + {nameTooltipContent && ( - + )} {displayName} {isDev() && ( @@ -207,7 +222,9 @@ export const ContactNameAndActions = ({ 0) + (!Array.isArray(displayedNumber) || + displayedNumber.length > 0) ? displayedNumber : '-'} @@ -255,8 +273,12 @@ export const ContactNameAndActions = ({ {contact.company} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx index 7c6806d8..f98c5880 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx @@ -29,8 +29,12 @@ export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { 'isCommandBarShortcutDialogOpen', ) const [isDeviceDialogOpen] = useNethlinkData('isDeviceDialogOpen') - const [isIncomingCallsDialogOpen] = useNethlinkData('isIncomingCallsDialogOpen') - const [isNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const [isIncomingCallsDialogOpen] = useNethlinkData( + 'isIncomingCallsDialogOpen', + ) + const [isNotificationsDialogOpen] = useNethlinkData( + 'isNotificationsDialogOpen', + ) const isCallSummaryEnabled = account?.data?.call_summary_enabled === true const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false) @@ -67,7 +71,9 @@ export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { {isCommandBarShortcutDialogOpen && } {isDeviceDialogOpen && } {isIncomingCallsDialogOpen && } - {isCallSummaryEnabled && isNotificationsDialogOpen && } + {isCallSummaryEnabled && isNotificationsDialogOpen && ( + + )} setIsProfileDialogOpen(false)} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/DeviceSettings/DeviceBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/DeviceSettings/DeviceBox.tsx index 7843bc26..55693485 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/DeviceSettings/DeviceBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/DeviceSettings/DeviceBox.tsx @@ -1,33 +1,41 @@ -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import { - NethLinkLogoIcon as DesktopDevice, -} from '@renderer/icons' +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { NethLinkLogoIcon as DesktopDevice } from '@renderer/icons' import { IconDefinition, - faOfficePhone as PhysicalDevice + faOfficePhone as PhysicalDevice, } from '@nethesis/nethesis-solid-svg-icons' -import { useSharedState } from "@renderer/store" -import { AvailableDevices, Extension, ExtensionsType, StatusTypes } from "@shared/types" -import { Log } from "@shared/utils/logger" -import { t } from "i18next" -import { OptionElement } from "../OptionElement" -import { debouncer } from "@shared/utils/utils" -import { IPC_EVENTS } from "@shared/constants" -import { useEffect, useState } from "react" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" +import { useSharedState } from '@renderer/store' +import { + AvailableDevices, + Extension, + ExtensionsType, + StatusTypes, +} from '@shared/types' +import { Log } from '@shared/utils/logger' +import { t } from 'i18next' +import { OptionElement } from '../OptionElement' +import { debouncer } from '@shared/utils/utils' +import { IPC_EVENTS } from '@shared/constants' +import { useEffect, useState } from 'react' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' export const DeviceIcons = { nethlink: { - Icon: + Icon: , }, webrtc: { - Icon: + Icon: , }, - physical: PhysicalDevice + physical: PhysicalDevice, +} +type DeviceType = { + name: AvailableDevices + label: string + icon?: IconProp | IconDefinition + iconElem?: JSX.Element } -type DeviceType = { name: AvailableDevices, label: string, icon?: IconProp | IconDefinition, iconElem?: JSX.Element } type AvailableDeviceOption = { - id: string, + id: string status: StatusTypes ext: Extension } & DeviceType @@ -42,50 +50,68 @@ export const DeviceBox = () => { }) }, []) - async function handleSetDevice(newDevice: AvailableDeviceOption) { Log.info('change device to', newDevice) setDevice(() => ({ id: newDevice.id, type: newDevice.name, - status: newDevice.status + status: newDevice.status, })) - debouncer('update-account-default-device', () => { - window.electron.send(IPC_EVENTS.CHANGE_DEFAULT_DEVICE, newDevice.ext, true) - }, 250) + debouncer( + 'update-account-default-device', + () => { + window.electron.send( + IPC_EVENTS.CHANGE_DEFAULT_DEVICE, + newDevice.ext, + true, + ) + }, + 250, + ) } const themeOptions = { - nethlink: { iconElem: DeviceIcons.nethlink.Icon, label: t('Settings.Only nethlink') }, + nethlink: { + iconElem: DeviceIcons.nethlink.Icon, + label: t('Settings.Only nethlink'), + }, physical: { icon: DeviceIcons.physical, label: t('Settings.IP Phone') }, } if (!account) return <>> - const nethlink = account.data!.endpoints.extension.find((e) => e.type === 'nethlink')! - const accountDevices: AvailableDeviceOption[] = account.data?.endpoints.extension.reduce((p, d) => { - if (d.type === 'physical') { - const status = devicesStatus[d.id]?.status - const isOffline = status !== 'online' - p.push({ - ...themeOptions.physical, - name: 'physical', - label: `${d.description || themeOptions.physical.label} ${isOffline ? '(offline)' : ''}`, - status, - id: d.id, - ext: d - } as AvailableDeviceOption) - } - return p - }, [{ - ...themeOptions.nethlink, - status: 'online', - name: 'nethlink', - id: nethlink?.id, - ext: nethlink - }]) || [] + const nethlink = account.data!.endpoints.extension.find( + (e) => e.type === 'nethlink', + )! + const accountDevices: AvailableDeviceOption[] = + account.data?.endpoints.extension.reduce( + (p, d) => { + if (d.type === 'physical') { + const status = devicesStatus[d.id]?.status + const isOffline = status !== 'online' + p.push({ + ...themeOptions.physical, + name: 'physical', + label: `${d.description || themeOptions.physical.label} ${isOffline ? '(offline)' : ''}`, + status, + id: d.id, + ext: d, + } as AvailableDeviceOption) + } + return p + }, + [ + { + ...themeOptions.nethlink, + status: 'online', + name: 'nethlink', + id: nethlink?.id, + ext: nethlink, + }, + ], + ) || [] return ( - + {accountDevices.map((availableDevices) => ( { - function handleGoToNethVoicePage() { window.api.openHostPage('/') } return ( - - {t('TopBar.Go to NethVoice CTI')} + + {t('TopBar.Go to NethVoice CTI')} ) } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/OptionElement.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/OptionElement.tsx index b5c92ef3..eba1e9e6 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/OptionElement.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/OptionElement.tsx @@ -1,17 +1,15 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { - faCheck as ChooseThemeMenuIcon, -} from '@fortawesome/free-solid-svg-icons' -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import classNames from "classnames" -import { IconDefinition } from "@nethesis/nethesis-solid-svg-icons" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCheck as ChooseThemeMenuIcon } from '@fortawesome/free-solid-svg-icons' +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import classNames from 'classnames' +import { IconDefinition } from '@nethesis/nethesis-solid-svg-icons' type SettingsOptionElementProps = { - icon?: IconProp | IconDefinition, - iconElem?: JSX.Element, - label: string, + icon?: IconProp | IconDefinition + iconElem?: JSX.Element + label: string isSelected: boolean - onClick: () => void, + onClick: () => void } export const OptionElement = ({ icon, @@ -20,33 +18,31 @@ export const OptionElement = ({ isSelected, onClick, }: SettingsOptionElementProps) => { - return ( onClick()} > - + {iconElem ? ( + iconElem + ) : icon ? ( + + ) : ( + <>> )} - > - {iconElem ? iconElem : icon ? : <>>} - - {label} - + {label} {isSelected && ( diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx index 7a9a2d0b..018b9428 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx @@ -1,5 +1,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faArrowRight, faMobile, faVoicemail } from '@fortawesome/free-solid-svg-icons' +import { + faArrowRight, + faMobile, + faVoicemail, +} from '@fortawesome/free-solid-svg-icons' import { Badge } from '@renderer/components/Nethesis/Badge' import { useNethlinkData, useSharedState } from '@renderer/store' import { StatusTypes } from '@shared/types' @@ -25,8 +29,13 @@ export const PresenceBadge = ({ className }: PresenceBadgeProps) => { }, [account]) if (PresenceBadgeVisibility.includes(mainPresence as string)) { - const isCallforward = ['callforward', 'voicemail'].includes(mainPresence as string) - if (!(account?.data?.mainextension && operators?.extensions[account.data.mainextension])) { + const isCallforward = ['callforward', 'voicemail'].includes( + mainPresence as string, + ) + if (!( + account?.data?.mainextension && + operators?.extensions[account.data.mainextension] + )) { return ( { theme.base, theme.rounded['full'], statuses[isCallforward ? 'callforward' : 'voicemail']?.badge.base, - theme.sizes['small'] + theme.sizes['small'], )} > ) @@ -42,26 +51,28 @@ export const PresenceBadge = ({ className }: PresenceBadgeProps) => { return ( { ? faMobile : faVoicemail } - className="h-4 w-4 text-topBarText dark:text-topBarTextDark" - aria-hidden="true" + className='h-4 w-4 text-topBarText dark:text-topBarTextDark' + aria-hidden='true' /> - ) } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceItem.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceItem.tsx index 5a1fd1e3..c77bcd04 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceItem.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceItem.tsx @@ -1,18 +1,18 @@ -import { StatusTypes } from "@shared/types"; +import { StatusTypes } from '@shared/types' import { IconDefinition, faCheck as ChooseThemeMenuIcon, -} from "@fortawesome/free-solid-svg-icons"; -import { StatusDot } from "@renderer/components/Nethesis"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useAccount } from "@renderer/hooks/useAccount"; -import classNames from "classnames"; +} from '@fortawesome/free-solid-svg-icons' +import { StatusDot } from '@renderer/components/Nethesis' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useAccount } from '@renderer/hooks/useAccount' +import classNames from 'classnames' export interface PresenceItemProps { - presenceName: string, - presenceDescription: string, - status: StatusTypes, - icon?: IconDefinition, - hasTopBar?: boolean, + presenceName: string + presenceDescription: string + status: StatusTypes + icon?: IconDefinition + hasTopBar?: boolean onClick: (status: StatusTypes) => void } @@ -22,9 +22,8 @@ export function PresenceItem({ status, hasTopBar, icon, - onClick + onClick, }: PresenceItemProps) { - const { status: currentStatus } = useAccount() const handleClick = (e) => { @@ -35,23 +34,29 @@ export function PresenceItem({ return ( <> - {hasTopBar ? : null} - + ) : null} + - - + + - + {presenceName} {icon && } - {presenceDescription} + {presenceDescription} {currentStatus === status && ( diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/usePresenceService.ts b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/usePresenceService.ts index ff899a3a..fdc37f2f 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/usePresenceService.ts +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/usePresenceService.ts @@ -1,15 +1,18 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { usePhoneIslandEventHandler } from "@renderer/hooks/usePhoneIslandEventHandler" -import { useSharedState } from "@renderer/store" -import { StatusTypes } from "@shared/types" -import { Log } from "@shared/utils/logger" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventHandler' +import { useSharedState } from '@renderer/store' +import { StatusTypes } from '@shared/types' +import { Log } from '@shared/utils/logger' export const usePresenceService = () => { const { saveOperators } = usePhoneIslandEventHandler() const [account, setAccount] = useSharedState('account') const { NethVoiceAPI } = useLoggedNethVoiceAPI() - async function onSelectPresence(status: StatusTypes, to: string | undefined = undefined) { + async function onSelectPresence( + status: StatusTypes, + to: string | undefined = undefined, + ) { try { await NethVoiceAPI.User.setPresence(status, to) const me = await NethVoiceAPI.User.me() @@ -20,16 +23,15 @@ export const usePresenceService = () => { ...account!, data: { ...account!.data!, - ...me - } + ...me, + }, }) } catch (e) { Log.error('ON PRESENCE CHANGE', e) - } finally { } } return { - onSelectPresence + onSelectPresence, } } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ProfileData.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ProfileData.tsx index 6939a7f2..20709e06 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ProfileData.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ProfileData.tsx @@ -1,24 +1,26 @@ -import { useSharedState } from "@renderer/store" -import { isDev } from "@shared/utils/utils" -import { t } from "i18next" -import { truncate } from "lodash" +import { useSharedState } from '@renderer/store' +import { isDev } from '@shared/utils/utils' +import { t } from 'i18next' +import { truncate } from 'lodash' export const ProfileData = () => { const [account] = useSharedState('account') if (!account) return <>No account> return ( - - {t('TopBar.Signed in as')} - - + + + {t('TopBar.Signed in as')} + + + {truncate(account.data?.name, { length: 20 })} - + {account.data?.endpoints.mainextension[0].id} {isDev() && ( - + [{account.data?.default_device.type}] )} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsBox.tsx index 6e45248e..15aac7d5 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsBox.tsx @@ -16,12 +16,16 @@ export function SettingsBox({ onClose }: { onClose?: () => void }) { 'isCommandBarShortcutDialogOpen', ) const [, setIsDeviceDialogOpen] = useNethlinkData('isDeviceDialogOpen') - const [, setIsIncomingCallsDialogOpen] = useNethlinkData('isIncomingCallsDialogOpen') - const [, setIsNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const [, setIsIncomingCallsDialogOpen] = useNethlinkData( + 'isIncomingCallsDialogOpen', + ) + const [, setIsNotificationsDialogOpen] = useNethlinkData( + 'isNotificationsDialogOpen', + ) const isCallSummaryEnabled = account?.data?.call_summary_enabled === true return ( - + newKeys.has(k)) - const others = [...newKeys].filter((k) => !orderedModifiers.includes(k as any)) + const others = [...newKeys].filter( + (k) => !orderedModifiers.includes(k as any), + ) // Require at least one modifier key — reject bare keys like "6", "R", etc. // Solo modifiers (e.g. just "Ctrl") are still allowed for double-tap. @@ -205,7 +208,10 @@ export function SettingsCommandBarShortcutDialog() { } - + diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx index fdf10055..35d7dc2e 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsDevicesDialog.tsx @@ -211,35 +211,40 @@ export function SettingsDeviceDialog() { { - return ( - { - console.log('change device:', device.deviceId) - handleDeviceChange(deviceType, device.deviceId) - }} - > - - - {device.label} - - - - - ) - }) || []} + items={ + devices?.[deviceType]?.map((device) => { + return ( + { + console.log('change device:', device.deviceId) + handleDeviceChange( + deviceType, + device.deviceId, + ) + }} + > + + + {device.label} + + + + + ) + }) || [] + } className='w-full' > @@ -297,4 +302,4 @@ export function SettingsDeviceDialog() { > ) -} \ No newline at end of file +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsIncomingCallsDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsIncomingCallsDialog.tsx index d9a98ce1..92e9eec8 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsIncomingCallsDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsIncomingCallsDialog.tsx @@ -96,9 +96,13 @@ const setOutputDeviceToLocalStorage = (value: string): void => { } export function SettingsIncomingCallsDialog() { - const [, setIsIncomingCallsDialogOpen] = useNethlinkData('isIncomingCallsDialogOpen') + const [, setIsIncomingCallsDialogOpen] = useNethlinkData( + 'isIncomingCallsDialogOpen', + ) const [availableRingtones] = useSharedState('availableRingtones') - const [audioOutputDevices, setAudioOutputDevices] = useState([]) + const [audioOutputDevices, setAudioOutputDevices] = useState< + MediaDeviceInfo[] + >([]) const [playingRingtone, setPlayingRingtone] = useState(null) const [formData, setFormData] = useState({ ringtone: '', @@ -106,9 +110,9 @@ export function SettingsIncomingCallsDialog() { }) // Convert availableRingtones from store to local format - const ringtones: Ringtone[] = (availableRingtones || []).map(r => ({ + const ringtones: Ringtone[] = (availableRingtones || []).map((r) => ({ name: r.name, - base64: r.base64 + base64: r.base64, })) useEffect(() => { @@ -136,7 +140,9 @@ export function SettingsIncomingCallsDialog() { const initAudioOutputDevices = async () => { try { const devices = await navigator.mediaDevices.enumerateDevices() - const audioOutput = devices.filter((device) => device.kind === 'audiooutput') + const audioOutput = devices.filter( + (device) => device.kind === 'audiooutput', + ) Log.info('Available audio output devices:', audioOutput) setAudioOutputDevices(audioOutput) } catch (err) { @@ -256,7 +262,9 @@ export function SettingsIncomingCallsDialog() { {capitalizeFirstLetter(ringtone.name)} @@ -280,11 +288,15 @@ export function SettingsIncomingCallsDialog() { - {capitalizeFirstLetter(getRingtoneByName(formData.ringtone)?.name || '') || '-'} + {capitalizeFirstLetter( + getRingtoneByName(formData.ringtone)?.name || + '', + ) || '-'} @@ -304,7 +316,9 @@ export function SettingsIncomingCallsDialog() { { - const selectedRingtone = getRingtoneByName(formData.ringtone) + const selectedRingtone = getRingtoneByName( + formData.ringtone, + ) if (playingRingtone === formData.ringtone) { stopRingtonePreview() } else if (selectedRingtone) { @@ -315,7 +329,11 @@ export function SettingsIncomingCallsDialog() { className='px-2.5 py-1.5 flex items-center justify-center rounded-md bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors flex-shrink-0' > diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx index 2f2a541f..22239aa8 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx @@ -13,13 +13,17 @@ import classNames from 'classnames' export function SettingsNotificationsDialog() { const tooltipId = 'tooltip-call-summary-notifications-info' - const [, setIsNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const [, setIsNotificationsDialogOpen] = useNethlinkData( + 'isNotificationsDialogOpen', + ) const [account] = useSharedState('account') const { NethVoiceAPI } = useLoggedNethVoiceAPI() const { updateAccountData } = useAccount() const [isSaving, setIsSaving] = useState(false) const isCallSummaryEnabled = account?.data?.call_summary_enabled === true - const [callSummaryNotifications, setCallSummaryNotifications] = useState(() => { + const [callSummaryNotifications, setCallSummaryNotifications] = useState< + boolean | null + >(() => { if (!account?.data?.settings || !isCallSummaryEnabled) { return null } @@ -108,12 +112,23 @@ export function SettingsNotificationsDialog() { - + @@ -128,7 +143,9 @@ export function SettingsNotificationsDialog() { type='button' role='switch' aria-checked={callSummaryNotifications} - aria-label={t('Settings.CallTranscriptionReady') as string} + aria-label={ + t('Settings.CallTranscriptionReady') as string + } onClick={() => { setCallSummaryNotifications((prev) => !prev) }} @@ -144,7 +161,9 @@ export function SettingsNotificationsDialog() { aria-hidden='true' className={classNames( 'block h-[18px] w-[18px] rounded-full bg-white shadow-lightXs transition-transform duration-200 ease-out', - callSummaryNotifications ? 'translate-x-5' : 'translate-x-0', + callSummaryNotifications + ? 'translate-x-5' + : 'translate-x-0', )} /> diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx index 07ecdf71..1627fe6e 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx @@ -103,7 +103,8 @@ export function SettingsShortcutDialog() { const newKeys = new Set(keysPressed) const code = (e as any).code as string | undefined - const hasGetModifierState = typeof (e as any).getModifierState === 'function' + const hasGetModifierState = + typeof (e as any).getModifierState === 'function' const modifierStateAltGraph = hasGetModifierState ? (e as any).getModifierState('AltGraph') : false @@ -129,7 +130,9 @@ export function SettingsShortcutDialog() { const orderedModifiers = ['Ctrl', 'Alt', 'AltGr', 'Cmd'] const modifiers = orderedModifiers.filter((k) => newKeys.has(k)) - const others = [...newKeys].filter((k) => !orderedModifiers.includes(k as any)) + const others = [...newKeys].filter( + (k) => !orderedModifiers.includes(k as any), + ) // Require at least one modifier key — reject bare keys like "6", "R", etc. if (others.length > 0 && modifiers.length === 0) return @@ -155,99 +158,102 @@ export function SettingsShortcutDialog() { - - {/* Dialog content */} - - {/* Title */} - - {t('TopBar.Keyboard shortcut to call')} - - - {/* Subtitle */} - - {t('TopBar.Shortcut title description')}{' '} - - {/* Inline notification */} - {( - + {/* Dialog content */} + + {/* Title */} + + {t('TopBar.Keyboard shortcut to call')} + + + {/* Subtitle */} + + {t('TopBar.Shortcut title description')}{' '} + + {/* Inline notification */} + { + + {t('TopBar.Shortcut subtitle description')} + + } + + {/* Form */} + - {t('TopBar.Shortcut subtitle description')} - - )} - - {/* Form */} - - {/* Input field with clear button next to it */} - - {}} - readOnly - autoFocus - /> - {!!combo && ( + {/* Input field with clear button next to it */} + + {}} + readOnly + autoFocus + /> + {!!combo && ( + + + + )} + + + + {/* Keys not supported info */} + + {t('TopBar.Shortcut body description')} + + + {/* Action buttons */} + { + e.preventDefault() + submit({ combo }) + }} > - + {t('Common.Save')} - )} - - - - {/* Keys not supported info */} - - {t('TopBar.Shortcut body description')} - - {/* Action buttons */} - - { - e.preventDefault() - submit({ combo }) - }} - > - {t('Common.Save')} - - - - {t('Common.Cancel')} - - - + + {t('Common.Cancel')} + + + + - > ) diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx index 715bb7c0..24755b7c 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx @@ -3,18 +3,18 @@ import { faSun as LightIcon, faMoon as DarkIcon, } from '@fortawesome/free-solid-svg-icons' -import { useSharedState } from "@renderer/store" -import { Account } from "@shared/types" -import { getAccountUID } from "@shared/utils/utils" -import { t } from "i18next" -import { OptionElement } from "../OptionElement" -import { Log } from "@shared/utils/logger" +import { useSharedState } from '@renderer/store' +import { Account } from '@shared/types' +import { getAccountUID } from '@shared/utils/utils' +import { t } from 'i18next' +import { OptionElement } from '../OptionElement' +import { Log } from '@shared/utils/logger' import { IPC_EVENTS } from '@shared/constants' export const ThemeIcons = { system: SystemIcon, light: LightIcon, - dark: DarkIcon + dark: DarkIcon, } export const ThemeBox = () => { const [account, setAccount] = useSharedState('account') @@ -30,22 +30,31 @@ export const ThemeBox = () => { isFirstStart: p?.isFirstStart ?? true, availableAccounts: { ...p?.availableAccounts, - [getAccountUID(updatedAccount as Account)]: updatedAccount - } + [getAccountUID(updatedAccount as Account)]: updatedAccount, + }, })) window.electron.send(IPC_EVENTS.CHANGE_THEME, theme) } const themeOptions = [ - { id: 1, name: 'system', icon: ThemeIcons.system, label: t('Settings.System') }, - { id: 2, name: 'light', icon: ThemeIcons.light, label: t('Settings.Light') }, - { id: 3, name: 'dark', icon: ThemeIcons.dark, label: t('Settings.Dark') } + { + id: 1, + name: 'system', + icon: ThemeIcons.system, + label: t('Settings.System'), + }, + { + id: 2, + name: 'light', + icon: ThemeIcons.light, + label: t('Settings.Light'), + }, + { id: 3, name: 'dark', icon: ThemeIcons.dark, label: t('Settings.Dark') }, ] - if (!account) return <>> return ( - + {themeOptions.map((availableTheme) => ( (MENU_ELEMENT.FAVOURITES) + const [lastMenu, setLastMenu] = useState( + MENU_ELEMENT.FAVOURITES, + ) const [isAboutVisited, setIsAboutVisited] = useState(false) - const viewedParkedCalls = useRef([]) const [parkedPulse, setParkedPulse] = useState(false) @@ -39,11 +43,10 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { setSelectedSidebarMenu(menuElement) setShowPhonebookSearchModule(false) setPhonebookSearchModule({ - searchText: undefined + searchText: undefined, }) } - useEffect(() => { if (selectedSidebarMenu && lastMenu !== selectedSidebarMenu) { setLastMenu(() => selectedSidebarMenu) @@ -54,13 +57,14 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { } }, [selectedSidebarMenu]) - useEffect(() => { if (selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS) { viewedParkedCalls.current = [...(parkedCalls || [])] } const currentNames = parkedCalls?.map((c) => c.parkedCaller.name) || [] - viewedParkedCalls.current = viewedParkedCalls.current.filter((p) => currentNames.includes(p.parkedCaller.name)) + viewedParkedCalls.current = viewedParkedCalls.current.filter((p) => + currentNames.includes(p.parkedCaller.name), + ) const names = viewedParkedCalls.current.map((c) => c.parkedCaller.name) const diff = difference(currentNames, names) if (diff.length > 0) { @@ -69,8 +73,8 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { }, [parkedCalls, selectedSidebarMenu]) return ( - - + + {/* FAVOURITE */} {/* PARKED CALLS */} - { - hasPermission(PERMISSION.PARKINGS) && ( - 0} - hasPulseNotification={parkedPulse} - onClick={() => { - handleSidebarMenuSelection(MENU_ELEMENT.PARKED_CALLS) - setParkedPulse(false) - }} - isSelected={selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS} - /> - ) - } + {hasPermission(PERMISSION.PARKINGS) && ( + 0} + hasPulseNotification={parkedPulse} + onClick={() => { + handleSidebarMenuSelection(MENU_ELEMENT.PARKED_CALLS) + setParkedPulse(false) + }} + isSelected={selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS} + /> + )} {/* APP UPDATE */} ) } - diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts b/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts index 3123c9cb..20e0cd44 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts @@ -1,4 +1,4 @@ export * from './ContactNameAndAction' export * from './FavouritesStar' export * from './Navbar' -export * from './Sidebar' \ No newline at end of file +export * from './Sidebar' diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx index 3854d07f..868308eb 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx @@ -102,8 +102,12 @@ export function LastCall({ const callName = getCallName(call) const callNumberValue = (call.direction === 'in' ? call.src : call.dst) || '' - const [nameRef, isNameTruncated] = useIsTruncated([callName]) - const [numberRef, isNumberTruncated] = useIsTruncated([callNumberValue]) + const [nameRef, isNameTruncated] = useIsTruncated([ + callName, + ]) + const [numberRef, isNumberTruncated] = useIsTruncated([ + callNumberValue, + ]) return ( @@ -135,7 +139,9 @@ export function LastCall({ {callName} @@ -246,8 +252,12 @@ export function LastCall({ { if (lastCalls) { - const missedCallIds = new Set(missedCalls?.map(c => c.uniqueid)) + const missedCallIds = new Set(missedCalls?.map((c) => c.uniqueid)) const preparedCalls: LastCallData[] = lastCalls .map((c) => ({ ...c, @@ -64,7 +64,7 @@ export function LastCallsBox({ showContactForm }): JSX.Element { } function getCallName(call: CallData): string { - let callName = + const callName = call.direction === 'out' ? call?.dst_cnam || call?.dst_ccompany : call.direction === 'in' @@ -85,7 +85,7 @@ export function LastCallsBox({ showContactForm }): JSX.Element { }, ) } - return operator?.username || t('Common.Unknown') //speeddialName || + return operator?.username || t('Common.Unknown') // speeddialName || } const handleClearNotification = (missedCall: CallData) => { diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsModule.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsModule.tsx index cd74477d..72395983 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsModule.tsx @@ -1,26 +1,27 @@ -import { useState } from "react" -import { LastCallsBox } from "./LastCallsBox" -import { AddToPhonebookBox } from "../PhonebookModule/AddToPhonebookBox" -import { usePhonebookModule } from "../PhonebookModule/hook/usePhonebookModule" +import { useState } from 'react' +import { LastCallsBox } from './LastCallsBox' +import { AddToPhonebookBox } from '../PhonebookModule/AddToPhonebookBox' +import { usePhonebookModule } from '../PhonebookModule/hook/usePhonebookModule' export const LastCallsModule = () => { - const phonebookModule = usePhonebookModule() const [selectedContact, setSelectedContact] = phonebookModule.selectedContact const [isContactFormOpen, setContactFormOpen] = useState(false) - return ( <> - {!(isContactFormOpen) && setContactFormOpen(true)} />} + {!isContactFormOpen && ( + setContactFormOpen(true)} /> + )} - {isContactFormOpen && { - setSelectedContact(undefined) - setContactFormOpen(false) - }} - /> - } + {isContactFormOpen && ( + { + setSelectedContact(undefined) + setContactFormOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx b/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx index 5d503dd4..531d24ea 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx @@ -1,16 +1,17 @@ -import { TimedComponent } from "@renderer/components/TimedComponent" +import { TimedComponent } from '@renderer/components/TimedComponent' export const ParkedCallSkeleton = () => { - return ( - - - + + + - + diff --git a/src/renderer/src/components/Modules/NethVoice/Parking/ParkingModule.tsx b/src/renderer/src/components/Modules/NethVoice/Parking/ParkingModule.tsx index 6d38d87c..fef1c720 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/ParkingModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Parking/ParkingModule.tsx @@ -1,8 +1,5 @@ -import { ParkingBox } from "./ParkingBox" +import { ParkingBox } from './ParkingBox' export const ParkingModule = () => { - - return ( - - ) + return } diff --git a/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts b/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts index 25b61cb0..5302f4d9 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts @@ -1,25 +1,22 @@ -import { useNethlinkData } from "@renderer/store" -import { ParkingType } from "@shared/types" -import { useEffect, useState } from "react" +import { useNethlinkData } from '@renderer/store' +import { ParkingType } from '@shared/types' +import { useEffect, useState } from 'react' export const useParkingModule = () => { - const [parkedCalls] = useNethlinkData('parkings') - const [validParkedCalls, setValidParkedCalls] = useState(undefined) + const [validParkedCalls, setValidParkedCalls] = useState< + ParkingType[] | undefined + >(undefined) useEffect(() => { parkedCalls && extractValidParkedCalls(parkedCalls) }, [parkedCalls]) - const extractValidParkedCalls = (parkedCalls: ParkingType[]) => { - setValidParkedCalls(() => [ ...(parkedCalls?.filter((p) => !!p.parkedCaller.name) || []), - ] - ) + ]) } - return { parkedCalls: validParkedCalls, } diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/AddToPhonebookBox.tsx b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/AddToPhonebookBox.tsx index 6048c304..b5a57796 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/AddToPhonebookBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/AddToPhonebookBox.tsx @@ -58,67 +58,71 @@ export function AddToPhonebookBox({ close }) { const submitButtonRef = useRef(null) const resultSchema = useMemo(() => { - const phoneNumberSchema = z - .string() - .trim() - .regex(/^[0-9*#+]*$/, 'This is not a phone number') - - const baseSchema = z.object({ - privacy: z.string(), - shared_groups: z.array(z.string()).default([]), - job: z.string(), - extension: phoneNumberSchema, - workphone: phoneNumberSchema, - workphone2: phoneNumberSchema, - cellphone: phoneNumberSchema, - cellphone2: phoneNumberSchema, - otherphone: phoneNumberSchema, - fax: phoneNumberSchema, - homephone: phoneNumberSchema, - workemail: z.string(), - otheremail: z.string(), - homeemail: z.string(), - facebook: z.string(), - instagram: z.string(), - linkedin: z.string(), - workstreet: z.string(), - workcity: z.string(), - workprovince: z.string(), - workpostalcode: z.string(), - workcountry: z.string(), - url: z.string(), - notes: z.string() - }) + const phoneNumberSchema = z + .string() + .trim() + .regex(/^[0-9*#+]*$/, 'This is not a phone number') - return z - .discriminatedUnion('type', [ - z.object({ - type: z.literal('person'), - firstname: z.string().trim(), - lastname: z.string().trim(), - company: z.string().trim() - }), - z.object({ - type: z.literal('company'), - firstname: z.string().trim(), - lastname: z.string().trim(), - company: z - .string() - .trim() - .min(1, `${t('Common.This field is required')}`) - }) - ]) - .and(baseSchema) - .superRefine((data, ctx) => { - // A person needs at least a first or last name (name is composed from them). - if (data.type === 'person' && !data.firstname?.trim() && !data.lastname?.trim()) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ['firstname'], - message: `${t('Common.This field is required')}` - }) - } + const baseSchema = z.object({ + privacy: z.string(), + shared_groups: z.array(z.string()).default([]), + job: z.string(), + extension: phoneNumberSchema, + workphone: phoneNumberSchema, + workphone2: phoneNumberSchema, + cellphone: phoneNumberSchema, + cellphone2: phoneNumberSchema, + otherphone: phoneNumberSchema, + fax: phoneNumberSchema, + homephone: phoneNumberSchema, + workemail: z.string(), + otheremail: z.string(), + homeemail: z.string(), + facebook: z.string(), + instagram: z.string(), + linkedin: z.string(), + workstreet: z.string(), + workcity: z.string(), + workprovince: z.string(), + workpostalcode: z.string(), + workcountry: z.string(), + url: z.string(), + notes: z.string(), }) + + return z + .discriminatedUnion('type', [ + z.object({ + type: z.literal('person'), + firstname: z.string().trim(), + lastname: z.string().trim(), + company: z.string().trim(), + }), + z.object({ + type: z.literal('company'), + firstname: z.string().trim(), + lastname: z.string().trim(), + company: z + .string() + .trim() + .min(1, `${t('Common.This field is required')}`), + }), + ]) + .and(baseSchema) + .superRefine((data, ctx) => { + // A person needs at least a first or last name (name is composed from them). + if ( + data.type === 'person' && + !data.firstname?.trim() && + !data.lastname?.trim() + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['firstname'], + message: `${t('Common.This field is required')}`, + }) + } + }) }, [t, i18next.language]) const [isLoading, setIsLoading] = useState(false) @@ -134,8 +138,12 @@ export function AddToPhonebookBox({ close }) { setVisibleFields((prev) => new Set(prev).add(key)) setPendingFocusKey(key) } - const hiddenPhoneFields = PHONE_FIELD_OPTIONS.filter((o) => !isFieldVisible(o.key)) - const hiddenEmailFields = EMAIL_FIELD_OPTIONS.filter((o) => !isFieldVisible(o.key)) + const hiddenPhoneFields = PHONE_FIELD_OPTIONS.filter( + (o) => !isFieldVisible(o.key), + ) + const hiddenEmailFields = EMAIL_FIELD_OPTIONS.filter( + (o) => !isFieldVisible(o.key), + ) // "Add field" menu (Address / Social flyout / Website): the only one with a nested // submenu, so it is hand-rolled (like the CTI) instead of using the generic Dropdown. @@ -145,7 +153,9 @@ export function AddToPhonebookBox({ close }) { // Delayed-close for the Social flyout: moving the pointer across the small gap // between the "Social" item and the flyout would otherwise fire mouseLeave and // dismiss it before the pointer reaches the panel. - const socialSubmenuCloseTimer = useRef | null>(null) + const socialSubmenuCloseTimer = useRef | null>( + null, + ) const openSocialSubmenu = () => { if (socialSubmenuCloseTimer.current) { clearTimeout(socialSubmenuCloseTimer.current) @@ -178,7 +188,7 @@ export function AddToPhonebookBox({ close }) { setValue, reset, trigger, - formState: { errors } + formState: { errors }, } = useForm({ defaultValues: { privacy: '', @@ -208,9 +218,9 @@ export function AddToPhonebookBox({ close }) { workpostalcode: '', workcountry: '', url: '', - notes: '' + notes: '', }, - resolver: zodResolver(resultSchema) + resolver: zodResolver(resultSchema), }) const watchType = watch('type') @@ -242,7 +252,10 @@ export function AddToPhonebookBox({ close }) { label: t('Phonebook.Group'), }, ].filter((option) => - canWritePhonebookVisibility(profile, option.id as 'public' | 'private' | 'group'), + canWritePhonebookVisibility( + profile, + option.id as 'public' | 'private' | 'group', + ), ), [profile], ) @@ -278,11 +291,19 @@ export function AddToPhonebookBox({ close }) { setValue('extension', selectedContact.number) setTimeout(() => setFocus('firstname'), 10) } - }, [searchText, selectedContact?.company, selectedContact?.number, setFocus, setValue]) + }, [ + searchText, + selectedContact?.company, + selectedContact?.number, + setFocus, + setValue, + ]) useEffect(() => { if (!pendingFocusKey) return - const focusNameByKey: Record = { address: 'workstreet' } + const focusNameByKey: Record = { + address: 'workstreet', + } const name = (focusNameByKey[pendingFocusKey] ?? pendingFocusKey) as keyof ContactType setFocus(name) @@ -309,7 +330,9 @@ export function AddToPhonebookBox({ close }) { ) const hasChanged = visibleSelectedGroups.length !== normalizedSelectedGroups.length || - visibleSelectedGroups.some((groupName, index) => groupName !== normalizedSelectedGroups[index]) + visibleSelectedGroups.some( + (groupName, index) => groupName !== normalizedSelectedGroups[index], + ) if (hasChanged) { setValue('shared_groups', visibleSelectedGroups) @@ -339,7 +362,12 @@ export function AddToPhonebookBox({ close }) { }, [isAddFieldOpen]) function handleSave(data: ContactType) { - if (!canWritePhonebookVisibility(profile, data.privacy as 'public' | 'private' | 'group')) { + if ( + !canWritePhonebookVisibility( + profile, + data.privacy as 'public' | 'private' | 'group', + ) + ) { setVisibilityError(`${t('Phonebook.Cannot create contact')}`) return } @@ -370,7 +398,7 @@ export function AddToPhonebookBox({ close }) { .then(() => { sendNotification( t('Notification.contact_created_title'), - t('Notification.contact_created_description') + t('Notification.contact_created_description'), ) reset() close() @@ -378,9 +406,12 @@ export function AddToPhonebookBox({ close }) { .catch((error) => { sendNotification( t('Notification.contact_not_created_title'), - t('Notification.contact_not_created_description') + t('Notification.contact_not_created_description'), + ) + Log.warning( + 'error during phonebookModule.handleAddContactToPhonebook:', + error, ) - Log.warning('error during phonebookModule.handleAddContactToPhonebook:', error) close() reset() }) @@ -417,38 +448,46 @@ export function AddToPhonebookBox({ close }) { { e.preventDefault() handleSubmit(onSubmitForm)(e) }} > - - - {t('Phonebook.Visibility')} + + + + {t('Phonebook.Visibility')} + - + - {t('Phonebook.Visibility')} - + {t('Phonebook.Visibility')} + {visibilityOptions.map((option) => ( - + {option.label} @@ -457,55 +496,73 @@ export function AddToPhonebookBox({ close }) { {visibilityError && ( - {visibilityError} + + {visibilityError} + )} {watchPrivacy === 'group' && ( - - {t('Phonebook.Groups')} + + + {t('Phonebook.Groups')} + `${t('Common.Delete')} ${groupName}`} + removeLabel={(groupName) => + `${t('Common.Delete')} ${groupName}` + } error={!!sharedGroupsError} /> {sharedGroupsError && ( - {sharedGroupsError} + + {sharedGroupsError} + )} )} - - {t('Phonebook.Type')} - - + + + {t('Phonebook.Type')} + + + - + {t('Phonebook.Person')} - + - + {t('Phonebook.Company')} @@ -516,52 +573,52 @@ export function AddToPhonebookBox({ close }) { <> {!isFieldVisible('job') && ( revealField('job')} > - + {t('Phonebook.Add job title')} @@ -570,129 +627,132 @@ export function AddToPhonebookBox({ close }) { ) : ( )} {hiddenPhoneFields.length > 0 && ( {hiddenPhoneFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add phone')} @@ -701,51 +761,54 @@ export function AddToPhonebookBox({ close }) { {hiddenEmailFields.length > 0 && ( {hiddenEmailFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add email')} @@ -754,94 +817,98 @@ export function AddToPhonebookBox({ close }) { {/* Company address sub-form */} - + - + {/* "Add field" menu (Address, Social flyout, Website) — opens upward, left-aligned */} - + - + {t('Phonebook.Add field')} {isAddFieldOpen && ( - + {!isFieldVisible('address') && ( { revealField('address') setIsAddFieldOpen(false) @@ -881,27 +948,36 @@ export function AddToPhonebookBox({ close }) { )} {SOCIAL_FIELD_OPTIONS.some((o) => !isFieldVisible(o.key)) && ( (isSocialSubmenuOpen ? setIsSocialSubmenuOpen(false) : openSocialSubmenu())} + onClick={() => + isSocialSubmenuOpen + ? setIsSocialSubmenuOpen(false) + : openSocialSubmenu() + } > {t('Phonebook.Social')} - + {isSocialSubmenuOpen && ( - - {SOCIAL_FIELD_OPTIONS.filter((o) => !isFieldVisible(o.key)).map((o) => ( + + {SOCIAL_FIELD_OPTIONS.filter( + (o) => !isFieldVisible(o.key), + ).map((o) => ( { revealField(o.key) setIsSocialSubmenuOpen(false) @@ -917,8 +993,8 @@ export function AddToPhonebookBox({ close }) { )} {!isFieldVisible('url') && ( { revealField('url') setIsAddFieldOpen(false) @@ -931,18 +1007,18 @@ export function AddToPhonebookBox({ close }) { )} - - - + + + {t('Common.Cancel')} - - + + {t('Common.Save')} {isLoading && ( - + )} diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx index ce31c240..0dbf4cf0 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx @@ -1,13 +1,14 @@ -import { AddToPhonebookBox } from "./AddToPhonebookBox" -import { usePhonebookModule } from "./hook/usePhonebookModule" -import { useNethlinkData } from "@renderer/store" +import { AddToPhonebookBox } from './AddToPhonebookBox' +import { usePhonebookModule } from './hook/usePhonebookModule' +import { useNethlinkData } from '@renderer/store' export const PhonebookModule = () => { - const phonebookModule = usePhonebookModule() const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [selectedContact, setSelectedContact] = phonebookModule.selectedContact return ( @@ -15,7 +16,7 @@ export const PhonebookModule = () => { close={() => { setSelectedContact(undefined) setShowAddContactModule(false) - //if i previously searched anything then i would return to the search page + // if i previously searched anything then i would return to the search page setShowPhonebookSearchModule(!!phonebookSearchModule) }} /> diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts index 8da89c4d..9f4c9abf 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts @@ -1,29 +1,40 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { ContactType, PhonebookModuleData, SelectedContact, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + ContactType, + PhonebookModuleData, + SelectedContact, + StateType, +} from '@shared/types' export const usePhonebookModule = (): { - selectedContact: StateType, + selectedContact: StateType handleAddContactToPhonebook: (contact: ContactType) => Promise } => { - - const [phonebookModule, setPhonebookModule] = useNethlinkData('phonebookModule') + const [phonebookModule, setPhonebookModule] = + useNethlinkData('phonebookModule') const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookModuleData) => (value: T | undefined) => { - setPhonebookModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookModuleData) => + (value: T | undefined) => { + setPhonebookModule((p) => ({ + ...p, + [selector]: value as any, + })) + } const handleAddContactToPhonebook = async (contact: ContactType) => { - const result: ContactType = await NethVoiceAPI.Phonebook.createContact(contact) + const result: ContactType = + await NethVoiceAPI.Phonebook.createContact(contact) return result } return { - selectedContact: [phonebookModule?.selectedContact, update('selectedContact')] as StateType, - handleAddContactToPhonebook + selectedContact: [ + phonebookModule?.selectedContact, + update('selectedContact'), + ] as StateType, + handleAddContactToPhonebook, } } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx index 55b368af..83f3df08 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx @@ -1,22 +1,23 @@ -import { debouncer } from "@shared/utils/utils" -import { SearchNumberBox } from "./SearchNumberBox" -import { useEffect, useState } from "react" -import { usePhonebookSearchModule } from "./hook/usePhoneBookSearchModule" -import { SearchData } from "@shared/types" -import { useNethlinkData } from "@renderer/store" -import { SearchNumberDetail } from "./SearchNumberDetail" -import classNames from "classnames" +import { debouncer } from '@shared/utils/utils' +import { SearchNumberBox } from './SearchNumberBox' +import { useEffect, useState } from 'react' +import { usePhonebookSearchModule } from './hook/usePhoneBookSearchModule' +import { SearchData } from '@shared/types' +import { useNethlinkData } from '@renderer/store' +import { SearchNumberDetail } from './SearchNumberDetail' +import classNames from 'classnames' export const PhoneBookSearchModule = () => { - const phoneBookModule = usePhonebookSearchModule() const { searchPhonebookContacts } = phoneBookModule const [searchText] = phoneBookModule.searchTextState const [searchResult, setSearchResult] = useState() - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') const [contactDetail, setContactDetail] = useState<{ - contact: SearchData, + contact: SearchData primaryNumber: string | null }>() @@ -28,7 +29,7 @@ export const PhoneBookSearchModule = () => { const result = await searchPhonebookContacts() setSearchResult(() => result) }, - 250 + 250, ) } else { setSearchResult(() => []) @@ -49,9 +50,12 @@ export const PhoneBookSearchModule = () => { setContactDetail(() => ({ contact, primaryNumber })) }} /> - { - setContactDetail(() => undefined) - }} /> + { + setContactDetail(() => undefined) + }} + /> > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx index c5c3dee7..8c0f55b5 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx @@ -1,6 +1,6 @@ import { faMagnifyingGlass as SearchIcon, - faXmark as DeleteSearchIcon + faXmark as DeleteSearchIcon, } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -13,27 +13,22 @@ import { Button, TextInput } from '@renderer/components/Nethesis' import { Log } from '@shared/utils/logger' import { debouncer } from '@shared/utils/utils' type FormDataType = { - searchText: string | undefined; + searchText: string | undefined } export function SearchBox(): JSX.Element { - const { callNumber } = usePhoneIslandEventHandler() const phoneBookSearchModule = usePhonebookSearchModule() const [searchText, setSearchText] = phoneBookSearchModule.searchTextState const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const { - register, - watch, - reset, - setValue, - handleSubmit - } = useForm({ + const { register, watch, reset, setValue, handleSubmit } = useForm({ defaultValues: { - searchText: '' - } + searchText: '', + }, }) useEffect(() => { @@ -47,7 +42,6 @@ export function SearchBox(): JSX.Element { const tempSearchText = watch('searchText') - useEffect(() => { if (tempSearchText) { setSearchText(tempSearchText) @@ -82,26 +76,29 @@ export function SearchBox(): JSX.Element { } return ( - + {tempSearchText !== '' && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx index fdade9eb..8134049a 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx @@ -10,7 +10,7 @@ import { ClassNames } from '@renderer/utils' export interface SearchNumberProps { user: SearchData - className?: string, + className?: string onClick?: (user: SearchData, primaryNumber: string | null) => void } @@ -20,12 +20,16 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const [operators] = useNethlinkData('operators') const { isSearchAlsoAFavourite } = useFavouriteModule() - const getUsernameFromPhoneNumber = (number: string) => { - return user.type !== 'extension' ? operators?.extensions[number]?.username : undefined + return user.type !== 'extension' + ? operators?.extensions[number]?.username + : undefined } - function highlightMatch(number: string | undefined, searchText: string): ReactNode[] { + function highlightMatch( + number: string | undefined, + searchText: string, + ): ReactNode[] { const parts: ReactNode[] = [] let lastIndex = 0 if (number) { @@ -37,10 +41,10 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { parts.push( {number.substring(index, index + searchText.length)} - + , ) lastIndex = index + searchText.length index = lowerText.indexOf(lowerSearchText, lastIndex) @@ -52,11 +56,21 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { } let phoneNumber: string | null = null - const keys = ['extension', 'cellphone', 'homephone', 'workphone', 'workphone2', 'cellphone2', 'otherphone'] + const keys = [ + 'extension', + 'cellphone', + 'homephone', + 'workphone', + 'workphone2', + 'cellphone2', + 'otherphone', + ] for (const key of keys) { if (!phoneNumber) { - phoneNumber = (user[key] || '').includes(`${searchText}`) ? user[key] : null + phoneNumber = (user[key] || '').includes(`${searchText}`) + ? user[key] + : null } else { break } @@ -72,9 +86,7 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const highlightedNumber = highlightMatch(phoneNumber, searchText || '') const otherDevicesCount = Array.from( new Set( - keys - .map((k) => user[k]) - .filter((n): n is string => !!n && n !== ' '), + keys.map((k) => user[k]).filter((n): n is string => !!n && n !== ' '), ), ).filter((n) => n !== phoneNumber).length @@ -82,37 +94,46 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const avatarSrc = username ? operators?.avatars?.[username] : '' return ( - - + - + 0 ? t('Common.PlusOther', { count: otherDevicesCount }) as string : ''} + otherNumber={ + otherDevicesCount > 0 + ? (t('Common.PlusOther', { + count: otherDevicesCount, + }) as string) + : '' + } isHighlight={true} username={username} isFavourite={false} isSearchData={true} showCompany={true} - onOpenDetail={onClick ? () => { - onClick?.(user, phoneNumber) - } : undefined} + onOpenDetail={ + onClick + ? () => { + onClick?.(user, phoneNumber) + } + : undefined + } /> {onClick && ( onClick(user, phoneNumber)} > - + {t('Phonebook.Details')} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx index 7ce69d84..0cb928e6 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx @@ -99,24 +99,20 @@ export function SearchNumberBox({ ), ) } - let operatorsResults = candidates.filter( - (op: any) => { - return ( - (op.name && - new RegExp(cleanQuery, 'i').test( - op.name.replace(cleanRegex, ''), - )) || - new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) - ) - }, - ) + let operatorsResults = candidates.filter((op: any) => { + return ( + (op.name && + new RegExp(cleanQuery, 'i').test(op.name.replace(cleanRegex, ''))) || + new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) + ) + }) if (operatorsResults.length) { operatorsResults = cloneDeep(operatorsResults) operatorsResults.forEach((op: any) => { op.resultType = 'operator' - op.extension = op.endpoints?.mainextension[0]?.id //for phoneNumber search + op.extension = op.endpoints?.mainextension[0]?.id // for phoneNumber search }) } operatorsResults.sort(sortByProperty('name')) @@ -155,7 +151,7 @@ export function SearchNumberBox({ const filteredOperators = getFoundedOperators() const copy = [...filteredOperators, ...unFilteredNumbers] // Allow creating a contact from any meaningful query — number or name. - let _canAddInPhonebook = cleanQuery.length > 1 + const _canAddInPhonebook = cleanQuery.length > 1 setFilteredPhoneNumbers(() => copy as any) setCanAddToPhonebook(() => _canAddInPhonebook) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx index 48705d66..1b41ea9f 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx @@ -29,67 +29,211 @@ const getSocialUrl = (value: string) => { interface SearchNumberBoxProps { contactDetail?: { - contact: SearchData, + contact: SearchData primaryNumber: string | null - }, + } onBack: () => void } -export function SearchNumberDetail({ contactDetail, onBack }: SearchNumberBoxProps) { +export function SearchNumberDetail({ + contactDetail, + onBack, +}: SearchNumberBoxProps) { const { contact } = contactDetail || {} return ( - - {contact && - + + {contact && ( + {/* Title row: back arrow + name + visibility badge (mock 936-24158). px-4 matches the search box padding above (Navbar.tsx px-4 pt-2). */} - - + + { - debouncer('onDetailBack', () => { - onBack() - }, 250) + debouncer( + 'onDetailBack', + () => { + onBack() + }, + 250, + ) }} /> {contact?.displayName} - + - {contact.company && {contact.company}} - {contact.job && {contact.job}} - {contact.extension && {contact.extension}} - {contact.workphone && {contact.workphone}} - {contact.workphone2 && {contact.workphone2}} - {contact.cellphone && {contact.cellphone}} - {contact.cellphone2 && {contact.cellphone2}} - {contact.homephone && {contact.homephone}} - {contact.otherphone && {contact.otherphone}} - {contact.fax && {contact.fax}} - {contact.workemail && {contact.workemail}} - {contact.homeemail && {contact.homeemail}} - {contact.otheremail && {contact.otheremail}} - {contact.facebook && {contact.facebook}} - {contact.instagram && {contact.instagram}} - {contact.linkedin && {contact.linkedin}} - {contact.url && {contact.url}} - {contact.notes && {contact.notes}} + {contact.company && ( + + {contact.company} + + )} + {contact.job && ( + + {contact.job} + + )} + {contact.extension && ( + + {contact.extension} + + )} + {contact.workphone && ( + + {contact.workphone} + + )} + {contact.workphone2 && ( + + {contact.workphone2} + + )} + {contact.cellphone && ( + + {contact.cellphone} + + )} + {contact.cellphone2 && ( + + {contact.cellphone2} + + )} + {contact.homephone && ( + + {contact.homephone} + + )} + {contact.otherphone && ( + + {contact.otherphone} + + )} + {contact.fax && ( + + {contact.fax} + + )} + {contact.workemail && ( + + {contact.workemail} + + )} + {contact.homeemail && ( + + {contact.homeemail} + + )} + {contact.otheremail && ( + + {contact.otheremail} + + )} + {contact.facebook && ( + + {contact.facebook} + + )} + {contact.instagram && ( + + {contact.instagram} + + )} + {contact.linkedin && ( + + {contact.linkedin} + + )} + {contact.url && ( + + {contact.url} + + )} + {contact.notes && ( + + {contact.notes} + + )} - } + )} ) } @@ -106,45 +250,81 @@ const VisibilityBadge = ({ contact }: { contact: SearchData }) => { if (visibility === 'group') { const visibleGroups = sharedGroups.slice(0, VISIBLE_GROUPS_LIMIT) const remainingGroups = sharedGroups.slice(VISIBLE_GROUPS_LIMIT) - const groupBadgeClassName = classNames(theme.badge.base, theme.badge.rounded.full, theme.badge.sizes.small, 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center') + const groupBadgeClassName = classNames( + theme.badge.base, + theme.badge.rounded.full, + theme.badge.sizes.small, + 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center', + ) - return - {visibleGroups.map((groupName) => ( - - - {groupName} - - ))} - {remainingGroups.length > 0 && ( - <> - - {t('Common.PlusOther', { count: remainingGroups.length })} - - - > - )} - + return ( + + {visibleGroups.map((groupName) => ( + + + {groupName} + + ))} + {remainingGroups.length > 0 && ( + <> + + {t('Common.PlusOther', { count: remainingGroups.length })} + + + > + )} + + ) } - return - - {visibility === 'private' ? t("Phonebook.Only me") : t("Phonebook.Public")} - + return ( + + + {visibility === 'private' + ? t('Phonebook.Only me') + : t('Phonebook.Public')} + + ) } -const ContactDetail = ({ children, label, copy, protocol, href, plain }: { - label: string | DefaultTFuncReturn, - children: string, - copy?: boolean, - protocol?: 'mailto' | 'callto', - href?: string, +const ContactDetail = ({ + children, + label, + copy, + protocol, + href, + plain, +}: { + label: string | DefaultTFuncReturn + children: string + copy?: boolean + protocol?: 'mailto' | 'callto' + href?: string plain?: boolean }) => { const { isCallsEnabled } = useAccount() @@ -167,7 +347,8 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { }, 2000) } - const linkClassName = 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' + const linkClassName = + 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' const cleaned = ('' + children).replace(/ /g, '') const isCalltoDisabled = protocol === 'callto' && !isCallsEnabled @@ -178,47 +359,67 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { } } - return - - {label} - - - {plain ? ( - - {children} - - ) : href ? ( - - {children} - - ) : ( - - {children} - - )} + return ( + + + {label} + + + {plain ? ( + + {children} + + ) : href ? ( + + {children} + + ) : ( + + {children} + + )} - {copy && - - + {copy && ( + + + + + )} - } + + - - - + ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts index c048cae4..da2d8a72 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts @@ -1,23 +1,32 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { PhonebookSearchModuleData, SearchCallData, SearchData, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + PhonebookSearchModuleData, + SearchCallData, + SearchData, + StateType, +} from '@shared/types' export const usePhonebookSearchModule = (): { - searchTextState: StateType, + searchTextState: StateType searchPhonebookContacts: () => Promise } => { - const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData('phonebookSearchModule') + const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData( + 'phonebookSearchModule', + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookSearchModuleData) => (value: T | undefined) => { - setPhonebookSearchModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookSearchModuleData) => + (value: T | undefined) => { + setPhonebookSearchModule((p) => ({ + ...p, + [selector]: value as any, + })) + } function mapContact(contact: SearchData) { // kind & display name - if (contact?.name && contact?.name !== '-') { + if (contact?.name && contact?.name !== '-') { contact.kind = 'person' contact.displayName = contact?.name } else { @@ -33,23 +42,30 @@ export const usePhonebookSearchModule = (): { } function prapareSearchData(receivedPhoneNumbers: SearchCallData) { - receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map((contact: SearchData) => { - return mapContact(contact) - }) + receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map( + (contact: SearchData) => { + return mapContact(contact) + }, + ) const filteredNumbers = receivedPhoneNumbers.rows.filter( - (phoneNumber) => !(!phoneNumber?.displayName || phoneNumber?.displayName === '') + (phoneNumber) => + !(!phoneNumber?.displayName || phoneNumber?.displayName === ''), ) return filteredNumbers } const searchPhonebookContacts = async () => { - const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search(phonebookSearchModule?.searchText || '') + const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search( + phonebookSearchModule?.searchText || '', + ) return prapareSearchData(searchResult) } - return { - searchTextState: [phonebookSearchModule?.searchText, update('searchText')] as StateType, - searchPhonebookContacts + searchTextState: [ + phonebookSearchModule?.searchText, + update('searchText'), + ] as StateType, + searchPhonebookContacts, } } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx index d7b31c67..d3253ddf 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx @@ -1,62 +1,78 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { Menu } from "@headlessui/react" -import classNames from "classnames" -import { useTheme } from "@renderer/theme/Context" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Menu } from '@headlessui/react' +import classNames from 'classnames' +import { useTheme } from '@renderer/theme/Context' import { faCheck as CheckedIcon, - faSortDown as ArrowMenuIcon + faSortDown as ArrowMenuIcon, } from '@fortawesome/free-solid-svg-icons' -import { t } from "i18next" -import { useNethlinkData } from "@renderer/store" -import { FilterTypes } from "@shared/constants" -import { NethLinkPageData } from "@shared/types" +import { t } from 'i18next' +import { useNethlinkData } from '@renderer/store' +import { FilterTypes } from '@shared/constants' +import { NethLinkPageData } from '@shared/types' export const FavouriteFilter = () => { const { theme: nethTheme } = useTheme() - const [speeddialsModule, setSpeeddialsModule] = useNethlinkData('speeddialsModule') + const [speeddialsModule, setSpeeddialsModule] = + useNethlinkData('speeddialsModule') const setFilter = (type: FilterTypes) => { setSpeeddialsModule((p) => ({ ...p, - favouriteOrder: type + favouriteOrder: type, })) } - - const RenderMenuItem = ({ type }: { type: FilterTypes }) => { - return - { - setFilter(type) - }} - > - - - - {t(`SpeedDial.${type}`)} - + return ( + + { + setFilter(type) + }} + > + + + + {t(`SpeedDial.${type}`)} + + - - + + ) } return ( <> - + - + {t(`SpeedDial.${speeddialsModule?.favouriteOrder} label`)} @@ -64,16 +80,16 @@ export const FavouriteFilter = () => { - + {t('SpeedDial.Order by')} - - - + + + > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx index 0a7bc044..dae12a50 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx @@ -1,17 +1,14 @@ -import { ModuleTitle } from "@renderer/components/ModuleTitle" -import { Scrollable } from "@renderer/components/Scrollable" -import { SkeletonRow } from "@renderer/components/SkeletonRow" -import { t } from "i18next" -import { useFavouriteModule } from "../hook/useFavouriteModule" -import { ContactNumber } from "../shared/ContactNumber" -import { EmptyList } from "@renderer/components/EmptyList" +import { ModuleTitle } from '@renderer/components/ModuleTitle' +import { Scrollable } from '@renderer/components/Scrollable' +import { SkeletonRow } from '@renderer/components/SkeletonRow' +import { t } from 'i18next' +import { useFavouriteModule } from '../hook/useFavouriteModule' +import { ContactNumber } from '../shared/ContactNumber' +import { EmptyList } from '@renderer/components/EmptyList' import { faStar as FavouriteIcon } from '@fortawesome/free-solid-svg-icons' -import { FavouriteFilter } from "./FavouriteFilter" - - +import { FavouriteFilter } from './FavouriteFilter' export const FavouritesBox = () => { - const { favourites } = useFavouriteModule() return ( @@ -20,21 +17,27 @@ export const FavouritesBox = () => { title={t('SpeedDial.Favourites')} actionComponent={} /> - + {favourites ? ( favourites.length > 0 ? ( favourites?.map((e, idx) => { return ( - - + + @@ -42,7 +45,10 @@ export const FavouritesBox = () => { ) }) ) : ( - + ) ) : ( Array(3) diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx index 7ef2b4e5..ca52dd21 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx @@ -1,8 +1,5 @@ -import { FavouritesBox } from "./FavouritesBox" +import { FavouritesBox } from './FavouritesBox' export const FavouritesModule = () => { - - return ( - - ) + return } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx index 9ceb55bd..9ca25b4e 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx @@ -1,39 +1,39 @@ -import { Modal } from "@renderer/components" -import { Button } from "@renderer/components/Nethesis" -import { useSharedState } from "@renderer/store" -import { parseThemeToClassName, sendNotification, truncate } from "@renderer/utils" -import { ContactType } from "@shared/types" -import { t } from "i18next" -import { createRef } from "react" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { Modal } from '@renderer/components' +import { Button } from '@renderer/components/Nethesis' +import { useSharedState } from '@renderer/store' import { - faTriangleExclamation as WarningIcon -} from '@fortawesome/free-solid-svg-icons' -export const SpeedDialDeleteDialog = ({ - close -}) => { - + parseThemeToClassName, + sendNotification, + truncate, +} from '@renderer/utils' +import { ContactType } from '@shared/types' +import { t } from 'i18next' +import { createRef } from 'react' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTriangleExclamation as WarningIcon } from '@fortawesome/free-solid-svg-icons' +export const SpeedDialDeleteDialog = ({ close }) => { const cancelDeleteButtonRef = createRef() const speedDialModule = useSpeedDialsModule() - const [selectedSpeedDial, setSelectedSpeedDial] = speedDialModule.speedDialsState + const [selectedSpeedDial, setSelectedSpeedDial] = + speedDialModule.speedDialsState const [theme] = useSharedState('theme') - - function confirmDeleteSpeedDial(speedDial: ContactType) { - speedDialModule.deleteSpeedDial(speedDial).then((_) => { - sendNotification( - t('Notification.speeddial_deleted_title'), - t('Notification.speeddial_deleted_description') - ) - setSelectedSpeedDial(undefined) - close() - }) + speedDialModule + .deleteSpeedDial(speedDial) + .then((_) => { + sendNotification( + t('Notification.speeddial_deleted_title'), + t('Notification.speeddial_deleted_description'), + ) + setSelectedSpeedDial(undefined) + close() + }) .catch((error) => { sendNotification( t('Notification.speeddial_not_deleted_title'), - t('Notification.speeddial_not_deleted_description') + t('Notification.speeddial_not_deleted_description'), ) }) } @@ -55,24 +55,24 @@ export const SpeedDialDeleteDialog = ({ onClose={() => close()} afterLeave={() => setSelectedSpeedDial(undefined)} themeMode={parseThemeToClassName(theme)} - className="font-Poppins w-[100px]" + className='font-Poppins w-[100px]' > - + - - + + {t('SpeedDial.Delete speed dial')} - - + + {t('SpeedDial.Speed dial delete message', { - deletingName: truncate(selectedSpeedDial?.name || '', 30) + deletingName: truncate(selectedSpeedDial?.name || '', 30), })} @@ -80,19 +80,19 @@ export const SpeedDialDeleteDialog = ({ {t('Common.Delete')} - + {t('Common.Cancel')} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx index 4530c192..9932b2fb 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx @@ -34,16 +34,16 @@ export function SpeedDialFormBox({ close }) { .trim() .min(1, `${t('Common.This field is required')}`) .min(3, `${t('Common.This field must be at least', { number: '2' })}`) - .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`) + .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`), }) const { register, handleSubmit, reset, - formState: { errors } + formState: { errors }, } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), }) const onSubmitForm: SubmitHandler = (data) => { @@ -67,12 +67,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_modified_title'), - t('Notification.speeddial_modified_description') + t('Notification.speeddial_modified_description'), ) } else { sendNotification( t('Notification.speeddial_created_title'), - t('Notification.speeddial_created_description') + t('Notification.speeddial_created_description'), ) } reset() @@ -82,12 +82,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_not_modified_title'), - t('Notification.speeddial_not_modified_description') + t('Notification.speeddial_not_modified_description'), ) } else { sendNotification( t('Notification.speeddial_not_created_title'), - t('Notification.speeddial_not_created_description') + t('Notification.speeddial_not_created_description'), ) } Log.warning('error during speedDialModule.upsertSpeedDial:', e) @@ -99,12 +99,16 @@ export function SpeedDialFormBox({ close }) { return ( <> - + { e.preventDefault() handleSubmit(onSubmitForm)(e) @@ -113,7 +117,7 @@ export function SpeedDialFormBox({ close }) { - - - + + + {t('Common.Cancel')} - - + + {selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')} {isLoading && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx index 896e3d3a..f9c89ee9 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx @@ -1,6 +1,6 @@ import { faCirclePlus as AddSpeedDialIcon, - faBolt as SpeedDialIcon + faBolt as SpeedDialIcon, } from '@fortawesome/free-solid-svg-icons' import { ContactType } from '@shared/types' import { t } from 'i18next' @@ -11,7 +11,10 @@ import { ModuleTitle } from '@renderer/components/ModuleTitle' import { ContactNumber } from '../shared/ContactNumber' import { EmptyList } from '@renderer/components/EmptyList' -export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): JSX.Element { +export function SpeedDialsBox({ + showSpeedDialForm, + showDeleteSpeedDialDialog, +}): JSX.Element { const speedDialModule = useSpeedDialsModule() const { speedDials } = speedDialModule const [, setSelectedSpeedDial] = speedDialModule.speedDialsState @@ -39,13 +42,16 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): actionIcon={AddSpeedDialIcon} actionText={t('SpeedDial.Create')} /> - + {speedDials ? ( speedDials.length > 0 ? ( speedDials?.map((e, idx) => { return ( - - + + @@ -54,7 +60,11 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): handleEditSpeedDial={handleEditSpeedDial} handleDeleteSpeedDial={handleDeleteSpeedDial} isFavouritePage={false} - isLastItem={speedDials.length === 1 ? false : idx === speedDials.length - 1} + isLastItem={ + speedDials.length === 1 + ? false + : idx === speedDials.length - 1 + } /> @@ -62,7 +72,10 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): ) }) ) : ( - + ) ) : ( Array(3) diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx index 47c4e02c..f33692fc 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx @@ -1,41 +1,42 @@ -import { SpeedDialsBox } from "./SpeedDialsBox" -import { useEffect, useState } from "react" -import { SpeedDialFormBox } from "./SpeedDialFormBox" -import { SpeedDialDeleteDialog } from "./SpeedDialDeleteDialog" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" - +import { SpeedDialsBox } from './SpeedDialsBox' +import { useEffect, useState } from 'react' +import { SpeedDialFormBox } from './SpeedDialFormBox' +import { SpeedDialDeleteDialog } from './SpeedDialDeleteDialog' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' export const SpeeddialsModule = () => { - const speedDialModule = useSpeedDialsModule() const [, setSelectedSpeedDial] = speedDialModule.speedDialsState const [isSpeedDialFormOpen, setSpeedDialFormOpen] = useState(false) - const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = useState(false) + const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = + useState(false) return ( <> - {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && setSpeedDialFormOpen(true)} - showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} - /> - } - - - {isSpeedDialFormOpen && { - setSelectedSpeedDial(undefined) - setSpeedDialFormOpen(false) - }} - /> - } - - {isDeleteSpeedDialDialogOpen && { - setSelectedSpeedDial(undefined) - setDeleteSpeedDialDialogOpen(false) - }} - />} - + {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && ( + setSpeedDialFormOpen(true)} + showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} + /> + )} + + {isSpeedDialFormOpen && ( + { + setSelectedSpeedDial(undefined) + setSpeedDialFormOpen(false) + }} + /> + )} + + {isDeleteSpeedDialDialogOpen && ( + { + setSelectedSpeedDial(undefined) + setDeleteSpeedDialDialogOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts index 8268bfc4..ebdd7ff5 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts @@ -1,43 +1,58 @@ -import { useNethlinkData } from "@renderer/store" -import { ContactType, SearchData } from "@shared/types" -import { useEffect, useState } from "react" -import { debouncer } from "@shared/utils/utils" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { FilterTypes, SpeeddialTypes } from "@shared/constants" -import { Log } from "@shared/utils/logger" +import { useNethlinkData } from '@renderer/store' +import { ContactType, SearchData } from '@shared/types' +import { useEffect, useState } from 'react' +import { debouncer } from '@shared/utils/utils' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { FilterTypes, SpeeddialTypes } from '@shared/constants' +import { Log } from '@shared/utils/logger' export const useFavouriteModule = () => { const [speeddialsModule] = useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') const [operators] = useNethlinkData('operators') - const [favourites, setFavourites] = useState(undefined) + const [favourites, setFavourites] = useState( + undefined, + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() const getUsernameFromContact = (contact: ContactType): string | undefined => { const extension = contact.speeddial_num || '' - const usernameFromExtension = extension ? operators?.extensions?.[extension]?.username : undefined + const usernameFromExtension = extension + ? operators?.extensions?.[extension]?.username + : undefined return usernameFromExtension || contact.name || undefined } useEffect(() => { - rawSpeedDials && filterFavourites(rawSpeedDials, speeddialsModule?.favouriteOrder || FilterTypes.AZ) + rawSpeedDials && + filterFavourites( + rawSpeedDials, + speeddialsModule?.favouriteOrder || FilterTypes.AZ, + ) }, [rawSpeedDials, speeddialsModule?.favouriteOrder, operators]) const getSorter = (order: FilterTypes) => { - let sorter: ((a: ContactType, b: ContactType) => number) | undefined = undefined + let sorter: ((a: ContactType, b: ContactType) => number) | undefined = + undefined switch (order) { case FilterTypes.ZA: - sorter = (a: ContactType, b: ContactType) => (a.name || '') < (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') < (b.name || '') ? 1 : -1 + break case FilterTypes.EXT: - sorter = (a: ContactType, b: ContactType) => (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 + break default: - sorter = (a: ContactType, b: ContactType) => (a.name || '') > (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') > (b.name || '') ? 1 : -1 + break } return sorter } - const filterFavourites = (rawSpeeddials: ContactType[], order: FilterTypes) => { + const filterFavourites = ( + rawSpeeddials: ContactType[], + order: FilterTypes, + ) => { const favourites = rawSpeeddials .filter(isFavourite) .filter(isActiveOperator) @@ -57,13 +72,15 @@ export const useFavouriteModule = () => { const username = getUsernameFromContact(contact) if (!username) return false // Check if the contact's username exists in any operator group - return Object.values(operators.groups).some(group => - group.users.includes(username) + return Object.values(operators.groups).some((group) => + group.users.includes(username), ) } const isSearchAlsoAFavourite = (contact: SearchData) => { - const foundedContact = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) + const foundedContact = favourites?.find( + (s) => s.speeddial_num === (contact.speeddial_num || contact.extension), + ) if (foundedContact) { return isFavourite(foundedContact) } @@ -77,53 +94,63 @@ export const useFavouriteModule = () => { } function toggleFavourite(contact: ContactType) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const extension = contact.speeddial_num || '' - const favouriteSpeedDial = extension - ? favourites?.find((s) => s.speeddial_num === extension) - : favourites?.find((s) => s.id === contact.id) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const extension = contact.speeddial_num || '' + const favouriteSpeedDial = extension + ? favourites?.find((s) => s.speeddial_num === extension) + : favourites?.find((s) => s.id === contact.id) - if (!favouriteSpeedDial) { - //if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name - const username = getUsernameFromContact(contact) - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - //if I am deleting favourite then I am in favourite, I can use the id directly - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${favouriteSpeedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + if (!favouriteSpeedDial) { + // if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name + const username = getUsernameFromContact(contact) + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + // if I am deleting favourite then I am in favourite, I can use the id directly + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${favouriteSpeedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } function toggleFavouriteFromSearch(contact: SearchData) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const speedDial = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) - if (!speedDial) { - //I am creating the favourite from the search so I find the username directly in searchData - const username = contact.username - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${speedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const speedDial = favourites?.find( + (s) => + s.speeddial_num === (contact.speeddial_num || contact.extension), + ) + if (!speedDial) { + // I am creating the favourite from the search so I find the username directly in searchData + const username = contact.username + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${speedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } - return { favourites, toggleFavourite, isFavourite, isSpeedDialAlsoAFavourite, isSearchAlsoAFavourite, - toggleFavouriteFromSearch + toggleFavouriteFromSearch, } } - - diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts index ad6919db..cb94f1b4 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts @@ -1,26 +1,37 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData, useSharedState } from "@renderer/store" -import { SpeeddialTypes } from "@shared/constants" -import { ContactType, NewContactType, NewSpeedDialType, SpeedDialModuleData, StateType } from "@shared/types" -import { useEffect, useState } from "react" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { SpeeddialTypes } from '@shared/constants' +import { + ContactType, + NewContactType, + NewSpeedDialType, + SpeedDialModuleData, + StateType, +} from '@shared/types' +import { useEffect, useState } from 'react' export const useSpeedDialsModule = (): { - speedDials: ContactType[] | undefined, - speedDialsState: StateType, - favouriteState: StateType, + speedDials: ContactType[] | undefined + speedDialsState: StateType + favouriteState: StateType deleteSpeedDial: (speedDial: ContactType) => Promise upsertSpeedDial(data: ContactType): Promise } => { - const [speeddialsModule, setSpeeddialsModule] = useNethlinkData('speeddialsModule') + const [speeddialsModule, setSpeeddialsModule] = + useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') - const [speedDials, setSpeedDials] = useState(undefined) + const [speedDials, setSpeedDials] = useState
{t('Common.No internet connection description')}
+ {t('Common.No internet connection description')} +
- {actionText} -
+ {actionText} +
{t('TopBar.Go to NethVoice CTI')}
- {label} -
{label}
{t('TopBar.Signed in as')}
+
+ {t('TopBar.Signed in as')} +
{truncate(account.data?.name, { length: 20 })}
{account.data?.endpoints.mainextension[0].id}
[{account.data?.default_device.type}]
- {t('TopBar.Shortcut title description')}{' '} -
+ {t('TopBar.Shortcut title description')}{' '} +
{t('TopBar.Shortcut subtitle description')}
+ {t('TopBar.Shortcut body description')} +
- {t('TopBar.Shortcut body description')} -
{callName} @@ -246,8 +252,12 @@ export function LastCall({
{t('Phonebook.Visibility')}
+ {t('Phonebook.Visibility')} +
{visibilityError}
+ {visibilityError} +
{t('Phonebook.Groups')}
+ {t('Phonebook.Groups')} +
{sharedGroupsError}
+ {sharedGroupsError} +
{t('Phonebook.Type')}
+ {t('Phonebook.Type')} +
{t('Phonebook.Add job title')}
{t('Phonebook.Add phone')}
{t('Phonebook.Add email')}
{t('Phonebook.Add field')}
{t('Common.Cancel')}
+ + {t('Common.Save')} {isLoading && ( - + )}
{t('Common.Save')}
{t('Phonebook.Details')}
- {t(`SpeedDial.${type}`)} -
+ {t(`SpeedDial.${type}`)} +
{t('SpeedDial.Speed dial delete message', { - deletingName: truncate(selectedSpeedDial?.name || '', 30) + deletingName: truncate(selectedSpeedDial?.name || '', 30), })}
+ + {selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')} {isLoading && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx index 896e3d3a..f9c89ee9 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx @@ -1,6 +1,6 @@ import { faCirclePlus as AddSpeedDialIcon, - faBolt as SpeedDialIcon + faBolt as SpeedDialIcon, } from '@fortawesome/free-solid-svg-icons' import { ContactType } from '@shared/types' import { t } from 'i18next' @@ -11,7 +11,10 @@ import { ModuleTitle } from '@renderer/components/ModuleTitle' import { ContactNumber } from '../shared/ContactNumber' import { EmptyList } from '@renderer/components/EmptyList' -export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): JSX.Element { +export function SpeedDialsBox({ + showSpeedDialForm, + showDeleteSpeedDialDialog, +}): JSX.Element { const speedDialModule = useSpeedDialsModule() const { speedDials } = speedDialModule const [, setSelectedSpeedDial] = speedDialModule.speedDialsState @@ -39,13 +42,16 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): actionIcon={AddSpeedDialIcon} actionText={t('SpeedDial.Create')} /> - + {speedDials ? ( speedDials.length > 0 ? ( speedDials?.map((e, idx) => { return ( - - + + @@ -54,7 +60,11 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): handleEditSpeedDial={handleEditSpeedDial} handleDeleteSpeedDial={handleDeleteSpeedDial} isFavouritePage={false} - isLastItem={speedDials.length === 1 ? false : idx === speedDials.length - 1} + isLastItem={ + speedDials.length === 1 + ? false + : idx === speedDials.length - 1 + } /> @@ -62,7 +72,10 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): ) }) ) : ( - + ) ) : ( Array(3) diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx index 47c4e02c..f33692fc 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx @@ -1,41 +1,42 @@ -import { SpeedDialsBox } from "./SpeedDialsBox" -import { useEffect, useState } from "react" -import { SpeedDialFormBox } from "./SpeedDialFormBox" -import { SpeedDialDeleteDialog } from "./SpeedDialDeleteDialog" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" - +import { SpeedDialsBox } from './SpeedDialsBox' +import { useEffect, useState } from 'react' +import { SpeedDialFormBox } from './SpeedDialFormBox' +import { SpeedDialDeleteDialog } from './SpeedDialDeleteDialog' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' export const SpeeddialsModule = () => { - const speedDialModule = useSpeedDialsModule() const [, setSelectedSpeedDial] = speedDialModule.speedDialsState const [isSpeedDialFormOpen, setSpeedDialFormOpen] = useState(false) - const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = useState(false) + const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = + useState(false) return ( <> - {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && setSpeedDialFormOpen(true)} - showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} - /> - } - - - {isSpeedDialFormOpen && { - setSelectedSpeedDial(undefined) - setSpeedDialFormOpen(false) - }} - /> - } - - {isDeleteSpeedDialDialogOpen && { - setSelectedSpeedDial(undefined) - setDeleteSpeedDialDialogOpen(false) - }} - />} - + {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && ( + setSpeedDialFormOpen(true)} + showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} + /> + )} + + {isSpeedDialFormOpen && ( + { + setSelectedSpeedDial(undefined) + setSpeedDialFormOpen(false) + }} + /> + )} + + {isDeleteSpeedDialDialogOpen && ( + { + setSelectedSpeedDial(undefined) + setDeleteSpeedDialDialogOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts index 8268bfc4..ebdd7ff5 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts @@ -1,43 +1,58 @@ -import { useNethlinkData } from "@renderer/store" -import { ContactType, SearchData } from "@shared/types" -import { useEffect, useState } from "react" -import { debouncer } from "@shared/utils/utils" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { FilterTypes, SpeeddialTypes } from "@shared/constants" -import { Log } from "@shared/utils/logger" +import { useNethlinkData } from '@renderer/store' +import { ContactType, SearchData } from '@shared/types' +import { useEffect, useState } from 'react' +import { debouncer } from '@shared/utils/utils' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { FilterTypes, SpeeddialTypes } from '@shared/constants' +import { Log } from '@shared/utils/logger' export const useFavouriteModule = () => { const [speeddialsModule] = useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') const [operators] = useNethlinkData('operators') - const [favourites, setFavourites] = useState(undefined) + const [favourites, setFavourites] = useState( + undefined, + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() const getUsernameFromContact = (contact: ContactType): string | undefined => { const extension = contact.speeddial_num || '' - const usernameFromExtension = extension ? operators?.extensions?.[extension]?.username : undefined + const usernameFromExtension = extension + ? operators?.extensions?.[extension]?.username + : undefined return usernameFromExtension || contact.name || undefined } useEffect(() => { - rawSpeedDials && filterFavourites(rawSpeedDials, speeddialsModule?.favouriteOrder || FilterTypes.AZ) + rawSpeedDials && + filterFavourites( + rawSpeedDials, + speeddialsModule?.favouriteOrder || FilterTypes.AZ, + ) }, [rawSpeedDials, speeddialsModule?.favouriteOrder, operators]) const getSorter = (order: FilterTypes) => { - let sorter: ((a: ContactType, b: ContactType) => number) | undefined = undefined + let sorter: ((a: ContactType, b: ContactType) => number) | undefined = + undefined switch (order) { case FilterTypes.ZA: - sorter = (a: ContactType, b: ContactType) => (a.name || '') < (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') < (b.name || '') ? 1 : -1 + break case FilterTypes.EXT: - sorter = (a: ContactType, b: ContactType) => (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 + break default: - sorter = (a: ContactType, b: ContactType) => (a.name || '') > (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') > (b.name || '') ? 1 : -1 + break } return sorter } - const filterFavourites = (rawSpeeddials: ContactType[], order: FilterTypes) => { + const filterFavourites = ( + rawSpeeddials: ContactType[], + order: FilterTypes, + ) => { const favourites = rawSpeeddials .filter(isFavourite) .filter(isActiveOperator) @@ -57,13 +72,15 @@ export const useFavouriteModule = () => { const username = getUsernameFromContact(contact) if (!username) return false // Check if the contact's username exists in any operator group - return Object.values(operators.groups).some(group => - group.users.includes(username) + return Object.values(operators.groups).some((group) => + group.users.includes(username), ) } const isSearchAlsoAFavourite = (contact: SearchData) => { - const foundedContact = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) + const foundedContact = favourites?.find( + (s) => s.speeddial_num === (contact.speeddial_num || contact.extension), + ) if (foundedContact) { return isFavourite(foundedContact) } @@ -77,53 +94,63 @@ export const useFavouriteModule = () => { } function toggleFavourite(contact: ContactType) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const extension = contact.speeddial_num || '' - const favouriteSpeedDial = extension - ? favourites?.find((s) => s.speeddial_num === extension) - : favourites?.find((s) => s.id === contact.id) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const extension = contact.speeddial_num || '' + const favouriteSpeedDial = extension + ? favourites?.find((s) => s.speeddial_num === extension) + : favourites?.find((s) => s.id === contact.id) - if (!favouriteSpeedDial) { - //if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name - const username = getUsernameFromContact(contact) - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - //if I am deleting favourite then I am in favourite, I can use the id directly - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${favouriteSpeedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + if (!favouriteSpeedDial) { + // if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name + const username = getUsernameFromContact(contact) + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + // if I am deleting favourite then I am in favourite, I can use the id directly + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${favouriteSpeedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } function toggleFavouriteFromSearch(contact: SearchData) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const speedDial = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) - if (!speedDial) { - //I am creating the favourite from the search so I find the username directly in searchData - const username = contact.username - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${speedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const speedDial = favourites?.find( + (s) => + s.speeddial_num === (contact.speeddial_num || contact.extension), + ) + if (!speedDial) { + // I am creating the favourite from the search so I find the username directly in searchData + const username = contact.username + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${speedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } - return { favourites, toggleFavourite, isFavourite, isSpeedDialAlsoAFavourite, isSearchAlsoAFavourite, - toggleFavouriteFromSearch + toggleFavouriteFromSearch, } } - - diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts index ad6919db..cb94f1b4 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts @@ -1,26 +1,37 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData, useSharedState } from "@renderer/store" -import { SpeeddialTypes } from "@shared/constants" -import { ContactType, NewContactType, NewSpeedDialType, SpeedDialModuleData, StateType } from "@shared/types" -import { useEffect, useState } from "react" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { SpeeddialTypes } from '@shared/constants' +import { + ContactType, + NewContactType, + NewSpeedDialType, + SpeedDialModuleData, + StateType, +} from '@shared/types' +import { useEffect, useState } from 'react' export const useSpeedDialsModule = (): { - speedDials: ContactType[] | undefined, - speedDialsState: StateType, - favouriteState: StateType, + speedDials: ContactType[] | undefined + speedDialsState: StateType + favouriteState: StateType deleteSpeedDial: (speedDial: ContactType) => Promise upsertSpeedDial(data: ContactType): Promise } => { - const [speeddialsModule, setSpeeddialsModule] = useNethlinkData('speeddialsModule') + const [speeddialsModule, setSpeeddialsModule] = + useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') - const [speedDials, setSpeedDials] = useState(undefined) + const [speedDials, setSpeedDials] = useState
{selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')}