diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f08101e5..5ba2f388 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -2,20 +2,17 @@ module.exports = { env: { browser: true, es2021: true, - jest: true + jest: true, }, extends: [ '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' ], rules: { // disable the rule for all files @@ -27,25 +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': false, - 'import/no-anonymous-default-export': '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: { - 'import/resolver': { - typescript: {} - } - } + react: { + 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/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", 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')} + - {buttonText} + + {buttonText} + > diff --git a/src/renderer/src/components/ElectronDraggableWindow.tsx b/src/renderer/src/components/ElectronDraggableWindow.tsx index 7faf71c9..127c2a4e 100644 --- a/src/renderer/src/components/ElectronDraggableWindow.tsx +++ b/src/renderer/src/components/ElectronDraggableWindow.tsx @@ -1,10 +1,9 @@ -import { useRef, useEffect } from 'react'; -import { IPC_EVENTS } from '@shared/constants'; -import classNames from 'classnames'; -import { isDev } from '@shared/utils/utils'; +import { useRef, useEffect } from 'react' +import { IPC_EVENTS } from '@shared/constants' +import classNames from 'classnames' +import { isDev } from '@shared/utils/utils' export const ElectronDraggableWindow = ({ children }) => { - const isDrag = useRef(false) const passthroughEvent = useRef(false) const mouseDownEvent = useRef(null) @@ -26,54 +25,54 @@ export const ElectronDraggableWindow = ({ children }) => { target.current = null passthroughEvent.current = false } - }; + } const handleMouseDown = (e: MouseEvent) => { - const now = Date.now(); + const now = Date.now() if (now - lastMouseDownTime.current < 100) { - return; + return } - lastMouseDownTime.current = now; + lastMouseDownTime.current = now if (!isDrag.current && !mouseDownEvent.current) { target.current = e.target passthroughEvent.current = false - window.electron.send(IPC_EVENTS.START_DRAG); + window.electron.send(IPC_EVENTS.START_DRAG) mouseDownEvent.current = e mouseUpEvent.current = null isDrag.current = true } - }; + } const handleMouseUp = (e: MouseEvent) => { mouseDownEvent.current = null - window.electron.send(IPC_EVENTS.STOP_DRAG); + window.electron.send(IPC_EVENTS.STOP_DRAG) isDrag.current = false mouseUpEvent.current = e - }; + } const handleMouseLeave = () => { if (isDrag.current) { - window.electron.send(IPC_EVENTS.STOP_DRAG); - isDrag.current = false; - mouseDownEvent.current = null; + window.electron.send(IPC_EVENTS.STOP_DRAG) + isDrag.current = false + mouseDownEvent.current = null } - }; + } const handleWindowBlur = () => { if (isDrag.current) { - isDrag.current = false; - mouseDownEvent.current = null; - window.electron.send(IPC_EVENTS.STOP_DRAG); + isDrag.current = false + mouseDownEvent.current = null + window.electron.send(IPC_EVENTS.STOP_DRAG) } - }; + } useEffect(() => { - document.addEventListener('click', handleMouseClick, { capture: true }); - document.addEventListener('mousedown', handleMouseDown, { capture: true }); - document.addEventListener('mouseup', handleMouseUp, { capture: true }); - window.addEventListener('mouseleave', handleMouseLeave); - window.addEventListener('blur', handleWindowBlur); + document.addEventListener('click', handleMouseClick, { capture: true }) + document.addEventListener('mousedown', handleMouseDown, { capture: true }) + document.addEventListener('mouseup', handleMouseUp, { capture: true }) + window.addEventListener('mouseleave', handleMouseLeave) + window.addEventListener('blur', handleWindowBlur) window.electron.receive(IPC_EVENTS.ENABLE_CLICK, () => { if (target.current && !passthroughEvent.current) { @@ -82,26 +81,29 @@ export const ElectronDraggableWindow = ({ children }) => { bubbles: true, cancelable: true, view: window, - }); + }) newEvent['nethlink'] = true - target.current.dispatchEvent(newEvent); + target.current.dispatchEvent(newEvent) } - }); + }) return () => { - document.removeEventListener('click', handleMouseClick); - document.removeEventListener('mousedown', handleMouseDown); - document.removeEventListener('mouseup', handleMouseUp); - window.removeEventListener('mouseleave', handleMouseLeave); - window.removeEventListener('blur', handleWindowBlur); - }; - }, []); + document.removeEventListener('click', handleMouseClick) + document.removeEventListener('mousedown', handleMouseDown) + document.removeEventListener('mouseup', handleMouseUp) + window.removeEventListener('mouseleave', handleMouseLeave) + window.removeEventListener('blur', handleWindowBlur) + } + }, []) return ( {children} - ); -}; \ No newline at end of file + ) +} diff --git a/src/renderer/src/components/EmptyList.tsx b/src/renderer/src/components/EmptyList.tsx index cd44f1dd..6b8197b8 100644 --- a/src/renderer/src/components/EmptyList.tsx +++ b/src/renderer/src/components/EmptyList.tsx @@ -1,25 +1,32 @@ -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import classNames from "classnames" -import { TimedComponent } from "./TimedComponent" - +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import classNames from 'classnames' +import { TimedComponent } from './TimedComponent' export interface EmptyListProps { - icon: IconProp, - text: string, + icon: IconProp + text: string subtitle?: string } export const EmptyList = ({ icon, text, subtitle }: EmptyListProps) => { return ( - - - + + + - - {text} + + + {text} + {subtitle && ( - {subtitle} + + {subtitle} + )} diff --git a/src/renderer/src/components/Modal/ModalActions.tsx b/src/renderer/src/components/Modal/ModalActions.tsx index 67d8af12..5e377702 100644 --- a/src/renderer/src/components/Modal/ModalActions.tsx +++ b/src/renderer/src/components/Modal/ModalActions.tsx @@ -13,7 +13,9 @@ import { useTheme } from '@renderer/theme/Context' import { cleanClassName } from '@renderer/utils' import type { FC, PropsWithChildren, ComponentProps } from 'react' -export type ModalActionsProps = PropsWithChildren, 'className'>> +export type ModalActionsProps = PropsWithChildren< + Omit, 'className'> +> export const ModalActions: FC = ({ children, ...props }) => { const { modal: theme } = useTheme().theme diff --git a/src/renderer/src/components/Modal/ModalContent.tsx b/src/renderer/src/components/Modal/ModalContent.tsx index 119426dd..bf15c4f9 100644 --- a/src/renderer/src/components/Modal/ModalContent.tsx +++ b/src/renderer/src/components/Modal/ModalContent.tsx @@ -13,7 +13,9 @@ import { useTheme } from '@renderer/theme/Context' import { cleanClassName } from '@renderer/utils' import type { FC, PropsWithChildren, ComponentProps } from 'react' -export type ModalContentProps = PropsWithChildren, 'className'>> +export type ModalContentProps = PropsWithChildren< + Omit, 'className'> +> export const ModalContent: FC = ({ children, ...props }) => { const { modal: theme } = useTheme().theme diff --git a/src/renderer/src/components/Modal/index.tsx b/src/renderer/src/components/Modal/index.tsx index 80a50315..2175c3be 100644 --- a/src/renderer/src/components/Modal/index.tsx +++ b/src/renderer/src/components/Modal/index.tsx @@ -13,8 +13,14 @@ * */ -import { FC, ComponentProps, PropsWithChildren, RefObject } from 'react' -import { Fragment, createRef } from 'react' +import { + FC, + ComponentProps, + PropsWithChildren, + RefObject, + Fragment, + createRef, +} from 'react' import { ModalContent } from './ModalContent' import { ModalActions } from './ModalActions' import { Transition, Dialog } from '@headlessui/react' @@ -46,25 +52,38 @@ const ModalComponent: FC = ({ const cleanProps = cleanClassName(props) return ( - afterLeave && afterLeave()}> + afterLeave && afterLeave()} + > onClose()} initialFocus={focus && focus} {...cleanProps} > - + - - + ' + > + - {children} + + {children} + @@ -79,5 +98,5 @@ ModalActions.displayName = 'Modal.Actions' export const Modal = Object.assign(ModalComponent, { Content: ModalContent, - Actions: ModalActions + Actions: ModalActions, }) diff --git a/src/renderer/src/components/ModuleTitle.tsx b/src/renderer/src/components/ModuleTitle.tsx index f4c69f3e..6a6bdc26 100644 --- a/src/renderer/src/components/ModuleTitle.tsx +++ b/src/renderer/src/components/ModuleTitle.tsx @@ -1,20 +1,19 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { Button } from "./Nethesis" -import { IconProp } from "@fortawesome/fontawesome-svg-core" -import { DefaultTFuncReturn } from "i18next" -import { ReactNode, useId } from "react" -import { ClassNames } from "@renderer/utils" -import { useIsTruncated } from "@renderer/hooks/useIsTruncated" -import { CustomThemedTooltip } from "./Nethesis/CustomThemedTooltip" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Button } from './Nethesis' +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { DefaultTFuncReturn } from 'i18next' +import { ReactNode, useId } from 'react' +import { ClassNames } from '@renderer/utils' +import { useIsTruncated } from '@renderer/hooks/useIsTruncated' +import { CustomThemedTooltip } from './Nethesis/CustomThemedTooltip' export interface ModuleTitleProps { - title?: string | DefaultTFuncReturn | ReactNode, - action?: () => void, - actionText?: string | DefaultTFuncReturn, + title?: string | DefaultTFuncReturn | ReactNode + action?: () => void + actionText?: string | DefaultTFuncReturn actionIcon?: IconProp - actionComponent?: ReactNode, + actionComponent?: ReactNode className?: string - } export const ModuleTitle = ({ title, @@ -22,40 +21,53 @@ export const ModuleTitle = ({ actionText, actionIcon, actionComponent, - className + className, }: ModuleTitleProps) => { - const tooltipId = useId() - const [titleRef, isTitleTruncated] = useIsTruncated([title]) + const [titleRef, isTitleTruncated] = useIsTruncated([ + title, + ]) const showTitleTooltip = typeof title === 'string' && isTitleTruncated return ( - - - {title && - {title} - {showTitleTooltip && } - } + + + {title && ( + + {title} + {showTitleTooltip && ( + + )} + + )} {action && ( - {actionIcon && } - {actionText && - {actionText} - - } + {actionIcon && ( + + )} + {actionText && ( + + {actionText} + + )} )} {actionComponent} diff --git a/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx b/src/renderer/src/components/Modules/NethVoice/About/AboutModule.tsx index 87eb69c1..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')} - - - - {t('About.download')} + + + + + {t('About.download')} + diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx index c77ca856..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() { { - const selectedRingtone = getRingtoneByName(formData.ringtone) + const selectedRingtone = getRingtoneByName( + formData.ringtone, + ) if (playingRingtone === formData.ringtone) { stopRingtonePreview() } else if (selectedRingtone) { @@ -315,7 +329,11 @@ export function SettingsIncomingCallsDialog() { className='px-2.5 py-1.5 flex items-center justify-center rounded-md bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors flex-shrink-0' > diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx index 2f2a541f..22239aa8 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx @@ -13,13 +13,17 @@ import classNames from 'classnames' export function SettingsNotificationsDialog() { const tooltipId = 'tooltip-call-summary-notifications-info' - const [, setIsNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const [, setIsNotificationsDialogOpen] = useNethlinkData( + 'isNotificationsDialogOpen', + ) const [account] = useSharedState('account') const { NethVoiceAPI } = useLoggedNethVoiceAPI() const { updateAccountData } = useAccount() const [isSaving, setIsSaving] = useState(false) const isCallSummaryEnabled = account?.data?.call_summary_enabled === true - const [callSummaryNotifications, setCallSummaryNotifications] = useState(() => { + const [callSummaryNotifications, setCallSummaryNotifications] = useState< + boolean | null + >(() => { if (!account?.data?.settings || !isCallSummaryEnabled) { return null } @@ -108,12 +112,23 @@ export function SettingsNotificationsDialog() { - + @@ -128,7 +143,9 @@ export function SettingsNotificationsDialog() { type='button' role='switch' aria-checked={callSummaryNotifications} - aria-label={t('Settings.CallTranscriptionReady') as string} + aria-label={ + t('Settings.CallTranscriptionReady') as string + } onClick={() => { setCallSummaryNotifications((prev) => !prev) }} @@ -144,7 +161,9 @@ export function SettingsNotificationsDialog() { aria-hidden='true' className={classNames( 'block h-[18px] w-[18px] rounded-full bg-white shadow-lightXs transition-transform duration-200 ease-out', - callSummaryNotifications ? 'translate-x-5' : 'translate-x-0', + callSummaryNotifications + ? 'translate-x-5' + : 'translate-x-0', )} /> diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx index 07ecdf71..1627fe6e 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsShortcutDialog.tsx @@ -103,7 +103,8 @@ export function SettingsShortcutDialog() { const newKeys = new Set(keysPressed) const code = (e as any).code as string | undefined - const hasGetModifierState = typeof (e as any).getModifierState === 'function' + const hasGetModifierState = + typeof (e as any).getModifierState === 'function' const modifierStateAltGraph = hasGetModifierState ? (e as any).getModifierState('AltGraph') : false @@ -129,7 +130,9 @@ export function SettingsShortcutDialog() { const orderedModifiers = ['Ctrl', 'Alt', 'AltGr', 'Cmd'] const modifiers = orderedModifiers.filter((k) => newKeys.has(k)) - const others = [...newKeys].filter((k) => !orderedModifiers.includes(k as any)) + const others = [...newKeys].filter( + (k) => !orderedModifiers.includes(k as any), + ) // Require at least one modifier key — reject bare keys like "6", "R", etc. if (others.length > 0 && modifiers.length === 0) return @@ -155,99 +158,102 @@ export function SettingsShortcutDialog() { - - {/* Dialog content */} - - {/* Title */} - - {t('TopBar.Keyboard shortcut to call')} - - - {/* Subtitle */} - - {t('TopBar.Shortcut title description')}{' '} - - {/* Inline notification */} - {( - + {/* Dialog content */} + + {/* Title */} + + {t('TopBar.Keyboard shortcut to call')} + + + {/* Subtitle */} + + {t('TopBar.Shortcut title description')}{' '} + + {/* Inline notification */} + { + + {t('TopBar.Shortcut subtitle description')} + + } + + {/* Form */} + - {t('TopBar.Shortcut subtitle description')} - - )} - - {/* Form */} - - {/* Input field with clear button next to it */} - - {}} - readOnly - autoFocus - /> - {!!combo && ( + {/* Input field with clear button next to it */} + + {}} + readOnly + autoFocus + /> + {!!combo && ( + + + + )} + + + + {/* Keys not supported info */} + + {t('TopBar.Shortcut body description')} + + + {/* Action buttons */} + { + e.preventDefault() + submit({ combo }) + }} > - + {t('Common.Save')} - )} - - - - {/* Keys not supported info */} - - {t('TopBar.Shortcut body description')} - - {/* Action buttons */} - - { - e.preventDefault() - submit({ combo }) - }} - > - {t('Common.Save')} - - - - {t('Common.Cancel')} - - - + + {t('Common.Cancel')} + + + + - > ) diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx index 715bb7c0..24755b7c 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx @@ -3,18 +3,18 @@ import { faSun as LightIcon, faMoon as DarkIcon, } from '@fortawesome/free-solid-svg-icons' -import { useSharedState } from "@renderer/store" -import { Account } from "@shared/types" -import { getAccountUID } from "@shared/utils/utils" -import { t } from "i18next" -import { OptionElement } from "../OptionElement" -import { Log } from "@shared/utils/logger" +import { useSharedState } from '@renderer/store' +import { Account } from '@shared/types' +import { getAccountUID } from '@shared/utils/utils' +import { t } from 'i18next' +import { OptionElement } from '../OptionElement' +import { Log } from '@shared/utils/logger' import { IPC_EVENTS } from '@shared/constants' export const ThemeIcons = { system: SystemIcon, light: LightIcon, - dark: DarkIcon + dark: DarkIcon, } export const ThemeBox = () => { const [account, setAccount] = useSharedState('account') @@ -30,22 +30,31 @@ export const ThemeBox = () => { isFirstStart: p?.isFirstStart ?? true, availableAccounts: { ...p?.availableAccounts, - [getAccountUID(updatedAccount as Account)]: updatedAccount - } + [getAccountUID(updatedAccount as Account)]: updatedAccount, + }, })) window.electron.send(IPC_EVENTS.CHANGE_THEME, theme) } const themeOptions = [ - { id: 1, name: 'system', icon: ThemeIcons.system, label: t('Settings.System') }, - { id: 2, name: 'light', icon: ThemeIcons.light, label: t('Settings.Light') }, - { id: 3, name: 'dark', icon: ThemeIcons.dark, label: t('Settings.Dark') } + { + id: 1, + name: 'system', + icon: ThemeIcons.system, + label: t('Settings.System'), + }, + { + id: 2, + name: 'light', + icon: ThemeIcons.light, + label: t('Settings.Light'), + }, + { id: 3, name: 'dark', icon: ThemeIcons.dark, label: t('Settings.Dark') }, ] - if (!account) return <>> return ( - + {themeOptions.map((availableTheme) => ( (MENU_ELEMENT.FAVOURITES) + const [lastMenu, setLastMenu] = useState( + MENU_ELEMENT.FAVOURITES, + ) const [isAboutVisited, setIsAboutVisited] = useState(false) - const viewedParkedCalls = useRef([]) const [parkedPulse, setParkedPulse] = useState(false) @@ -39,11 +43,10 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { setSelectedSidebarMenu(menuElement) setShowPhonebookSearchModule(false) setPhonebookSearchModule({ - searchText: undefined + searchText: undefined, }) } - useEffect(() => { if (selectedSidebarMenu && lastMenu !== selectedSidebarMenu) { setLastMenu(() => selectedSidebarMenu) @@ -54,13 +57,14 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { } }, [selectedSidebarMenu]) - useEffect(() => { if (selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS) { viewedParkedCalls.current = [...(parkedCalls || [])] } const currentNames = parkedCalls?.map((c) => c.parkedCaller.name) || [] - viewedParkedCalls.current = viewedParkedCalls.current.filter((p) => currentNames.includes(p.parkedCaller.name)) + viewedParkedCalls.current = viewedParkedCalls.current.filter((p) => + currentNames.includes(p.parkedCaller.name), + ) const names = viewedParkedCalls.current.map((c) => c.parkedCaller.name) const diff = difference(currentNames, names) if (diff.length > 0) { @@ -69,8 +73,8 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { }, [parkedCalls, selectedSidebarMenu]) return ( - - + + {/* FAVOURITE */} {/* PARKED CALLS */} - { - hasPermission(PERMISSION.PARKINGS) && ( - 0} - hasPulseNotification={parkedPulse} - onClick={() => { - handleSidebarMenuSelection(MENU_ELEMENT.PARKED_CALLS) - setParkedPulse(false) - }} - isSelected={selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS} - /> - ) - } + {hasPermission(PERMISSION.PARKINGS) && ( + 0} + hasPulseNotification={parkedPulse} + onClick={() => { + handleSidebarMenuSelection(MENU_ELEMENT.PARKED_CALLS) + setParkedPulse(false) + }} + isSelected={selectedSidebarMenu === MENU_ELEMENT.PARKED_CALLS} + /> + )} {/* APP UPDATE */} ) } - diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts b/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts index 3123c9cb..20e0cd44 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/index.ts @@ -1,4 +1,4 @@ export * from './ContactNameAndAction' export * from './FavouritesStar' export * from './Navbar' -export * from './Sidebar' \ No newline at end of file +export * from './Sidebar' diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx index 3854d07f..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) => ( - + {option.label} @@ -457,55 +496,73 @@ export function AddToPhonebookBox({ close }) { {visibilityError && ( - {visibilityError} + + {visibilityError} + )} {watchPrivacy === 'group' && ( - - {t('Phonebook.Groups')} + + + {t('Phonebook.Groups')} + `${t('Common.Delete')} ${groupName}`} + removeLabel={(groupName) => + `${t('Common.Delete')} ${groupName}` + } error={!!sharedGroupsError} /> {sharedGroupsError && ( - {sharedGroupsError} + + {sharedGroupsError} + )} )} - - {t('Phonebook.Type')} - - + + + {t('Phonebook.Type')} + + + - + {t('Phonebook.Person')} - + - + {t('Phonebook.Company')} @@ -516,52 +573,52 @@ export function AddToPhonebookBox({ close }) { <> {!isFieldVisible('job') && ( revealField('job')} > - + {t('Phonebook.Add job title')} @@ -570,129 +627,132 @@ export function AddToPhonebookBox({ close }) { ) : ( )} {hiddenPhoneFields.length > 0 && ( {hiddenPhoneFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add phone')} @@ -701,51 +761,54 @@ export function AddToPhonebookBox({ close }) { {hiddenEmailFields.length > 0 && ( {hiddenEmailFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add email')} @@ -754,94 +817,98 @@ export function AddToPhonebookBox({ close }) { {/* Company address sub-form */} - + - + {/* "Add field" menu (Address, Social flyout, Website) — opens upward, left-aligned */} - + - + {t('Phonebook.Add field')} {isAddFieldOpen && ( - + {!isFieldVisible('address') && ( { revealField('address') setIsAddFieldOpen(false) @@ -881,27 +948,36 @@ export function AddToPhonebookBox({ close }) { )} {SOCIAL_FIELD_OPTIONS.some((o) => !isFieldVisible(o.key)) && ( (isSocialSubmenuOpen ? setIsSocialSubmenuOpen(false) : openSocialSubmenu())} + onClick={() => + isSocialSubmenuOpen + ? setIsSocialSubmenuOpen(false) + : openSocialSubmenu() + } > {t('Phonebook.Social')} - + {isSocialSubmenuOpen && ( - - {SOCIAL_FIELD_OPTIONS.filter((o) => !isFieldVisible(o.key)).map((o) => ( + + {SOCIAL_FIELD_OPTIONS.filter( + (o) => !isFieldVisible(o.key), + ).map((o) => ( { revealField(o.key) setIsSocialSubmenuOpen(false) @@ -917,8 +993,8 @@ export function AddToPhonebookBox({ close }) { )} {!isFieldVisible('url') && ( { revealField('url') setIsAddFieldOpen(false) @@ -931,18 +1007,18 @@ export function AddToPhonebookBox({ close }) { )} - - - + + + {t('Common.Cancel')} - - + + {t('Common.Save')} {isLoading && ( - + )} diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx index ce31c240..0dbf4cf0 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx @@ -1,13 +1,14 @@ -import { AddToPhonebookBox } from "./AddToPhonebookBox" -import { usePhonebookModule } from "./hook/usePhonebookModule" -import { useNethlinkData } from "@renderer/store" +import { AddToPhonebookBox } from './AddToPhonebookBox' +import { usePhonebookModule } from './hook/usePhonebookModule' +import { useNethlinkData } from '@renderer/store' export const PhonebookModule = () => { - const phonebookModule = usePhonebookModule() const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [selectedContact, setSelectedContact] = phonebookModule.selectedContact return ( @@ -15,7 +16,7 @@ export const PhonebookModule = () => { close={() => { setSelectedContact(undefined) setShowAddContactModule(false) - //if i previously searched anything then i would return to the search page + // if i previously searched anything then i would return to the search page setShowPhonebookSearchModule(!!phonebookSearchModule) }} /> diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts index 8da89c4d..9f4c9abf 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts @@ -1,29 +1,40 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { ContactType, PhonebookModuleData, SelectedContact, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + ContactType, + PhonebookModuleData, + SelectedContact, + StateType, +} from '@shared/types' export const usePhonebookModule = (): { - selectedContact: StateType, + selectedContact: StateType handleAddContactToPhonebook: (contact: ContactType) => Promise } => { - - const [phonebookModule, setPhonebookModule] = useNethlinkData('phonebookModule') + const [phonebookModule, setPhonebookModule] = + useNethlinkData('phonebookModule') const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookModuleData) => (value: T | undefined) => { - setPhonebookModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookModuleData) => + (value: T | undefined) => { + setPhonebookModule((p) => ({ + ...p, + [selector]: value as any, + })) + } const handleAddContactToPhonebook = async (contact: ContactType) => { - const result: ContactType = await NethVoiceAPI.Phonebook.createContact(contact) + const result: ContactType = + await NethVoiceAPI.Phonebook.createContact(contact) return result } return { - selectedContact: [phonebookModule?.selectedContact, update('selectedContact')] as StateType, - handleAddContactToPhonebook + selectedContact: [ + phonebookModule?.selectedContact, + update('selectedContact'), + ] as StateType, + handleAddContactToPhonebook, } } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx index 55b368af..83f3df08 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx @@ -1,22 +1,23 @@ -import { debouncer } from "@shared/utils/utils" -import { SearchNumberBox } from "./SearchNumberBox" -import { useEffect, useState } from "react" -import { usePhonebookSearchModule } from "./hook/usePhoneBookSearchModule" -import { SearchData } from "@shared/types" -import { useNethlinkData } from "@renderer/store" -import { SearchNumberDetail } from "./SearchNumberDetail" -import classNames from "classnames" +import { debouncer } from '@shared/utils/utils' +import { SearchNumberBox } from './SearchNumberBox' +import { useEffect, useState } from 'react' +import { usePhonebookSearchModule } from './hook/usePhoneBookSearchModule' +import { SearchData } from '@shared/types' +import { useNethlinkData } from '@renderer/store' +import { SearchNumberDetail } from './SearchNumberDetail' +import classNames from 'classnames' export const PhoneBookSearchModule = () => { - const phoneBookModule = usePhonebookSearchModule() const { searchPhonebookContacts } = phoneBookModule const [searchText] = phoneBookModule.searchTextState const [searchResult, setSearchResult] = useState() - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') const [contactDetail, setContactDetail] = useState<{ - contact: SearchData, + contact: SearchData primaryNumber: string | null }>() @@ -28,7 +29,7 @@ export const PhoneBookSearchModule = () => { const result = await searchPhonebookContacts() setSearchResult(() => result) }, - 250 + 250, ) } else { setSearchResult(() => []) @@ -49,9 +50,12 @@ export const PhoneBookSearchModule = () => { setContactDetail(() => ({ contact, primaryNumber })) }} /> - { - setContactDetail(() => undefined) - }} /> + { + setContactDetail(() => undefined) + }} + /> > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx index c5c3dee7..8c0f55b5 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx @@ -1,6 +1,6 @@ import { faMagnifyingGlass as SearchIcon, - faXmark as DeleteSearchIcon + faXmark as DeleteSearchIcon, } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -13,27 +13,22 @@ import { Button, TextInput } from '@renderer/components/Nethesis' import { Log } from '@shared/utils/logger' import { debouncer } from '@shared/utils/utils' type FormDataType = { - searchText: string | undefined; + searchText: string | undefined } export function SearchBox(): JSX.Element { - const { callNumber } = usePhoneIslandEventHandler() const phoneBookSearchModule = usePhonebookSearchModule() const [searchText, setSearchText] = phoneBookSearchModule.searchTextState const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const { - register, - watch, - reset, - setValue, - handleSubmit - } = useForm({ + const { register, watch, reset, setValue, handleSubmit } = useForm({ defaultValues: { - searchText: '' - } + searchText: '', + }, }) useEffect(() => { @@ -47,7 +42,6 @@ export function SearchBox(): JSX.Element { const tempSearchText = watch('searchText') - useEffect(() => { if (tempSearchText) { setSearchText(tempSearchText) @@ -82,26 +76,29 @@ export function SearchBox(): JSX.Element { } return ( - + {tempSearchText !== '' && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx index fdade9eb..8134049a 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx @@ -10,7 +10,7 @@ import { ClassNames } from '@renderer/utils' export interface SearchNumberProps { user: SearchData - className?: string, + className?: string onClick?: (user: SearchData, primaryNumber: string | null) => void } @@ -20,12 +20,16 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const [operators] = useNethlinkData('operators') const { isSearchAlsoAFavourite } = useFavouriteModule() - const getUsernameFromPhoneNumber = (number: string) => { - return user.type !== 'extension' ? operators?.extensions[number]?.username : undefined + return user.type !== 'extension' + ? operators?.extensions[number]?.username + : undefined } - function highlightMatch(number: string | undefined, searchText: string): ReactNode[] { + function highlightMatch( + number: string | undefined, + searchText: string, + ): ReactNode[] { const parts: ReactNode[] = [] let lastIndex = 0 if (number) { @@ -37,10 +41,10 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { parts.push( {number.substring(index, index + searchText.length)} - + , ) lastIndex = index + searchText.length index = lowerText.indexOf(lowerSearchText, lastIndex) @@ -52,11 +56,21 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { } let phoneNumber: string | null = null - const keys = ['extension', 'cellphone', 'homephone', 'workphone', 'workphone2', 'cellphone2', 'otherphone'] + const keys = [ + 'extension', + 'cellphone', + 'homephone', + 'workphone', + 'workphone2', + 'cellphone2', + 'otherphone', + ] for (const key of keys) { if (!phoneNumber) { - phoneNumber = (user[key] || '').includes(`${searchText}`) ? user[key] : null + phoneNumber = (user[key] || '').includes(`${searchText}`) + ? user[key] + : null } else { break } @@ -72,9 +86,7 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const highlightedNumber = highlightMatch(phoneNumber, searchText || '') const otherDevicesCount = Array.from( new Set( - keys - .map((k) => user[k]) - .filter((n): n is string => !!n && n !== ' '), + keys.map((k) => user[k]).filter((n): n is string => !!n && n !== ' '), ), ).filter((n) => n !== phoneNumber).length @@ -82,37 +94,46 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const avatarSrc = username ? operators?.avatars?.[username] : '' return ( - - + - + 0 ? t('Common.PlusOther', { count: otherDevicesCount }) as string : ''} + otherNumber={ + otherDevicesCount > 0 + ? (t('Common.PlusOther', { + count: otherDevicesCount, + }) as string) + : '' + } isHighlight={true} username={username} isFavourite={false} isSearchData={true} showCompany={true} - onOpenDetail={onClick ? () => { - onClick?.(user, phoneNumber) - } : undefined} + onOpenDetail={ + onClick + ? () => { + onClick?.(user, phoneNumber) + } + : undefined + } /> {onClick && ( onClick(user, phoneNumber)} > - + {t('Phonebook.Details')} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx index 7ce69d84..0cb928e6 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx @@ -99,24 +99,20 @@ export function SearchNumberBox({ ), ) } - let operatorsResults = candidates.filter( - (op: any) => { - return ( - (op.name && - new RegExp(cleanQuery, 'i').test( - op.name.replace(cleanRegex, ''), - )) || - new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) - ) - }, - ) + let operatorsResults = candidates.filter((op: any) => { + return ( + (op.name && + new RegExp(cleanQuery, 'i').test(op.name.replace(cleanRegex, ''))) || + new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) + ) + }) if (operatorsResults.length) { operatorsResults = cloneDeep(operatorsResults) operatorsResults.forEach((op: any) => { op.resultType = 'operator' - op.extension = op.endpoints?.mainextension[0]?.id //for phoneNumber search + op.extension = op.endpoints?.mainextension[0]?.id // for phoneNumber search }) } operatorsResults.sort(sortByProperty('name')) @@ -155,7 +151,7 @@ export function SearchNumberBox({ const filteredOperators = getFoundedOperators() const copy = [...filteredOperators, ...unFilteredNumbers] // Allow creating a contact from any meaningful query — number or name. - let _canAddInPhonebook = cleanQuery.length > 1 + const _canAddInPhonebook = cleanQuery.length > 1 setFilteredPhoneNumbers(() => copy as any) setCanAddToPhonebook(() => _canAddInPhonebook) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx index 48705d66..1b41ea9f 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx @@ -29,67 +29,211 @@ const getSocialUrl = (value: string) => { interface SearchNumberBoxProps { contactDetail?: { - contact: SearchData, + contact: SearchData primaryNumber: string | null - }, + } onBack: () => void } -export function SearchNumberDetail({ contactDetail, onBack }: SearchNumberBoxProps) { +export function SearchNumberDetail({ + contactDetail, + onBack, +}: SearchNumberBoxProps) { const { contact } = contactDetail || {} return ( - - {contact && - + + {contact && ( + {/* Title row: back arrow + name + visibility badge (mock 936-24158). px-4 matches the search box padding above (Navbar.tsx px-4 pt-2). */} - - + + { - debouncer('onDetailBack', () => { - onBack() - }, 250) + debouncer( + 'onDetailBack', + () => { + onBack() + }, + 250, + ) }} /> {contact?.displayName} - + - {contact.company && {contact.company}} - {contact.job && {contact.job}} - {contact.extension && {contact.extension}} - {contact.workphone && {contact.workphone}} - {contact.workphone2 && {contact.workphone2}} - {contact.cellphone && {contact.cellphone}} - {contact.cellphone2 && {contact.cellphone2}} - {contact.homephone && {contact.homephone}} - {contact.otherphone && {contact.otherphone}} - {contact.fax && {contact.fax}} - {contact.workemail && {contact.workemail}} - {contact.homeemail && {contact.homeemail}} - {contact.otheremail && {contact.otheremail}} - {contact.facebook && {contact.facebook}} - {contact.instagram && {contact.instagram}} - {contact.linkedin && {contact.linkedin}} - {contact.url && {contact.url}} - {contact.notes && {contact.notes}} + {contact.company && ( + + {contact.company} + + )} + {contact.job && ( + + {contact.job} + + )} + {contact.extension && ( + + {contact.extension} + + )} + {contact.workphone && ( + + {contact.workphone} + + )} + {contact.workphone2 && ( + + {contact.workphone2} + + )} + {contact.cellphone && ( + + {contact.cellphone} + + )} + {contact.cellphone2 && ( + + {contact.cellphone2} + + )} + {contact.homephone && ( + + {contact.homephone} + + )} + {contact.otherphone && ( + + {contact.otherphone} + + )} + {contact.fax && ( + + {contact.fax} + + )} + {contact.workemail && ( + + {contact.workemail} + + )} + {contact.homeemail && ( + + {contact.homeemail} + + )} + {contact.otheremail && ( + + {contact.otheremail} + + )} + {contact.facebook && ( + + {contact.facebook} + + )} + {contact.instagram && ( + + {contact.instagram} + + )} + {contact.linkedin && ( + + {contact.linkedin} + + )} + {contact.url && ( + + {contact.url} + + )} + {contact.notes && ( + + {contact.notes} + + )} - } + )} ) } @@ -106,45 +250,81 @@ const VisibilityBadge = ({ contact }: { contact: SearchData }) => { if (visibility === 'group') { const visibleGroups = sharedGroups.slice(0, VISIBLE_GROUPS_LIMIT) const remainingGroups = sharedGroups.slice(VISIBLE_GROUPS_LIMIT) - const groupBadgeClassName = classNames(theme.badge.base, theme.badge.rounded.full, theme.badge.sizes.small, 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center') + const groupBadgeClassName = classNames( + theme.badge.base, + theme.badge.rounded.full, + theme.badge.sizes.small, + 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center', + ) - return - {visibleGroups.map((groupName) => ( - - - {groupName} - - ))} - {remainingGroups.length > 0 && ( - <> - - {t('Common.PlusOther', { count: remainingGroups.length })} - - - > - )} - + return ( + + {visibleGroups.map((groupName) => ( + + + {groupName} + + ))} + {remainingGroups.length > 0 && ( + <> + + {t('Common.PlusOther', { count: remainingGroups.length })} + + + > + )} + + ) } - return - - {visibility === 'private' ? t("Phonebook.Only me") : t("Phonebook.Public")} - + return ( + + + {visibility === 'private' + ? t('Phonebook.Only me') + : t('Phonebook.Public')} + + ) } -const ContactDetail = ({ children, label, copy, protocol, href, plain }: { - label: string | DefaultTFuncReturn, - children: string, - copy?: boolean, - protocol?: 'mailto' | 'callto', - href?: string, +const ContactDetail = ({ + children, + label, + copy, + protocol, + href, + plain, +}: { + label: string | DefaultTFuncReturn + children: string + copy?: boolean + protocol?: 'mailto' | 'callto' + href?: string plain?: boolean }) => { const { isCallsEnabled } = useAccount() @@ -167,7 +347,8 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { }, 2000) } - const linkClassName = 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' + const linkClassName = + 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' const cleaned = ('' + children).replace(/ /g, '') const isCalltoDisabled = protocol === 'callto' && !isCallsEnabled @@ -178,47 +359,67 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { } } - return - - {label} - - - {plain ? ( - - {children} - - ) : href ? ( - - {children} - - ) : ( - - {children} - - )} + return ( + + + {label} + + + {plain ? ( + + {children} + + ) : href ? ( + + {children} + + ) : ( + + {children} + + )} - {copy && - - + {copy && ( + + + + + )} - } + + - - - + ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts index c048cae4..da2d8a72 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts @@ -1,23 +1,32 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { PhonebookSearchModuleData, SearchCallData, SearchData, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + PhonebookSearchModuleData, + SearchCallData, + SearchData, + StateType, +} from '@shared/types' export const usePhonebookSearchModule = (): { - searchTextState: StateType, + searchTextState: StateType searchPhonebookContacts: () => Promise } => { - const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData('phonebookSearchModule') + const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData( + 'phonebookSearchModule', + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookSearchModuleData) => (value: T | undefined) => { - setPhonebookSearchModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookSearchModuleData) => + (value: T | undefined) => { + setPhonebookSearchModule((p) => ({ + ...p, + [selector]: value as any, + })) + } function mapContact(contact: SearchData) { // kind & display name - if (contact?.name && contact?.name !== '-') { + if (contact?.name && contact?.name !== '-') { contact.kind = 'person' contact.displayName = contact?.name } else { @@ -33,23 +42,30 @@ export const usePhonebookSearchModule = (): { } function prapareSearchData(receivedPhoneNumbers: SearchCallData) { - receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map((contact: SearchData) => { - return mapContact(contact) - }) + receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map( + (contact: SearchData) => { + return mapContact(contact) + }, + ) const filteredNumbers = receivedPhoneNumbers.rows.filter( - (phoneNumber) => !(!phoneNumber?.displayName || phoneNumber?.displayName === '') + (phoneNumber) => + !(!phoneNumber?.displayName || phoneNumber?.displayName === ''), ) return filteredNumbers } const searchPhonebookContacts = async () => { - const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search(phonebookSearchModule?.searchText || '') + const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search( + phonebookSearchModule?.searchText || '', + ) return prapareSearchData(searchResult) } - return { - searchTextState: [phonebookSearchModule?.searchText, update('searchText')] as StateType, - searchPhonebookContacts + searchTextState: [ + phonebookSearchModule?.searchText, + update('searchText'), + ] as StateType, + searchPhonebookContacts, } } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx index d7b31c67..61d9505b 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx @@ -1,79 +1,97 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { Menu } from "@headlessui/react" -import classNames from "classnames" -import { useTheme } from "@renderer/theme/Context" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Menu } from '@headlessui/react' +import classNames from 'classnames' +import { useTheme } from '@renderer/theme/Context' import { faCheck as CheckedIcon, - faSortDown as ArrowMenuIcon + faSortDown as ArrowMenuIcon, } from '@fortawesome/free-solid-svg-icons' -import { t } from "i18next" -import { useNethlinkData } from "@renderer/store" -import { FilterTypes } from "@shared/constants" -import { NethLinkPageData } from "@shared/types" +import { t } from 'i18next' +import { useNethlinkData } from '@renderer/store' +import { FilterTypes } from '@shared/constants' +import { NethLinkPageData } from '@shared/types' export const FavouriteFilter = () => { const { theme: nethTheme } = useTheme() - const [speeddialsModule, setSpeeddialsModule] = useNethlinkData('speeddialsModule') + const [speeddialsModule, setSpeeddialsModule] = + useNethlinkData('speeddialsModule') const setFilter = (type: FilterTypes) => { setSpeeddialsModule((p) => ({ ...p, - favouriteOrder: type + favouriteOrder: type, })) } - - const RenderMenuItem = ({ type }: { type: FilterTypes }) => { - return - { - setFilter(type) - }} - > - - - - {t(`SpeedDial.${type}`)} - + return ( + + { + setFilter(type) + }} + > + + + + {t(`SpeedDial.${type}`)} + + - - + + ) } return ( <> - + - + {t(`SpeedDial.${speeddialsModule?.favouriteOrder} label`)} - + {t('SpeedDial.Order by')} - - - + + + > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx index 0a7bc044..778c00ad 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx @@ -1,17 +1,14 @@ -import { ModuleTitle } from "@renderer/components/ModuleTitle" -import { Scrollable } from "@renderer/components/Scrollable" -import { SkeletonRow } from "@renderer/components/SkeletonRow" -import { t } from "i18next" -import { useFavouriteModule } from "../hook/useFavouriteModule" -import { ContactNumber } from "../shared/ContactNumber" -import { EmptyList } from "@renderer/components/EmptyList" +import { ModuleTitle } from '@renderer/components/ModuleTitle' +import { Scrollable } from '@renderer/components/Scrollable' +import { SkeletonRow } from '@renderer/components/SkeletonRow' +import { t } from 'i18next' +import { useFavouriteModule } from '../hook/useFavouriteModule' +import { ContactNumber } from '../shared/ContactNumber' +import { EmptyList } from '@renderer/components/EmptyList' import { faStar as FavouriteIcon } from '@fortawesome/free-solid-svg-icons' -import { FavouriteFilter } from "./FavouriteFilter" - - +import { FavouriteFilter } from './FavouriteFilter' export const FavouritesBox = () => { - const { favourites } = useFavouriteModule() return ( @@ -20,21 +17,27 @@ export const FavouritesBox = () => { title={t('SpeedDial.Favourites')} actionComponent={} /> - + {favourites ? ( favourites.length > 0 ? ( favourites?.map((e, idx) => { return ( - - + + @@ -42,7 +45,10 @@ export const FavouritesBox = () => { ) }) ) : ( - + ) ) : ( Array(3) @@ -50,7 +56,7 @@ export const FavouritesBox = () => { .map((_, idx) => { return ( diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx index 7ef2b4e5..ca52dd21 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx @@ -1,8 +1,5 @@ -import { FavouritesBox } from "./FavouritesBox" +import { FavouritesBox } from './FavouritesBox' export const FavouritesModule = () => { - - return ( - - ) + return } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx index 9ceb55bd..9ca25b4e 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx @@ -1,39 +1,39 @@ -import { Modal } from "@renderer/components" -import { Button } from "@renderer/components/Nethesis" -import { useSharedState } from "@renderer/store" -import { parseThemeToClassName, sendNotification, truncate } from "@renderer/utils" -import { ContactType } from "@shared/types" -import { t } from "i18next" -import { createRef } from "react" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { Modal } from '@renderer/components' +import { Button } from '@renderer/components/Nethesis' +import { useSharedState } from '@renderer/store' import { - faTriangleExclamation as WarningIcon -} from '@fortawesome/free-solid-svg-icons' -export const SpeedDialDeleteDialog = ({ - close -}) => { - + parseThemeToClassName, + sendNotification, + truncate, +} from '@renderer/utils' +import { ContactType } from '@shared/types' +import { t } from 'i18next' +import { createRef } from 'react' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTriangleExclamation as WarningIcon } from '@fortawesome/free-solid-svg-icons' +export const SpeedDialDeleteDialog = ({ close }) => { const cancelDeleteButtonRef = createRef() const speedDialModule = useSpeedDialsModule() - const [selectedSpeedDial, setSelectedSpeedDial] = speedDialModule.speedDialsState + const [selectedSpeedDial, setSelectedSpeedDial] = + speedDialModule.speedDialsState const [theme] = useSharedState('theme') - - function confirmDeleteSpeedDial(speedDial: ContactType) { - speedDialModule.deleteSpeedDial(speedDial).then((_) => { - sendNotification( - t('Notification.speeddial_deleted_title'), - t('Notification.speeddial_deleted_description') - ) - setSelectedSpeedDial(undefined) - close() - }) + speedDialModule + .deleteSpeedDial(speedDial) + .then((_) => { + sendNotification( + t('Notification.speeddial_deleted_title'), + t('Notification.speeddial_deleted_description'), + ) + setSelectedSpeedDial(undefined) + close() + }) .catch((error) => { sendNotification( t('Notification.speeddial_not_deleted_title'), - t('Notification.speeddial_not_deleted_description') + t('Notification.speeddial_not_deleted_description'), ) }) } @@ -55,24 +55,24 @@ export const SpeedDialDeleteDialog = ({ onClose={() => close()} afterLeave={() => setSelectedSpeedDial(undefined)} themeMode={parseThemeToClassName(theme)} - className="font-Poppins w-[100px]" + className='font-Poppins w-[100px]' > - + - - + + {t('SpeedDial.Delete speed dial')} - - + + {t('SpeedDial.Speed dial delete message', { - deletingName: truncate(selectedSpeedDial?.name || '', 30) + deletingName: truncate(selectedSpeedDial?.name || '', 30), })} @@ -80,19 +80,19 @@ export const SpeedDialDeleteDialog = ({ {t('Common.Delete')} - + {t('Common.Cancel')} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx index 4530c192..9932b2fb 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx @@ -34,16 +34,16 @@ export function SpeedDialFormBox({ close }) { .trim() .min(1, `${t('Common.This field is required')}`) .min(3, `${t('Common.This field must be at least', { number: '2' })}`) - .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`) + .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`), }) const { register, handleSubmit, reset, - formState: { errors } + formState: { errors }, } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), }) const onSubmitForm: SubmitHandler = (data) => { @@ -67,12 +67,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_modified_title'), - t('Notification.speeddial_modified_description') + t('Notification.speeddial_modified_description'), ) } else { sendNotification( t('Notification.speeddial_created_title'), - t('Notification.speeddial_created_description') + t('Notification.speeddial_created_description'), ) } reset() @@ -82,12 +82,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_not_modified_title'), - t('Notification.speeddial_not_modified_description') + t('Notification.speeddial_not_modified_description'), ) } else { sendNotification( t('Notification.speeddial_not_created_title'), - t('Notification.speeddial_not_created_description') + t('Notification.speeddial_not_created_description'), ) } Log.warning('error during speedDialModule.upsertSpeedDial:', e) @@ -99,12 +99,16 @@ export function SpeedDialFormBox({ close }) { return ( <> - + { e.preventDefault() handleSubmit(onSubmitForm)(e) @@ -113,7 +117,7 @@ export function SpeedDialFormBox({ close }) { - - - + + + {t('Common.Cancel')} - - + + {selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')} {isLoading && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx index 896e3d3a..6b0ca6d7 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx @@ -1,6 +1,6 @@ import { faCirclePlus as AddSpeedDialIcon, - faBolt as SpeedDialIcon + faBolt as SpeedDialIcon, } from '@fortawesome/free-solid-svg-icons' import { ContactType } from '@shared/types' import { t } from 'i18next' @@ -11,7 +11,10 @@ import { ModuleTitle } from '@renderer/components/ModuleTitle' import { ContactNumber } from '../shared/ContactNumber' import { EmptyList } from '@renderer/components/EmptyList' -export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): JSX.Element { +export function SpeedDialsBox({ + showSpeedDialForm, + showDeleteSpeedDialDialog, +}): JSX.Element { const speedDialModule = useSpeedDialsModule() const { speedDials } = speedDialModule const [, setSelectedSpeedDial] = speedDialModule.speedDialsState @@ -39,22 +42,29 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): actionIcon={AddSpeedDialIcon} actionText={t('SpeedDial.Create')} /> - + {speedDials ? ( speedDials.length > 0 ? ( speedDials?.map((e, idx) => { return ( - - + + @@ -62,7 +72,10 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): ) }) ) : ( - + ) ) : ( Array(3) @@ -70,7 +83,7 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): .map((_, idx) => { return ( diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx index 47c4e02c..f33692fc 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx @@ -1,41 +1,42 @@ -import { SpeedDialsBox } from "./SpeedDialsBox" -import { useEffect, useState } from "react" -import { SpeedDialFormBox } from "./SpeedDialFormBox" -import { SpeedDialDeleteDialog } from "./SpeedDialDeleteDialog" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" - +import { SpeedDialsBox } from './SpeedDialsBox' +import { useEffect, useState } from 'react' +import { SpeedDialFormBox } from './SpeedDialFormBox' +import { SpeedDialDeleteDialog } from './SpeedDialDeleteDialog' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' export const SpeeddialsModule = () => { - const speedDialModule = useSpeedDialsModule() const [, setSelectedSpeedDial] = speedDialModule.speedDialsState const [isSpeedDialFormOpen, setSpeedDialFormOpen] = useState(false) - const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = useState(false) + const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = + useState(false) return ( <> - {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && setSpeedDialFormOpen(true)} - showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} - /> - } - - - {isSpeedDialFormOpen && { - setSelectedSpeedDial(undefined) - setSpeedDialFormOpen(false) - }} - /> - } - - {isDeleteSpeedDialDialogOpen && { - setSelectedSpeedDial(undefined) - setDeleteSpeedDialDialogOpen(false) - }} - />} - + {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && ( + setSpeedDialFormOpen(true)} + showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} + /> + )} + + {isSpeedDialFormOpen && ( + { + setSelectedSpeedDial(undefined) + setSpeedDialFormOpen(false) + }} + /> + )} + + {isDeleteSpeedDialDialogOpen && ( + { + setSelectedSpeedDial(undefined) + setDeleteSpeedDialDialogOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts index 8268bfc4..ebdd7ff5 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts @@ -1,43 +1,58 @@ -import { useNethlinkData } from "@renderer/store" -import { ContactType, SearchData } from "@shared/types" -import { useEffect, useState } from "react" -import { debouncer } from "@shared/utils/utils" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { FilterTypes, SpeeddialTypes } from "@shared/constants" -import { Log } from "@shared/utils/logger" +import { useNethlinkData } from '@renderer/store' +import { ContactType, SearchData } from '@shared/types' +import { useEffect, useState } from 'react' +import { debouncer } from '@shared/utils/utils' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { FilterTypes, SpeeddialTypes } from '@shared/constants' +import { Log } from '@shared/utils/logger' export const useFavouriteModule = () => { const [speeddialsModule] = useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') const [operators] = useNethlinkData('operators') - const [favourites, setFavourites] = useState(undefined) + const [favourites, setFavourites] = useState( + undefined, + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() const getUsernameFromContact = (contact: ContactType): string | undefined => { const extension = contact.speeddial_num || '' - const usernameFromExtension = extension ? operators?.extensions?.[extension]?.username : undefined + const usernameFromExtension = extension + ? operators?.extensions?.[extension]?.username + : undefined return usernameFromExtension || contact.name || undefined } useEffect(() => { - rawSpeedDials && filterFavourites(rawSpeedDials, speeddialsModule?.favouriteOrder || FilterTypes.AZ) + rawSpeedDials && + filterFavourites( + rawSpeedDials, + speeddialsModule?.favouriteOrder || FilterTypes.AZ, + ) }, [rawSpeedDials, speeddialsModule?.favouriteOrder, operators]) const getSorter = (order: FilterTypes) => { - let sorter: ((a: ContactType, b: ContactType) => number) | undefined = undefined + let sorter: ((a: ContactType, b: ContactType) => number) | undefined = + undefined switch (order) { case FilterTypes.ZA: - sorter = (a: ContactType, b: ContactType) => (a.name || '') < (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') < (b.name || '') ? 1 : -1 + break case FilterTypes.EXT: - sorter = (a: ContactType, b: ContactType) => (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 + break default: - sorter = (a: ContactType, b: ContactType) => (a.name || '') > (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') > (b.name || '') ? 1 : -1 + break } return sorter } - const filterFavourites = (rawSpeeddials: ContactType[], order: FilterTypes) => { + const filterFavourites = ( + rawSpeeddials: ContactType[], + order: FilterTypes, + ) => { const favourites = rawSpeeddials .filter(isFavourite) .filter(isActiveOperator) @@ -57,13 +72,15 @@ export const useFavouriteModule = () => { const username = getUsernameFromContact(contact) if (!username) return false // Check if the contact's username exists in any operator group - return Object.values(operators.groups).some(group => - group.users.includes(username) + return Object.values(operators.groups).some((group) => + group.users.includes(username), ) } const isSearchAlsoAFavourite = (contact: SearchData) => { - const foundedContact = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) + const foundedContact = favourites?.find( + (s) => s.speeddial_num === (contact.speeddial_num || contact.extension), + ) if (foundedContact) { return isFavourite(foundedContact) } @@ -77,53 +94,63 @@ export const useFavouriteModule = () => { } function toggleFavourite(contact: ContactType) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const extension = contact.speeddial_num || '' - const favouriteSpeedDial = extension - ? favourites?.find((s) => s.speeddial_num === extension) - : favourites?.find((s) => s.id === contact.id) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const extension = contact.speeddial_num || '' + const favouriteSpeedDial = extension + ? favourites?.find((s) => s.speeddial_num === extension) + : favourites?.find((s) => s.id === contact.id) - if (!favouriteSpeedDial) { - //if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name - const username = getUsernameFromContact(contact) - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - //if I am deleting favourite then I am in favourite, I can use the id directly - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${favouriteSpeedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + if (!favouriteSpeedDial) { + // if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name + const username = getUsernameFromContact(contact) + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + // if I am deleting favourite then I am in favourite, I can use the id directly + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${favouriteSpeedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } function toggleFavouriteFromSearch(contact: SearchData) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const speedDial = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) - if (!speedDial) { - //I am creating the favourite from the search so I find the username directly in searchData - const username = contact.username - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${speedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const speedDial = favourites?.find( + (s) => + s.speeddial_num === (contact.speeddial_num || contact.extension), + ) + if (!speedDial) { + // I am creating the favourite from the search so I find the username directly in searchData + const username = contact.username + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${speedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } - return { favourites, toggleFavourite, isFavourite, isSpeedDialAlsoAFavourite, isSearchAlsoAFavourite, - toggleFavouriteFromSearch + toggleFavouriteFromSearch, } } - - diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts index ad6919db..cb94f1b4 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts @@ -1,26 +1,37 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData, useSharedState } from "@renderer/store" -import { SpeeddialTypes } from "@shared/constants" -import { ContactType, NewContactType, NewSpeedDialType, SpeedDialModuleData, StateType } from "@shared/types" -import { useEffect, useState } from "react" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { SpeeddialTypes } from '@shared/constants' +import { + ContactType, + NewContactType, + NewSpeedDialType, + SpeedDialModuleData, + StateType, +} from '@shared/types' +import { useEffect, useState } from 'react' export const useSpeedDialsModule = (): { - speedDials: ContactType[] | undefined, - speedDialsState: StateType
{t('Common.No internet connection description')}
+ {t('Common.No internet connection description')} +
- {actionText} -
+ {actionText} +
{t('TopBar.Go to NethVoice CTI')}
- {label} -
{label}
{t('TopBar.Signed in as')}
+
+ {t('TopBar.Signed in as')} +
{truncate(account.data?.name, { length: 20 })}
{account.data?.endpoints.mainextension[0].id}
[{account.data?.default_device.type}]
- {t('TopBar.Shortcut title description')}{' '} -
+ {t('TopBar.Shortcut title description')}{' '} +
{t('TopBar.Shortcut subtitle description')}
+ {t('TopBar.Shortcut body description')} +
- {t('TopBar.Shortcut body description')} -
{callName} @@ -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) => ( - + {option.label} @@ -457,55 +496,73 @@ export function AddToPhonebookBox({ close }) { {visibilityError && ( - {visibilityError} + + {visibilityError} + )} {watchPrivacy === 'group' && ( - - {t('Phonebook.Groups')} + + + {t('Phonebook.Groups')} + `${t('Common.Delete')} ${groupName}`} + removeLabel={(groupName) => + `${t('Common.Delete')} ${groupName}` + } error={!!sharedGroupsError} /> {sharedGroupsError && ( - {sharedGroupsError} + + {sharedGroupsError} + )} )} - - {t('Phonebook.Type')} - - + + + {t('Phonebook.Type')} + + + - + {t('Phonebook.Person')} - + - + {t('Phonebook.Company')} @@ -516,52 +573,52 @@ export function AddToPhonebookBox({ close }) { <> {!isFieldVisible('job') && ( revealField('job')} > - + {t('Phonebook.Add job title')} @@ -570,129 +627,132 @@ export function AddToPhonebookBox({ close }) { ) : ( )} {hiddenPhoneFields.length > 0 && ( {hiddenPhoneFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add phone')} @@ -701,51 +761,54 @@ export function AddToPhonebookBox({ close }) { {hiddenEmailFields.length > 0 && ( {hiddenEmailFields.map((o) => ( - revealField(o.key)}> + revealField(o.key)} + > {t(o.labelKey)} ))} > } > - + - + {t('Phonebook.Add email')} @@ -754,94 +817,98 @@ export function AddToPhonebookBox({ close }) { {/* Company address sub-form */} - + - + {/* "Add field" menu (Address, Social flyout, Website) — opens upward, left-aligned */} - + - + {t('Phonebook.Add field')} {isAddFieldOpen && ( - + {!isFieldVisible('address') && ( { revealField('address') setIsAddFieldOpen(false) @@ -881,27 +948,36 @@ export function AddToPhonebookBox({ close }) { )} {SOCIAL_FIELD_OPTIONS.some((o) => !isFieldVisible(o.key)) && ( (isSocialSubmenuOpen ? setIsSocialSubmenuOpen(false) : openSocialSubmenu())} + onClick={() => + isSocialSubmenuOpen + ? setIsSocialSubmenuOpen(false) + : openSocialSubmenu() + } > {t('Phonebook.Social')} - + {isSocialSubmenuOpen && ( - - {SOCIAL_FIELD_OPTIONS.filter((o) => !isFieldVisible(o.key)).map((o) => ( + + {SOCIAL_FIELD_OPTIONS.filter( + (o) => !isFieldVisible(o.key), + ).map((o) => ( { revealField(o.key) setIsSocialSubmenuOpen(false) @@ -917,8 +993,8 @@ export function AddToPhonebookBox({ close }) { )} {!isFieldVisible('url') && ( { revealField('url') setIsAddFieldOpen(false) @@ -931,18 +1007,18 @@ export function AddToPhonebookBox({ close }) { )} - - - + + + {t('Common.Cancel')} - - + + {t('Common.Save')} {isLoading && ( - + )} diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx index ce31c240..0dbf4cf0 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/PhonebookModule.tsx @@ -1,13 +1,14 @@ -import { AddToPhonebookBox } from "./AddToPhonebookBox" -import { usePhonebookModule } from "./hook/usePhonebookModule" -import { useNethlinkData } from "@renderer/store" +import { AddToPhonebookBox } from './AddToPhonebookBox' +import { usePhonebookModule } from './hook/usePhonebookModule' +import { useNethlinkData } from '@renderer/store' export const PhonebookModule = () => { - const phonebookModule = usePhonebookModule() const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [selectedContact, setSelectedContact] = phonebookModule.selectedContact return ( @@ -15,7 +16,7 @@ export const PhonebookModule = () => { close={() => { setSelectedContact(undefined) setShowAddContactModule(false) - //if i previously searched anything then i would return to the search page + // if i previously searched anything then i would return to the search page setShowPhonebookSearchModule(!!phonebookSearchModule) }} /> diff --git a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts index 8da89c4d..9f4c9abf 100644 --- a/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/PhonebookModule/hook/usePhonebookModule.ts @@ -1,29 +1,40 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { ContactType, PhonebookModuleData, SelectedContact, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + ContactType, + PhonebookModuleData, + SelectedContact, + StateType, +} from '@shared/types' export const usePhonebookModule = (): { - selectedContact: StateType, + selectedContact: StateType handleAddContactToPhonebook: (contact: ContactType) => Promise } => { - - const [phonebookModule, setPhonebookModule] = useNethlinkData('phonebookModule') + const [phonebookModule, setPhonebookModule] = + useNethlinkData('phonebookModule') const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookModuleData) => (value: T | undefined) => { - setPhonebookModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookModuleData) => + (value: T | undefined) => { + setPhonebookModule((p) => ({ + ...p, + [selector]: value as any, + })) + } const handleAddContactToPhonebook = async (contact: ContactType) => { - const result: ContactType = await NethVoiceAPI.Phonebook.createContact(contact) + const result: ContactType = + await NethVoiceAPI.Phonebook.createContact(contact) return result } return { - selectedContact: [phonebookModule?.selectedContact, update('selectedContact')] as StateType, - handleAddContactToPhonebook + selectedContact: [ + phonebookModule?.selectedContact, + update('selectedContact'), + ] as StateType, + handleAddContactToPhonebook, } } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx index 55b368af..83f3df08 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/PhoneBookSearchModule.tsx @@ -1,22 +1,23 @@ -import { debouncer } from "@shared/utils/utils" -import { SearchNumberBox } from "./SearchNumberBox" -import { useEffect, useState } from "react" -import { usePhonebookSearchModule } from "./hook/usePhoneBookSearchModule" -import { SearchData } from "@shared/types" -import { useNethlinkData } from "@renderer/store" -import { SearchNumberDetail } from "./SearchNumberDetail" -import classNames from "classnames" +import { debouncer } from '@shared/utils/utils' +import { SearchNumberBox } from './SearchNumberBox' +import { useEffect, useState } from 'react' +import { usePhonebookSearchModule } from './hook/usePhoneBookSearchModule' +import { SearchData } from '@shared/types' +import { useNethlinkData } from '@renderer/store' +import { SearchNumberDetail } from './SearchNumberDetail' +import classNames from 'classnames' export const PhoneBookSearchModule = () => { - const phoneBookModule = usePhonebookSearchModule() const { searchPhonebookContacts } = phoneBookModule const [searchText] = phoneBookModule.searchTextState const [searchResult, setSearchResult] = useState() - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') const [contactDetail, setContactDetail] = useState<{ - contact: SearchData, + contact: SearchData primaryNumber: string | null }>() @@ -28,7 +29,7 @@ export const PhoneBookSearchModule = () => { const result = await searchPhonebookContacts() setSearchResult(() => result) }, - 250 + 250, ) } else { setSearchResult(() => []) @@ -49,9 +50,12 @@ export const PhoneBookSearchModule = () => { setContactDetail(() => ({ contact, primaryNumber })) }} /> - { - setContactDetail(() => undefined) - }} /> + { + setContactDetail(() => undefined) + }} + /> > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx index c5c3dee7..8c0f55b5 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchBox.tsx @@ -1,6 +1,6 @@ import { faMagnifyingGlass as SearchIcon, - faXmark as DeleteSearchIcon + faXmark as DeleteSearchIcon, } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -13,27 +13,22 @@ import { Button, TextInput } from '@renderer/components/Nethesis' import { Log } from '@shared/utils/logger' import { debouncer } from '@shared/utils/utils' type FormDataType = { - searchText: string | undefined; + searchText: string | undefined } export function SearchBox(): JSX.Element { - const { callNumber } = usePhoneIslandEventHandler() const phoneBookSearchModule = usePhonebookSearchModule() const [searchText, setSearchText] = phoneBookSearchModule.searchTextState const [phonebookSearchModule] = useNethlinkData('phonebookSearchModule') - const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') + const [, setShowPhonebookSearchModule] = useNethlinkData( + 'showPhonebookSearchModule', + ) const [, setShowAddContactModule] = useNethlinkData('showAddContactModule') - const { - register, - watch, - reset, - setValue, - handleSubmit - } = useForm({ + const { register, watch, reset, setValue, handleSubmit } = useForm({ defaultValues: { - searchText: '' - } + searchText: '', + }, }) useEffect(() => { @@ -47,7 +42,6 @@ export function SearchBox(): JSX.Element { const tempSearchText = watch('searchText') - useEffect(() => { if (tempSearchText) { setSearchText(tempSearchText) @@ -82,26 +76,29 @@ export function SearchBox(): JSX.Element { } return ( - + {tempSearchText !== '' && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx index fdade9eb..8134049a 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx @@ -10,7 +10,7 @@ import { ClassNames } from '@renderer/utils' export interface SearchNumberProps { user: SearchData - className?: string, + className?: string onClick?: (user: SearchData, primaryNumber: string | null) => void } @@ -20,12 +20,16 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const [operators] = useNethlinkData('operators') const { isSearchAlsoAFavourite } = useFavouriteModule() - const getUsernameFromPhoneNumber = (number: string) => { - return user.type !== 'extension' ? operators?.extensions[number]?.username : undefined + return user.type !== 'extension' + ? operators?.extensions[number]?.username + : undefined } - function highlightMatch(number: string | undefined, searchText: string): ReactNode[] { + function highlightMatch( + number: string | undefined, + searchText: string, + ): ReactNode[] { const parts: ReactNode[] = [] let lastIndex = 0 if (number) { @@ -37,10 +41,10 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { parts.push( {number.substring(index, index + searchText.length)} - + , ) lastIndex = index + searchText.length index = lowerText.indexOf(lowerSearchText, lastIndex) @@ -52,11 +56,21 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { } let phoneNumber: string | null = null - const keys = ['extension', 'cellphone', 'homephone', 'workphone', 'workphone2', 'cellphone2', 'otherphone'] + const keys = [ + 'extension', + 'cellphone', + 'homephone', + 'workphone', + 'workphone2', + 'cellphone2', + 'otherphone', + ] for (const key of keys) { if (!phoneNumber) { - phoneNumber = (user[key] || '').includes(`${searchText}`) ? user[key] : null + phoneNumber = (user[key] || '').includes(`${searchText}`) + ? user[key] + : null } else { break } @@ -72,9 +86,7 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const highlightedNumber = highlightMatch(phoneNumber, searchText || '') const otherDevicesCount = Array.from( new Set( - keys - .map((k) => user[k]) - .filter((n): n is string => !!n && n !== ' '), + keys.map((k) => user[k]).filter((n): n is string => !!n && n !== ' '), ), ).filter((n) => n !== phoneNumber).length @@ -82,37 +94,46 @@ export function SearchNumber({ user, className, onClick }: SearchNumberProps) { const avatarSrc = username ? operators?.avatars?.[username] : '' return ( - - + - + 0 ? t('Common.PlusOther', { count: otherDevicesCount }) as string : ''} + otherNumber={ + otherDevicesCount > 0 + ? (t('Common.PlusOther', { + count: otherDevicesCount, + }) as string) + : '' + } isHighlight={true} username={username} isFavourite={false} isSearchData={true} showCompany={true} - onOpenDetail={onClick ? () => { - onClick?.(user, phoneNumber) - } : undefined} + onOpenDetail={ + onClick + ? () => { + onClick?.(user, phoneNumber) + } + : undefined + } /> {onClick && ( onClick(user, phoneNumber)} > - + {t('Phonebook.Details')} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx index 7ce69d84..0cb928e6 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberBox.tsx @@ -99,24 +99,20 @@ export function SearchNumberBox({ ), ) } - let operatorsResults = candidates.filter( - (op: any) => { - return ( - (op.name && - new RegExp(cleanQuery, 'i').test( - op.name.replace(cleanRegex, ''), - )) || - new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) - ) - }, - ) + let operatorsResults = candidates.filter((op: any) => { + return ( + (op.name && + new RegExp(cleanQuery, 'i').test(op.name.replace(cleanRegex, ''))) || + new RegExp(cleanQuery, 'i').test(op.endpoints?.mainextension[0]?.id) + ) + }) if (operatorsResults.length) { operatorsResults = cloneDeep(operatorsResults) operatorsResults.forEach((op: any) => { op.resultType = 'operator' - op.extension = op.endpoints?.mainextension[0]?.id //for phoneNumber search + op.extension = op.endpoints?.mainextension[0]?.id // for phoneNumber search }) } operatorsResults.sort(sortByProperty('name')) @@ -155,7 +151,7 @@ export function SearchNumberBox({ const filteredOperators = getFoundedOperators() const copy = [...filteredOperators, ...unFilteredNumbers] // Allow creating a contact from any meaningful query — number or name. - let _canAddInPhonebook = cleanQuery.length > 1 + const _canAddInPhonebook = cleanQuery.length > 1 setFilteredPhoneNumbers(() => copy as any) setCanAddToPhonebook(() => _canAddInPhonebook) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx index 48705d66..1b41ea9f 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumberDetail.tsx @@ -29,67 +29,211 @@ const getSocialUrl = (value: string) => { interface SearchNumberBoxProps { contactDetail?: { - contact: SearchData, + contact: SearchData primaryNumber: string | null - }, + } onBack: () => void } -export function SearchNumberDetail({ contactDetail, onBack }: SearchNumberBoxProps) { +export function SearchNumberDetail({ + contactDetail, + onBack, +}: SearchNumberBoxProps) { const { contact } = contactDetail || {} return ( - - {contact && - + + {contact && ( + {/* Title row: back arrow + name + visibility badge (mock 936-24158). px-4 matches the search box padding above (Navbar.tsx px-4 pt-2). */} - - + + { - debouncer('onDetailBack', () => { - onBack() - }, 250) + debouncer( + 'onDetailBack', + () => { + onBack() + }, + 250, + ) }} /> {contact?.displayName} - + - {contact.company && {contact.company}} - {contact.job && {contact.job}} - {contact.extension && {contact.extension}} - {contact.workphone && {contact.workphone}} - {contact.workphone2 && {contact.workphone2}} - {contact.cellphone && {contact.cellphone}} - {contact.cellphone2 && {contact.cellphone2}} - {contact.homephone && {contact.homephone}} - {contact.otherphone && {contact.otherphone}} - {contact.fax && {contact.fax}} - {contact.workemail && {contact.workemail}} - {contact.homeemail && {contact.homeemail}} - {contact.otheremail && {contact.otheremail}} - {contact.facebook && {contact.facebook}} - {contact.instagram && {contact.instagram}} - {contact.linkedin && {contact.linkedin}} - {contact.url && {contact.url}} - {contact.notes && {contact.notes}} + {contact.company && ( + + {contact.company} + + )} + {contact.job && ( + + {contact.job} + + )} + {contact.extension && ( + + {contact.extension} + + )} + {contact.workphone && ( + + {contact.workphone} + + )} + {contact.workphone2 && ( + + {contact.workphone2} + + )} + {contact.cellphone && ( + + {contact.cellphone} + + )} + {contact.cellphone2 && ( + + {contact.cellphone2} + + )} + {contact.homephone && ( + + {contact.homephone} + + )} + {contact.otherphone && ( + + {contact.otherphone} + + )} + {contact.fax && ( + + {contact.fax} + + )} + {contact.workemail && ( + + {contact.workemail} + + )} + {contact.homeemail && ( + + {contact.homeemail} + + )} + {contact.otheremail && ( + + {contact.otheremail} + + )} + {contact.facebook && ( + + {contact.facebook} + + )} + {contact.instagram && ( + + {contact.instagram} + + )} + {contact.linkedin && ( + + {contact.linkedin} + + )} + {contact.url && ( + + {contact.url} + + )} + {contact.notes && ( + + {contact.notes} + + )} - } + )} ) } @@ -106,45 +250,81 @@ const VisibilityBadge = ({ contact }: { contact: SearchData }) => { if (visibility === 'group') { const visibleGroups = sharedGroups.slice(0, VISIBLE_GROUPS_LIMIT) const remainingGroups = sharedGroups.slice(VISIBLE_GROUPS_LIMIT) - const groupBadgeClassName = classNames(theme.badge.base, theme.badge.rounded.full, theme.badge.sizes.small, 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center') + const groupBadgeClassName = classNames( + theme.badge.base, + theme.badge.rounded.full, + theme.badge.sizes.small, + 'w-fit justify-start bg-sky-100 dark:bg-sky-700 text-sky-800 dark:text-sky-100 flex flex-row gap-1 items-center', + ) - return - {visibleGroups.map((groupName) => ( - - - {groupName} - - ))} - {remainingGroups.length > 0 && ( - <> - - {t('Common.PlusOther', { count: remainingGroups.length })} - - - > - )} - + return ( + + {visibleGroups.map((groupName) => ( + + + {groupName} + + ))} + {remainingGroups.length > 0 && ( + <> + + {t('Common.PlusOther', { count: remainingGroups.length })} + + + > + )} + + ) } - return - - {visibility === 'private' ? t("Phonebook.Only me") : t("Phonebook.Public")} - + return ( + + + {visibility === 'private' + ? t('Phonebook.Only me') + : t('Phonebook.Public')} + + ) } -const ContactDetail = ({ children, label, copy, protocol, href, plain }: { - label: string | DefaultTFuncReturn, - children: string, - copy?: boolean, - protocol?: 'mailto' | 'callto', - href?: string, +const ContactDetail = ({ + children, + label, + copy, + protocol, + href, + plain, +}: { + label: string | DefaultTFuncReturn + children: string + copy?: boolean + protocol?: 'mailto' | 'callto' + href?: string plain?: boolean }) => { const { isCallsEnabled } = useAccount() @@ -167,7 +347,8 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { }, 2000) } - const linkClassName = 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' + const linkClassName = + 'text-textBlueLight dark:text-textBlueDark hover:text-primaryHover dark:hover:text-primaryDarkHover cursor-pointer rounded-md font-normal dark:focus:outline-none dark:focus:ring-2 focus:outline-none focus:ring-2 dark:ring-offset-1 ring-offset-1 dark:ring-offset-slate-900 ring-offset-slate-50 focus:ring-primaryRing dark:focus:ring-primaryRingDark hover:underline' const cleaned = ('' + children).replace(/ /g, '') const isCalltoDisabled = protocol === 'callto' && !isCallsEnabled @@ -178,47 +359,67 @@ const ContactDetail = ({ children, label, copy, protocol, href, plain }: { } } - return - - {label} - - - {plain ? ( - - {children} - - ) : href ? ( - - {children} - - ) : ( - - {children} - - )} + return ( + + + {label} + + + {plain ? ( + + {children} + + ) : href ? ( + + {children} + + ) : ( + + {children} + + )} - {copy && - - + {copy && ( + + + + + )} - } + + - - - + ) } diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts index c048cae4..da2d8a72 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/hook/usePhoneBookSearchModule.ts @@ -1,23 +1,32 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData } from "@renderer/store" -import { PhonebookSearchModuleData, SearchCallData, SearchData, StateType } from "@shared/types" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData } from '@renderer/store' +import { + PhonebookSearchModuleData, + SearchCallData, + SearchData, + StateType, +} from '@shared/types' export const usePhonebookSearchModule = (): { - searchTextState: StateType, + searchTextState: StateType searchPhonebookContacts: () => Promise } => { - const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData('phonebookSearchModule') + const [phonebookSearchModule, setPhonebookSearchModule] = useNethlinkData( + 'phonebookSearchModule', + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const update = (selector: keyof PhonebookSearchModuleData) => (value: T | undefined) => { - setPhonebookSearchModule((p) => ({ - ...p, - [selector]: value as any - })) - } + const update = + (selector: keyof PhonebookSearchModuleData) => + (value: T | undefined) => { + setPhonebookSearchModule((p) => ({ + ...p, + [selector]: value as any, + })) + } function mapContact(contact: SearchData) { // kind & display name - if (contact?.name && contact?.name !== '-') { + if (contact?.name && contact?.name !== '-') { contact.kind = 'person' contact.displayName = contact?.name } else { @@ -33,23 +42,30 @@ export const usePhonebookSearchModule = (): { } function prapareSearchData(receivedPhoneNumbers: SearchCallData) { - receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map((contact: SearchData) => { - return mapContact(contact) - }) + receivedPhoneNumbers.rows = receivedPhoneNumbers.rows.map( + (contact: SearchData) => { + return mapContact(contact) + }, + ) const filteredNumbers = receivedPhoneNumbers.rows.filter( - (phoneNumber) => !(!phoneNumber?.displayName || phoneNumber?.displayName === '') + (phoneNumber) => + !(!phoneNumber?.displayName || phoneNumber?.displayName === ''), ) return filteredNumbers } const searchPhonebookContacts = async () => { - const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search(phonebookSearchModule?.searchText || '') + const searchResult: SearchCallData = await NethVoiceAPI.Phonebook.search( + phonebookSearchModule?.searchText || '', + ) return prapareSearchData(searchResult) } - return { - searchTextState: [phonebookSearchModule?.searchText, update('searchText')] as StateType, - searchPhonebookContacts + searchTextState: [ + phonebookSearchModule?.searchText, + update('searchText'), + ] as StateType, + searchPhonebookContacts, } } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx index d7b31c67..61d9505b 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouriteFilter.tsx @@ -1,79 +1,97 @@ -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { Menu } from "@headlessui/react" -import classNames from "classnames" -import { useTheme } from "@renderer/theme/Context" +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { Menu } from '@headlessui/react' +import classNames from 'classnames' +import { useTheme } from '@renderer/theme/Context' import { faCheck as CheckedIcon, - faSortDown as ArrowMenuIcon + faSortDown as ArrowMenuIcon, } from '@fortawesome/free-solid-svg-icons' -import { t } from "i18next" -import { useNethlinkData } from "@renderer/store" -import { FilterTypes } from "@shared/constants" -import { NethLinkPageData } from "@shared/types" +import { t } from 'i18next' +import { useNethlinkData } from '@renderer/store' +import { FilterTypes } from '@shared/constants' +import { NethLinkPageData } from '@shared/types' export const FavouriteFilter = () => { const { theme: nethTheme } = useTheme() - const [speeddialsModule, setSpeeddialsModule] = useNethlinkData('speeddialsModule') + const [speeddialsModule, setSpeeddialsModule] = + useNethlinkData('speeddialsModule') const setFilter = (type: FilterTypes) => { setSpeeddialsModule((p) => ({ ...p, - favouriteOrder: type + favouriteOrder: type, })) } - - const RenderMenuItem = ({ type }: { type: FilterTypes }) => { - return - { - setFilter(type) - }} - > - - - - {t(`SpeedDial.${type}`)} - + return ( + + { + setFilter(type) + }} + > + + + + {t(`SpeedDial.${type}`)} + + - - + + ) } return ( <> - + - + {t(`SpeedDial.${speeddialsModule?.favouriteOrder} label`)} - + {t('SpeedDial.Order by')} - - - + + + > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx index 0a7bc044..778c00ad 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesBox.tsx @@ -1,17 +1,14 @@ -import { ModuleTitle } from "@renderer/components/ModuleTitle" -import { Scrollable } from "@renderer/components/Scrollable" -import { SkeletonRow } from "@renderer/components/SkeletonRow" -import { t } from "i18next" -import { useFavouriteModule } from "../hook/useFavouriteModule" -import { ContactNumber } from "../shared/ContactNumber" -import { EmptyList } from "@renderer/components/EmptyList" +import { ModuleTitle } from '@renderer/components/ModuleTitle' +import { Scrollable } from '@renderer/components/Scrollable' +import { SkeletonRow } from '@renderer/components/SkeletonRow' +import { t } from 'i18next' +import { useFavouriteModule } from '../hook/useFavouriteModule' +import { ContactNumber } from '../shared/ContactNumber' +import { EmptyList } from '@renderer/components/EmptyList' import { faStar as FavouriteIcon } from '@fortawesome/free-solid-svg-icons' -import { FavouriteFilter } from "./FavouriteFilter" - - +import { FavouriteFilter } from './FavouriteFilter' export const FavouritesBox = () => { - const { favourites } = useFavouriteModule() return ( @@ -20,21 +17,27 @@ export const FavouritesBox = () => { title={t('SpeedDial.Favourites')} actionComponent={} /> - + {favourites ? ( favourites.length > 0 ? ( favourites?.map((e, idx) => { return ( - - + + @@ -42,7 +45,10 @@ export const FavouritesBox = () => { ) }) ) : ( - + ) ) : ( Array(3) @@ -50,7 +56,7 @@ export const FavouritesBox = () => { .map((_, idx) => { return ( diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx index 7ef2b4e5..ca52dd21 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/Favourites/FavouritesModule.tsx @@ -1,8 +1,5 @@ -import { FavouritesBox } from "./FavouritesBox" +import { FavouritesBox } from './FavouritesBox' export const FavouritesModule = () => { - - return ( - - ) + return } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx index 9ceb55bd..9ca25b4e 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialDeleteDialog.tsx @@ -1,39 +1,39 @@ -import { Modal } from "@renderer/components" -import { Button } from "@renderer/components/Nethesis" -import { useSharedState } from "@renderer/store" -import { parseThemeToClassName, sendNotification, truncate } from "@renderer/utils" -import { ContactType } from "@shared/types" -import { t } from "i18next" -import { createRef } from "react" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { Modal } from '@renderer/components' +import { Button } from '@renderer/components/Nethesis' +import { useSharedState } from '@renderer/store' import { - faTriangleExclamation as WarningIcon -} from '@fortawesome/free-solid-svg-icons' -export const SpeedDialDeleteDialog = ({ - close -}) => { - + parseThemeToClassName, + sendNotification, + truncate, +} from '@renderer/utils' +import { ContactType } from '@shared/types' +import { t } from 'i18next' +import { createRef } from 'react' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faTriangleExclamation as WarningIcon } from '@fortawesome/free-solid-svg-icons' +export const SpeedDialDeleteDialog = ({ close }) => { const cancelDeleteButtonRef = createRef() const speedDialModule = useSpeedDialsModule() - const [selectedSpeedDial, setSelectedSpeedDial] = speedDialModule.speedDialsState + const [selectedSpeedDial, setSelectedSpeedDial] = + speedDialModule.speedDialsState const [theme] = useSharedState('theme') - - function confirmDeleteSpeedDial(speedDial: ContactType) { - speedDialModule.deleteSpeedDial(speedDial).then((_) => { - sendNotification( - t('Notification.speeddial_deleted_title'), - t('Notification.speeddial_deleted_description') - ) - setSelectedSpeedDial(undefined) - close() - }) + speedDialModule + .deleteSpeedDial(speedDial) + .then((_) => { + sendNotification( + t('Notification.speeddial_deleted_title'), + t('Notification.speeddial_deleted_description'), + ) + setSelectedSpeedDial(undefined) + close() + }) .catch((error) => { sendNotification( t('Notification.speeddial_not_deleted_title'), - t('Notification.speeddial_not_deleted_description') + t('Notification.speeddial_not_deleted_description'), ) }) } @@ -55,24 +55,24 @@ export const SpeedDialDeleteDialog = ({ onClose={() => close()} afterLeave={() => setSelectedSpeedDial(undefined)} themeMode={parseThemeToClassName(theme)} - className="font-Poppins w-[100px]" + className='font-Poppins w-[100px]' > - + - - + + {t('SpeedDial.Delete speed dial')} - - + + {t('SpeedDial.Speed dial delete message', { - deletingName: truncate(selectedSpeedDial?.name || '', 30) + deletingName: truncate(selectedSpeedDial?.name || '', 30), })} @@ -80,19 +80,19 @@ export const SpeedDialDeleteDialog = ({ {t('Common.Delete')} - + {t('Common.Cancel')} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx index 4530c192..9932b2fb 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialFormBox.tsx @@ -34,16 +34,16 @@ export function SpeedDialFormBox({ close }) { .trim() .min(1, `${t('Common.This field is required')}`) .min(3, `${t('Common.This field must be at least', { number: '2' })}`) - .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`) + .regex(/^[0-9*#+]*$/, `${t('Common.This is not a phone number')}`), }) const { register, handleSubmit, reset, - formState: { errors } + formState: { errors }, } = useForm({ - resolver: zodResolver(schema) + resolver: zodResolver(schema), }) const onSubmitForm: SubmitHandler = (data) => { @@ -67,12 +67,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_modified_title'), - t('Notification.speeddial_modified_description') + t('Notification.speeddial_modified_description'), ) } else { sendNotification( t('Notification.speeddial_created_title'), - t('Notification.speeddial_created_description') + t('Notification.speeddial_created_description'), ) } reset() @@ -82,12 +82,12 @@ export function SpeedDialFormBox({ close }) { if (selectedSpeedDial) { sendNotification( t('Notification.speeddial_not_modified_title'), - t('Notification.speeddial_not_modified_description') + t('Notification.speeddial_not_modified_description'), ) } else { sendNotification( t('Notification.speeddial_not_created_title'), - t('Notification.speeddial_not_created_description') + t('Notification.speeddial_not_created_description'), ) } Log.warning('error during speedDialModule.upsertSpeedDial:', e) @@ -99,12 +99,16 @@ export function SpeedDialFormBox({ close }) { return ( <> - + { e.preventDefault() handleSubmit(onSubmitForm)(e) @@ -113,7 +117,7 @@ export function SpeedDialFormBox({ close }) { - - - + + + {t('Common.Cancel')} - - + + {selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')} {isLoading && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx index 896e3d3a..6b0ca6d7 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx @@ -1,6 +1,6 @@ import { faCirclePlus as AddSpeedDialIcon, - faBolt as SpeedDialIcon + faBolt as SpeedDialIcon, } from '@fortawesome/free-solid-svg-icons' import { ContactType } from '@shared/types' import { t } from 'i18next' @@ -11,7 +11,10 @@ import { ModuleTitle } from '@renderer/components/ModuleTitle' import { ContactNumber } from '../shared/ContactNumber' import { EmptyList } from '@renderer/components/EmptyList' -export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): JSX.Element { +export function SpeedDialsBox({ + showSpeedDialForm, + showDeleteSpeedDialDialog, +}): JSX.Element { const speedDialModule = useSpeedDialsModule() const { speedDials } = speedDialModule const [, setSelectedSpeedDial] = speedDialModule.speedDialsState @@ -39,22 +42,29 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): actionIcon={AddSpeedDialIcon} actionText={t('SpeedDial.Create')} /> - + {speedDials ? ( speedDials.length > 0 ? ( speedDials?.map((e, idx) => { return ( - - + + @@ -62,7 +72,10 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): ) }) ) : ( - + ) ) : ( Array(3) @@ -70,7 +83,7 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): .map((_, idx) => { return ( diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx index 47c4e02c..f33692fc 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx @@ -1,41 +1,42 @@ -import { SpeedDialsBox } from "./SpeedDialsBox" -import { useEffect, useState } from "react" -import { SpeedDialFormBox } from "./SpeedDialFormBox" -import { SpeedDialDeleteDialog } from "./SpeedDialDeleteDialog" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" - +import { SpeedDialsBox } from './SpeedDialsBox' +import { useEffect, useState } from 'react' +import { SpeedDialFormBox } from './SpeedDialFormBox' +import { SpeedDialDeleteDialog } from './SpeedDialDeleteDialog' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' export const SpeeddialsModule = () => { - const speedDialModule = useSpeedDialsModule() const [, setSelectedSpeedDial] = speedDialModule.speedDialsState const [isSpeedDialFormOpen, setSpeedDialFormOpen] = useState(false) - const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = useState(false) + const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = + useState(false) return ( <> - {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && setSpeedDialFormOpen(true)} - showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} - /> - } - - - {isSpeedDialFormOpen && { - setSelectedSpeedDial(undefined) - setSpeedDialFormOpen(false) - }} - /> - } - - {isDeleteSpeedDialDialogOpen && { - setSelectedSpeedDial(undefined) - setDeleteSpeedDialDialogOpen(false) - }} - />} - + {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && ( + setSpeedDialFormOpen(true)} + showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} + /> + )} + + {isSpeedDialFormOpen && ( + { + setSelectedSpeedDial(undefined) + setSpeedDialFormOpen(false) + }} + /> + )} + + {isDeleteSpeedDialDialogOpen && ( + { + setSelectedSpeedDial(undefined) + setDeleteSpeedDialDialogOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts index 8268bfc4..ebdd7ff5 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts @@ -1,43 +1,58 @@ -import { useNethlinkData } from "@renderer/store" -import { ContactType, SearchData } from "@shared/types" -import { useEffect, useState } from "react" -import { debouncer } from "@shared/utils/utils" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { FilterTypes, SpeeddialTypes } from "@shared/constants" -import { Log } from "@shared/utils/logger" +import { useNethlinkData } from '@renderer/store' +import { ContactType, SearchData } from '@shared/types' +import { useEffect, useState } from 'react' +import { debouncer } from '@shared/utils/utils' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { FilterTypes, SpeeddialTypes } from '@shared/constants' +import { Log } from '@shared/utils/logger' export const useFavouriteModule = () => { const [speeddialsModule] = useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') const [operators] = useNethlinkData('operators') - const [favourites, setFavourites] = useState(undefined) + const [favourites, setFavourites] = useState( + undefined, + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() const getUsernameFromContact = (contact: ContactType): string | undefined => { const extension = contact.speeddial_num || '' - const usernameFromExtension = extension ? operators?.extensions?.[extension]?.username : undefined + const usernameFromExtension = extension + ? operators?.extensions?.[extension]?.username + : undefined return usernameFromExtension || contact.name || undefined } useEffect(() => { - rawSpeedDials && filterFavourites(rawSpeedDials, speeddialsModule?.favouriteOrder || FilterTypes.AZ) + rawSpeedDials && + filterFavourites( + rawSpeedDials, + speeddialsModule?.favouriteOrder || FilterTypes.AZ, + ) }, [rawSpeedDials, speeddialsModule?.favouriteOrder, operators]) const getSorter = (order: FilterTypes) => { - let sorter: ((a: ContactType, b: ContactType) => number) | undefined = undefined + let sorter: ((a: ContactType, b: ContactType) => number) | undefined = + undefined switch (order) { case FilterTypes.ZA: - sorter = (a: ContactType, b: ContactType) => (a.name || '') < (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') < (b.name || '') ? 1 : -1 + break case FilterTypes.EXT: - sorter = (a: ContactType, b: ContactType) => (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 + break default: - sorter = (a: ContactType, b: ContactType) => (a.name || '') > (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') > (b.name || '') ? 1 : -1 + break } return sorter } - const filterFavourites = (rawSpeeddials: ContactType[], order: FilterTypes) => { + const filterFavourites = ( + rawSpeeddials: ContactType[], + order: FilterTypes, + ) => { const favourites = rawSpeeddials .filter(isFavourite) .filter(isActiveOperator) @@ -57,13 +72,15 @@ export const useFavouriteModule = () => { const username = getUsernameFromContact(contact) if (!username) return false // Check if the contact's username exists in any operator group - return Object.values(operators.groups).some(group => - group.users.includes(username) + return Object.values(operators.groups).some((group) => + group.users.includes(username), ) } const isSearchAlsoAFavourite = (contact: SearchData) => { - const foundedContact = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) + const foundedContact = favourites?.find( + (s) => s.speeddial_num === (contact.speeddial_num || contact.extension), + ) if (foundedContact) { return isFavourite(foundedContact) } @@ -77,53 +94,63 @@ export const useFavouriteModule = () => { } function toggleFavourite(contact: ContactType) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const extension = contact.speeddial_num || '' - const favouriteSpeedDial = extension - ? favourites?.find((s) => s.speeddial_num === extension) - : favourites?.find((s) => s.id === contact.id) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const extension = contact.speeddial_num || '' + const favouriteSpeedDial = extension + ? favourites?.find((s) => s.speeddial_num === extension) + : favourites?.find((s) => s.id === contact.id) - if (!favouriteSpeedDial) { - //if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name - const username = getUsernameFromContact(contact) - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - //if I am deleting favourite then I am in favourite, I can use the id directly - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${favouriteSpeedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + if (!favouriteSpeedDial) { + // if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name + const username = getUsernameFromContact(contact) + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + // if I am deleting favourite then I am in favourite, I can use the id directly + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${favouriteSpeedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } function toggleFavouriteFromSearch(contact: SearchData) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const speedDial = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) - if (!speedDial) { - //I am creating the favourite from the search so I find the username directly in searchData - const username = contact.username - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${speedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const speedDial = favourites?.find( + (s) => + s.speeddial_num === (contact.speeddial_num || contact.extension), + ) + if (!speedDial) { + // I am creating the favourite from the search so I find the username directly in searchData + const username = contact.username + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${speedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } - return { favourites, toggleFavourite, isFavourite, isSpeedDialAlsoAFavourite, isSearchAlsoAFavourite, - toggleFavouriteFromSearch + toggleFavouriteFromSearch, } } - - diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts index ad6919db..cb94f1b4 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts @@ -1,26 +1,37 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData, useSharedState } from "@renderer/store" -import { SpeeddialTypes } from "@shared/constants" -import { ContactType, NewContactType, NewSpeedDialType, SpeedDialModuleData, StateType } from "@shared/types" -import { useEffect, useState } from "react" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { SpeeddialTypes } from '@shared/constants' +import { + ContactType, + NewContactType, + NewSpeedDialType, + SpeedDialModuleData, + StateType, +} from '@shared/types' +import { useEffect, useState } from 'react' export const useSpeedDialsModule = (): { - speedDials: ContactType[] | undefined, - speedDialsState: StateType
{t('Phonebook.Visibility')}
+ {t('Phonebook.Visibility')} +
{visibilityError}
+ {visibilityError} +
{t('Phonebook.Groups')}
+ {t('Phonebook.Groups')} +
{sharedGroupsError}
+ {sharedGroupsError} +
{t('Phonebook.Type')}
+ {t('Phonebook.Type')} +
{t('Phonebook.Add job title')}
{t('Phonebook.Add phone')}
{t('Phonebook.Add email')}
{t('Phonebook.Add field')}
{t('Common.Cancel')}
+ + {t('Common.Save')} {isLoading && ( - + )}
{t('Common.Save')}
{t('Phonebook.Details')}
- {t(`SpeedDial.${type}`)} -
+ {t(`SpeedDial.${type}`)} +
{t('SpeedDial.Speed dial delete message', { - deletingName: truncate(selectedSpeedDial?.name || '', 30) + deletingName: truncate(selectedSpeedDial?.name || '', 30), })}
+ + {selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')} {isLoading && ( )} diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx index 896e3d3a..6b0ca6d7 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeedDialsBox.tsx @@ -1,6 +1,6 @@ import { faCirclePlus as AddSpeedDialIcon, - faBolt as SpeedDialIcon + faBolt as SpeedDialIcon, } from '@fortawesome/free-solid-svg-icons' import { ContactType } from '@shared/types' import { t } from 'i18next' @@ -11,7 +11,10 @@ import { ModuleTitle } from '@renderer/components/ModuleTitle' import { ContactNumber } from '../shared/ContactNumber' import { EmptyList } from '@renderer/components/EmptyList' -export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): JSX.Element { +export function SpeedDialsBox({ + showSpeedDialForm, + showDeleteSpeedDialDialog, +}): JSX.Element { const speedDialModule = useSpeedDialsModule() const { speedDials } = speedDialModule const [, setSelectedSpeedDial] = speedDialModule.speedDialsState @@ -39,22 +42,29 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): actionIcon={AddSpeedDialIcon} actionText={t('SpeedDial.Create')} /> - + {speedDials ? ( speedDials.length > 0 ? ( speedDials?.map((e, idx) => { return ( - - + + @@ -62,7 +72,10 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): ) }) ) : ( - + ) ) : ( Array(3) @@ -70,7 +83,7 @@ export function SpeedDialsBox({ showSpeedDialForm, showDeleteSpeedDialDialog }): .map((_, idx) => { return ( diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx index 47c4e02c..f33692fc 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/SpeedDials/SpeeddialsModule.tsx @@ -1,41 +1,42 @@ -import { SpeedDialsBox } from "./SpeedDialsBox" -import { useEffect, useState } from "react" -import { SpeedDialFormBox } from "./SpeedDialFormBox" -import { SpeedDialDeleteDialog } from "./SpeedDialDeleteDialog" -import { useSpeedDialsModule } from "../hook/useSpeedDialsModule" - +import { SpeedDialsBox } from './SpeedDialsBox' +import { useEffect, useState } from 'react' +import { SpeedDialFormBox } from './SpeedDialFormBox' +import { SpeedDialDeleteDialog } from './SpeedDialDeleteDialog' +import { useSpeedDialsModule } from '../hook/useSpeedDialsModule' export const SpeeddialsModule = () => { - const speedDialModule = useSpeedDialsModule() const [, setSelectedSpeedDial] = speedDialModule.speedDialsState const [isSpeedDialFormOpen, setSpeedDialFormOpen] = useState(false) - const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = useState(false) + const [isDeleteSpeedDialDialogOpen, setDeleteSpeedDialDialogOpen] = + useState(false) return ( <> - {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && setSpeedDialFormOpen(true)} - showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} - /> - } - - - {isSpeedDialFormOpen && { - setSelectedSpeedDial(undefined) - setSpeedDialFormOpen(false) - }} - /> - } - - {isDeleteSpeedDialDialogOpen && { - setSelectedSpeedDial(undefined) - setDeleteSpeedDialDialogOpen(false) - }} - />} - + {!(isSpeedDialFormOpen || isDeleteSpeedDialDialogOpen) && ( + setSpeedDialFormOpen(true)} + showDeleteSpeedDialDialog={() => setDeleteSpeedDialDialogOpen(true)} + /> + )} + + {isSpeedDialFormOpen && ( + { + setSelectedSpeedDial(undefined) + setSpeedDialFormOpen(false) + }} + /> + )} + + {isDeleteSpeedDialDialogOpen && ( + { + setSelectedSpeedDial(undefined) + setDeleteSpeedDialDialogOpen(false) + }} + /> + )} > ) } diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts index 8268bfc4..ebdd7ff5 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useFavouriteModule.ts @@ -1,43 +1,58 @@ -import { useNethlinkData } from "@renderer/store" -import { ContactType, SearchData } from "@shared/types" -import { useEffect, useState } from "react" -import { debouncer } from "@shared/utils/utils" -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { FilterTypes, SpeeddialTypes } from "@shared/constants" -import { Log } from "@shared/utils/logger" +import { useNethlinkData } from '@renderer/store' +import { ContactType, SearchData } from '@shared/types' +import { useEffect, useState } from 'react' +import { debouncer } from '@shared/utils/utils' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { FilterTypes, SpeeddialTypes } from '@shared/constants' +import { Log } from '@shared/utils/logger' export const useFavouriteModule = () => { const [speeddialsModule] = useNethlinkData('speeddialsModule') const [rawSpeedDials, setRawSpeedDials] = useNethlinkData('speeddials') const [operators] = useNethlinkData('operators') - const [favourites, setFavourites] = useState(undefined) + const [favourites, setFavourites] = useState( + undefined, + ) const { NethVoiceAPI } = useLoggedNethVoiceAPI() const getUsernameFromContact = (contact: ContactType): string | undefined => { const extension = contact.speeddial_num || '' - const usernameFromExtension = extension ? operators?.extensions?.[extension]?.username : undefined + const usernameFromExtension = extension + ? operators?.extensions?.[extension]?.username + : undefined return usernameFromExtension || contact.name || undefined } useEffect(() => { - rawSpeedDials && filterFavourites(rawSpeedDials, speeddialsModule?.favouriteOrder || FilterTypes.AZ) + rawSpeedDials && + filterFavourites( + rawSpeedDials, + speeddialsModule?.favouriteOrder || FilterTypes.AZ, + ) }, [rawSpeedDials, speeddialsModule?.favouriteOrder, operators]) const getSorter = (order: FilterTypes) => { - let sorter: ((a: ContactType, b: ContactType) => number) | undefined = undefined + let sorter: ((a: ContactType, b: ContactType) => number) | undefined = + undefined switch (order) { case FilterTypes.ZA: - sorter = (a: ContactType, b: ContactType) => (a.name || '') < (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') < (b.name || '') ? 1 : -1 + break case FilterTypes.EXT: - sorter = (a: ContactType, b: ContactType) => (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.speeddial_num || '') > (b.speeddial_num || '') ? 1 : -1 + break default: - sorter = (a: ContactType, b: ContactType) => (a.name || '') > (b.name || '') ? 1 : -1 - break; + sorter = (a: ContactType, b: ContactType) => + (a.name || '') > (b.name || '') ? 1 : -1 + break } return sorter } - const filterFavourites = (rawSpeeddials: ContactType[], order: FilterTypes) => { + const filterFavourites = ( + rawSpeeddials: ContactType[], + order: FilterTypes, + ) => { const favourites = rawSpeeddials .filter(isFavourite) .filter(isActiveOperator) @@ -57,13 +72,15 @@ export const useFavouriteModule = () => { const username = getUsernameFromContact(contact) if (!username) return false // Check if the contact's username exists in any operator group - return Object.values(operators.groups).some(group => - group.users.includes(username) + return Object.values(operators.groups).some((group) => + group.users.includes(username), ) } const isSearchAlsoAFavourite = (contact: SearchData) => { - const foundedContact = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) + const foundedContact = favourites?.find( + (s) => s.speeddial_num === (contact.speeddial_num || contact.extension), + ) if (foundedContact) { return isFavourite(foundedContact) } @@ -77,53 +94,63 @@ export const useFavouriteModule = () => { } function toggleFavourite(contact: ContactType) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const extension = contact.speeddial_num || '' - const favouriteSpeedDial = extension - ? favourites?.find((s) => s.speeddial_num === extension) - : favourites?.find((s) => s.id === contact.id) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const extension = contact.speeddial_num || '' + const favouriteSpeedDial = extension + ? favourites?.find((s) => s.speeddial_num === extension) + : favourites?.find((s) => s.id === contact.id) - if (!favouriteSpeedDial) { - //if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name - const username = getUsernameFromContact(contact) - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - //if I am deleting favourite then I am in favourite, I can use the id directly - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${favouriteSpeedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + if (!favouriteSpeedDial) { + // if I am creating a favourite it means that I am from speeddial. in speeddial the username is saved in name + const username = getUsernameFromContact(contact) + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + // if I am deleting favourite then I am in favourite, I can use the id directly + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${favouriteSpeedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } function toggleFavouriteFromSearch(contact: SearchData) { - debouncer(`toggleFavourite-${contact.id}`, async () => { - const speedDial = favourites?.find((s) => s.speeddial_num === (contact.speeddial_num || contact.extension)) - if (!speedDial) { - //I am creating the favourite from the search so I find the username directly in searchData - const username = contact.username - const operator = username && operators?.operators[username] - if (operator) - await NethVoiceAPI.Phonebook.createFavourite(operator) - } else { - await NethVoiceAPI.Phonebook.deleteSpeeddial({ id: `${speedDial.id!}` }) - } - const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() - setRawSpeedDials(() => updatedSpeedDials) - }, 100) + debouncer( + `toggleFavourite-${contact.id}`, + async () => { + const speedDial = favourites?.find( + (s) => + s.speeddial_num === (contact.speeddial_num || contact.extension), + ) + if (!speedDial) { + // I am creating the favourite from the search so I find the username directly in searchData + const username = contact.username + const operator = username && operators?.operators[username] + if (operator) await NethVoiceAPI.Phonebook.createFavourite(operator) + } else { + await NethVoiceAPI.Phonebook.deleteSpeeddial({ + id: `${speedDial.id!}`, + }) + } + const updatedSpeedDials = await NethVoiceAPI.Phonebook.getSpeeddials() + setRawSpeedDials(() => updatedSpeedDials) + }, + 100, + ) } - return { favourites, toggleFavourite, isFavourite, isSpeedDialAlsoAFavourite, isSearchAlsoAFavourite, - toggleFavouriteFromSearch + toggleFavouriteFromSearch, } } - - diff --git a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts index ad6919db..cb94f1b4 100644 --- a/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Speeddials/hook/useSpeedDialsModule.ts @@ -1,26 +1,37 @@ -import { useLoggedNethVoiceAPI } from "@renderer/hooks/useLoggedNethVoiceAPI" -import { useNethlinkData, useSharedState } from "@renderer/store" -import { SpeeddialTypes } from "@shared/constants" -import { ContactType, NewContactType, NewSpeedDialType, SpeedDialModuleData, StateType } from "@shared/types" -import { useEffect, useState } from "react" +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { SpeeddialTypes } from '@shared/constants' +import { + ContactType, + NewContactType, + NewSpeedDialType, + SpeedDialModuleData, + StateType, +} from '@shared/types' +import { useEffect, useState } from 'react' export const useSpeedDialsModule = (): { - speedDials: ContactType[] | undefined, - speedDialsState: StateType
{selectedSpeedDial ? t('Common.Edit') : t('SpeedDial.Create')}