Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -2557,6 +2557,7 @@
"Incoming_Livechats": "Queued chats",
"Incoming_WebHook": "Incoming WebHook",
"Incoming_call": "Incoming call",
"Incoming_call_ellipsis": "Incoming call...",
"Incoming_call_from": "Incoming call from",
"Incoming_call_from__roomName__": "Incoming call from {{roomName}}",
"Incoming_call_transfer": "Incoming call transfer",
Expand Down
1 change: 1 addition & 0 deletions packages/ui-voip/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"dependencies": {
"@rocket.chat/desktop-api": "workspace:^",
"@rocket.chat/emitter": "~0.31.25",
"@rocket.chat/media-signaling": "workspace:~",
"@tanstack/react-query": "~5.65.1",
Expand Down
7 changes: 7 additions & 0 deletions packages/ui-voip/src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { IRocketChatDesktop } from '@rocket.chat/desktop-api';

declare global {
interface Window {
RocketChatDesktop?: IRocketChatDesktop;
}
}
3 changes: 3 additions & 0 deletions packages/ui-voip/src/v2/MediaCallProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import MediaCallContext, { PeerInfo } from './MediaCallContext';
import MediaCallWidget from './MediaCallWidget';
import TransferModal from './TransferModal';
import { useCallSounds } from './useCallSounds';
import { useDesktopNotifications } from './useDesktopNotifications';
import { getExtensionFromPeerInfo, useMediaSession } from './useMediaSession';
import { useMediaSessionInstance } from './useMediaSessionInstance';
import useMediaStream from './useMediaStream';
Expand All @@ -37,6 +38,8 @@ const MediaCallProvider = ({ children }: { children: React.ReactNode }) => {
const instance = useMediaSessionInstance(userId ?? undefined);
const session = useMediaSession(instance);

useDesktopNotifications(session);

const [remoteStreamRefCallback, audioElement] = useMediaStream(instance);

const setOutputMediaDevice = useSetOutputMediaDevice();
Expand Down
75 changes: 75 additions & 0 deletions packages/ui-voip/src/v2/useDesktopNotifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';

import { PeerInfo } from './MediaCallContext';
import { SessionInfo } from './useMediaSessionInstance';
import { convertAvatarUrlToPng } from './utils/convertAvatarUrlToPng';

const getDisplayInfo = (peerInfo?: PeerInfo) => {
if (!peerInfo) {
return undefined;
}

if ('number' in peerInfo) {
return { title: peerInfo.number };
}
if ('displayName' in peerInfo) {
return { title: peerInfo.displayName, avatar: peerInfo.avatarUrl };
}
return undefined;
};

export const useDesktopNotifications = (sessionInfo: SessionInfo) => {
const previousCallId = useRef<string | undefined>(undefined);
const { t } = useTranslation();

const displayInfo = getDisplayInfo(sessionInfo.peerInfo);
useEffect(() => {
if (
typeof window.RocketChatDesktop?.dispatchCustomNotification !== 'function' ||
typeof window.RocketChatDesktop?.closeCustomNotification !== 'function'
) {
return;
}

let isMounted = true;

if (sessionInfo.state !== 'ringing') {
if (previousCallId.current) {
window.RocketChatDesktop.closeCustomNotification(previousCallId.current);
previousCallId.current = undefined;
}
return;
}

if (!displayInfo?.title) {
return;
}

const notifyDesktop = async () => {
const avatarAsPng = await convertAvatarUrlToPng(displayInfo.avatar);

if (!isMounted) {
return;
}

window.RocketChatDesktop?.dispatchCustomNotification({
type: 'voice',
id: sessionInfo.callId,
payload: {
title: displayInfo.title,
body: t('Incoming_call_ellipsis'),
avatar: avatarAsPng || undefined,
requireInteraction: true,
},
});
};

notifyDesktop();
previousCallId.current = sessionInfo.callId;

return () => {
isMounted = false;
};
}, [displayInfo?.avatar, displayInfo?.title, sessionInfo.callId, sessionInfo.state, t]);
};
12 changes: 8 additions & 4 deletions packages/ui-voip/src/v2/useMediaSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { SessionInfo } from './useMediaSessionInstance';

const defaultSessionInfo: SessionInfo = {
state: 'closed' as const,
callId: undefined,
connectionState: 'CONNECTING' as const,
peerInfo: undefined,
transferredBy: undefined,
Expand Down Expand Up @@ -46,7 +47,10 @@ export const getExtensionFromPeerInfo = (peerInfo: PeerInfo): string | undefined
return undefined;
};

const deriveWidgetStateFromCallState = (callState: CallState, callRole: CallRole): State | undefined => {
const deriveWidgetStateFromCallState = (
callState: CallState,
callRole: CallRole,
): Extract<State, 'ongoing' | 'ringing' | 'calling'> | undefined => {
switch (callState) {
case 'active':
case 'accepted':
Expand Down Expand Up @@ -141,7 +145,7 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSession
return;
}

const { contact, transferredBy: callTransferredBy, state: callState, role, muted, held, hidden } = mainCall;
const { contact, transferredBy: callTransferredBy, state: callState, role, muted, held, hidden, callId } = mainCall;
const state = deriveWidgetStateFromCallState(callState, role);
const connectionState = deriveConnectionStateFromCallState(callState);

Expand All @@ -150,7 +154,7 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSession
if (contact.type === 'sip') {
dispatch({
type: 'instance_updated',
payload: { peerInfo: { number: contact.id || 'unknown' }, transferredBy, state, muted, held, connectionState, hidden },
payload: { peerInfo: { number: contact.id || 'unknown' }, transferredBy, state, muted, held, connectionState, hidden, callId },
});
return;
}
Expand All @@ -175,7 +179,7 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSession
callerId: contact.sipExtension,
} as PeerInfo;

dispatch({ type: 'instance_updated', payload: { state, peerInfo, transferredBy, muted, held, connectionState, hidden } });
dispatch({ type: 'instance_updated', payload: { state, peerInfo, transferredBy, muted, held, connectionState, hidden, callId } });
};

const offCbs = [instance.on('sessionStateChange', updateSessionState), instance.on('hiddenCall', updateSessionState)];
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-voip/src/v2/useMediaSessionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ interface BaseSession {

interface EmptySession extends BaseSession {
state: Extract<State, 'closed' | 'new'>;
callId: undefined;
}

interface CallSession extends BaseSession {
state: Extract<State, 'calling' | 'ringing' | 'ongoing'>;
callId: string;
peerInfo: PeerInfo;
}

Expand Down
36 changes: 36 additions & 0 deletions packages/ui-voip/src/v2/utils/convertAvatarUrlToPng.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const convertAvatarUrlToPng = (avatarUrl: string | undefined): Promise<string> => {
return new Promise((resolve) => {
if (!avatarUrl) {
resolve('');
return;
}

const image = new Image();

const onLoad = (): void => {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const context = canvas.getContext('2d');

if (!context) {
resolve('');
return;
}

context.drawImage(image, 0, 0);
try {
resolve(canvas.toDataURL('image/png'));
} catch (e) {
resolve('');
}
};

const onError = (): void => resolve('');

image.onload = onLoad;
image.onerror = onError;
image.crossOrigin = 'anonymous';
image.src = avatarUrl;
});
};
3 changes: 2 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8474,7 +8474,7 @@ __metadata:
languageName: unknown
linkType: soft

"@rocket.chat/desktop-api@workspace:packages/desktop-api, @rocket.chat/desktop-api@workspace:~":
"@rocket.chat/desktop-api@workspace:^, @rocket.chat/desktop-api@workspace:packages/desktop-api, @rocket.chat/desktop-api@workspace:~":
version: 0.0.0-use.local
resolution: "@rocket.chat/desktop-api@workspace:packages/desktop-api"
dependencies:
Expand Down Expand Up @@ -10601,6 +10601,7 @@ __metadata:
"@playwright/test": "npm:^1.52.0"
"@react-spectrum/test-utils": "npm:~1.0.0-alpha.8"
"@rocket.chat/css-in-js": "npm:~0.31.25"
"@rocket.chat/desktop-api": "workspace:^"
"@rocket.chat/emitter": "npm:~0.31.25"
"@rocket.chat/eslint-config": "workspace:^"
"@rocket.chat/fuselage": "npm:^0.68.1"
Expand Down
Loading