Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions ee/packages/media-calls/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { CallFeature } from '@rocket.chat/media-signaling';

export const DEFAULT_CALL_FEATURES: CallFeature[] = ['audio', 'transfer', 'hold'];
export const SIP_CALL_FEATURES = DEFAULT_CALL_FEATURES;
2 changes: 1 addition & 1 deletion ee/packages/media-calls/src/definition/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type InternalCallParams = {
requestedService?: CallService;
parentCallId?: string;
requestedBy?: MediaCallSignedContact;
features?: CallFeature[];
features: CallFeature[];
};

export type MediaCallHeader = AtLeast<IMediaCall, '_id' | 'caller' | 'callee'>;
Expand Down
3 changes: 2 additions & 1 deletion ee/packages/media-calls/src/internal/SignalProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
} from '@rocket.chat/media-signaling';
import { MediaCalls } from '@rocket.chat/models';

import { DEFAULT_CALL_FEATURES } from '../constants';
import type { InternalCallParams } from '../definition/common';
import { logger } from '../logger';
import { mediaCallDirector } from '../server/CallDirector';
Expand Down Expand Up @@ -181,7 +182,7 @@ export class GlobalSignalProcessor {

const services = signal.supportedServices ?? [];
const requestedService = services.includes('webrtc') ? 'webrtc' : services[0];
const features = signal.supportedFeatures ?? ['audio'];
const features = signal.supportedFeatures ?? DEFAULT_CALL_FEATURES;

const params: InternalCallParams = {
caller: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
} from '@rocket.chat/media-signaling';
import { MediaCallChannels, MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

import { DEFAULT_CALL_FEATURES } from '../../constants';
import type { IMediaCallAgent } from '../../definition/IMediaCallAgent';
import { logger } from '../../logger';
import { mediaCallDirector } from '../../server/CallDirector';
Expand Down Expand Up @@ -137,7 +138,7 @@ export class UserActorSignalProcessor {
case 'ack':
return this.clientIsReachable();
case 'accept':
return this.clientHasAccepted(signal.supportedFeatures || ['audio']);
return this.clientHasAccepted(signal.supportedFeatures || DEFAULT_CALL_FEATURES);
case 'unavailable':
return this.clientIsUnavailable();
case 'reject':
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/media-calls/src/internal/agents/UserActorAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

import { UserActorSignalProcessor } from './CallSignalProcessor';
import { BaseMediaCallAgent } from '../../base/BaseAgent';
import { DEFAULT_CALL_FEATURES } from '../../constants';
import { logger } from '../../logger';
import { buildNewCallSignal } from '../../server/buildNewCallSignal';
import { getMediaCallServer } from '../../server/injection';
Expand Down Expand Up @@ -151,6 +152,7 @@ export class UserActorAgent extends BaseMediaCallAgent {
requestedService: call.service,
requestedBy: call.transferredBy,
parentCallId: call._id,
features: DEFAULT_CALL_FEATURES,
});
}

Expand Down
5 changes: 3 additions & 2 deletions ee/packages/media-calls/src/server/CallDirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { InsertionModel } from '@rocket.chat/model-typings';
import { MediaCallNegotiations, MediaCalls } from '@rocket.chat/models';

import { getCastDirector, getMediaCallServer } from './injection';
import { DEFAULT_CALL_FEATURES } from '../constants';
import type { IMediaCallAgent } from '../definition/IMediaCallAgent';
import type { IMediaCallCastDirector } from '../definition/IMediaCallCastDirector';
import type { InternalCallParams, MediaCallHeader } from '../definition/common';
Expand Down Expand Up @@ -79,7 +80,7 @@ class MediaCallDirector {
this.scheduleExpirationCheckByCallId(call._id);

const updatedCall = await MediaCalls.findOneById(call._id, { projection: { features: 1 } });
const features = (updatedCall?.features || ['audio']) as CallFeature[];
const features = (updatedCall?.features || DEFAULT_CALL_FEATURES) as CallFeature[];

await calleeAgent.onCallAccepted(call._id, { signedContractId: data.calleeContractId, features });
await calleeAgent.oppositeAgent?.onCallAccepted(call._id, { signedContractId: call.caller.contractId, features });
Expand Down Expand Up @@ -186,7 +187,7 @@ class MediaCallDirector {
calleeAgent,
parentCallId,
requestedBy,
features = ['audio'],
features = DEFAULT_CALL_FEATURES,
} = params;

// The caller must always have a contract to create the call
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/media-calls/src/sip/providers/IncomingSipCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { SipMessage, SrfRequest, SrfResponse } from 'drachtio-srf';
import type Srf from 'drachtio-srf';

import { BaseSipCall } from './BaseSipCall';
import { SIP_CALL_FEATURES } from '../../constants';
import { logger } from '../../logger';
import { BroadcastActorAgent } from '../../server/BroadcastAgent';
import { mediaCallDirector } from '../../server/CallDirector';
Expand Down Expand Up @@ -103,6 +104,7 @@ export class IncomingSipCall extends BaseSipCall {
callee,
callerAgent,
calleeAgent,
features: SIP_CALL_FEATURES,
});

const negotiationId = await mediaCallDirector.startNewNegotiation(call, 'caller', webrtcOffer);
Expand Down
5 changes: 3 additions & 2 deletions ee/packages/media-calls/src/sip/providers/OutgoingSipCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type Srf from 'drachtio-srf';
import type { SrfRequest, SrfResponse } from 'drachtio-srf';

import { BaseSipCall } from './BaseSipCall';
import { SIP_CALL_FEATURES } from '../../constants';
import type { InternalCallParams } from '../../definition/common';
import { logger } from '../../logger';
import { BroadcastActorAgent } from '../../server/BroadcastAgent';
Expand Down Expand Up @@ -72,7 +73,7 @@ export class OutgoingSipCall extends BaseSipCall {
callee: signedCallee,
calleeAgent,
callerAgent,
features: ['audio'],
features: SIP_CALL_FEATURES,
});

const channel = await calleeAgent.getOrCreateChannel(call, session.sessionId);
Expand Down Expand Up @@ -267,7 +268,7 @@ export class OutgoingSipCall extends BaseSipCall {
await mediaCallDirector.acceptCall(call, this.agent, {
calleeContractId: this.session.sessionId,
webrtcAnswer: { type: 'answer', sdp: this.sipDialog.remote.sdp },
supportedFeatures: ['audio'],
supportedFeatures: SIP_CALL_FEATURES,
});
}

Expand Down
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 @@ -989,6 +989,7 @@
"Call_ended_bold": "*Voice call ended*",
"Call_not_answered_bold": "*Voice call not answered*",
"Call_failed_bold": "*Voice call failed*",
"Call_feature_unsupported": "Other party doesn't support this",
"Call_transferred_bold": "*Voice call transferred*",
"Call_history": "Call history",
"Call_history_provides_a_record_of_when_calls_took_place_and_who_joined": "Call history provides a record of when calls took place and who joined.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export type CallRole = 'caller' | 'callee';

export type CallService = 'webrtc';

export const callFeatureList = ['audio'] as const;
export const callFeatureList = ['audio', 'transfer', 'hold'] as const;

export type CallFeature = (typeof callFeatureList)[number];

Expand Down Expand Up @@ -80,6 +80,7 @@ export interface IClientMediaCall {
role: CallRole;
service: CallService | null;
flags: readonly CallFlag[];
features: readonly CallFeature[];

state: CallState;
ignored: boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/media-signaling/src/lib/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ export class ClientMediaCall implements IClientMediaCall {
return this._flags;
}

public get features(): CallFeature[] {
return [...(this.enabledFeatures || [])];
}

constructor(
private readonly config: IClientMediaCallConfig,
callId: string,
Expand Down
6 changes: 3 additions & 3 deletions packages/ui-voip/src/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ type ActionButtonProps = {
icon: Keys;
disabled?: boolean;
onClick?: () => void;
} & Omit<ComponentProps<typeof IconButton>, 'icon' | 'title' | 'aria-label' | 'disabled' | 'onClick'>;
} & Omit<ComponentProps<typeof IconButton>, 'icon' | 'aria-label' | 'disabled' | 'onClick'>;

const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(function ActionButton(
{ disabled, label, icon, onClick, secondary = true, ...props },
{ disabled, label, icon, onClick, title, secondary = true, ...props },
ref,
) {
return (
Expand All @@ -20,7 +20,7 @@ const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(function A
medium
secondary={secondary}
icon={<Icon size={16} name={icon} />}
title={label}
title={title || label}
aria-label={label}
disabled={disabled}
onClick={onClick}
Expand Down
1 change: 1 addition & 0 deletions packages/ui-voip/src/context/MediaCallViewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const defaultSessionState: SessionState = {
remoteMuted: false,
remoteHeld: false,
callId: undefined,
supportedFeatures: ['audio', 'transfer', 'hold'],
};

export const defaultMediaCallContextValue: MediaCallViewContextValue = {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-voip/src/context/definitions.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UserStatus } from '@rocket.chat/core-typings';
import type { CallFeature } from '@rocket.chat/media-signaling';

export type InternalPeerInfo = {
displayName: string;
Expand Down Expand Up @@ -30,6 +31,7 @@ interface IBaseSession {
remoteHeld: boolean;
startedAt?: Date | null; // todo not sure if I need this
hidden: boolean;
supportedFeatures: readonly CallFeature[];
}

interface IEmptySession extends IBaseSession {
Expand Down
17 changes: 16 additions & 1 deletion packages/ui-voip/src/providers/useMediaSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const defaultSessionInfo: SessionState = {
remoteHeld: false,
startedAt: new Date(),
hidden: false,
supportedFeatures: ['audio', 'transfer', 'hold'],
};

export const getExtensionFromInstanceContact = (contact: CallContact): string | undefined => {
Expand Down Expand Up @@ -135,6 +136,7 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS
remoteHeld,
remoteMute,
callId,
features: supportedFeatures,
} = mainCall;
const state = deriveWidgetStateFromCallState(callState, role);

Expand All @@ -161,6 +163,7 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS
remoteHeld,
remoteMuted: remoteMute,
callId,
supportedFeatures,
},
});
return;
Expand All @@ -182,7 +185,19 @@ export const useMediaSession = (instance?: MediaSignalingSession): MediaSessionS

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

Expand Down
2 changes: 1 addition & 1 deletion packages/ui-voip/src/providers/useMediaSessionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class MediaSessionStore extends Emitter<{ change: void }> {
randomStringFactory,
oldSessionId: this.getOldSessionId(userId),
logger: new MediaCallLogger(),
features: ['audio'],
features: ['audio', 'transfer', 'hold'],
});

if (window.sessionStorage) {
Expand Down
16 changes: 13 additions & 3 deletions packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const OngoingCall = () => {
const { t } = useTranslation();

const { sessionState, onMute, onHold, onForward, onEndCall, onTone, onClickDirectMessage } = useMediaCallView();
const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState } = sessionState;
const { muted, held, remoteMuted, remoteHeld, peerInfo, connectionState, supportedFeatures } = sessionState;

const { element: keypad, buttonProps: keypadButtonProps } = useKeypad(onTone);

Expand All @@ -32,6 +32,9 @@ const OngoingCall = () => {
const connecting = connectionState === 'CONNECTING';
const reconnecting = connectionState === 'RECONNECTING';

const transferDisabled = !supportedFeatures.includes('transfer');
const holdDisabled = !supportedFeatures.includes('hold');

// TODO: Figure out how to ensure this always exist before rendering the component
if (!peerInfo) {
throw new Error('Peer info is required');
Expand All @@ -58,11 +61,18 @@ const OngoingCall = () => {
<ToggleButton
label={t('Hold')}
icons={['pause-shape-unfilled', 'pause-shape-unfilled']}
titles={[t('Hold'), t('Resume')]}
titles={[holdDisabled ? t('Call_feature_unsupported') : t('Hold'), t('Resume')]}
pressed={held}
onToggle={onHold}
disabled={connecting || reconnecting || holdDisabled}
/>
<ActionButton
disabled={connecting || reconnecting || transferDisabled}
label={t('Forward')}
icon='arrow-forward'
title={transferDisabled ? t('Call_feature_unsupported') : t('Forward')}
onClick={onForward}
/>
<ActionButton disabled={connecting || reconnecting} label={t('Forward')} icon='arrow-forward' onClick={onForward} />
<ActionButton
label={t('Voice_call__user__hangup', { user: 'userId' in peerInfo ? peerInfo.displayName : peerInfo.number })}
icon='phone-off'
Expand Down
Loading