diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index d8b17718..887e2303 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 7e5bf0d9..bd7a0079 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 ac720809..88fa8585 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -136,8 +136,8 @@ 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) => { diff --git a/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx b/src/renderer/src/components/Modules/NethVoice/BaseModule/ContactNameAndAction.tsx index 343858fd..6e2925fc 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,12 @@ 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 +225,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/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 01dd4ccb..81af4785 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -70,9 +70,11 @@ export function PhoneIslandPage() { } }) - window.electron.receive(IPC_EVENTS.START_CALL, (number: string) => { + 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 } : {}), }) }) diff --git a/src/shared/utils/utils.ts b/src/shared/utils/utils.ts index d22875b5..145755c9 100644 --- a/src/shared/utils/utils.ts +++ b/src/shared/utils/utils.ts @@ -60,3 +60,16 @@ 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 || '') +}