diff --git a/.vscode/launch.json b/.vscode/launch.json index d462b880..ad08126a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -85,29 +85,6 @@ "presentation": { "hidden": false } - }, - { - "name": "Debug Renderer Process", - "port": 9222, - "request": "attach", - "type": "chrome", - "webRoot": "${workspaceFolder}/src/renderer", - "timeout": 15000, - "presentation": { - "hidden": true - } - } - ], - "compounds": [ - { - "name": "Debug main and renderer processes", - "configurations": [ - "Debug Main Process", - "Debug Renderer Process" - ], - "presentation": { - "order": 1 - } } ] } \ No newline at end of file diff --git a/README.md b/README.md index b39d0e4e..59ba7b9b 100644 --- a/README.md +++ b/README.md @@ -53,3 +53,13 @@ $ npm run publish:patch > `INSTANCE=`\ > enables multiple instances of the process. Use this function with extreme caution, it can cause many problems - for testing purposes only. When this variable is set, a new `user_data_.json` is created and the instance only changes its related file. + +### User data folders + +- Windows: `%APPDATA%/nethlink/` +- Linux: `~/.config/nethlink/` +- macOS: `~/Library/Application Support/nethlink/` + + +We then have two files: user_data.json and available_users.json the first file contains the data of the currently logged in user, the other contains the data of all available users in that device who have logged in at least once + diff --git a/package-lock.json b/package-lock.json index 5582d707..a064da26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.18", "@hookform/resolvers": "^3.3.4", - "@nethesis/phone-island": "^0.8.28", + "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", + "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", + "@nethesis/phone-island": "^0.8.40", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -52,6 +55,7 @@ "i18next-http-backend": "^2.1.1", "lodash": "^4.17.21", "moment": "^2.30.1", + "motion": "^11.13.1", "path-browserify": "^1.0.1", "postcss": "^8.4.35", "prettier": "^3.2.4", @@ -4963,6 +4967,30 @@ "tslib": "^2.3.1" } }, + "node_modules/@nethesis/nethesis-brands-svg-icons": { + "version": "6.2.1", + "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#dbcf431acf0e504145486c9ac70f14946279e2e7", + "dev": true, + "hasInstallScript": true, + "license": "UNLICENSED", + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.2.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@nethesis/nethesis-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.2.1.tgz", + "integrity": "sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/@nethesis/nethesis-light-svg-icons": { "version": "6.2.1", "resolved": "git+ssh://git@github.com/nethesis/Font-Awesome.git#3e110fb0ae63495fb8b7ca9811e5f07eed1ac962", @@ -5012,11 +5040,10 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.8.28", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.8.28.tgz", - "integrity": "sha512-4t9cf3AwvKa8G+AVgA+6y5JT0rZJ0q2qF7XNLl3Xrwgd4aICpVOVbd2D6lLJPlanG8dFFwENCwnej4Zq6KMIPA==", + "version": "0.8.40", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.8.40.tgz", + "integrity": "sha512-b8pvewUgOOXOgQcUT32bfZ3p8AValZt0OEayFFmfXC3Uxo/gbSJi09xMvKQdFcEDqM1JtV4cnwlKUpjzZvx0sA==", "dev": true, - "license": "GPL-3.0-or-later", "dependencies": { "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", @@ -14195,12 +14222,14 @@ } }, "node_modules/framer-motion": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.12.0.tgz", - "integrity": "sha512-gZaZeqFM6pX9kMVti60hYAa75jGpSsGYWAHbBfIkuHN7DkVHVkxSxeNYnrGmHuM0zPkWTzQx10ZT+fDjn7N4SA==", + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.13.1.tgz", + "integrity": "sha512-F40tpGTHByhn9h3zdBQPcEro+pSLtzARcocbNqAyfBI+u9S+KZuHH/7O9+z+GEkoF3eqFxfvVw0eBDytohwqmQ==", "dev": true, "license": "MIT", "dependencies": { + "motion-dom": "^11.13.0", + "motion-utils": "^11.13.0", "tslib": "^2.4.0" }, "peerDependencies": { @@ -19415,6 +19444,45 @@ "node": "*" } }, + "node_modules/motion": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/motion/-/motion-11.13.1.tgz", + "integrity": "sha512-64+QpZQv8WJJFn+tEEzX04il9s6ReA6lhKRZaxzD6SunGqoaq5g+AFVfcKWme8N83eytUOpGp7mpfJ9cyZlhAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "framer-motion": "^11.13.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.13.0.tgz", + "integrity": "sha512-Oc1MLGJQ6nrvXccXA89lXtOqFyBmvHtaDcTRGT66o8Czl7nuA8BeHAd9MQV1pQKX0d2RHFBFaw5g3k23hQJt0w==", + "dev": true + }, + "node_modules/motion-utils": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.13.0.tgz", + "integrity": "sha512-lq6TzXkH5c/ysJQBxgLXgM01qwBH1b4goTPh57VvZWJbVJZF/0SB31UWEn4EIqbVPf3au88n2rvK17SpDTja1A==", + "dev": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index fc76f125..d158b5e0 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "npm run typecheck:node && npm run typecheck:web", "start": "electron-vite preview", - "dev": "electron-vite dev", + "dev": "DEV=true electron-vite dev", "pack": "electron-builder --dir", "dist": "electron-builder", "preversion": "git add . ", @@ -46,7 +46,10 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.18", "@hookform/resolvers": "^3.3.4", - "@nethesis/phone-island": "^0.8.28", + "@nethesis/nethesis-brands-svg-icons": "github:nethesis/Font-Awesome#ns-brands", + "@nethesis/nethesis-light-svg-icons": "github:nethesis/Font-Awesome#ns-light", + "@nethesis/nethesis-solid-svg-icons": "github:nethesis/Font-Awesome#ns-solid", + "@nethesis/phone-island": "^0.8.40", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -76,6 +79,7 @@ "i18next-http-backend": "^2.1.1", "lodash": "^4.17.21", "moment": "^2.30.1", + "motion": "^11.13.1", "path-browserify": "^1.0.1", "postcss": "^8.4.35", "prettier": "^3.2.4", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 4930fac7..05ce9265 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -127,6 +127,7 @@ "application_update_body": "Click here to open the page where you can download the new release", "lost_call_title": "Missed call from {{user}}", "lost_call_body": "You received a call from {{number}} at {{datetime}}", + "physical_phone_error": "the phone {{phone}} is not reachable. Make sure it is connected", "call_transferred_title": "Call tranferred", "call_transferred_body": "The call was succesfully tranferred" }, @@ -322,7 +323,9 @@ "For the following states": "For the following states", "Callforward": "Forward", "Do not disturb": "Do not disturb", - "Available presence": "Available presence" + "Available presence": "Available presence", + "IP Phone": "IP Phone", + "Only nethlink": "Only Nethlink" }, "TopBar": { "Signed in as": "Signed in as", @@ -344,7 +347,9 @@ "Mobile": "Mobile", "Cellphone": "Cellphone", "Activate voicemail": "Activate voicemail", - "Go to NethVoice CTI": "Go to NethVoice CTI" + "Go to NethVoice CTI": "Go to NethVoice CTI", + "Pair device": "Pair device", + "Theme": "Theme" }, "OperatorDrawer": { "Book": "Book", @@ -893,19 +898,22 @@ "download": "Download the update" }, "Errors": { - "browser_permissions": "Errore di autorizzazione", - "user_permissions": "Errore di autorizzazione", - "unknown_media_permissions": "Errore di autorizzazione", - "webrtc_down": "WebRTC non disponibile", - "socket_down": "Socket non disponibile", - "busy_camera": "Camera occupata", - "call_transfered": "Chiamata trasferita con successo", - "The browser doesn't have permission to access camera or microphone.": "Il browser non ha il permesso di accedere alla fotocamera o al microfono.", - "You must accept audio and video permissions.": "Devi accettare i permessi audio e video.", - "Web Phone can't access audio or camera on this device.": "Il Web Phone non può accedere all'audio o alla fotocamera su questo dispositivo.", - "Web Phone connection is down.": "La connessione del Web Phone è interrotta.", - "Server connection is down.": "La connessione al server è interrotta.", - "Camera is used by another application.": "La fotocamera è utilizzata da un'altra applicazione.", - "Call transferred successfully.": "Chiamata trasferita con successo." + "browser_permissions": "Browser permissions error", + "user_permissions": "Media permissions error", + "unknown_media_permissions": "Generic media permission error", + "webrtc_down": "WebRTC is unavailable", + "socket_down": "Socket is unavailable", + "busy_camera": "Webcam is already in use", + "call_transfered": "Transfer successful", + "The browser doesn't have permission to access camera or microphone.": "The browser doesn't have permission to access camera or microphone.", + "You must accept audio and video permissions.": "You must accept audio and video permissions.", + "Web Phone can't access audio or camera on this device.": "Web Phone can't access audio or camera on this device.", + "Web Phone connection is down.": "Device not communicating with server.", + "NethLink connection is down.": "Server connection is unavailable", + "Server connection is down.": "Server connection is unavailable", + "Camera is used by another application.": "Webcam is used by another application.", + "Unknown audio or camera permissions.": "Microphone or Webcam is unavailable", + "No microphone or camera permissions.": "You must accept borwser permissions.", + "Call transferred successfully.": "The call is transferred successfully." } } diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index 94db954f..7785e94c 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -127,6 +127,7 @@ "application_update_body": "Clicca quì per aprire la pagina dove potrai scaricare la nuova release", "lost_call_title": "Chiamata persa da {{user}}", "lost_call_body": "Hai ricevuto una chiamata da {{number}} alle {{datetime}}", + "physical_phone_error": "Il telefono {{phone}} non è raggiungibile. Assicurarsi che sia collegato", "call_transferred_title": "Traferimento di chiamata", "call_transferred_body": "La chiamata è stata trasferita con successo" }, @@ -322,7 +323,9 @@ "For the following states": "Per i seguenti stati", "Callforward": "Inoltro", "Do not disturb": "Non disturbare", - "Available presence": "Presence disponibili" + "Available presence": "Presence disponibili", + "IP Phone": "Telefono IP", + "Only nethlink": "Solo Nethlink" }, "TopBar": { "Signed in as": "Accesso effettuato come", @@ -344,7 +347,9 @@ "Mobile": "Cellulare", "Cellphone": "Cellulare", "Activate voicemail": "Attiva voicemail", - "Go to NethVoice CTI": "Vai a NethVoice CTI" + "Go to NethVoice CTI": "Vai a NethVoice CTI", + "Pair device": "Abbina dispositivo", + "Theme": "Tema" }, "OperatorDrawer": { "Book": "Prenota", @@ -893,19 +898,22 @@ "download": "Scarica l'aggiornamento" }, "Errors": { - "browser_permissions": "Browser Permissions Error", - "user_permissions": "User Permissions Error", - "unknown_media_permissions": "Unknown Media Permissions Error", - "webrtc_down": "WebRTC Down", - "socket_down": "Socket Down", - "busy_camera": "Busy Camera", - "call_transfered": "Call Transferred Successfully", - "The browser doesn't have permission to access camera or microphone.": "The browser doesn't have permission to access camera or microphone.", - "You must accept audio and video permissions.": "You must accept audio and video permissions.", - "Web Phone can't access audio or camera on this device.": "Web Phone can't access audio or camera on this device.", - "Web Phone connection is down.": "Web Phone connection is down.", - "Server connection is down.": "Server connection is down.", - "Camera is used by another application.": "Camera is used by another application.", - "Call transferred successfully.": "Call transferred successfully." + "browser_permissions": "Errore nei permessi del browser", + "user_permissions": "Errore nei permessi dei media", + "unknown_media_permissions": "Errore generico nei permessi", + "webrtc_down": "WebRTC non disponibile", + "socket_down": "Socket non disponibile", + "busy_camera": "La webcam è già in uso", + "call_transfered": "Trasferimento riuscito", + "The browser doesn't have permission to access camera or microphone.": "Il browser non ha il permesso di accedere alla fotocamera o al microfono.", + "You must accept audio and video permissions.": "Devi accettare i permessi per audio e video.", + "Web Phone can't access audio or camera on this device.": "Il Web Phone non può accedere all'audio o alla fotocamera su questo dispositivo.", + "Web Phone connection is down.": "Il dispositivo non comunica con il server.", + "NethLink connection is down.": "Connessione al server non disponibile", + "Server connection is down.": "Connessione al server non disponibile", + "Camera is used by another application.": "La fotocamera è utilizzata da un'altra applicazione.", + "Unknown audio or camera permissions.": "Microfono o fotocamera non disponibili.", + "No microphone or camera permissions.": "Devi accettare i permessi del browser.", + "Call transferred successfully.": "La chiamata è stata trasferita con successo." } } diff --git a/src/main/classes/controllers/AccountController.ts b/src/main/classes/controllers/AccountController.ts index ed66252f..64cedef7 100644 --- a/src/main/classes/controllers/AccountController.ts +++ b/src/main/classes/controllers/AccountController.ts @@ -1,4 +1,4 @@ -import { Account, AuthAppData, ConfigFile } from '@shared/types' +import { Account, AuthAppData, AvailableDevices, ConfigFile } from '@shared/types' import { Log } from '@shared/utils/logger' import { safeStorage } from 'electron' import { store } from '@/lib/mainStore' @@ -77,6 +77,7 @@ export class AccountController { let loggedAccount: Account = { ...lastLoggedAccount, ...tempLoggedAccount, + theme: lastLoggedAccount.theme || tempLoggedAccount.theme } const { parseConfig } = useLogin() @@ -111,6 +112,10 @@ export class AccountController { 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 }, 'saveLoggedAccount') store.saveToDisk() @@ -137,6 +142,7 @@ export class AccountController { auth!.availableAccounts[getAccountUID(account)] = account store.set('auth', auth) } + store.saveToDisk() } } @@ -170,6 +176,7 @@ export class AccountController { setAccountNethLinkBounds(nethlinkBounds: Electron.Rectangle | undefined): void { const account = store.store.account + Log.info('MAIN PRESENCE BACK', account?.data?.mainPresence) const auth = store.store.auth if (account) { account!.nethlinkBounds = nethlinkBounds diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 07bf3ea1..dec46e8c 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -7,7 +7,7 @@ import { once } from '@/lib/ipcEvents' import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { store } from '@/lib/mainStore' import { screen } from 'electron' -import { Size } from '@shared/types' +import { Extension, Size } from '@shared/types' export class PhoneIslandController { static instance: PhoneIslandController @@ -120,6 +120,18 @@ export class PhoneIslandController { this.window.emit(IPC_EVENTS.TRANSFER_CALL, to) } + updateDefaultDevice(ext: Extension, force: boolean) { + try { + + //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) + } + } + reconnect() { try { Log.info('PHONE ISLAND RECONNECT') diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 252f9f95..48171d04 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -1,7 +1,7 @@ import { AccountController, DevToolsController } from '@/classes/controllers' import { LoginController } from '@/classes/controllers/LoginController' import { PhoneIslandController } from '@/classes/controllers/PhoneIslandController' -import { IPC_EVENTS } from '@shared/constants' +import { IPC_EVENTS, PHONE_ISLAND_EVENTS } from '@shared/constants' import { Account, OnDraggingWindow, PAGES } from '@shared/types' import { BrowserWindow, app, ipcMain, screen, shell } from 'electron' import { join } from 'path' @@ -13,7 +13,7 @@ import { debouncer, getAccountUID, getPageFromQuery } from '@shared/utils/utils' import { NetworkController } from '@/classes/controllers/NetworkController' import { useLogin } from '@shared/useLogin' import { PhoneIslandWindow } from '@/classes/windows' - +import http from 'http' function onSyncEmitter( channel: IPC_EVENTS, @@ -51,6 +51,37 @@ export function registerIpcEvents() { return app.getSystemLocale() }) + ipcMain.on(IPC_EVENTS.EMIT_START_CALL, async (_event, phoneNumber) => { + PhoneIslandController.instance.call(phoneNumber) + }) + + ipcMain.on(IPC_EVENTS.START_CALL_BY_URL, async (_event, url) => { + function triggerError(e, request: http.ClientRequest | undefined = undefined) { + Log.error(e) + PhoneIslandController.instance.window.emit(IPC_EVENTS.END_CALL) + NethLinkController.instance.window.emit(IPC_EVENTS.RESPONSE_START_CALL_BY_URL, false) + request && request.destroy() + } + try { + const request = http.get(url, { + 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) + PhoneIslandController.instance.window.show() + Log.info('START_CALL_BY_URL', url, res.statusCode) + }) + + request.on('error', (e) => { + triggerError(e, request) + }) + } catch (e) { + triggerError(e) + } + }) + ipcMain.on(IPC_EVENTS.UPDATE_SHARED_STATE, (_, newState, page, selector) => { const windows = BrowserWindow.getAllWindows(); store.updateStore(newState, `${page}[${selector}]`) @@ -180,6 +211,11 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.LOGIN_WINDOW_RESIZE, (_, h) => { LoginController.instance.resize(h) }) + + ipcMain.on(IPC_EVENTS.CHANGE_DEFAULT_DEVICE, (_, ext, force) => { + PhoneIslandController.instance.updateDefaultDevice(ext, force ?? false) + }) + ipcMain.on(IPC_EVENTS.DELETE_ACCOUNT, (_, account: Account) => { Log.info('DELETE ACCOUNT', account) const accountUID = getAccountUID(account) @@ -195,10 +231,10 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.CHANGE_THEME, (_, theme) => { AccountController.instance.updateTheme(theme) - PhoneIslandController.instance.window.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) - LoginController.instance.window.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) + //PhoneIslandController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) + //LoginController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) DevToolsController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) - NethLinkController.instance.window.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) + NethLinkController.instance?.window?.emit(IPC_EVENTS.ON_CHANGE_THEME, theme) }) ipcMain.on(IPC_EVENTS.GET_NETHVOICE_CONFIG, async (e, account) => { @@ -225,6 +261,16 @@ export function registerIpcEvents() { NethLinkController.instance.window.emit(IPC_EVENTS.EMIT_PARKING_UPDATE) }) -} - + ipcMain.on(IPC_EVENTS.UPDATE_ACCOUNT, (_) => { + NethLinkController.instance.window.emit(IPC_EVENTS.UPDATE_ACCOUNT) + }) + ipcMain.on(IPC_EVENTS.RECONNECT_SOCKET, async () => { + try { + await AccountController.instance.autoLogin() + NethLinkController.instance.window.emit(IPC_EVENTS.RECONNECT_SOCKET) + } catch (e) { + Log.error('SOCKET Reconnection error on logout', e) + } + }) +} diff --git a/src/main/lib/mainStore.ts b/src/main/lib/mainStore.ts index ed488ddd..cb2625df 100644 --- a/src/main/lib/mainStore.ts +++ b/src/main/lib/mainStore.ts @@ -59,7 +59,7 @@ class Store { updateStore(newState: T, from: string) { const diff = difference(Object.values(newState as any || {}), Object.values(this.store as any || {})) - Log.info('STORE update shared store from', from, diff.length) + Log.info('STORE update shared store from', from, Object.keys(newState as any || {})) if (diff.length > 0 || this.store === undefined) { this.store = Object.assign({}, newState) } diff --git a/src/main/main.ts b/src/main/main.ts index ab4a5511..e9748fc1 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -22,6 +22,7 @@ import { uniq } from 'lodash' import { Registry } from 'rage-edit'; import { useNethVoiceAPI } from '@shared/useNethVoiceAPI' import { URL } from 'url' +import { platform } from 'os' //get app parameter const params = process.argv @@ -30,9 +31,10 @@ for (const arg of params) { const kv: any[] = arg.split('=') if (['DEV', 'DEVTOOLS'].includes(kv[0])) { kv[1] = kv[1] === 'true' - } else { - kv[1] = undefined } + // } else { + // kv[1] = undefined + // } if (kv[1]) process.env[kv[0]] = kv[1] } @@ -65,9 +67,6 @@ function startup() { }) } - ipcMain.on(IPC_EVENTS.EMIT_START_CALL, async (_event, phoneNumber) => { - PhoneIslandController.instance.call(phoneNumber) - }) 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) { @@ -260,10 +259,6 @@ function attachOnReadyProcess() { } }) - //TODO:da me.endpoints.extensions cerco il tipo e prendo il primo diverso da nethlink tra [webrtc, physical] (con questa priorità) - //post del defaultDevice - - async function startApp(attempt = 0) { let data = store.store || store.getFromDisk() if (!checkData(data)) { @@ -484,7 +479,6 @@ async function attachProtocolListeners() { Log.info('HandleProtocol Nethlink:', url) const data = new URL("nethlink://" + url) - //TODO: define actions const action = data.host try { switch (action) { @@ -541,8 +535,12 @@ function attachPowerMonitor() { if (store.store.account && NethLinkController.instance) { const isOpen = NethLinkController.instance.window.isOpen() showNethlink = isOpen ?? true - await PhoneIslandController.instance.logout() - NethLinkController.instance.logout() + try { + await PhoneIslandController.instance.logout() + NethLinkController.instance.logout() + } catch (e) { + Log.error('POWER RESUME ERROR on logout', e) + } const autoLoginResult = await AccountController.instance.autoLogin() if (autoLoginResult) { ipcMain.emit(IPC_EVENTS.LOGIN, undefined, { showNethlink }) @@ -553,22 +551,41 @@ function attachPowerMonitor() { } } +function changeNethlinkTheme() { + let updatedSystemTheme: AvailableThemes = nativeTheme.shouldUseDarkColors + ? 'dark' + : 'light' + + //set nethlink pages theme + if (store.store) { + 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 + + 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') + }); + } else { + TrayController.instance?.changeIconByTheme(updatedSystemTheme) + } +} + function attachThemeChangeListener() { + + changeNethlinkTheme() nativeTheme.on('updated', () => { - if (store.store) { - const theme = store.store.theme - const updatedSystemTheme: AvailableThemes = nativeTheme.shouldUseDarkColors - ? 'dark' - : 'light' - - if (store.store.account?.theme === 'dark' || store.store.account?.theme === 'light') { - store.set('theme', store.store.account?.theme) - } else { - store.set('theme', updatedSystemTheme) - } - //update theme state on the store - TrayController.instance?.changeIconByTheme(updatedSystemTheme) - } + changeNethlinkTheme() }) } /** diff --git a/src/renderer/public/locales/en/translations.json b/src/renderer/public/locales/en/translations.json index 4930fac7..05ce9265 100644 --- a/src/renderer/public/locales/en/translations.json +++ b/src/renderer/public/locales/en/translations.json @@ -127,6 +127,7 @@ "application_update_body": "Click here to open the page where you can download the new release", "lost_call_title": "Missed call from {{user}}", "lost_call_body": "You received a call from {{number}} at {{datetime}}", + "physical_phone_error": "the phone {{phone}} is not reachable. Make sure it is connected", "call_transferred_title": "Call tranferred", "call_transferred_body": "The call was succesfully tranferred" }, @@ -322,7 +323,9 @@ "For the following states": "For the following states", "Callforward": "Forward", "Do not disturb": "Do not disturb", - "Available presence": "Available presence" + "Available presence": "Available presence", + "IP Phone": "IP Phone", + "Only nethlink": "Only Nethlink" }, "TopBar": { "Signed in as": "Signed in as", @@ -344,7 +347,9 @@ "Mobile": "Mobile", "Cellphone": "Cellphone", "Activate voicemail": "Activate voicemail", - "Go to NethVoice CTI": "Go to NethVoice CTI" + "Go to NethVoice CTI": "Go to NethVoice CTI", + "Pair device": "Pair device", + "Theme": "Theme" }, "OperatorDrawer": { "Book": "Book", @@ -893,19 +898,22 @@ "download": "Download the update" }, "Errors": { - "browser_permissions": "Errore di autorizzazione", - "user_permissions": "Errore di autorizzazione", - "unknown_media_permissions": "Errore di autorizzazione", - "webrtc_down": "WebRTC non disponibile", - "socket_down": "Socket non disponibile", - "busy_camera": "Camera occupata", - "call_transfered": "Chiamata trasferita con successo", - "The browser doesn't have permission to access camera or microphone.": "Il browser non ha il permesso di accedere alla fotocamera o al microfono.", - "You must accept audio and video permissions.": "Devi accettare i permessi audio e video.", - "Web Phone can't access audio or camera on this device.": "Il Web Phone non può accedere all'audio o alla fotocamera su questo dispositivo.", - "Web Phone connection is down.": "La connessione del Web Phone è interrotta.", - "Server connection is down.": "La connessione al server è interrotta.", - "Camera is used by another application.": "La fotocamera è utilizzata da un'altra applicazione.", - "Call transferred successfully.": "Chiamata trasferita con successo." + "browser_permissions": "Browser permissions error", + "user_permissions": "Media permissions error", + "unknown_media_permissions": "Generic media permission error", + "webrtc_down": "WebRTC is unavailable", + "socket_down": "Socket is unavailable", + "busy_camera": "Webcam is already in use", + "call_transfered": "Transfer successful", + "The browser doesn't have permission to access camera or microphone.": "The browser doesn't have permission to access camera or microphone.", + "You must accept audio and video permissions.": "You must accept audio and video permissions.", + "Web Phone can't access audio or camera on this device.": "Web Phone can't access audio or camera on this device.", + "Web Phone connection is down.": "Device not communicating with server.", + "NethLink connection is down.": "Server connection is unavailable", + "Server connection is down.": "Server connection is unavailable", + "Camera is used by another application.": "Webcam is used by another application.", + "Unknown audio or camera permissions.": "Microphone or Webcam is unavailable", + "No microphone or camera permissions.": "You must accept borwser permissions.", + "Call transferred successfully.": "The call is transferred successfully." } } diff --git a/src/renderer/public/locales/it/translations.json b/src/renderer/public/locales/it/translations.json index 94db954f..7785e94c 100644 --- a/src/renderer/public/locales/it/translations.json +++ b/src/renderer/public/locales/it/translations.json @@ -127,6 +127,7 @@ "application_update_body": "Clicca quì per aprire la pagina dove potrai scaricare la nuova release", "lost_call_title": "Chiamata persa da {{user}}", "lost_call_body": "Hai ricevuto una chiamata da {{number}} alle {{datetime}}", + "physical_phone_error": "Il telefono {{phone}} non è raggiungibile. Assicurarsi che sia collegato", "call_transferred_title": "Traferimento di chiamata", "call_transferred_body": "La chiamata è stata trasferita con successo" }, @@ -322,7 +323,9 @@ "For the following states": "Per i seguenti stati", "Callforward": "Inoltro", "Do not disturb": "Non disturbare", - "Available presence": "Presence disponibili" + "Available presence": "Presence disponibili", + "IP Phone": "Telefono IP", + "Only nethlink": "Solo Nethlink" }, "TopBar": { "Signed in as": "Accesso effettuato come", @@ -344,7 +347,9 @@ "Mobile": "Cellulare", "Cellphone": "Cellulare", "Activate voicemail": "Attiva voicemail", - "Go to NethVoice CTI": "Vai a NethVoice CTI" + "Go to NethVoice CTI": "Vai a NethVoice CTI", + "Pair device": "Abbina dispositivo", + "Theme": "Tema" }, "OperatorDrawer": { "Book": "Prenota", @@ -893,19 +898,22 @@ "download": "Scarica l'aggiornamento" }, "Errors": { - "browser_permissions": "Browser Permissions Error", - "user_permissions": "User Permissions Error", - "unknown_media_permissions": "Unknown Media Permissions Error", - "webrtc_down": "WebRTC Down", - "socket_down": "Socket Down", - "busy_camera": "Busy Camera", - "call_transfered": "Call Transferred Successfully", - "The browser doesn't have permission to access camera or microphone.": "The browser doesn't have permission to access camera or microphone.", - "You must accept audio and video permissions.": "You must accept audio and video permissions.", - "Web Phone can't access audio or camera on this device.": "Web Phone can't access audio or camera on this device.", - "Web Phone connection is down.": "Web Phone connection is down.", - "Server connection is down.": "Server connection is down.", - "Camera is used by another application.": "Camera is used by another application.", - "Call transferred successfully.": "Call transferred successfully." + "browser_permissions": "Errore nei permessi del browser", + "user_permissions": "Errore nei permessi dei media", + "unknown_media_permissions": "Errore generico nei permessi", + "webrtc_down": "WebRTC non disponibile", + "socket_down": "Socket non disponibile", + "busy_camera": "La webcam è già in uso", + "call_transfered": "Trasferimento riuscito", + "The browser doesn't have permission to access camera or microphone.": "Il browser non ha il permesso di accedere alla fotocamera o al microfono.", + "You must accept audio and video permissions.": "Devi accettare i permessi per audio e video.", + "Web Phone can't access audio or camera on this device.": "Il Web Phone non può accedere all'audio o alla fotocamera su questo dispositivo.", + "Web Phone connection is down.": "Il dispositivo non comunica con il server.", + "NethLink connection is down.": "Connessione al server non disponibile", + "Server connection is down.": "Connessione al server non disponibile", + "Camera is used by another application.": "La fotocamera è utilizzata da un'altra applicazione.", + "Unknown audio or camera permissions.": "Microfono o fotocamera non disponibili.", + "No microphone or camera permissions.": "Devi accettare i permessi del browser.", + "Call transferred successfully.": "La chiamata è stata trasferita con successo." } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index a738e0ad..f289fafc 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -13,7 +13,6 @@ import { useRegisterStoreHook, useSharedState } from "@renderer/store"; import { PageContext, usePageCtx } from './contexts/pageContext' import { GIT_RELEASES_URL, IPC_EVENTS } from '@shared/constants' import { useNetwork } from '@shared/useNetwork' -import { useRefState } from './hooks/useRefState' const RequestStateComponent = () => { @@ -21,7 +20,7 @@ const RequestStateComponent = () => { useRegisterStoreHook() const [theme,] = useSharedState('theme') const [account,] = useSharedState('account') - const [connection,] = useRefState(useSharedState('connection')) + const [connection,] = useSharedState('connection') const [hasWindowConfig, setHasWindowConfig] = useState(false) const { GET } = useNetwork() @@ -33,8 +32,8 @@ const RequestStateComponent = () => { resolve(false) }) }) - Log.info('check connection', { connected, connection: connection.current }) - if (connected !== connection.current) { + Log.info('check connection', { connected, connection: connection }) + if (connected !== connection) { window.electron.send(IPC_EVENTS.UPDATE_CONNECTION_STATE, connected); } } @@ -43,7 +42,7 @@ const RequestStateComponent = () => { if (account) { if (!window['CONFIG']) { // @ts-ignore (define in dts) - window.CONFIG = { + window['CONFIG'] = { PRODUCT_NAME: 'NethLink', COMPANY_NAME: 'Nethesis', COMPANY_SUBNAME: 'CTI', @@ -57,14 +56,14 @@ const RequestStateComponent = () => { TIMEZONE: account.timezone, VOICE_ENDPOINT: account.voiceEndpoint } - Log.info(window['CONFIG']) + Log.info('WINDOW CONFIG', pageData?.page, window['CONFIG']) setHasWindowConfig(true) } } else { window['CONFIG'] = undefined setHasWindowConfig(false) } - }, [account, pageData?.page]) + }, [account?.username, pageData?.page]) const loader = async () => { Log.info('check i18n initialization') @@ -146,7 +145,6 @@ export default function App() { Log.info('initialize i18n') loadI18n() }) - return ( diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx new file mode 100644 index 00000000..c77ca856 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Backdrop.tsx @@ -0,0 +1,17 @@ +import classNames from "classnames" +interface BackdropProps { + onBackdropClick: () => void, + className?: string +} +export function Backdrop({ onBackdropClick, className }: BackdropProps) { + return ( +
{ + e.preventDefault() + e.stopPropagation() + onBackdropClick() + }} + >
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx index 9f666905..cc21b9e8 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx @@ -4,7 +4,7 @@ import { isDev } from "@shared/utils/utils" import { FavouriteStar } from "./FavouritesStar" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { useAccount } from "@renderer/hooks/useAccount" -import { useSharedState } from "@renderer/store" +import { useNethlinkData } from "@renderer/store" import { usePhoneIslandEventHandler } from "@renderer/hooks/usePhoneIslandEventHandler" import { Avatar } from "../../../Nethesis" import { @@ -24,7 +24,7 @@ export const ContactNameAndActions = ({ contact, number, isHighlight, displayedN isSearchData: boolean }) => { const { isCallsEnabled } = useAccount() - const [operators] = useSharedState('operators') + const [operators] = useNethlinkData('operators') const { callNumber } = usePhoneIslandEventHandler() const avatarSrc = username && operators?.avatars?.[username] diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx index 3f9670ce..ade9cbd7 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx @@ -15,151 +15,44 @@ import { Account } from '@shared/types' import { t } from 'i18next' import { StatusDot } from '../../../Nethesis' import { useAccount } from '@renderer/hooks/useAccount' -import { debouncer, getAccountUID, isDev } from '@shared/utils/utils' -import { useSharedState } from '@renderer/store' -import { createRef, useState } from 'react' -import { useTheme } from '@renderer/theme/Context' -import { PresenceBox } from '../Presence/PresenceBox' +import { debouncer } from '@shared/utils/utils' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { useState } from 'react' import classNames from 'classnames' import { truncate } from 'lodash' -import { PresenceBadge } from '../Presence/PresenceBadge' +import { PresenceBadge } from './ProfileDialog/PresenceSettings/PresenceBadge' import { SearchBox } from '../SearchResults/SearchBox' +import { ThemeBox } from './ProfileDialog/ThemeSettings/ThemeBox' +import { ProfileDialog } from './ProfileDialog' export interface NavbarProps { onClickAccount: () => void } -const themeOptions = [ - { id: 1, name: 'system', icon: SystemIcon }, - { id: 2, name: 'light', icon: LightIcon }, - { id: 3, name: 'dark', icon: DarkIcon } -] - export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { - const { theme: nethTheme } = useTheme() - const { status } = useAccount() - const [account, setAccount] = useSharedState('account') - const [, setAuth] = useSharedState('auth') - const [operators] = useSharedState('operators') - const [, setTheme] = useSharedState('theme') - const [isPresenceDialogVisible, setIsPresenceDialogVisible] = useState(false) - - const btnRef = createRef() - - function handleSetTheme(theme) { - setTheme(() => theme) - const updatedAccount = { ...account!, theme: theme } - setAccount(() => updatedAccount) - setAuth((p) => ({ - ...p, - isFirstStart: p?.isFirstStart ?? true, - availableAccounts: { - ...p?.availableAccounts, - [getAccountUID(updatedAccount as Account)]: updatedAccount - } - })) - } - function handleGoToNethVoicePage() { - window.api.openHostPage('/') - } - - function handleExitNethLink() { - window.api.exitNethLink() - } - - function handleLogout() { - window.api.logout() - } + const { status } = useAccount() + const [account] = useSharedState('account') + const [operators] = useNethlinkData('operators') - function showPresenceDialog(e) { - e.preventDefault() - setIsPresenceDialogVisible(true) - } + const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false) if (!account) return <> return ( -
- -
- -
- - - - - { - const el: HTMLButtonElement = e.target.parentElement! - .children[0]! as HTMLButtonElement - el.addEventListener( - 'focus', - (e) => { - ; (e.target! as HTMLButtonElement).blur() - }, - { - once: true - } - ) - }} - className={`dark:bg-bgDark bg-bgLight border dark:border-borderDark border-borderLight rounded-lg mt-2 fixed min-w-[225px] min-h-[145px] z-[200] translate-x-[calc(-100%+36px)]`} - > -

- {t('Settings.Theme')} -

- {themeOptions.map((availableTheme) => ( - -
handleSetTheme(availableTheme.name)} - > - {account.theme === availableTheme.name && ( - - )} -
- -

- {availableTheme.name === 'system' - ? t('Settings.System') - : availableTheme.name === 'light' - ? t('Settings.Light') - : t('Settings.Dark')} -

-
-
-
- ))} -
-
-
+ <> +
+ +
+ -
- - +
{ + setIsProfileDialogOpen((p) => !p) debouncer('reload_me', onClickAccount, 1000) }} > @@ -169,98 +62,14 @@ export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { src={operators?.avatars?.[account.username] || undefined} placeholderType="operator" /> - - { - const el: HTMLButtonElement = e.target.parentElement! - .children[0]! as HTMLButtonElement - el.addEventListener( - 'focus', - (e) => { - ; (e.target! as HTMLButtonElement).blur() - }, - { - once: true - } - ) - }} - static={isPresenceDialogVisible} - className={`dark:bg-bgDark bg-bgLight border dark:border-borderDark border-borderLight mt-2 pb-2 fixed rounded-lg min-w-[225px] min-h-[125px] z-[200] translate-x-[calc(-100%+36px)]`} - > - -
-

{t('TopBar.Signed in as')}

-
-

- {truncate(account.data?.name, { length: 20 })} -

-

- {account.data?.endpoints.mainextension[0].id} -

- {isDev() && ( -

- [{account.data?.default_device.type}] -

- )} -
-
-
- -
- -

{t('TopBar.Go to NethVoice CTI')}

-
-
- -
- -

{t('TopBar.Presence')}

-
-
- -
- -

{t('TopBar.Logout')}

-
-
- -
- -

{t('Common.Quit')}

-
-
-
-
+
+
-
- setIsPresenceDialogVisible(false)} +
+ setIsProfileDialogOpen(false)} /> - + ) } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog.tsx new file mode 100644 index 00000000..b72fb77c --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog.tsx @@ -0,0 +1,159 @@ +import { + faXmarkCircle as ExitIcon, + faArrowRightFromBracket as LogoutIcon, +} from '@fortawesome/free-solid-svg-icons' +import { motion } from 'motion/react' +import { ProfileData } from './ProfileDialog/ProfileData' +import { MenuAction } from './ProfileDialog/MenuAction' +import { t } from 'i18next' +import { useEffect, useState } from 'react' +import classNames from 'classnames' +import { StatusDot } from '@renderer/components/Nethesis' +import { useAccount } from '@renderer/hooks/useAccount' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { IconProp } from '@fortawesome/fontawesome-svg-core' +import { MenuPage } from './ProfileDialog/MenuPage' +import { Line } from './ProfileDialog/Line' +import { PresenceBox } from './ProfileDialog/PresenceSettings/PresenceBox' +import { ThemeBox, ThemeIcons } from './ProfileDialog/ThemeSettings/ThemeBox' +import { DeviceBox, DeviceIcons } from './ProfileDialog/DeviceSettings/DeviceBox' +import { PresenceForwardDialog } from './ProfileDialog/PresenceSettings/PresenceForwardDialog' +import { Backdrop } from './Backdrop' +import { Log } from '@shared/utils/logger' +import { IconDefinition } from '@nethesis/nethesis-solid-svg-icons' + +enum MenuItem { + device = 1, + presence, + theme, + +} +export const ProfileDialog = ({ + isOpen, + onClose, +}: { + isOpen: boolean, + onClose: () => void +}) => { + const { status, } = useAccount() + const [selectedMenu, setSelectedMenu] = useState(undefined) + const [account] = useSharedState('account') + const [device] = useSharedState('device') + const [x, setX] = useState(0) + const [dialogPageTitle, setDialogPageTitle] = useState('') + const [themeIcon, setThemeIcon] = useState(ThemeIcons['system']) + const [deviceIcon, setDeviceIcon] = useState(DeviceIcons['nethlink']) + const [isForwardDialogOpen, setIsForwardDialogOpen] = useNethlinkData('isForwardDialogOpen') + function handleExitNethLink() { + window.api.exitNethLink() + } + + useEffect(() => { + if (selectedMenu) { + setX(250) + switch (selectedMenu) { + case MenuItem.device: setDialogPageTitle(() => t("TopBar.Pair device")); break; + case MenuItem.theme: setDialogPageTitle(() => t("TopBar.Theme")); break; + case MenuItem.presence: setDialogPageTitle(() => t("TopBar.Presence")); break; + } + } else { + setDialogPageTitle(() => '') + setX(0) + } + }, [selectedMenu]) + + function handleLogout() { + window.api.logout() + } + + useEffect(() => { + if (account) { + setDeviceIcon(() => DeviceIcons[device?.type || 'nethlink']) + } + + }, [device]) + + useEffect(() => { + if (account) { + setThemeIcon(() => ThemeIcons[account?.theme || 'system']) + } + }, [account]) + if (!isOpen) return <> + return
+ {isForwardDialogOpen && } +
+
+
+ + +
+ setSelectedMenu(() => MenuItem.presence)} > + + +

{t('TopBar.Presence')}

+
+ setSelectedMenu(() => MenuItem.device)} + label={t("TopBar.Pair device")} + {...( + deviceIcon.hasOwnProperty('Icon') ? { + iconElem: (deviceIcon as { Icon: JSX.Element }).Icon + } + : { + icon: deviceIcon as IconProp + } + )} + /> + setSelectedMenu(() => MenuItem.theme)} icon={themeIcon} label={t('Settings.Theme')} /> +
+ + + + +
+ + setSelectedMenu(() => undefined)} + title={dialogPageTitle} + > + {selectedMenu === MenuItem.presence && } + {selectedMenu === MenuItem.device && } + {selectedMenu === MenuItem.theme && } + + + +
+
+ { + if (isForwardDialogOpen) { + setIsForwardDialogOpen(() => false) + } else { + setSelectedMenu(() => undefined) + onClose() + + } + + }} + /> +
+ +} 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 new file mode 100644 index 00000000..e87a8e4a --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/DeviceSettings/DeviceBox.tsx @@ -0,0 +1,98 @@ +import { IconProp } from "@fortawesome/fontawesome-svg-core" +import { + NethLinkLogoIcon as DesktopDevice, +} from '@renderer/icons' +import { + IconDefinition, + 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" + +export const DeviceIcons = { + nethlink: { + Icon: + }, + physical: PhysicalDevice +} +type DeviceType = { name: AvailableDevices, label: string, icon?: IconProp | IconDefinition, iconElem?: JSX.Element } +type AvailableDeviceOption = { + id: string, + status: StatusTypes + ext: Extension +} & DeviceType +export const DeviceBox = () => { + const [account] = useSharedState('account') + const [device, setDevice] = useSharedState('device') + const [devicesStatus, setDevicesStatus] = useState({}) + const { NethVoiceAPI } = useLoggedNethVoiceAPI() + useEffect(() => { + NethVoiceAPI.AstProxy.extensions().then((devices: ExtensionsType) => { + setDevicesStatus(() => ({ ...devices })) + }) + }, []) + + + async function handleSetDevice(newDevice: AvailableDeviceOption) { + Log.info('change device to', newDevice) + setDevice(() => ({ + id: newDevice.id, + type: newDevice.name, + status: newDevice.status + })) + 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') }, + 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 + }]) || [] + + return ( +
+ {accountDevices.map((availableDevices) => ( + handleSetDevice(availableDevices)} + /> + ))} +
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/Line.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/Line.tsx new file mode 100644 index 00000000..6822fc3c --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/Line.tsx @@ -0,0 +1,3 @@ +export const Line = () => { + return
+} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuAction.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuAction.tsx new file mode 100644 index 00000000..6f447bd0 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuAction.tsx @@ -0,0 +1,47 @@ +import { IconProp } from "@fortawesome/fontawesome-svg-core" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import classNames from "classnames" +import { ReactElement } from "react" + +type BaseMenuActionProps = { + className?: string, + onClick: () => void, +} + +type MenuActionProps = { + label: string, + icon?: IconProp + iconElem?: JSX.Element +} & BaseMenuActionProps + +type MenuActionWrapProps = { + children: ReactElement | ReactElement[] +} & BaseMenuActionProps + +function MenuAction({ className, children, onClick }: MenuActionWrapProps) { + + return ( +
+
+
+ {children} +
+
+
+ ) +} + +MenuAction.item = (props: MenuActionProps) => + {props.icon + ? + : props.iconElem || <> + } +

{props.label}

+
+MenuAction.itemWrap = (props: MenuActionWrapProps) => {props.children} + +export { + MenuAction +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuPage.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuPage.tsx new file mode 100644 index 00000000..7d0d55e5 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/MenuPage.tsx @@ -0,0 +1,45 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { + faArrowLeft as ArrowIcon, +} from '@fortawesome/free-solid-svg-icons' +import { Button } from "@renderer/components/Nethesis" +import { t } from "i18next" +import { Line } from "./Line" +import classNames from "classnames" +import { Scrollable } from "@renderer/components/Scrollable" + +export const MenuPage = ({ goBack, title, children }) => { + return ( +
+
+ +
+ +
+

+ {title} +

+ + {children} + +
+
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/NethVoiceLink.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/NethVoiceLink.tsx new file mode 100644 index 00000000..1c16ca32 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/NethVoiceLink.tsx @@ -0,0 +1,21 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { + faArrowUpRightFromSquare as GoToNethVoiceIcon +} from '@fortawesome/free-solid-svg-icons' +import { t } from "i18next" +export const NethVoiceLink = () => { + + function handleGoToNethVoicePage() { + window.api.openHostPage('/') + } + + return ( +
+ +

{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 new file mode 100644 index 00000000..b5c92ef3 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/OptionElement.tsx @@ -0,0 +1,56 @@ +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, + isSelected: boolean + onClick: () => void, +} +export const OptionElement = ({ + icon, + iconElem, + label, + isSelected, + onClick, +}: SettingsOptionElementProps) => { + + return ( +
onClick()} + > +
+ {iconElem ? iconElem : icon ? : <>} +

+ {label} +

+
+ {isSelected && ( + + )} +
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceBadge.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx similarity index 88% rename from src/renderer/src/components/Modules/NethVoice/Presence/PresenceBadge.tsx rename to src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx index a0d9c11a..45dd4b39 100644 --- a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceBadge.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBadge.tsx @@ -1,23 +1,28 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faArrowRight, faMobile, faVoicemail } from '@fortawesome/free-solid-svg-icons' import { Badge } from '@renderer/components/Nethesis/Badge' -import { useSharedState } from '@renderer/store' +import { useNethlinkData, useSharedState } from '@renderer/store' import { StatusTypes } from '@shared/types' import { t } from 'i18next' import { useTheme } from '@renderer/theme/Context' import classNames from 'classnames' import { Tooltip } from 'react-tooltip' +import { useEffect, useState } from 'react' export interface PresenceBadgeProps { - mainPresence: StatusTypes | undefined className?: string } export const PresenceBadgeVisibility = ['callforward', 'voicemail', 'cellphone'] -export const PresenceBadge = ({ mainPresence, className }: PresenceBadgeProps) => { +export const PresenceBadge = ({ className }: PresenceBadgeProps) => { const [account] = useSharedState('account') - const [operators] = useSharedState('operators') + const [operators] = useNethlinkData('operators') const { badge: theme, status: statuses } = useTheme().theme + const [mainPresence, setMainPresence] = useState(account?.data?.mainPresence) + + useEffect(() => { + setMainPresence(() => account?.data?.mainPresence) + }, [account]) if (PresenceBadgeVisibility.includes(mainPresence as string)) { const isCallforward = ['callforward', 'voicemail'].includes(mainPresence as string) diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBox.tsx new file mode 100644 index 00000000..ffa80233 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceBox.tsx @@ -0,0 +1,73 @@ +import { t } from 'i18next' +import { PresenceItem } from './PresenceItem' +import { + faArrowRight as CallForwardIcon, + faMobile as CallForwardMobileIcon, + faVoicemail as VoiceMailIcon +} from '@fortawesome/free-solid-svg-icons' +import { Log } from '@shared/utils/logger' +import { isEmpty } from 'lodash' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventHandler' +import { useAccount } from '@renderer/hooks/useAccount' +import { PERMISSION } from '@shared/constants' +import { usePresenceService } from './usePresenceService' + +export function PresenceBox() { + const [, setIsForwardDialogOpen] = useNethlinkData('isForwardDialogOpen') + const [account] = useSharedState('account') + const { hasPermission } = useAccount() + const { onSelectPresence } = usePresenceService() + + return ( +
+ + {/* check callforward permission */} + {hasPermission(PERMISSION.CALL_FORWARD) && ( + setIsForwardDialogOpen(true)} + status="callforward" + presenceName={t('TopBar.Call forward')} + presenceDescription={t('TopBar.Forward incoming calls to another phone number')} + icon={CallForwardIcon} + /> + )} + {!isEmpty(account?.data?.endpoints.cellphone) && ( + + onSelectPresence('callforward', account!.data!.endpoints.cellphone[0]!.id) + } + status="callforward" + presenceName={t('TopBar.Mobile')} + presenceDescription={t('TopBar.Do not receive any calls')} + icon={CallForwardMobileIcon} + /> + )} + {!isEmpty(account?.data?.endpoints.voicemail) && ( + onSelectPresence('voicemail')} + status="voicemail" + presenceName={t('TopBar.Voicemail')} + presenceDescription={t('TopBar.Activate voicemail')} + icon={VoiceMailIcon} + /> + )} + {/* check dnd permission */} + {hasPermission(PERMISSION.DND) && ( + + )} +
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceForwardDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceForwardDialog.tsx new file mode 100644 index 00000000..d4a53fa1 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceForwardDialog.tsx @@ -0,0 +1,103 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, TextInput } from "@renderer/components/Nethesis" +import { getIsPhoneNumber } from "@renderer/lib/utils" +import { useNethlinkData } from "@renderer/store" +import { Log } from "@shared/utils/logger" +import { t } from "i18next" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { usePresenceService } from "./usePresenceService" + +export function PresenceForwardDialog() { + + const [, setIsForwardDialogOpen] = useNethlinkData('isForwardDialogOpen') + const { onSelectPresence } = usePresenceService() + useEffect(() => { + setFocus('to') + }, []) + + const onSubmit = (data) => { + onSelectPresence('callforward', data.to) + } + + const schema: z.ZodType<{ to: string }> = z.object({ + to: z + .string() + .trim() + .min(1, `${t('Common.This field is required')}`) + }) + + const { + register, + handleSubmit, + watch, + setError, + setFocus, + formState: { errors } + } = useForm({ + defaultValues: { + to: '' + }, + resolver: zodResolver(schema) + }) + function handleCancel(e) { + e.preventDefault() + e.stopPropagation() + setIsForwardDialogOpen(false) + } + + async function submit(data) { + if (!getIsPhoneNumber(data.to)) { + setError('to', { + message: t('Common.This is not a phone number') as string + }) + } else { + try { + await onSubmit(data) + } catch (e) { + Log.warning('error during the presence change', e) + } finally { + setIsForwardDialogOpen(false) + } + } + } + + const to = watch('to') + + return ( +
+
+
+
{t('TopBar.Enter phone number for call forward')}
+
+
+ + +
+
+
+ + +
+
+
+ ) +} 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 new file mode 100644 index 00000000..5a1fd1e3 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/PresenceItem.tsx @@ -0,0 +1,62 @@ +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"; +export interface PresenceItemProps { + presenceName: string, + presenceDescription: string, + status: StatusTypes, + icon?: IconDefinition, + hasTopBar?: boolean, + onClick: (status: StatusTypes) => void +} + +export function PresenceItem({ + presenceName, + presenceDescription, + status, + hasTopBar, + icon, + onClick +}: PresenceItemProps) { + + const { status: currentStatus } = useAccount() + + const handleClick = (e) => { + e.preventDefault() + e.stopPropagation() + onClick(status) + } + + return ( + <> + {hasTopBar ?
: null} +
+
+
+ +
+ {presenceName} + {icon && } +
+
+
{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 new file mode 100644 index 00000000..ff899a3a --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/PresenceSettings/usePresenceService.ts @@ -0,0 +1,35 @@ +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) { + try { + await NethVoiceAPI.User.setPresence(status, to) + const me = await NethVoiceAPI.User.me() + if (to) { + NethVoiceAPI.fetchOperators().then(saveOperators) + } + setAccount({ + ...account!, + data: { + ...account!.data!, + ...me + } + }) + } catch (e) { + Log.error('ON PRESENCE CHANGE', e) + } finally { + } + } + + return { + 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 new file mode 100644 index 00000000..6939a7f2 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ProfileData.tsx @@ -0,0 +1,28 @@ +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')}

+
+

+ {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/ThemeSettings/ThemeBox.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx new file mode 100644 index 00000000..715bb7c0 --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/ThemeSettings/ThemeBox.tsx @@ -0,0 +1,60 @@ +import { + faPalette as SystemIcon, + 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 { IPC_EVENTS } from '@shared/constants' + +export const ThemeIcons = { + system: SystemIcon, + light: LightIcon, + dark: DarkIcon +} +export const ThemeBox = () => { + const [account, setAccount] = useSharedState('account') + const [, setAuth] = useSharedState('auth') + const [, setTheme] = useSharedState('theme') + function handleSetTheme(theme) { + Log.info('change theme to', theme) + setTheme(() => theme) + const updatedAccount = { ...account!, theme: theme } + setAccount(() => updatedAccount) + setAuth((p) => ({ + ...p, + isFirstStart: p?.isFirstStart ?? true, + availableAccounts: { + ...p?.availableAccounts, + [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') } + ] + + + if (!account) return <> + return ( +
+ {themeOptions.map((availableTheme) => ( + handleSetTheme(availableTheme.name)} + /> + ))} +
+ ) +} diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx index 297a943b..aed7a8b9 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Sidebar.tsx @@ -23,7 +23,7 @@ export function Sidebar({ onChangeMenu }: SidebarProps): JSX.Element { const [selectedSidebarMenu, setSelectedSidebarMenu] = useNethlinkData('selectedSidebarMenu') const [, setShowPhonebookSearchModule] = useNethlinkData('showPhonebookSearchModule') const [, setPhonebookSearchModule] = useNethlinkData('phonebookSearchModule') - const [missedCalls] = useSharedState('missedCalls') + const [missedCalls] = useNethlinkData('missedCalls') const [notifications] = useSharedState('notifications') const [lastMenu, setLastMenu] = useState(MENU_ELEMENT.FAVOURITES) const [isAboutVisited, setIsAboutVisited] = useState(false) diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx index 4ff54a83..e90ea23c 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCall.tsx @@ -16,7 +16,7 @@ import { truncate } from '@renderer/utils' import { Tooltip } from 'react-tooltip' import { Badge } from '../../../Nethesis/Badge' import { useAccount } from '@renderer/hooks/useAccount' -import { useSharedState } from '@renderer/store' +import { useNethlinkData } from '@renderer/store' import { usePhonebookModule } from '../PhonebookModule/hook/usePhonebookModule' import { OutCallAnsweredIcon, @@ -41,8 +41,8 @@ export function LastCall({ }: LastCallProps): JSX.Element { const phonebookModule = usePhonebookModule() const [, setSelectedContact] = phonebookModule.selectedContact - const [queues] = useSharedState('queues') - const [operators] = useSharedState('operators') + const [queues] = useNethlinkData('queues') + const [operators] = useNethlinkData('operators') const { isCallsEnabled } = useAccount() const [showCreateButton, setShowCreateButton] = useState(false) const [isQueueLoading, setIsQueueLoading] = useState(true) diff --git a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx index 22268556..4af4f0fb 100644 --- a/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx +++ b/src/renderer/src/components/Modules/NethVoice/LastCalls/LastCallsBox.tsx @@ -5,7 +5,7 @@ import { import { LastCall } from './LastCall' import { CallData, LastCallData } from '@shared/types' import { t } from 'i18next' -import { useSharedState } from '@renderer/store' +import { useNethlinkData, useSharedState } from '@renderer/store' import { SkeletonRow } from '@renderer/components/SkeletonRow' import { useEffect, useState } from 'react' import { Scrollable } from '@renderer/components/Scrollable' @@ -14,9 +14,9 @@ import { EmptyList } from '@renderer/components/EmptyList' export function LastCallsBox({ showContactForm }): JSX.Element { - const [lastCalls] = useSharedState('lastCalls') - const [operators] = useSharedState('operators') - const [missedCalls, setMissedCalls] = useSharedState('missedCalls') + const [lastCalls] = useNethlinkData('lastCalls') + const [operators] = useNethlinkData('operators') + const [missedCalls, setMissedCalls] = useNethlinkData('missedCalls') const [preparedCalls, setPreparedCalls] = useState([]) const title = `${t('LastCalls.Calls', { count: lastCalls?.length })} (${lastCalls?.length || 0})` 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 c6093e62..25b61cb0 100644 --- a/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts +++ b/src/renderer/src/components/Modules/NethVoice/Parking/hook/useParkingModule.ts @@ -1,10 +1,10 @@ -import { useSharedState } from "@renderer/store" +import { useNethlinkData } from "@renderer/store" import { ParkingType } from "@shared/types" import { useEffect, useState } from "react" export const useParkingModule = () => { - const [parkedCalls] = useSharedState('parkings') + const [parkedCalls] = useNethlinkData('parkings') const [validParkedCalls, setValidParkedCalls] = useState(undefined) useEffect(() => { parkedCalls && extractValidParkedCalls(parkedCalls) diff --git a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceBox.tsx b/src/renderer/src/components/Modules/NethVoice/Presence/PresenceBox.tsx deleted file mode 100644 index e82d654f..00000000 --- a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceBox.tsx +++ /dev/null @@ -1,230 +0,0 @@ -import { useEffect, useState } from 'react' -import { t } from 'i18next' -import { PresenceItem } from './PresenceItem' -import { - faArrowRight as CallForwardIcon, - faMobile as CallForwardMobileIcon, - faVoicemail as VoiceMailIcon -} from '@fortawesome/free-solid-svg-icons' -import { Button, TextInput } from '../../../Nethesis' -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' -import { getIsPhoneNumber } from '@renderer/lib/utils' -import { Log } from '@shared/utils/logger' -import { isEmpty } from 'lodash' -import { useSharedState } from '@renderer/store' -import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' -import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventHandler' -import { Scrollable } from '@renderer/components/Scrollable' -import classNames from 'classnames' -import { useAccount } from '@renderer/hooks/useAccount' -import { PERMISSION } from '@shared/constants' - -export interface PresenceBoxProps { - isOpen: boolean - onClose: () => void -} -export function PresenceBox({ isOpen, onClose: onClosePresenceDialog }: PresenceBoxProps) { - const { saveOperators } = usePhoneIslandEventHandler() - const [isForwardDialogOpen, setIsForwardDialogOpen] = useState(false) - const [account, setAccount] = useSharedState('account') - const { NethVoiceAPI } = useLoggedNethVoiceAPI() - const { hasPermission } = useAccount() - - const schema: z.ZodType<{ to: string }> = z.object({ - to: z - .string() - .trim() - .min(1, `${t('Common.This field is required')}`) - }) - - function ForwardDialog({ onSubmit }) { - useEffect(() => { - setFocus('to') - }, []) - - const { - register, - handleSubmit, - watch, - setError, - setFocus, - formState: { errors } - } = useForm({ - defaultValues: { - to: '' - }, - resolver: zodResolver(schema) - }) - function handleCancel(e) { - e.preventDefault() - e.stopPropagation() - setIsForwardDialogOpen(false) - } - - async function submit(data) { - if (!getIsPhoneNumber(data.to)) { - setError('to', { - message: t('Common.This is not a phone number') as string - }) - } else { - try { - await onSubmit(data) - } catch (e) { - Log.warning('error during the presence change', e) - } finally { - setIsForwardDialogOpen(false) - onClosePresenceDialog() - } - } - } - - const to = watch('to') - - return ( -
-
-
-
{t('TopBar.Enter phone number for call forward')}
-
-
- - -
-
-
- - -
-
-
- ) - } - - function Backdrop({ onBackdropClick, className }) { - return ( -
{ - e.preventDefault() - e.stopPropagation() - onBackdropClick() - }} - >
- ) - } - - async function onSelectPresence(status, to: string | undefined = undefined) { - try { - await NethVoiceAPI.User.setPresence(status, to) - const me = await NethVoiceAPI.User.me() - if (to) { - const operators = await NethVoiceAPI.fetchOperators() - saveOperators(operators) - } - setAccount({ - ...account!, - data: { - ...account!.data!, - ...me - } - }) - } catch (e) { - Log.error('ON PRESENCE CHANGE', e) - } finally { - onClosePresenceDialog() - } - } - - return ( - isOpen && ( - <> -
- - - {/* check callforward permission */} - {hasPermission(PERMISSION.CALL_FORWARD) && ( - setIsForwardDialogOpen(true)} - status="callforward" - presenceName={t('TopBar.Call forward')} - presenceDescription={t('TopBar.Forward incoming calls to another phone number')} - icon={CallForwardIcon} - > - )} - {!isEmpty(account?.data?.endpoints.cellphone) && ( - - onSelectPresence('callforward', account!.data!.endpoints.cellphone[0]!.id) - } - status="callforward" - presenceName={t('TopBar.Mobile')} - presenceDescription={t('TopBar.Do not receive any calls')} - icon={CallForwardMobileIcon} - > - )} - {!isEmpty(account?.data?.endpoints.voicemail) && ( - onSelectPresence('voicemail')} - status="voicemail" - presenceName={t('TopBar.Voicemail')} - presenceDescription={t('TopBar.Activate voicemail')} - icon={VoiceMailIcon} - > - )} - {/* check dnd permission */} - {hasPermission(PERMISSION.DND) && ( - - )} - -
- - {isForwardDialogOpen && ( - <> - { - onSelectPresence('callforward', data.to) - }} - /> - setIsForwardDialogOpen(false)} - className={'bg-white opacity-[0.25] z-[204]'} - /> - - )} - - ) - ) -} diff --git a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceItem.tsx b/src/renderer/src/components/Modules/NethVoice/Presence/PresenceItem.tsx deleted file mode 100644 index c785c183..00000000 --- a/src/renderer/src/components/Modules/NethVoice/Presence/PresenceItem.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { StatusTypes } from "@shared/types"; -import { IconDefinition } from "@fortawesome/free-solid-svg-icons"; -import { StatusDot } from "@renderer/components/Nethesis"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -export interface PresenceItemProps { - presenceName: string, - presenceDescription: string, - status: StatusTypes, - icon?: IconDefinition, - hasTopBar?: boolean, - onClick: (status: StatusTypes) => void -} - -export function PresenceItem({ - presenceName, - presenceDescription, - status, - hasTopBar, - icon, - onClick -}: PresenceItemProps) { - - - const handleClick = (e) => { - e.preventDefault() - e.stopPropagation() - onClick(status) - } - - return ( - <> - {hasTopBar ?
: null} -
-
- -
- {presenceName} - {icon && } -
-
-
{presenceDescription}
-
- - ) -} diff --git a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx index 8ceb1dee..13ffd4d7 100644 --- a/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx +++ b/src/renderer/src/components/Modules/NethVoice/SearchResults/SearchNumber.tsx @@ -2,7 +2,7 @@ import { ReactNode } from 'react' import { t } from 'i18next' import { SearchData } from '@shared/types' import { useAccount } from '@renderer/hooks/useAccount' -import { useSharedState } from '@renderer/store' +import { useNethlinkData, useSharedState } from '@renderer/store' import { Button } from '@renderer/components/Nethesis' import { usePhonebookSearchModule } from './hook/usePhoneBookSearchModule' import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventHandler' @@ -18,7 +18,7 @@ export function SearchNumber({ user, className }: SearchNumberProps) { const phoneBookModule = usePhonebookSearchModule() const { callNumber } = usePhoneIslandEventHandler() const [searchText] = phoneBookModule.searchTextState - const [operators] = useSharedState('operators') + const [operators] = useNethlinkData('operators') const { isCallsEnabled } = useAccount() const { isSearchAlsoAFavourite } = useFavouriteModule() @@ -82,7 +82,7 @@ export function SearchNumber({ user, className }: SearchNumberProps) { displayedNumber={highlightedNumber} isHighlight={true} username={username} - isFavourite={isSearchAlsoAFavourite(user) || false} + isFavourite={false} isSearchData={true} />