diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts index 257bcfd01847..8d41129c99e7 100644 --- a/apps/meteor/client/lib/VideoConfManager.ts +++ b/apps/meteor/client/lib/VideoConfManager.ts @@ -22,10 +22,10 @@ export type DirectCallParams = { uid: IUser['_id']; rid: IRoom['_id']; callId: string; + + // #ToDo: The attributes below should not be part of DirectCallParams - they are used by local events only, never notification events. dismissed?: boolean; acceptTimeout?: ReturnType | undefined; - // TODO: improve this, nowadays there is not possible check if the video call has finished, but ist a nice improvement - // state: 'incoming' | 'outgoing' | 'connected' | 'disconnected' | 'dismissed'; }; type IncomingDirectCall = DirectCallParams & { timeout: number }; diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 1209434b5f39..c43d3ea4737e 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -1,4 +1,5 @@ import { UserStatus, isSettingColor } from '@rocket.chat/core-typings'; +import type { IUser, IRoom, VideoConference } from '@rocket.chat/core-typings'; import { parse } from '@rocket.chat/message-parser'; import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; @@ -96,6 +97,25 @@ export class ListenersModule { notifications.notifyLoggedInThisInstance('roles-change', update); }); + service.onEvent( + 'user.video-conference', + ({ + userId, + action, + params, + }: { + userId: string; + action: string; + params: { + callId: VideoConference['_id']; + uid: IUser['_id']; + rid: IRoom['_id']; + }; + }) => { + notifications.notifyUserInThisInstance(userId, 'video-conference', { action, params }); + }, + ); + service.onEvent('presence.status', ({ user }) => { const { _id, username, name, status, statusText, roles } = user; if (!status) { diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 11e0a167d25e..4240066313d7 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -401,7 +401,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf if (call.endedAt || call.status > VideoConferenceStatus.STARTED) { // If the caller is still calling about a call that has already ended, notify it if (action === 'call' && caller === call.createdBy._id) { - Notifications.notifyUser(call.createdBy._id, 'video-conference.end', { rid, uid, callId }); + this.notifyUser(call.createdBy._id, 'end', { rid, uid, callId }); } return false; @@ -410,6 +410,14 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf return true; } + private notifyUser( + userId: IUser['_id'], + action: string, + params: { uid: IUser['_id']; rid: IRoom['_id']; callId: VideoConference['_id'] }, + ): void { + api.broadcast('user.video-conference', { userId, action, params }); + } + private notifyVideoConfUpdate(rid: IRoom['_id'], callId: VideoConference['_id']): void { Notifications.notifyRoom(rid, callId); } @@ -442,7 +450,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf const params = { rid: call.rid, uid: call.createdBy._id, callId: call._id }; // Notify the caller that the call was ended by the server - Notifications.notifyUser(call.createdBy._id, 'video-conference.end', params); + this.notifyUser(call.createdBy._id, 'end', params); // If the callee hasn't joined the call yet, notify them that it has already ended const subscriptions = await Subscriptions.findByRoomIdAndNotUserId(call.rid, call.createdBy._id, { @@ -450,11 +458,12 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }).toArray(); for (const subscription of subscriptions) { + // Skip notifying users that already joined the call if (call.users.find(({ _id }) => _id === subscription.u._id)) { continue; } - Notifications.notifyUser(subscription.u._id, 'video-conference.end', params); + this.notifyUser(subscription.u._id, 'end', params); } } @@ -614,13 +623,16 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf // After 40 seconds if the status is still "calling", we cancel the call automatically. setTimeout(async () => { try { - const call = await VideoConferenceModel.findOneById>(callId, { projection: { status: 1 } }); + const call = await VideoConferenceModel.findOneById(callId); - if (call?.status !== VideoConferenceStatus.CALLING) { - return; - } + if (call) { + await this.endDirectCall(call); + if (call.status !== VideoConferenceStatus.CALLING) { + return; + } - await this.cancel(user._id, callId); + await this.cancel(user._id, callId); + } } catch { // Ignore errors on this timeout } @@ -633,12 +645,17 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }; } - private async notifyUsersOfRoom(rid: IRoom['_id'], uid: IUser['_id'], eventName: string, ...args: any[]): Promise { + private async notifyUsersOfRoom( + rid: IRoom['_id'], + uid: IUser['_id'], + action: string, + params: { uid: IUser['_id']; rid: IRoom['_id']; callId: VideoConference['_id'] }, + ): Promise { const subscriptions = Subscriptions.findByRoomIdAndNotUserId(rid, uid, { projection: { 'u._id': 1, '_id': 0 }, }); - await subscriptions.forEach((subscription) => Notifications.notifyUser(subscription.u._id, eventName, ...args)); + await subscriptions.forEach((subscription) => this.notifyUser(subscription.u._id, action, params)); } private async startGroup( @@ -677,7 +694,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf VideoConferenceModel.setMessageById(callId, 'started', messageId); if (call.ringing) { - await this.notifyUsersOfRoom(rid, user._id, 'video-conference.ring', { callId, rid, title, uid: call.createdBy, providerName }); + await this.notifyUsersOfRoom(rid, user._id, 'ring', { callId, rid, uid: call.createdBy._id }); } return { @@ -917,9 +934,9 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf private async updateDirectCall(call: IDirectVideoConference, newUserId: IUser['_id']): Promise { // If it's an user that hasn't joined yet if (call.ringing && !call.users.find(({ _id }) => _id === newUserId)) { - Notifications.notifyUser(call.createdBy._id, 'video-conference.join', { rid: call.rid, uid: newUserId, callId: call._id }); + this.notifyUser(call.createdBy._id, 'join', { rid: call.rid, uid: newUserId, callId: call._id }); if (newUserId !== call.createdBy._id) { - Notifications.notifyUser(newUserId, 'video-conference.join', { rid: call.rid, uid: newUserId, callId: call._id }); + this.notifyUser(newUserId, 'join', { rid: call.rid, uid: newUserId, callId: call._id }); // If the callee joined the direct call, then we stopped ringing await VideoConferenceModel.setRingingById(call._id, false); } diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts index 71e5694fabaf..737655d0a5ff 100644 --- a/packages/core-services/src/Events.ts +++ b/packages/core-services/src/Events.ts @@ -24,6 +24,7 @@ import type { VoipEventDataSignature, AtLeast, UserStatus, + VideoConference, } from '@rocket.chat/core-typings'; import type { AutoUpdateRecord } from './types/IMeteor'; @@ -91,6 +92,15 @@ export type EventSignatures = { 'user.roleUpdate'(update: Record): void; 'user.updateCustomStatus'(userStatus: IUserStatus): void; 'user.typing'(data: { user: Partial; isTyping: boolean; roomId: string }): void; + 'user.video-conference'(data: { + userId: IUser['_id']; + action: string; + params: { + callId: VideoConference['_id']; + uid: IUser['_id']; + rid: IRoom['_id']; + }; + }): void; 'presence.status'(data: { user: Pick; previousStatus: UserStatus | undefined;