Skip to content
Open
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
22 changes: 22 additions & 0 deletions .changeset/fix-conference-call-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@rocket.chat/meteor': patch
---

fix(video-conference): ring notification for group calls and channels not shown

Group video conference calls (channels and group DMs) sent a `ring` action to room members
but the client's VideoConfManager never handled it — no incoming call popup or ringtone
was shown to any member regardless of license tier.

Additionally, accepting a group call ring incorrectly triggered the 1-1 direct-call
accept/confirm handshake, causing a 5-second timeout error for anyone who clicked Accept.

Fixes:
- Add `ring` case in `VideoConfManager.onVideoConfNotification` delegating to new
`onGroupCallRing` method which marks the call with `isGroupCall: true`
- In `acceptIncomingCall`, group calls skip the accepted→confirmed signalling and
call `joinCall` directly
- Register a CE video conference type handler (priority 0) that enables `ringing: true`
for non-livechat rooms with ≤10 members, so CE installs also dispatch ring notifications

Closes #34910
29 changes: 28 additions & 1 deletion apps/meteor/client/lib/VideoConfManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const ACCEPT_TIMEOUT = 5000;
type IncomingDirectCall = DirectCallParams & {
timeout: ReturnType<typeof setTimeout> | undefined;
acceptTimeout?: ReturnType<typeof setTimeout> | undefined;
isGroupCall?: boolean;
};

type CurrentCallParams = {
Expand Down Expand Up @@ -184,6 +185,16 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
return;
}

// Group calls (ring action): skip the accepted→confirmed handshake and join directly.
// The creator does not participate in the accept/confirm protocol for group rings.
if (callData.isGroupCall) {
this.debugLog(`[VideoConf] Accepting group call ${callId}, joining directly.`);
this.dismissIncomingCall(callId);
this.removeIncomingCall(callId);
void this.joinCall(callId);
return;
}

this.debugLog(`[VideoConf] Accepting incoming call ${callId}.`);

if (callData.timeout) {
Expand Down Expand Up @@ -501,6 +512,8 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
switch (action) {
case 'call':
return this.onDirectCall(params);
case 'ring':
return this.onGroupCallRing(params);
case 'canceled':
return this.onDirectCallCanceled(params);
case 'accepted':
Expand Down Expand Up @@ -579,7 +592,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
return setTimeout(() => this.abortIncomingCall(callId), CALL_TIMEOUT);
}

private startNewIncomingCall({ callId, uid, rid }: DirectCallParams): void {
private startNewIncomingCall({ callId, uid, rid }: DirectCallParams, isGroupCall = false): void {
if (this.isCallDismissed(callId)) {
this.debugLog(`[VideoConf] Ignoring dismissed call.`);
return;
Expand All @@ -593,6 +606,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
callId,
uid,
rid,
isGroupCall,
timeout: this.createAbortTimeout(callId),
});

Expand All @@ -618,6 +632,19 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
}
}

private onGroupCallRing({ callId, uid, rid }: DirectCallParams): void {
if (this.incomingDirectCalls.get(callId)?.acceptTimeout) {
return;
}

this.infoLog(`[VideoConf] Group call ${callId} is ringing.`);
if (this.incomingDirectCalls.has(callId)) {
this.refreshExistingIncomingCall({ callId, uid, rid });
} else {
this.startNewIncomingCall({ callId, uid, rid }, true);
}
}

private onDirectCall({ callId, uid, rid }: DirectCallParams): void {
// If we already accepted this call, then don't ring again
if (this.incomingDirectCalls.get(callId)?.acceptTimeout) {
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/server/configuration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { configureLDAP } from './ldap';
import { configureOAuth } from './oauth';
import { configurePushNotifications } from './pushNotification';
import type { ICachedSettings } from '../../app/settings/server/CachedSettings';
import './videoConference';

export async function configureServer(settings: ICachedSettings) {
await Promise.all([
Expand Down
23 changes: 23 additions & 0 deletions apps/meteor/server/configuration/videoConference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Subscriptions } from '@rocket.chat/models';

import { videoConfTypes } from '../lib/videoConfTypes';

// Maximum number of room members for which ringing is enabled in CE.
// Mirrors the EE limit so that behavior is consistent: EE handlers (priority 1) take
// precedence for EE installs; this handler (priority 0) activates only when no higher-
// priority handler matched (i.e. pure CE, or EE rooms that the EE handler excluded).
// Using the same cap means EE rooms that were intentionally excluded (>10 members) are
// also excluded here — no unintended ringing for large EE rooms.
const MAX_RINGING_MEMBERS = 10;

videoConfTypes.registerVideoConferenceType(
{ type: 'videoconference', ringing: true },
async ({ _id, t }, allowRinging) => {
if (!allowRinging || t === 'l') {
return false;
}

return (await Subscriptions.countByRoomId(_id)) <= MAX_RINGING_MEMBERS;
},
0,
);