diff --git a/package-lock.json b/package-lock.json index 31a30419..f31c10aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "@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.18.13", + "@nethesis/phone-island": "^1.0.0-dev.10", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", @@ -5643,9 +5643,9 @@ } }, "node_modules/@nethesis/phone-island": { - "version": "0.18.13", - "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-0.18.13.tgz", - "integrity": "sha512-pPL/r6I1z1WB+5m48rL44isHM1FlJ2ZM8eXXeZql5gg1jvYpIILa5rU9SIlFfrfXxDcZqoRxIPjrDF/9stg2uA==", + "version": "1.0.0-dev.10", + "resolved": "https://registry.npmjs.org/@nethesis/phone-island/-/phone-island-1.0.0-dev.10.tgz", + "integrity": "sha512-2UuLv6eGDE8Dh3eC1B3fyS4tKb1y8woRaIhWJbUFIpMpm8Sk2PMQE0MyVsTLUZ5M7OuL/fSLehdVv+BMpYNIVw==", "dev": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 3fc38513..9ea0cf01 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@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.18.13", + "@nethesis/phone-island": "^1.0.0-dev.10", "@tailwindcss/forms": "^0.5.7", "@types/lodash": "^4.14.202", "@types/node": "^18.19.9", diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 73d62d6e..be595c06 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -148,7 +148,10 @@ "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 transferred", - "call_transferred_body": "The call was succesfully transferred" + "call_transferred_body": "The call was succesfully transferred", + "call_summary_ready_title": "Call summary ready", + "call_summary_ready_body": "The summary is ready for review and editing", + "call_summary_ready_body_with_contact": "The summary of your call with {{contact}} is ready" }, "Phonebook": { "Phonebook": "Phonebook", @@ -333,6 +336,7 @@ "Delete profile picture": "Delete profile picture", "Devices": "Audio and Video", "IncomingCalls": "Incoming Calls", + "Notifications": "Notifications", "Ringtone": "Ringtone", "Time preferences": "Time preferences", "Login/logout preferences": "Login/logout preferences", @@ -349,6 +353,14 @@ "Only nethlink": "Only Nethlink", "ShortcutToCall": "Shortcut to Call", "CommandBarShortcut": "Shortcut for Command Bar", + "NotificationsDescription": "Manage notifications for call features and AI-generated content.", + "NotificationsScopeDescription": "Settings apply to web phone and desktop app.", + "CallSummaryNotifications": "Call summary notifications", + "CallSummaryNotificationsDescription": "Receive an operating system notification when a call summary is ready.", + "CallSummaryNotificationsSaveError": "Unable to save notification preferences.", + "CallTranscriptionReady": "Call transcription ready", + "Enabled": "Enabled", + "Disabled": "Disabled", "ShortcutHelp": "Enter a keyboard shortcut to start a call after selecting some text.", "ShortcutHelpDesc": "Unsupported keys: \"Tab\", \"CapsLock\", \"NumLock\", \"Insert\", \"Escape\", \"Shift\".", "Clear and remove shortcut": "Clear and remove shortcut" diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index bef48a86..ae3843da 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -148,7 +148,10 @@ "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" + "call_transferred_body": "La chiamata è stata trasferita con successo", + "call_summary_ready_title": "Riepilogo chiamata pronto", + "call_summary_ready_body": "Il riepilogo e' pronto per la revisione e la modifica", + "call_summary_ready_body_with_contact": "Il riepilogo della tua chiamata con {{contact}} e' pronto" }, "Phonebook": { "Phonebook": "Rubrica", @@ -333,6 +336,7 @@ "Delete profile picture": "Elimina immagine profilo", "Devices": "Audio e Video", "IncomingCalls": "Chiamate in Arrivo", + "Notifications": "Notifiche", "Ringtone": "Suoneria", "Time preferences": "Preferenze di tempo", "Login/logout preferences": "Preferenze di entrata/uscita", @@ -349,6 +353,14 @@ "Only nethlink": "Solo Nethlink", "ShortcutToCall": "Scorciatoia per Chiamare", "CommandBarShortcut": "Scorciatoia per Command Bar", + "NotificationsDescription": "Gestisci le notifiche per le funzioni di chiamata e i contenuti generati dall'AI.", + "NotificationsScopeDescription": "Le impostazioni si applicano al web phone e all'app desktop.", + "CallSummaryNotifications": "Notifica riassunto chiamata", + "CallSummaryNotificationsDescription": "Ricevi una notifica di sistema operativo quando il riassunto della chiamata e' pronto.", + "CallSummaryNotificationsSaveError": "Impossibile salvare le preferenze di notifica.", + "CallTranscriptionReady": "Trascrizione chiamata pronta", + "Enabled": "Attivato", + "Disabled": "Disattivato", "ShortcutHelp": "Inserisci una scorciatoia da tastiera per far partire una chiamata dopo aver selezionato un testo.", "ShortcutHelpDesc": "Caratteri non consentiti: \"Tab\", \"CapsLock\", \"NumLock\", \"Insert\", \"Escape\", \"Shift\".", "Clear and remove shortcut": "Ripristina e rimuovi scorciatoia" diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index 02e092ba..fc608d14 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -4,8 +4,7 @@ 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 } from 'electron' -import { join } from 'path' +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' @@ -69,6 +68,10 @@ function isUserLoggedIn(): boolean { return !!store.store.account } +function buildHostUrl(host: string, path: string): string { + return new URL(path, `https://${host}`).toString() +} + // Keep exactly one Command Bar shortcut active at a time. let activeCommandBarAccelerator: string | undefined let activeCommandBarLastTrigger = 0 @@ -255,13 +258,29 @@ export function registerIpcEvents() { ipcMain.on(IPC_EVENTS.OPEN_HOST_PAGE, async (_, path) => { const account = store.store.account - shell.openExternal(join('https://' + account!.host, path)) + shell.openExternal(buildHostUrl(account!.host, path)) }) ipcMain.on(IPC_EVENTS.OPEN_EXTERNAL_PAGE, async (event, path) => { shell.openExternal(path) }) + 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.show() + }) + ipcMain.on(IPC_EVENTS.COPY_TO_CLIPBOARD, async (_, text) => { clipboard.writeText(text) }) diff --git a/src/preload/index.ts b/src/preload/index.ts index 9b5f98bf..83dc12d1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -29,7 +29,11 @@ export interface IElectronAPI { //EMITTER - only emit, no response openDevTool(hash: string): unknown - sendNotification(notificationoption: NotificationConstructorOptions, openUrl: string | undefined): void + sendNotification( + title: string, + notificationoption: NotificationConstructorOptions, + openUrl?: string, + ): void logout: () => void startCall(phoneNumber: string): void hideLoginWindow(): void @@ -102,6 +106,7 @@ const api: IElectronAPI = { hidePhoneIsland: setEmitter(IPC_EVENTS.HIDE_PHONE_ISLAND), showPhoneIsland: setEmitter(IPC_EVENTS.SHOW_PHONE_ISLAND), copyToClipboard: setEmitter(IPC_EVENTS.COPY_TO_CLIPBOARD), + sendNotification: setEmitter(IPC_EVENTS.SEND_NOTIFICATION), //LISTENERS - receive data async onUpdateAppNotification: addListener(IPC_EVENTS.UPDATE_APP_NOTIFICATION), diff --git a/src/renderer/public/locales/en/translations.json b/src/renderer/public/locales/en/translations.json index 3707b604..584b65eb 100644 --- a/src/renderer/public/locales/en/translations.json +++ b/src/renderer/public/locales/en/translations.json @@ -148,7 +148,10 @@ "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 transferred", - "call_transferred_body": "The call was succesfully transferred" + "call_transferred_body": "The call was succesfully transferred", + "call_summary_ready_title": "Call summary ready", + "call_summary_ready_body": "The summary is ready for review and editing", + "call_summary_ready_body_with_contact": "The summary of your call with {{contact}} is ready" }, "Phonebook": { "Phonebook": "Phonebook", @@ -333,6 +336,7 @@ "Delete profile picture": "Delete profile picture", "Devices": "Audio and Video", "IncomingCalls": "Incoming Calls", + "Notifications": "Notifications", "Ringtone": "Ringtone", "Time preferences": "Time preferences", "Login/logout preferences": "Login/logout preferences", @@ -349,6 +353,14 @@ "Only nethlink": "Only Nethlink", "ShortcutToCall": "Shortcut to Call", "CommandBarShortcut": "Shortcut for Command Bar", + "NotificationsDescription": "Manage notifications for call features and AI-generated content.", + "NotificationsScopeDescription": "Settings apply to web phone and desktop app.", + "CallSummaryNotifications": "Call summary notifications", + "CallSummaryNotificationsDescription": "Receive an operating system notification when a call summary is ready.", + "CallSummaryNotificationsSaveError": "Unable to save notification preferences.", + "CallTranscriptionReady": "Call transcription ready", + "Enabled": "Enabled", + "Disabled": "Disabled", "ShortcutHelp": "Enter a keyboard shortcut to start a call after selecting some text.", "ShortcutHelpDesc": "Unsupported keys: \"Tab\", \"CapsLock\", \"NumLock\", \"Insert\", \"Escape\", \"Shift\".", "Clear and remove shortcut": "Clear and remove shortcut" diff --git a/src/renderer/public/locales/it/translations.json b/src/renderer/public/locales/it/translations.json index c076fb73..6ec6cda7 100644 --- a/src/renderer/public/locales/it/translations.json +++ b/src/renderer/public/locales/it/translations.json @@ -148,7 +148,10 @@ "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" + "call_transferred_body": "La chiamata è stata trasferita con successo", + "call_summary_ready_title": "Riepilogo chiamata pronto", + "call_summary_ready_body": "Il riepilogo e' pronto per la revisione e la modifica", + "call_summary_ready_body_with_contact": "Il riepilogo della tua chiamata con {{contact}} e' pronto" }, "Phonebook": { "Phonebook": "Rubrica", @@ -333,6 +336,7 @@ "Delete profile picture": "Elimina immagine profilo", "Devices": "Audio e Video", "IncomingCalls": "Chiamate in Arrivo", + "Notifications": "Notifiche", "Ringtone": "Suoneria", "Time preferences": "Preferenze di tempo", "Login/logout preferences": "Preferenze di entrata/uscita", @@ -349,6 +353,14 @@ "Only nethlink": "Solo Nethlink", "ShortcutToCall": "Scorciatoia per Chiamare", "CommandBarShortcut": "Scorciatoia per Command Bar", + "NotificationsDescription": "Gestisci le notifiche per le funzioni di chiamata e i contenuti generati dall'AI.", + "NotificationsScopeDescription": "Le impostazioni si applicano al web phone e all'app desktop.", + "CallSummaryNotifications": "Notifica riassunto chiamata", + "CallSummaryNotificationsDescription": "Ricevi una notifica di sistema operativo quando il riassunto della chiamata e' pronto.", + "CallSummaryNotificationsSaveError": "Impossibile salvare le preferenze di notifica.", + "CallTranscriptionReady": "Trascrizione chiamata pronta", + "Enabled": "Attivato", + "Disabled": "Disattivato", "ShortcutHelp": "Inserisci una scorciatoia da tastiera per far partire una chiamata dopo aver selezionato un testo.", "ShortcutHelpDesc": "Caratteri non consentiti: \"Tab\", \"CapsLock\", \"NumLock\", \"Insert\", \"Escape\", \"Shift\".", "Clear and remove shortcut": "Ripristina e rimuovi scorciatoia" diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx index 1cd3f2ef..7c6806d8 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/Navbar.tsx @@ -13,6 +13,7 @@ import { SettingsShortcutDialog } from './ProfileDialog/SettingsSettings/Setting import { SettingsCommandBarShortcutDialog } from './ProfileDialog/SettingsSettings/SettingsCommandBarShortcutDialog' import { SettingsDeviceDialog } from './ProfileDialog/SettingsSettings/SettingsDevicesDialog' import { SettingsIncomingCallsDialog } from './ProfileDialog/SettingsSettings/SettingsIncomingCallsDialog' +import { SettingsNotificationsDialog } from './ProfileDialog/SettingsSettings/SettingsNotificationsDialog' export interface NavbarProps { onClickAccount: () => void @@ -29,6 +30,8 @@ export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { ) const [isDeviceDialogOpen] = useNethlinkData('isDeviceDialogOpen') const [isIncomingCallsDialogOpen] = useNethlinkData('isIncomingCallsDialogOpen') + const [isNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const isCallSummaryEnabled = account?.data?.call_summary_enabled === true const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false) @@ -64,6 +67,7 @@ export function Navbar({ onClickAccount }: NavbarProps): JSX.Element { {isCommandBarShortcutDialogOpen && } {isDeviceDialogOpen && } {isIncomingCallsDialogOpen && } + {isCallSummaryEnabled && isNotificationsDialogOpen && } setIsProfileDialogOpen(false)} 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 330f8826..6e45248e 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 @@ -4,17 +4,21 @@ import { faKeyboard as KeyboardIcon, faHeadphones as DevicesIcon, faPhoneVolume as IncomingCallsIcon, + faBell as NotificationsIcon, } from '@fortawesome/free-solid-svg-icons' -import { useNethlinkData } from '@renderer/store' +import { useNethlinkData, useSharedState } from '@renderer/store' import { OptionElement } from '../OptionElement' export function SettingsBox({ onClose }: { onClose?: () => void }) { + const [account] = useSharedState('account') const [, setIsShortcutDialogOpen] = useNethlinkData('isShortcutDialogOpen') const [, setIsCommandBarShortcutDialogOpen] = useNethlinkData( 'isCommandBarShortcutDialogOpen', ) const [, setIsDeviceDialogOpen] = useNethlinkData('isDeviceDialogOpen') const [, setIsIncomingCallsDialogOpen] = useNethlinkData('isIncomingCallsDialogOpen') + const [, setIsNotificationsDialogOpen] = useNethlinkData('isNotificationsDialogOpen') + const isCallSummaryEnabled = account?.data?.call_summary_enabled === true return ( @@ -54,6 +58,17 @@ export function SettingsBox({ onClose }: { onClose?: () => void }) { if (onClose) onClose() }} /> + {isCallSummaryEnabled && ( + { + setIsNotificationsDialogOpen(true) + if (onClose) onClose() + }} + /> + )} ) } diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsCommandBarShortcutDialog.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsCommandBarShortcutDialog.tsx index 90ae099e..c5ebac12 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsCommandBarShortcutDialog.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsCommandBarShortcutDialog.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { Backdrop } from '../../Backdrop' -import { CustomThemedTooltip } from '@renderer/components/Nethesis/CurstomThemedTooltip' +import { CustomThemedTooltip } from '@renderer/components/Nethesis/CustomThemedTooltip' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faXmark } from '@fortawesome/free-solid-svg-icons' import { InlineNotification } from '@renderer/components/Nethesis/InlineNotification' 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 new file mode 100644 index 00000000..2f2a541f --- /dev/null +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ProfileDialog/SettingsSettings/SettingsNotificationsDialog.tsx @@ -0,0 +1,188 @@ +import { Button } from '@renderer/components/Nethesis' +import { Backdrop } from '../../Backdrop' +import { CustomThemedTooltip } from '@renderer/components/Nethesis/CustomThemedTooltip' +import { useNethlinkData, useSharedState } from '@renderer/store' +import { t } from 'i18next' +import { useEffect, useState } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faCircleInfo } from '@fortawesome/free-solid-svg-icons' +import { useLoggedNethVoiceAPI } from '@renderer/hooks/useLoggedNethVoiceAPI' +import { useAccount } from '@renderer/hooks/useAccount' +import { sendNotification } from '@renderer/utils' +import classNames from 'classnames' + +export function SettingsNotificationsDialog() { + const tooltipId = 'tooltip-call-summary-notifications-info' + 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(() => { + if (!account?.data?.settings || !isCallSummaryEnabled) { + return null + } + + return account.data.settings.call_summary_notifications !== false + }) + + useEffect(() => { + if (!isCallSummaryEnabled) { + setIsNotificationsDialogOpen(false) + return + } + + if (callSummaryNotifications === null && account?.data?.settings) { + setCallSummaryNotifications( + account.data.settings.call_summary_notifications !== false, + ) + } + }, [ + account?.data?.settings, + callSummaryNotifications, + isCallSummaryEnabled, + setIsNotificationsDialogOpen, + ]) + + function handleCancel(e) { + e.preventDefault() + e.stopPropagation() + setIsNotificationsDialogOpen(false) + } + + async function handleSubmit(e) { + e.preventDefault() + e.stopPropagation() + + if (isSaving) { + return + } + if (!isCallSummaryEnabled || callSummaryNotifications === null) { + return + } + + setIsSaving(true) + try { + await NethVoiceAPI.User.settings({ + call_summary_notifications: callSummaryNotifications, + }) + await updateAccountData() + setIsNotificationsDialogOpen(false) + } catch (error) { + sendNotification( + t('Common.Warning'), + t('Settings.CallSummaryNotificationsSaveError'), + ) + } finally { + setIsSaving(false) + } + } + + return ( + <> + + setIsNotificationsDialogOpen(false)} + /> + + + + + + + {t('Settings.Notifications')} + + + {t('Settings.NotificationsDescription')} + {t('Settings.NotificationsScopeDescription')} + + + + + + + {t('Settings.CallTranscriptionReady')} + + + + + + + + + {callSummaryNotifications !== null ? ( + { + setCallSummaryNotifications((prev) => !prev) + }} + className={classNames( + 'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full p-0.5 transition-colors duration-200 ease-out', + 'focus:outline-none focus:ring-2 focus:ring-primaryRing focus:ring-offset-2 focus:ring-offset-elevationL1 dark:focus:ring-primaryRingDark dark:focus:ring-offset-bgDark', + callSummaryNotifications + ? 'bg-productPrimaryNethlinkActive dark:bg-productPrimaryNethlinkActiveDark' + : 'bg-surfaceToggleBackgroundDisabled dark:bg-surfaceToggleBackgroundDisabledDark', + )} + > + + + ) : ( + + )} + + + {t( + callSummaryNotifications + ? 'Settings.Enabled' + : 'Settings.Disabled', + )} + + + + + + + {t('Common.Save')} + + + {t('Common.Cancel')} + + + + + + + > + ) +} 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 37b0e02b..07ecdf71 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 @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { z } from 'zod' import { Backdrop } from '../../Backdrop' -import { CustomThemedTooltip } from '@renderer/components/Nethesis/CurstomThemedTooltip' +import { CustomThemedTooltip } from '@renderer/components/Nethesis/CustomThemedTooltip' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faXmark } from '@fortawesome/free-solid-svg-icons' import { InlineNotification } from '@renderer/components/Nethesis/InlineNotification' diff --git a/src/renderer/src/components/Nethesis/CurstomThemedTooltip.tsx b/src/renderer/src/components/Nethesis/CurstomThemedTooltip.tsx deleted file mode 100644 index 14528659..00000000 --- a/src/renderer/src/components/Nethesis/CurstomThemedTooltip.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// src/components/CustomThemedTooltip.tsx -import React, { FC } from 'react' -import { useSelector } from 'react-redux' -import { Tooltip } from 'react-tooltip' -import { useSharedState } from '@renderer/store' - -interface CustomThemedTooltipProps { - id: string - place?: 'top' | 'right' | 'bottom' | 'left' - className?: string -} - -export const CustomThemedTooltip: FC = ({ - id, - place = 'bottom', - className = '', -}) => { - const [theme] = useSharedState('theme') - const tooltipStyle = - theme === 'dark' - ? { - backgroundColor: 'rgb(243, 244, 246)', - color: 'rgb(17, 24, 39)', - fontSize: '0.875rem', - fontWeight: 400, - lineHeight: '1.25rem', - padding: '0.375rem 0.625rem', - maxWidth: '185px', - borderRadius: '4px', - } - : { - backgroundColor: 'rgb(31, 41, 55)', - color: 'rgb(249, 250, 251)', - fontSize: '0.875rem', - fontWeight: 400, - lineHeight: '1.25rem', - padding: '0.375rem 0.625rem', - maxWidth: '185px', - borderRadius: '4px', - } - - return ( - - ) -} diff --git a/src/renderer/src/components/Nethesis/CustomThemedTooltip.tsx b/src/renderer/src/components/Nethesis/CustomThemedTooltip.tsx new file mode 100644 index 00000000..5508c6b0 --- /dev/null +++ b/src/renderer/src/components/Nethesis/CustomThemedTooltip.tsx @@ -0,0 +1,100 @@ +// src/components/CustomThemedTooltip.tsx +import React, { FC, useEffect, useState } from 'react' +import { Tooltip } from 'react-tooltip' +import { useSharedState } from '@renderer/store' +import { getSystemTheme, parseThemeToClassName } from '@renderer/utils' + +interface CustomThemedTooltipProps { + id: string + place?: 'top' | 'right' | 'bottom' | 'left' + className?: string +} + +export const CustomThemedTooltip: FC = ({ + id, + place = 'bottom', + className = '', +}) => { + const [theme] = useSharedState('theme') + const getResolvedTheme = () => { + const appContainer = document.getElementById('app-container') + + if (appContainer?.classList.contains('dark')) { + return 'dark' + } + + if (appContainer?.classList.contains('light')) { + return 'light' + } + + return theme ? parseThemeToClassName(theme) : getSystemTheme() + } + + const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>(() => getResolvedTheme()) + + useEffect(() => { + const updateResolvedTheme = () => { + setResolvedTheme(getResolvedTheme()) + } + + updateResolvedTheme() + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + const appContainer = document.getElementById('app-container') + const observer = appContainer + ? new MutationObserver(() => { + updateResolvedTheme() + }) + : null + + if (observer && appContainer) { + observer.observe(appContainer, { + attributes: true, + attributeFilter: ['class'], + }) + } + + mediaQuery.addEventListener('change', updateResolvedTheme) + + return () => { + observer?.disconnect() + mediaQuery.removeEventListener('change', updateResolvedTheme) + } + }, [theme]) + + const tooltipStyle = + resolvedTheme === 'dark' + ? { + backgroundColor: 'rgb(243, 244, 246)', + color: 'rgb(17, 24, 39)', + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.25rem', + padding: '0.375rem 0.625rem', + maxWidth: '320px', + borderRadius: '4px', + } + : { + backgroundColor: 'rgb(31, 41, 55)', + color: 'rgb(249, 250, 251)', + fontSize: '0.875rem', + fontWeight: 400, + lineHeight: '1.25rem', + padding: '0.375rem 0.625rem', + maxWidth: '320px', + borderRadius: '4px', + } + + return ( + + ) +} diff --git a/src/renderer/src/components/pageComponents/phoneIsland/phoneIslandContainer.tsx b/src/renderer/src/components/pageComponents/phoneIsland/phoneIslandContainer.tsx index 37061fa1..3731763f 100644 --- a/src/renderer/src/components/pageComponents/phoneIsland/phoneIslandContainer.tsx +++ b/src/renderer/src/components/pageComponents/phoneIsland/phoneIslandContainer.tsx @@ -21,7 +21,16 @@ export const PhoneIslandContainer = ({ dataConfig, deviceInformationObject, isDa } const PhoneIslandComponent = useMemo(() => { - return dataConfig && isDataConfigCreated && + return ( + dataConfig && + isDataConfigCreated && ( + + ) + ) }, [account?.username, dataConfig, isDataConfigCreated]) return PhoneIslandComponent diff --git a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts index 1b9c8c28..3e0d0887 100644 --- a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts +++ b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts @@ -6,9 +6,9 @@ import { PhoneIslandSizes, } from "@shared/types" import { Log } from "@shared/utils/logger" -import { useState, useRef, useCallback } from "react" +import { useState, useRef, useCallback, useEffect } from "react" import { t } from "i18next" -import { sendNotification } from "@renderer/utils" +import { sendNotification, sendSystemNotification } from "@renderer/utils" import { useSharedState } from "@renderer/store" // Track readiness state for both WebRTC and Socket @@ -63,6 +63,8 @@ export const usePhoneIslandEventListener = () => { const [account] = useSharedState('account') const [connected, setConnected] = useSharedState('connection') const [availableRingtones, setAvailableRingtones] = useSharedState('availableRingtones') + const notifiedSummaryIdsRef = useRef>(new Set()) + const watchedSummaryIdsRef = useRef>(new Set()) const [phoneIslandData, setPhoneIslandData] = useState({ activeAlerts: {}, @@ -76,6 +78,11 @@ export const usePhoneIslandEventListener = () => { }) const [phoneIsalndSizes, setPhoneIslandSizes] = useState(defaultSize) + useEffect(() => { + notifiedSummaryIdsRef.current.clear() + watchedSummaryIdsRef.current.clear() + }, [account?.username]) + const eventHandler = (event: PHONE_ISLAND_EVENTS, callback?: (data?: any) => void | Promise) => ({ [event]: (...data) => { @@ -211,7 +218,33 @@ export const usePhoneIslandEventListener = () => { ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-expand"]), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-expanded"]), - ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-conversations"]), + ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-conversations"], (data) => { + const username = account?.username + const conversations = username ? data?.[username]?.conversations : undefined + + if (!conversations) { + return + } + + let latestOutgoingConversation: any = null + Object.values(conversations).forEach((conversation: any) => { + if (!conversation?.connected || conversation?.direction !== 'out' || !conversation?.linkedId) { + return + } + + if (!latestOutgoingConversation) { + latestOutgoingConversation = conversation + return + } + + const latestStartTime = latestOutgoingConversation.startTime ?? 0 + const currentStartTime = conversation.startTime ?? 0 + if (currentStartTime > latestStartTime) { + latestOutgoingConversation = conversation + } + }) + + }), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-default-device-change"]), ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-default-device-changed"]), @@ -334,6 +367,80 @@ export const usePhoneIslandEventListener = () => { ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-ringing-tone-output-changed"], (data) => { Log.info('Phone-island confirmed output device changed:', data?.deviceId) }), + ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-summary-not-ready"], (data) => { + const linkedid = data?.linkedid + const isSummaryEnabled = account?.data?.call_summary_enabled === true + const isSummaryNotificationEnabled = + account?.data?.settings?.call_summary_notifications !== false + + if (!linkedid) { + return + } + + if (!account) { + return + } + + if (!isSummaryEnabled) { + return + } + + if (!isSummaryNotificationEnabled) { + return + } + + if (watchedSummaryIdsRef.current.has(linkedid)) { + return + } + + watchedSummaryIdsRef.current.add(linkedid) + window.dispatchEvent(new CustomEvent('phone-island-call-summary-notify', { + detail: { linkedid } + })) + }), + ...eventHandler(PHONE_ISLAND_EVENTS["phone-island-summary-ready"], (data) => { + const linkedid = data?.linkedid + const displayName = data?.display_name?.trim?.() || '' + const displayNumber = data?.display_number?.trim?.() || '' + const isSummaryEnabled = account?.data?.call_summary_enabled === true + const isSummaryNotificationEnabled = + account?.data?.settings?.call_summary_notifications !== false + + if (!linkedid) { + return + } + + if (!account) { + return + } + + if (!isSummaryEnabled) { + return + } + + if (!isSummaryNotificationEnabled) { + return + } + + if (notifiedSummaryIdsRef.current.has(linkedid)) { + return + } + + notifiedSummaryIdsRef.current.add(linkedid) + watchedSummaryIdsRef.current.delete(linkedid) + + const contact = displayName || displayNumber + + const notificationBody = contact + ? t('Notification.call_summary_ready_body_with_contact', { contact }) + : t('Notification.call_summary_ready_body') + + sendSystemNotification( + t('Notification.call_summary_ready_title'), + notificationBody, + `/history?section=Calls&summaryLinkedid=${encodeURIComponent(linkedid)}`, + ) + }), } } } diff --git a/src/renderer/src/store.ts b/src/renderer/src/store.ts index ca086699..de47eece 100644 --- a/src/renderer/src/store.ts +++ b/src/renderer/src/store.ts @@ -145,6 +145,7 @@ export const useNethlinkData = createGlobalStateHook({ isCommandBarShortcutDialogOpen: false, isDeviceDialogOpen: false, isIncomingCallsDialogOpen: false, + isNotificationsDialogOpen: false, phonebookModule: { selectedContact: undefined }, @@ -168,4 +169,3 @@ export const useLoginPageData = createGlobalStateHook({ showTwoFactor: false } as LoginPageData).useGlobalState - diff --git a/src/renderer/src/utils/utils.ts b/src/renderer/src/utils/utils.ts index 165a785a..ffe260bd 100644 --- a/src/renderer/src/utils/utils.ts +++ b/src/renderer/src/utils/utils.ts @@ -40,6 +40,18 @@ export async function sendNotification(title: string, body: string, openUrl?: st } } +export function sendSystemNotification(title: string, body: string, openUrl?: string) { + window.api.sendNotification( + title, + { + body, + icon: "./icons/Nethlink-logo.svg", + silent: false, + }, + openUrl, + ) +} + export const ClassNames = (...args: ClassValue[]) => { return twMerge(clsx(...args)) } diff --git a/src/shared/constants.ts b/src/shared/constants.ts index 734baf03..0d8d372f 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -103,6 +103,7 @@ export enum IPC_EVENTS { PLAY_RINGTONE_PREVIEW = "PLAY_RINGTONE_PREVIEW", STOP_RINGTONE_PREVIEW = "STOP_RINGTONE_PREVIEW", AUDIO_PLAYER_CLOSED = "AUDIO_PLAYER_CLOSED", + SEND_NOTIFICATION = "SEND_NOTIFICATION", TOGGLE_COMMAND_BAR = "TOGGLE_COMMAND_BAR", SHOW_COMMAND_BAR = "SHOW_COMMAND_BAR", HIDE_COMMAND_BAR = "HIDE_COMMAND_BAR", @@ -254,4 +255,6 @@ export enum PHONE_ISLAND_EVENTS { 'phone-island-ringing-tone-selected' = 'phone-island-ringing-tone-selected', 'phone-island-ringing-tone-output' = 'phone-island-ringing-tone-output', 'phone-island-ringing-tone-output-changed' = 'phone-island-ringing-tone-output-changed', + 'phone-island-summary-not-ready' = 'phone-island-summary-not-ready', + 'phone-island-summary-ready' = 'phone-island-summary-ready', } diff --git a/src/shared/types.ts b/src/shared/types.ts index c4005f3b..e0225311 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -100,6 +100,9 @@ export type AccountData = BaseAccountData & { outbound_routes_permissions: OutboundRoutePermission[] } default_device: Extension + call_summary_enabled?: boolean + call_transcription_enabled?: boolean + voicemail_transcription_enabled?: boolean settings: UserSettings, mainextension?: string } @@ -127,9 +130,10 @@ export type OutboundRoutePermission = { permission: boolean } export type UserSettings = { - desktop_notifications: true + desktop_notifications: boolean open_ccard: 'enabled' | 'disabled' - chat_notifications: true + chat_notifications: boolean + call_summary_notifications?: boolean avatar?: string } @@ -455,6 +459,7 @@ export type NethLinkPageData = { isCommandBarShortcutDialogOpen?: boolean, isDeviceDialogOpen?: boolean, isIncomingCallsDialogOpen?: boolean, + isNotificationsDialogOpen?: boolean, showAddContactModule?: boolean, speeddialsModule?: SpeedDialModuleData phonebookSearchModule?: PhonebookSearchModuleData diff --git a/src/shared/useNethVoiceAPI.ts b/src/shared/useNethVoiceAPI.ts index e0f4bb26..8bcbcb3b 100644 --- a/src/shared/useNethVoiceAPI.ts +++ b/src/shared/useNethVoiceAPI.ts @@ -585,6 +585,8 @@ export const useNethVoiceAPI = (loggedAccount: Account | undefined = undefined) all_avatars: async () => await _GET(buildApiPath('/user/all_avatars')), all_endpoints: async () => await _GET(buildApiPath('/user/endpoints/all')), heartbeat: async (extension: string, username: string) => await _POST(buildApiPath('/user/nethlink'), { extension, username }), + settings: async (settings: Partial) => + await _POST(buildApiPath('/user/settings'), settings), default_device: async (deviceIdInformation: Extension, force = false): Promise => { try { if (account?.data?.default_device.type !== 'physical' || force) { diff --git a/tailwind.config.js b/tailwind.config.js index a5387b62..e3aa27e4 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,6 +9,23 @@ export default { borderDark: '#374151', //gray-700 borderLight: '#E5E7EB', //gray-200 + //design system surfaces + elevationL1: '#F9FAFB', // gray-50 + + //design system text + textPrimaryNeutral: '#111827', // gray-900 + textPrimaryNeutralDark: '#F9FAFB', // gray-50 + textSecondaryNeutral: '#374151', // gray-700 + textSecondaryNeutralDark: '#E5E7EB', // gray-200 + + //design system product + productPrimaryNethlinkActive: '#1d4ed8', // blue-700 + productPrimaryNethlinkActiveDark: '#3b82f6', // blue-500 + + //design system surfaces + surfaceToggleBackgroundDisabled: '#9CA3AF', // gray-400 + surfaceToggleBackgroundDisabledDark: '#4B5563', // gray-600 + //primary primary: '#1d4ed8', // blue-700 primaryHover: '#1e40af', // blue-800 @@ -37,6 +54,8 @@ export default { //amber icon iconAmberLight: '#b45309', //amber-700 iconAmberDark: '#fef3c7', // amber-100 + iconTooltip: '#6366F1', // indigo-500 + iconTooltipDark: '#A5B4FC', // indigo-300 //buttonPrimary primaryButtonText: '#fff', // white @@ -112,6 +131,11 @@ export default { '6xl': '3072px', '7xl': '3584px', }, + boxShadow: { + lightXs: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)', + lightXl: + '0px 20px 25px -5px rgba(0, 0, 0, 0.10), 0px 8px 10px -6px rgba(0, 0, 0, 0.10)', + }, fontFamily: { Poppins: ['Poppins', 'sans-serif'], },
{t('Settings.NotificationsDescription')}
{t('Settings.NotificationsScopeDescription')}