diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 8ecbd30d..df50f8b2 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -302,6 +302,7 @@ "Attend transfer": "Attend transfer", "Call": "Call", "Open user devices": "Open user devices", + "Video call": "Video call", "Busy": "Busy", "Ringing": "Ringing" }, diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index 75e1efd7..5511681b 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -302,6 +302,7 @@ "Attend transfer": "Trasferimento consultativo", "Call": "Chiama", "Open user devices": "Apri dispositivi utente", + "Video call": "Videochiamata", "Busy": "Occupato", "Ringing": "Ringing" }, diff --git a/src/main/classes/controllers/PhoneIslandController.ts b/src/main/classes/controllers/PhoneIslandController.ts index 23332124..28947f9d 100644 --- a/src/main/classes/controllers/PhoneIslandController.ts +++ b/src/main/classes/controllers/PhoneIslandController.ts @@ -314,9 +314,9 @@ export class PhoneIslandController { }) } - call(number: string) { + call(number: string, video: boolean = false) { try { - this.window.emit(IPC_EVENTS.START_CALL, number) + this.window.emit(IPC_EVENTS.START_CALL, number, video) } catch (e) { Log.error(`Unable to call ${number}`) } diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index b25ffab9..e69cedc5 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -137,9 +137,12 @@ export function registerIpcEvents() { return app.getLocale() }) - ipcMain.on(IPC_EVENTS.EMIT_START_CALL, async (_event, phoneNumber) => { - PhoneIslandController.instance.call(phoneNumber) - }) + ipcMain.on( + IPC_EVENTS.EMIT_START_CALL, + async (_event, phoneNumber, video?: boolean) => { + PhoneIslandController.instance.call(phoneNumber, video) + }, + ) ipcMain.on(IPC_EVENTS.START_CALL_BY_URL, async (_event, url) => { function triggerError(e, request: ClientRequest | undefined = undefined) { diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx index 343858fd..c47f087c 100644 --- a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx +++ b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx @@ -1,16 +1,17 @@ import { NumberCaller } from '../../../NumberCaller' import { ContactType } from '@shared/types' -import { debouncer, isDev } from '@shared/utils/utils' +import { debouncer, isDev, isVideoCapableExtension } from '@shared/utils/utils' import { FavouriteStar } from './FavouritesStar' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useAccount } from '@renderer/hooks/useAccount' -import { useNethlinkData } from '@renderer/store' +import { useNethlinkData, useSharedState } from '@renderer/store' import { usePhoneIslandEventHandler } from '@renderer/hooks/usePhoneIslandEventHandler' import { useIsTruncated } from '@renderer/hooks/useIsTruncated' import { Avatar } from '../../../Nethesis' import { CustomThemedTooltip } from '../../../Nethesis/CustomThemedTooltip' import { faPhone as CallIcon, + faVideo as VideoCallIcon, faBriefcase as CompanyIcon, } from '@fortawesome/free-solid-svg-icons' import { t } from 'i18next' @@ -56,6 +57,16 @@ export const ContactNameAndActions = ({ operatorPresence === 'online' || operatorPresence === 'available' const isCallDisabled = !isCallsEnabled || !isOperatorOnline + const [account] = useSharedState('account') + // Video calls start from the NethLink WebRTC device and need a video-capable device on the other side + const canVideoCall = + isOperator && + ['nethlink', 'webrtc'].includes( + account?.data?.default_device?.type || '', + ) && + (operators?.operators?.[username!]?.endpoints?.extension || []).some( + isVideoCapableExtension, + ) const isDisplayedNumberEmpty = displayedNumber === null || @@ -218,6 +229,28 @@ export const ContactNameAndActions = ({ }} /> )} + {!isHighlight && canVideoCall && ( + { + if (isCallDisabled) return + debouncer( + 'onVideoCallNumber', + () => { + callNumber(contact.speeddial_num!, { video: true }) + }, + 250, + ) + }} + /> + )}
{ ) const callNumber = useCallback( - (number: string) => { + (number: string, options: { video?: boolean } = {}) => { const numberCleanerRegex = /\s+/g const cleanNumber = `${number}`.replace(numberCleanerRegex, '') const isNumberValid = validatePhoneNumber(cleanNumber) if (number && isNumberValid && isCallsEnabled) - window.electron.send(IPC_EVENTS.EMIT_START_CALL, cleanNumber) + window.electron.send( + IPC_EVENTS.EMIT_START_CALL, + cleanNumber, + Boolean(options.video), + ) else Log.warning('unable to call', { number, isCallsEnabled, isNumberValid }) }, diff --git a/src/renderer/src/pages/LoginPage.tsx b/src/renderer/src/pages/LoginPage.tsx index ffd2b23d..635af6bf 100644 --- a/src/renderer/src/pages/LoginPage.tsx +++ b/src/renderer/src/pages/LoginPage.tsx @@ -28,7 +28,7 @@ export interface LoginPageProps { enum LoginSizes { BASE = 550, HOST_STEP = 400, - SSO_STEP = 400, + SSO_STEP = HOST_STEP, ACCOUNT_FORM = 488, TWO_FACTOR_AUTH = 420, BACK_BUTTON = 60, diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 01dd4ccb..0a3e4c3b 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -70,11 +70,16 @@ export function PhoneIslandPage() { } }) - window.electron.receive(IPC_EVENTS.START_CALL, (number: string) => { - eventDispatch(PHONE_ISLAND_EVENTS['phone-island-call-start'], { - number, - }) - }) + window.electron.receive( + IPC_EVENTS.START_CALL, + (number: string, video?: boolean) => { + // video: true offers the camera in the first INVITE (desk video phones refuse a later upgrade) + eventDispatch(PHONE_ISLAND_EVENTS['phone-island-call-start'], { + number, + ...(video ? { video: true } : {}), + }) + }, + ) window.electron.receive(IPC_EVENTS.END_CALL, () => { eventDispatch(PHONE_ISLAND_EVENTS['phone-island-call-end']) diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index d22875b5..dd23ff53 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -60,3 +60,24 @@ export const isDevTools = getBoolEnvParam('DEVTOOLS') export const getAccountUID = (a: Account) => { return `${a.host}@${a.username}` } + +// Desk phones known to handle video calls, matched on the registered User-Agent +const VIDEO_PHONE_USER_AGENTS = + /Yealink SIP-(T58|VP59)|Fanvil (V6|X7|i[0-9])|Nethesis NP-V|Akuvox|Grandstream GXV/i + +/** + * True when the extension can receive a video call: WebRTC, NethLink and the mobile app always, + * physical phones only for known video models + */ +export function isVideoCapableExtension(extension?: { + type?: string + description?: string +}) { + if (!extension) return false + if (['webrtc', 'nethlink', 'mobile'].includes(extension.type || '')) + return true + return ( + extension.type === 'physical' && + VIDEO_PHONE_USER_AGENTS.test(extension.description || '') + ) +}