From d645eb766e191aa6c6c9641ec6e6add3c645b5ee Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Tue, 15 Sep 2026 21:02:38 +0200 Subject: [PATCH 1/2] chore: fix broken eslint configuration eslint aborted on every run: .eslintrc.cjs extended react-app and react-app/jest from eslint-config-react-app, which is neither installed nor listed in package.json. - drop the react-app configs, CRA era and unmaintained, not applicable to an electron app - install eslint-plugin-react-hooks, already declared in plugins but missing, and extend plugin:react-hooks/recommended to keep the hook rules react-app used to provide - 'react/display-name': false is not a valid severity, use 'off' - drop import/no-anonymous-default-export and the import resolver setting, both referring to the uninstalled eslint-plugin-import - set settings.react.version to detect and dedupe the repeated extends entries This only makes eslint run again. It does not address the 4149 problems it now reports on the existing codebase, and note that the lint script runs with --fix, so it would reformat the whole repository. --- .eslintrc.cjs | 16 ++++++---------- package-lock.json | 14 ++++++++++++++ package.json | 1 + 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f08101e5..9a944b8d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -8,14 +8,11 @@ module.exports = { 'eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended', '@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' + 'prettier' ], rules: { // disable the rule for all files @@ -31,8 +28,7 @@ module.exports = { 'spaced-comment': 'error', quotes: ['error', 'single'], 'no-duplicate-imports': 'error', - 'react/display-name': false, - 'import/no-anonymous-default-export': 'off' + 'react/display-name': 'off' }, parser: '@typescript-eslint/parser', parserOptions: { @@ -44,8 +40,8 @@ module.exports = { }, plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], settings: { - 'import/resolver': { - typescript: {} + react: { + version: 'detect' } } } diff --git a/package-lock.json b/package-lock.json index 02c236c4..af140cb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "electron-vite": "^2.0.0", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^5.2.0", "framer-motion": "^11.11.8", "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.0.1", @@ -7383,6 +7384,19 @@ "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", diff --git a/package.json b/package.json index b0ececa7..3e10c27a 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "electron-vite": "^2.0.0", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^5.2.0", "framer-motion": "^11.11.8", "i18next": "^22.4.9", "i18next-browser-languagedetector": "^7.0.1", From 79e9b066311b17e2174d5091380492121ff885f5 Mon Sep 17 00:00:00 2001 From: Edoardo Spadoni Date: Wed, 16 Sep 2026 09:38:12 +0200 Subject: [PATCH 2/2] style: format the codebase so eslint passes Runs the autofixer over the whole repository and settles the rules that did not fit this codebase, taking eslint from 4149 problems (682 errors) to 0 errors. Rule changes, all of them cases where the rule was wrong for this project rather than the code: - spaced-comment keeps the '/' marker, otherwise the autofix rewrites TypeScript triple-slash directives into // / and the asset module declarations silently stop being picked up - camelcase only checks declarations: the NethVoice API and the design tokens are snake_case on the wire - quotes allows double quotes to avoid escaping, for strings that embed single quotes - react/prop-types off, TypeScript prop types already cover it - react-hooks rules do not apply to main, preload and shared: a use* factory there is not a React hook - ban-ts-comment downgraded to a warning Hand fixes for what the autofixer could not resolve: duplicate imports, empty blocks, an empty props interface, Function as a type, and a Promise wrapper around an already async function whose executor was async, which swallowed rejections. 317 warnings are left (mostly no-explicit-any and unused vars) and are untouched on purpose: they need real code changes, not formatting. --- .eslintrc.cjs | 40 +- electron.vite.config.ts | 18 +- postcss.config.js | 4 +- .../classes/controllers/AccountController.ts | 206 +++-- src/main/classes/controllers/AppController.ts | 17 +- .../controllers/CommandBarController.ts | 8 +- .../classes/controllers/DevToolsController.ts | 1 - .../classes/controllers/LoginController.ts | 27 +- .../classes/controllers/NethLinkController.ts | 19 +- .../classes/controllers/NetworkController.ts | 53 +- .../controllers/PhoneIslandController.ts | 238 ++++-- .../classes/controllers/TrayController.ts | 91 +- src/main/classes/windows/BaseWindow.ts | 20 +- src/main/classes/windows/CommandBarWindow.ts | 4 +- src/main/classes/windows/DevToolsWindow.ts | 1 - src/main/classes/windows/LoginWindow.ts | 10 +- src/main/classes/windows/NethLinkWindow.ts | 33 +- src/main/classes/windows/PhoneIslandWindow.ts | 29 +- .../classes/windows/SplashScreenWindow.ts | 9 +- src/main/lib/commandBarShortcut.ts | 9 +- src/main/lib/ipcEvents.ts | 359 +++++--- src/main/lib/mainStore.ts | 100 ++- src/main/lib/windowConstructor.ts | 38 +- src/main/lib/windowsFocus.ts | 17 +- src/main/main.ts | 533 +++++++----- src/preload/index.ts | 57 +- src/renderer/src/App.tsx | 72 +- .../src/components/ConnectionErrorDialog.tsx | 64 +- .../components/ElectronDraggableWindow.tsx | 80 +- src/renderer/src/components/EmptyList.tsx | 33 +- .../src/components/Modal/ModalActions.tsx | 4 +- .../src/components/Modal/ModalContent.tsx | 4 +- src/renderer/src/components/Modal/index.tsx | 39 +- src/renderer/src/components/ModuleTitle.tsx | 88 +- .../Modules/NethVoice/About/AboutModule.tsx | 34 +- .../Modules/NethVoice/BaseModule/Backdrop.tsx | 9 +- .../BaseModule/ContactNameAndAction.tsx | 40 +- .../Modules/NethVoice/BaseModule/Navbar.tsx | 12 +- .../DeviceSettings/DeviceBox.tsx | 122 +-- .../ProfileDialog/NethVoiceLink.tsx | 15 +- .../ProfileDialog/OptionElement.tsx | 56 +- .../PresenceSettings/PresenceBadge.tsx | 62 +- .../PresenceSettings/PresenceItem.tsx | 45 +- .../PresenceSettings/usePresenceService.ts | 22 +- .../BaseModule/ProfileDialog/ProfileData.tsx | 22 +- .../SettingsSettings/SettingsBox.tsx | 10 +- .../SettingsCommandBarShortcutDialog.tsx | 16 +- .../SettingsDevicesDialog.tsx | 65 +- .../SettingsIncomingCallsDialog.tsx | 42 +- .../SettingsNotificationsDialog.tsx | 33 +- .../SettingsShortcutDialog.tsx | 182 ++-- .../ProfileDialog/ThemeSettings/ThemeBox.tsx | 37 +- .../Modules/NethVoice/BaseModule/Sidebar.tsx | 57 +- .../Modules/NethVoice/BaseModule/index.ts | 2 +- .../Modules/NethVoice/LastCalls/LastCall.tsx | 32 +- .../NethVoice/LastCalls/LastCallsBox.tsx | 10 +- .../NethVoice/LastCalls/LastCallsModule.tsx | 29 +- .../NethVoice/Parking/ParkedCallSkeleton.tsx | 15 +- .../Modules/NethVoice/Parking/ParkingBox.tsx | 4 +- .../NethVoice/Parking/ParkingModule.tsx | 7 +- .../Parking/hook/useParkingModule.ts | 17 +- .../PhonebookModule/AddToPhonebookBox.tsx | 494 ++++++----- .../PhonebookModule/PhonebookModule.tsx | 13 +- .../hook/usePhonebookModule.ts | 41 +- .../SearchResults/PhoneBookSearchModule.tsx | 34 +- .../NethVoice/SearchResults/SearchBox.tsx | 39 +- .../NethVoice/SearchResults/SearchNumber.tsx | 73 +- .../SearchResults/SearchNumberBox.tsx | 22 +- .../SearchResults/SearchNumberDetail.tsx | 419 ++++++--- .../hook/usePhoneBookSearchModule.ts | 56 +- .../Speeddials/Favourites/FavouriteFilter.tsx | 94 +- .../Speeddials/Favourites/FavouritesBox.tsx | 44 +- .../Favourites/FavouritesModule.tsx | 7 +- .../SpeedDials/SpeedDialDeleteDialog.tsx | 82 +- .../SpeedDials/SpeedDialFormBox.tsx | 46 +- .../Speeddials/SpeedDials/SpeedDialsBox.tsx | 31 +- .../SpeedDials/SpeeddialsModule.tsx | 61 +- .../Speeddials/hook/useFavouriteModule.ts | 141 +-- .../Speeddials/hook/useSpeedDialsModule.ts | 77 +- .../components/Modules/NethlinkModules.tsx | 46 +- .../src/components/Nethesis/Avatar.tsx | 42 +- .../src/components/Nethesis/Badge.tsx | 2 +- .../src/components/Nethesis/Button.tsx | 15 +- .../src/components/Nethesis/CallsDate.tsx | 10 +- .../Nethesis/CustomThemedTooltip.tsx | 4 +- .../Nethesis/MultiSelectCombobox.tsx | 72 +- .../src/components/Nethesis/StatusDot.tsx | 5 +- .../src/components/Nethesis/TextInput.tsx | 6 +- .../Nethesis/dropdown/DropdownHeader.tsx | 12 +- .../Nethesis/dropdown/DropdownItem.tsx | 19 +- .../components/Nethesis/dropdown/index.tsx | 32 +- src/renderer/src/components/NumberCaller.tsx | 12 +- src/renderer/src/components/Scrollable.tsx | 15 +- src/renderer/src/components/SidebarButton.tsx | 37 +- src/renderer/src/components/SkeletonRow.tsx | 28 +- .../src/components/TimedComponent.tsx | 16 +- .../login/AvailableAccountDeleteDialog.tsx | 49 +- .../pageComponents/login/LoginForm.tsx | 2 +- .../pageComponents/login/OTPInput.tsx | 25 +- .../pageComponents/phoneIsland/index.ts | 2 +- .../phoneIsland/phoneIslandContainer.tsx | 21 +- src/renderer/src/contexts/pageContext.tsx | 17 +- src/renderer/src/hooks/eventDispatch.ts | 10 +- src/renderer/src/hooks/useAccount.ts | 52 +- .../src/hooks/useIsomorphicLayoutEffect.ts | 3 +- .../src/hooks/useLoggedNethVoiceAPI.ts | 6 +- src/renderer/src/hooks/usePhoneIsland.ts | 42 +- .../src/hooks/usePhoneIslandEventHandler.ts | 180 ++-- .../src/hooks/usePhoneIslandEventListeners.ts | 801 ++++++++++-------- src/renderer/src/hooks/useRefState.ts | 30 +- src/renderer/src/icons/InCallAnsweredIcon.tsx | 10 +- .../src/icons/InCallNotAnsweredIcon.tsx | 10 +- src/renderer/src/icons/NethLingLogoIcon.tsx | 11 +- .../src/icons/OutCallAnsweredIcon.tsx | 10 +- .../src/icons/OutCallNotAnsweredIcon.tsx | 10 +- src/renderer/src/index.tsx | 2 +- src/renderer/src/lib/dateTime.ts | 25 +- src/renderer/src/lib/i18n.ts | 39 +- src/renderer/src/lib/utils.ts | 23 +- src/renderer/src/pages/CommandBarPage.tsx | 259 +++--- src/renderer/src/pages/DevToolsPage.tsx | 77 +- src/renderer/src/pages/NethLinkPage.tsx | 106 ++- src/renderer/src/pages/PhoneIslandPage.tsx | 435 ++++++---- src/renderer/src/pages/SplashScreenPage.tsx | 32 +- src/renderer/src/store.ts | 190 +++-- src/renderer/src/theme/Theme.ts | 39 +- src/renderer/src/utils/utils.ts | 36 +- src/shared/constants.ts | 133 +-- src/shared/phonebook.ts | 13 +- src/shared/types.ts | 159 ++-- src/shared/useLogin.ts | 10 +- src/shared/useNethVoiceAPI.ts | 270 ++++-- src/shared/useNetwork.ts | 79 +- src/shared/utils/jwt.ts | 4 +- src/shared/utils/logger.ts | 49 +- src/shared/utils/utils.ts | 42 +- tailwind.config.js | 66 +- 137 files changed, 5389 insertions(+), 3459 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 9a944b8d..5ba2f388 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,7 +2,7 @@ module.exports = { env: { browser: true, es2021: true, - jest: true + jest: true, }, extends: [ 'eslint:recommended', @@ -12,7 +12,7 @@ module.exports = { 'plugin:@typescript-eslint/recommended', '@electron-toolkit/eslint-config-ts/recommended', '@electron-toolkit/eslint-config-prettier', - 'prettier' + 'prettier', ], rules: { // disable the rule for all files @@ -24,24 +24,42 @@ module.exports = { '@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'], + // the NethVoice API and the design tokens use snake_case on the wire: + // check declarations, not properties + camelcase: ['error', { properties: 'never' }], + // the '/' marker keeps TypeScript triple-slash directives intact: + // without it the autofix rewrites /// into // / + 'spaced-comment': ['error', 'always', { markers: ['/'] }], + quotes: ['error', 'single', { avoidEscape: true }], 'no-duplicate-imports': 'error', - 'react/display-name': 'off' + 'react/display-name': 'off', + // TypeScript prop types already cover what prop-types would check + 'react/prop-types': 'off', + '@typescript-eslint/ban-ts-comment': 'warn', }, + overrides: [ + { + // main, preload and shared are plain TypeScript: a use* factory there is not + // a React hook, so the React rules can only produce false positives + files: ['src/main/**', 'src/preload/**', 'src/shared/**'], + rules: { + 'react-hooks/rules-of-hooks': 'off', + 'react-hooks/exhaustive-deps': 'off', + }, + }, + ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { - jsx: true + jsx: true, }, ecmaVersion: 'latest', - sourceType: 'module' + sourceType: 'module', }, plugins: ['react', 'react-hooks', '@typescript-eslint', 'prettier'], settings: { react: { - version: 'detect' - } - } + version: 'detect', + }, + }, } 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/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 a563097c..2ff2ec60 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,28 +155,36 @@ 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 } // 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', + ) return true } else { @@ -162,10 +193,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 } @@ -183,7 +222,9 @@ export class AccountController { } const { parseConfig } = useLogin() - const config: string = await NetworkController.instance.get(`https://${loggedAccount.host}/config/config.production.js`) + const config: string = await NetworkController.instance.get( + `https://${loggedAccount.host}/config/config.production.js`, + ) loggedAccount = parseConfig(loggedAccount, config) await this.saveLoggedAccount(loggedAccount, password) return true @@ -196,14 +237,23 @@ export class AccountController { return false } - 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 @@ -212,31 +262,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) { @@ -246,8 +301,6 @@ export class AccountController { } } - - async updateTheme(theme: any) { if (store.store) { const account = store.store.account @@ -311,12 +364,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) { @@ -326,8 +380,8 @@ export class AccountController { ...auth, availableAccounts: { ...auth?.availableAccounts, - [getAccountUID(account)]: account - } + [getAccountUID(account)]: account, + }, } store.set('auth', _auth, true) store.saveToDisk() @@ -338,7 +392,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 @@ -349,8 +405,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 4a3faf39..7c9c8cf7 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, + ) } } @@ -375,7 +449,10 @@ export class PhoneIslandController { new PhoneIslandController() }) } catch (e) { - Log.warning('error during emitting reconnect event to the PhoneIslandWindow:', e) + Log.warning( + 'error during emitting reconnect event to the PhoneIslandWindow:', + e, + ) } } @@ -429,7 +506,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 9eca1fb7..b1d9cb7e 100644 --- a/src/main/classes/windows/NethLinkWindow.ts +++ b/src/main/classes/windows/NethLinkWindow.ts @@ -34,8 +34,7 @@ export class NethLinkWindow extends BaseWindow { fullscreen: false, thickFrame: true, icon: '../../public/LogoBlueSimpleDark.svg', - titleBarOverlay: true - + titleBarOverlay: true, }) this.size = NethLinkPageSize NethLinkWindow.instance = this @@ -70,18 +69,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._window?.setBounds(accountBounds, false) } else { @@ -94,8 +96,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() @@ -137,7 +138,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 fc608d14..a11e86f4 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 @@ -37,10 +52,13 @@ export function isCallActive(): boolean { 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] @@ -51,7 +69,7 @@ function onSyncEmitter( } else if (typeof e === 'string') { error.message = e } else { - error.message = "Unknown error" + error.message = 'Unknown error' } syncResponse = [undefined, error] } @@ -92,7 +110,6 @@ export function disableCommandBarShortcuts() { } export function registerIpcEvents() { - let draggingWindows: OnDraggingWindow = {} onSyncEmitter(IPC_EVENTS.GET_LOCALE, async () => { @@ -108,7 +125,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 { @@ -119,16 +139,19 @@ export function registerIpcEvents() { const request = get( url, { - timeout: 3000 + timeout: 3000, }, (res) => { if (res.statusCode !== 200) { triggerError(new Error('status error'), request) } - 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, res.statusCode) - } + }, ) request.on('error', (e) => { @@ -140,103 +163,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) { - + // best effort: a failure here must not block the event handler } } - - ipcMain.on(IPC_EVENTS.UPDATE_CONNECTION_STATE, (_, isOnline) => { if (store.store) { Log.info('Update connection state:', isOnline) @@ -245,12 +278,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() @@ -265,21 +298,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) @@ -318,12 +354,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) } @@ -345,29 +387,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, () => { @@ -376,7 +430,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) }) }) @@ -387,7 +441,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) @@ -407,36 +462,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 @@ -450,16 +507,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) } @@ -467,7 +529,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 } }) @@ -484,7 +548,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) } @@ -517,27 +584,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 { @@ -573,13 +644,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()) { @@ -599,12 +673,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)) } @@ -632,7 +716,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 a6626148..16495acd 100644 --- a/src/main/lib/mainStore.ts +++ b/src/main/lib/mainStore.ts @@ -1,18 +1,21 @@ -import { Account, 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'; +import { Account, 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`); +const AVAILABLE_USER_DATA_PATH = path.join( + app.getPath('userData'), + 'available_user_data.json', +) 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] @@ -20,22 +23,32 @@ class Store { this.assignedInstanceID = 0 } Log.info({ assignedInstanceID: this.assignedInstanceID }) - this.USER_DATA_PATH = path.join(app.getPath("userData"), `user_data${this.assignedInstanceID || ''}.json`); - if (!fs.existsSync(this.USER_DATA_PATH) || !fs.existsSync(AVAILABLE_USER_DATA_PATH)) { + this.USER_DATA_PATH = path.join( + app.getPath('userData'), + `user_data${this.assignedInstanceID || ''}.json`, + ) + if ( + !fs.existsSync(this.USER_DATA_PATH) || + !fs.existsSync(AVAILABLE_USER_DATA_PATH) + ) { if (!fs.existsSync(AVAILABLE_USER_DATA_PATH)) { if (!fs.existsSync(this.USER_DATA_PATH)) { fs.writeFileSync(AVAILABLE_USER_DATA_PATH, JSON.stringify({})) } else { - const data = fs.readFileSync(this.USER_DATA_PATH, 'utf-8'); - const retrivedStore: LocalStorageData = JSON.parse(data); + const data = fs.readFileSync(this.USER_DATA_PATH, 'utf-8') + const retrivedStore: LocalStorageData = JSON.parse(data) if (retrivedStore.auth?.availableAccounts) { - fs.writeFileSync(AVAILABLE_USER_DATA_PATH, JSON.stringify(retrivedStore.auth!.availableAccounts)) + fs.writeFileSync( + AVAILABLE_USER_DATA_PATH, + JSON.stringify(retrivedStore.auth!.availableAccounts), + ) } else { fs.writeFileSync(AVAILABLE_USER_DATA_PATH, JSON.stringify({})) } } } - !fs.existsSync(this.USER_DATA_PATH) && fs.writeFileSync(this.USER_DATA_PATH, JSON.stringify({})) + !fs.existsSync(this.USER_DATA_PATH) && + fs.writeFileSync(this.USER_DATA_PATH, JSON.stringify({})) } else { this.store = this.getFromDisk() } @@ -53,29 +66,49 @@ 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, 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) } } saveToDisk(forceSave: boolean = false) { - const availableUserData = (this.store as LocalStorageData).auth?.availableAccounts - fs.writeFileSync(this.USER_DATA_PATH, JSON.stringify(this.store)); + const availableUserData = (this.store as LocalStorageData).auth + ?.availableAccounts + fs.writeFileSync(this.USER_DATA_PATH, JSON.stringify(this.store)) if (Object.keys(availableUserData || {}).length > 0 || forceSave) { - fs.writeFileSync(AVAILABLE_USER_DATA_PATH, JSON.stringify(availableUserData)); + fs.writeFileSync( + AVAILABLE_USER_DATA_PATH, + JSON.stringify(availableUserData), + ) } } - getAvailableFromDisk(): { [accountUID: string]: Account; } { + getAvailableFromDisk(): { [accountUID: string]: Account } { if (fs.existsSync(AVAILABLE_USER_DATA_PATH)) { - const availableUserData = fs.readFileSync(AVAILABLE_USER_DATA_PATH, 'utf-8'); + const availableUserData = fs.readFileSync( + AVAILABLE_USER_DATA_PATH, + 'utf-8', + ) return JSON.parse(availableUserData) } return {} @@ -83,20 +116,21 @@ class Store { getFromDisk() { try { - const data = fs.readFileSync(this.USER_DATA_PATH, 'utf-8'); - const availableUserData = fs.readFileSync(AVAILABLE_USER_DATA_PATH, 'utf-8'); - const retrivedStore = JSON.parse(data); - (retrivedStore as LocalStorageData).auth!.availableAccounts = JSON.parse(availableUserData) + const data = fs.readFileSync(this.USER_DATA_PATH, 'utf-8') + const availableUserData = fs.readFileSync( + AVAILABLE_USER_DATA_PATH, + 'utf-8', + ) + const retrivedStore = JSON.parse(data) + ;(retrivedStore as LocalStorageData).auth!.availableAccounts = + JSON.parse(availableUserData) return retrivedStore } catch (error) { - Log.error('retrieving user data', error); + Log.error('retrieving user data', error) // you may want to propagate the error, up to you - return null; + return null } } } export const store: Store = new Store() - - - 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..8fdc60db 100644 --- a/src/main/lib/windowsFocus.ts +++ b/src/main/lib/windowsFocus.ts @@ -45,14 +45,23 @@ 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)') + // eslint-disable-next-line camelcase -- mirrors the Win32 symbol name + 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 b0ccaf87..cb82ac96 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -1,15 +1,30 @@ -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 } 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 +39,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 +57,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,43 +66,55 @@ 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() - //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') @@ -108,7 +133,7 @@ function startup() { AccountController.instance.logout() await delay(1000) TrayController.instance.updateTray({ - enableShowButton: true + enableShowButton: true, }) showLogin() }) @@ -126,13 +151,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`) @@ -163,52 +185,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() @@ -224,21 +251,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...') @@ -286,7 +315,7 @@ function attachOnReadyProcess() { 'web-contents-created', 'will-continue-activity', 'will-finish-launching', - 'will-quit' + 'will-quit', ] events.forEach((e: any) => { app.on(e, (...args) => { @@ -306,17 +335,16 @@ function attachOnReadyProcess() { data = store.getFromDisk() store.updateStore(data, 'startApp') startApp(++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) @@ -324,11 +352,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) } @@ -354,19 +386,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 { @@ -374,9 +409,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, }) } } @@ -400,7 +435,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 } @@ -428,8 +465,7 @@ function attachOnReadyProcess() { app.dock?.hide() }) app.on('before-quit', async (e) => { - - e.preventDefault(); + e.preventDefault() if (retryAppStart) { clearTimeout(retryAppStart) } @@ -437,7 +473,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) @@ -451,7 +489,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 @@ -463,35 +501,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() { @@ -505,7 +565,6 @@ async function attachProtocolListeners() { app.removeAsDefaultProtocolClient('tel') app.removeAsDefaultProtocolClient('callto') app.removeAsDefaultProtocolClient('nethlink') - } } if (process.platform === 'win32') { @@ -520,46 +579,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) }) @@ -572,24 +647,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]) @@ -599,7 +674,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:\/?\/?/, '') @@ -617,21 +692,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) @@ -645,10 +720,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) } @@ -670,7 +745,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) { @@ -679,7 +754,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() @@ -691,37 +768,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() @@ -732,20 +816,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) @@ -753,51 +840,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, + }) } } @@ -813,8 +906,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() @@ -840,7 +932,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) } @@ -850,37 +942,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 } @@ -890,18 +986,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')} +

- +
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 && ( )} {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..f2bbc261 100644 --- a/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx @@ -10,9 +10,7 @@ import { Scrollable } from '@renderer/components/Scrollable' import { ModuleTitle } from '@renderer/components/ModuleTitle' import { parseThemeToClassName } from '@renderer/utils' -export interface AboutBoxProps { } - -export function AboutModule({ }: AboutBoxProps) { +export function AboutModule() { const page = usePageCtx() const [theme] = useSharedState('theme') const [notifications] = useSharedState('notifications') @@ -35,38 +33,44 @@ export function AboutModule({ }: AboutBoxProps) { <> -
-
+
+
-
+
NethLink by{' '} {account?.companyName}
-
+
{t('About.current_version', { version: window.api.appVersion })}
{!!notifications?.system?.update && ( -
- +
+ {t('About.update_available')} -
diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx index c77ca856..8da2dc87 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..9f970020 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,30 @@ 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..b954943b 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() { 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 */} +
- )} - -
- - {/* Keys not supported info */} -

- {t('TopBar.Shortcut body description')} -

- {/* Action buttons */} -
- - - -
- + +
+ +
-
) 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..8ddeda87 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 (
@@ -111,9 +115,11 @@ export function LastCall({ className={`flex flex-grow gap-3 min-h-[72px] py-6 px-3 ${className}`} > {call.hasNotification && ( -
+
)} @@ -134,8 +140,10 @@ export function LastCall({

{callName} @@ -154,14 +162,14 @@ 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) => { @@ -131,7 +131,7 @@ export function LastCallsBox({ showContactForm }): JSX.Element { >

{ return (
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..a9f4c6d6 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Parking/ParkedCallSkeleton.tsx @@ -1,16 +1,19 @@ -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/ParkingBox.tsx b/src/renderer/src/components/Modules/NethVoice/Parking/ParkingBox.tsx index 6e3eb5de..c2871bd1 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/ParkingBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Parking/ParkingBox.tsx @@ -51,7 +51,7 @@ export const ParkingBox = () => { >
@@ -68,7 +68,7 @@ export const ParkingBox = () => { .map((_, idx) => { 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) => ( -
+
@@ -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} +

)}
)} -