Skip to content

Commit

Permalink
[NEW] Audio and Video calling in Livechat using Jitsi and WebRTC (#23004
Browse files Browse the repository at this point in the history
)

* [NEW] Livechat WebRTC call settings (#22559)

* [NEW] Livechat WebRTC call settings

* [FIX] Omnichannel Call Settings

- Change Livechat call to Video and Audio Call as it will apply to other omnichannels in future
- Change Audio and Video Setting alert to description to be conformant with the other settings
- Remove unrelated changes(base.css) that got induced unknowingly
- Refactor to remove translation for "Jitsi" and "WebRTC" and remove unnecessary dependency on t
- Refactor to add return type of handleClick
- Add/remove related i18n labels

* [FIX] Livechat videoCall api and method

* [FIX] Add migrations for webRTC enabled settings and omnichannel call provider

* [FIX] 'Jitsi' typo

* [NEW] Join call action button (#22689)

* [NEW] WebRTC Call Session

* [IMPROVEMENT] Use API endpoint instead of method, fix handleClick dependency warning

* [FIX] Return updated callStatus, use translation for join call message, remove system logger, make callStatus optional in room interface

* [NEW] Join and End Call Action Message Button for Agent

* [FIX] Use translation for Call ALready Ended toastr, convert actionLink to tsx

* [IMPROVE] Remove redundant callStatus from message collection, Display Call Ended message with call duration

* [REF] Use translation for call ended message, Store danger field in db with other action fields, store actionAlignment field in db

* [NEW] update call status (#22854)

* add code for update call status

* remove fourth param

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>

* [NEW] P2P WebRTC Connection Establishment (#22847)

* [NEW] WebRTC P2P Connection with Basic Call UI

* [FIX] Set Stream on a stable connection

* [FIX] userId typecheck error

* [REFACTOR]
 - Restore type of userId to string by removing `| undefined`
 - Add translation for visitor does not exist toastr
 - Set visitorId from room object fetched instead of fetching from livechat widget as query param
 - Type Checking

* [FIX] Running startCall 2 times for agent

* [FIX] Call declined Page

* [NEW] Control Buttons - mic, cam, expand and end call (#22928)

* [NEW] Control Buttons - mic, cam, expand and end call

* [REFACTOR] Add an empty file on EOF en18.i18n that was mistakenly removed

* [FIX] UI responsiveness (#22934)

* [NEW] Control Buttons - mic, cam, expand and end call

* [REFACTOR] Add an empty file on EOF en18.i18n that was mistakenly removed

* [FIX] UI Responsiveness

* [REF] Use const and ternary op

* [FIX] Handle decline message action link (#22936)

* [NEW] Control Buttons - mic, cam, expand and end call

* [REFACTOR] Add an empty file on EOF en18.i18n that was mistakenly removed

* [FIX] UI Responsiveness

* [REF] Use const and ternary op

* [FIX] Action Link not updating when call declined

* [FIX] WebRTC_call_declined_message

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>

* [REF] Use a single IF statement to handle status

Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>

* [FIX] Call button visible even when chat is queued (#22943)

* [FIX] add callstatus attribute in room object (#22959)

* [IMPROVE] Attach jitsi link to message object for Livechat  (#22690)

* [Improve] Attach jitsi link to message object for Livechat

(cherry picked from commit c888961da3313de06eaeb0700b7ce0b6371ef469)

* Update WebRTCClass.js

* [NEW] Webrtc meet page layout (#22932)

* [NEW] Control Buttons - mic, cam, expand and end call

* webrtc meet page desgin

* [REFACTOR] Add an empty file on EOF en18.i18n that was mistakenly removed

* [FIX] UI Responsiveness

* [FIX] Action Link not updating when call declined

* webrtc meet page desgin

* [FIX] Remote user avatar screen and video switching

* fix-alert

* fix 2 aletrs

* improve codebase

* make ui responsive

* fix issue

* Add call timer component + minor refactoring

* some css changes

Co-authored-by: Dhruv Jain <dhruv.jain93@gmail.com>
Co-authored-by: murtaza98 <murtaza.patrawala@rocket.chat>
Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com>

* Add migration

* Changing files to tsx

* Update default value for setting -Jitsi_Open_New_Window

* Update invalid-livechat-config issue

* Fix typescript errors

* Fix build errors caused by new room prop - webRtcCallStartTime

* Simplify call duration calculation logic

* Add definition PUT method for REST on client

Co-authored-by: Dhruv Jain <51796498+djcruz93@users.noreply.github.com>
Co-authored-by: Deepak Agarwal <deepak710agarwal@gmail.com>
Co-authored-by: Dhruv Jain <dhruv.jain93@gmail.com>
  • Loading branch information
4 people committed Nov 22, 2021
1 parent 9771ee6 commit 6d7752f
Show file tree
Hide file tree
Showing 34 changed files with 1,144 additions and 114 deletions.
18 changes: 18 additions & 0 deletions app/livechat/lib/messageTypes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import formatDistance from 'date-fns/formatDistance';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import moment from 'moment';

import { MessageTypes } from '../../ui-utils';

Expand Down Expand Up @@ -81,6 +83,22 @@ MessageTypes.registerType({
message: 'New_videocall_request',
});

MessageTypes.registerType({
id: 'livechat_webrtc_video_call',
render(message) {
if (message.msg === 'ended' && message.webRtcCallEndTs && message.ts) {
return TAPi18n.__('WebRTC_call_ended_message', {
callDuration: formatDistance(new Date(message.webRtcCallEndTs), new Date(message.ts)),
endTime: moment(message.webRtcCallEndTs).format('h:mm A'),
});
}
if (message.msg === 'declined' && message.webRtcCallEndTs) {
return TAPi18n.__('WebRTC_call_declined_message');
}
return message.msg;
},
});

MessageTypes.registerType({
id: 'omnichannel_placed_chat_on_hold',
system: true,
Expand Down
18 changes: 13 additions & 5 deletions app/livechat/server/api/lib/livechat.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models/server';
import { EmojiCustom } from '../../../../models/server/raw';
Expand Down Expand Up @@ -56,6 +57,7 @@ export function findOpenRoom(token, departmentId) {
departmentId: 1,
servedBy: 1,
open: 1,
callStatus: 1,
},
};

Expand Down Expand Up @@ -101,7 +103,7 @@ export async function settings() {
nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form,
emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form,
displayOfflineForm: initSettings.Livechat_display_offline_form,
videoCall: initSettings.Livechat_videocall_enabled === true && initSettings.Jitsi_Enabled === true,
videoCall: initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true,
fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled,
language: initSettings.Language,
transcript: initSettings.Livechat_enable_transcript,
Expand All @@ -117,10 +119,16 @@ export async function settings() {
color: initSettings.Livechat_title_color,
offlineTitle: initSettings.Livechat_offline_title,
offlineColor: initSettings.Livechat_offline_title_color,
actionLinks: [
{ icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' },
{ icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' },
],
actionLinks: {
webrtc: [
{ actionLinksAlignment: 'flex-start', i18nLabel: 'Join_call', label: TAPi18n.__('Join_call'), method_id: 'joinLivechatWebRTCCall' },
{ i18nLabel: 'End_call', label: TAPi18n.__('End_call'), method_id: 'endLivechatWebRTCCall', danger: true },
],
jitsi: [
{ icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall' },
{ icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall' },
],
},
},
messages: {
offlineMessage: initSettings.Livechat_offline_message,
Expand Down
110 changes: 106 additions & 4 deletions app/livechat/server/api/v1/videoCall.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';

import { Messages } from '../../../../models';
import { settings as rcSettings } from '../../../../settings';
import { Messages, Rooms } from '../../../../models';
import { settings as rcSettings } from '../../../../settings/server';
import { API } from '../../../../api/server';
import { findGuest, getRoom, settings } from '../lib/livechat';
import { OmnichannelSourceType } from '../../../../../definition/IRoom';
import { hasPermission, canSendMessage } from '../../../../authorization';
import { Livechat } from '../../lib/Livechat';

API.v1.addRoute('livechat/video.call/:token', {
get() {
Expand Down Expand Up @@ -36,12 +39,12 @@ API.v1.addRoute('livechat/video.call/:token', {
};
const { room } = getRoom({ guest, rid, roomInfo });
const config = Promise.await(settings());
if (!config.theme || !config.theme.actionLinks) {
if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.jitsi) {
throw new Meteor.Error('invalid-livechat-config');
}

Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, {
actionLinks: config.theme.actionLinks,
actionLinks: config.theme.actionLinks.jitsi,
});
let rname;
if (rcSettings.get('Jitsi_URL_Room_Hash')) {
Expand All @@ -63,3 +66,102 @@ API.v1.addRoute('livechat/video.call/:token', {
}
},
});

API.v1.addRoute('livechat/webrtc.call', { authRequired: true }, {
get() {
try {
check(this.queryParams, {
rid: Match.Maybe(String),
});

if (!hasPermission(this.userId, 'view-l-room')) {
return API.v1.unauthorized();
}

const room = canSendMessage(this.queryParams.rid, {
uid: this.userId,
username: this.user.username,
type: this.user.type,
});
if (!room) {
throw new Meteor.Error('invalid-room');
}

const webrtcCallingAllowed = (rcSettings.get('WebRTC_Enabled') === true) && (rcSettings.get('Omnichannel_call_provider') === 'WebRTC');
if (!webrtcCallingAllowed) {
throw new Meteor.Error('webRTC calling not enabled');
}

const config = Promise.await(settings());
if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) {
throw new Meteor.Error('invalid-livechat-config');
}

let { callStatus } = room;

if (!callStatus || callStatus === 'ended' || callStatus === 'declined') {
callStatus = 'ringing';
Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus));
Promise.await(Messages.createWithTypeRoomIdMessageAndUser(
'livechat_webrtc_video_call',
room._id,
TAPi18n.__('Join_my_room_to_start_the_video_call'),
this.user,
{
actionLinks: config.theme.actionLinks.webrtc,
},
));
}
const videoCall = {
rid: room._id,
provider: 'webrtc',
callStatus,
};
return API.v1.success({ videoCall });
} catch (e) {
return API.v1.failure(e);
}
},
});

API.v1.addRoute('livechat/webrtc.call/:callId', { authRequired: true }, {
put() {
try {
check(this.urlParams, {
callId: String,
});

check(this.bodyParams, {
rid: Match.Maybe(String),
status: Match.Maybe(String),
});

const { callId } = this.urlParams;
const { rid, status } = this.bodyParams;

if (!hasPermission(this.userId, 'view-l-room')) {
return API.v1.unauthorized();
}

const room = canSendMessage(rid, {
uid: this.userId,
username: this.user.username,
type: this.user.type,
});
if (!room) {
throw new Meteor.Error('invalid-room');
}

const call = Promise.await(Messages.findOneById(callId));
if (!call || call.t !== 'livechat_webrtc_video_call') {
throw new Meteor.Error('invalid-callId');
}

Livechat.updateCallStatus(callId, rid, status, this.user);

return API.v1.success({ status });
} catch (e) {
return API.v1.failure(e);
}
},
});
24 changes: 24 additions & 0 deletions app/livechat/server/api/v1/visitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,30 @@ API.v1.addRoute('livechat/visitor/:token/room', { authRequired: true }, {
},
});

API.v1.addRoute('livechat/visitor.callStatus', {
post() {
try {
check(this.bodyParams, {
token: String,
callStatus: String,
rid: String,
callId: String,
});

const { token, callStatus, rid, callId } = this.bodyParams;
const guest = findGuest(token);
if (!guest) {
throw new Meteor.Error('invalid-token');
}
const status = callStatus;
Livechat.updateCallStatus(callId, rid, status, guest);
return API.v1.success({ token, callStatus });
} catch (e) {
return API.v1.failure(e);
}
},
});

API.v1.addRoute('livechat/visitor.status', {
post() {
try {
Expand Down
25 changes: 15 additions & 10 deletions app/livechat/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,6 @@ Meteor.startup(function() {
enableQuery: omnichannelEnabledQuery,
});

this.add('Livechat_videocall_enabled', false, {
type: 'boolean',
group: 'Omnichannel',
section: 'Livechat',
public: true,
i18nLabel: 'Videocall_enabled',
i18nDescription: 'Beta_feature_Depends_on_Video_Conference_to_be_enabled',
enableQuery: [{ _id: 'Jitsi_Enabled', value: true }, omnichannelEnabledQuery],
});

this.add('Livechat_fileupload_enabled', true, {
type: 'boolean',
group: 'Omnichannel',
Expand Down Expand Up @@ -616,5 +606,20 @@ Meteor.startup(function() {
i18nDescription: 'Time_in_seconds',
enableQuery: omnichannelEnabledQuery,
});

this.add('Omnichannel_call_provider', 'none', {
type: 'select',
public: true,
group: 'Omnichannel',
section: 'Video_and_Audio_Call',
values: [
{ key: 'none', i18nLabel: 'None' },
{ key: 'Jitsi', i18nLabel: 'Jitsi' },
{ key: 'WebRTC', i18nLabel: 'WebRTC' },
],
i18nDescription: 'Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings',
i18nLabel: 'Call_provider',
enableQuery: omnichannelEnabledQuery,
});
});
});
8 changes: 7 additions & 1 deletion app/livechat/server/lib/Livechat.js
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ export const Livechat = {
'Livechat_offline_success_message',
'Livechat_offline_form_unavailable',
'Livechat_display_offline_form',
'Livechat_videocall_enabled',
'Omnichannel_call_provider',
'Jitsi_Enabled',
'Language',
'Livechat_enable_transcript',
Expand Down Expand Up @@ -1278,6 +1278,12 @@ export const Livechat = {
};
LivechatVisitors.updateById(contactId, updateUser);
},
updateCallStatus(callId, rid, status, user) {
Rooms.setCallStatus(rid, status);
if (status === 'ended' || status === 'declined') {
return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user);
}
},
};

settings.watch('Livechat_history_monitor_type', (value) => {
Expand Down
2 changes: 1 addition & 1 deletion app/livechat/server/methods/getInitialData.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Meteor.methods({
info.offlineUnavailableMessage = initSettings.Livechat_offline_form_unavailable;
info.displayOfflineForm = initSettings.Livechat_display_offline_form;
info.language = initSettings.Language;
info.videoCall = initSettings.Livechat_videocall_enabled === true && initSettings.Jitsi_Enabled === true;
info.videoCall = initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true;
info.fileUpload = initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled;
info.transcript = initSettings.Livechat_enable_transcript;
info.transcriptMessage = initSettings.Livechat_transcript_message;
Expand Down
29 changes: 29 additions & 0 deletions app/models/server/models/Rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,35 @@ export class Rooms extends Base {
return this.update(query, update);
}

setCallStatus(_id, status) {
const query = {
_id,
};

const update = {
$set: {
callStatus: status,
},
};

return this.update(query, update);
}

setCallStatusAndCallStartTime(_id, status) {
const query = {
_id,
};

const update = {
$set: {
callStatus: status,
webRtcCallStartTime: new Date(),
},
};

return this.update(query, update);
}

findByTokenpass(tokens) {
const query = {
'tokenpass.tokens.token': {
Expand Down
13 changes: 12 additions & 1 deletion app/models/server/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,18 @@ export class SubscriptionsRaw extends BaseRaw<T> {
return this.find(query, options);
}

countByRoomIdAndUserId(rid: string, uid: string): Promise<number> {
findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<T> = {}): Cursor<T> {
const query = {
rid: roomId,
'servedBy._id': {
$ne: userId,
},
};

return this.find(query, options);
}

countByRoomIdAndUserId(rid: string, uid: string | undefined): Promise<number> {
const query = {
rid,
'u._id': uid,
Expand Down
10 changes: 5 additions & 5 deletions app/notifications/client/lib/Notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ class Notifications {
return this.streamRoom.on(`${ room }/${ eventName }`, callback);
}

async onUser(eventName, callback) {
await this.streamUser.on(`${ Meteor.userId() }/${ eventName }`, callback);
return () => this.unUser(eventName, callback);
async onUser(eventName, callback, visitorId = null) {
await this.streamUser.on(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
return () => this.unUser(eventName, callback, visitorId);
}

unAll(callback) {
Expand All @@ -95,8 +95,8 @@ class Notifications {
return this.streamRoom.removeListener(`${ room }/${ eventName }`, callback);
}

unUser(eventName, callback) {
return this.streamUser.removeListener(`${ Meteor.userId() }/${ eventName }`, callback);
unUser(eventName, callback, visitorId = null) {
return this.streamUser.removeListener(`${ Meteor.userId() || visitorId }/${ eventName }`, callback);
}
}

Expand Down
1 change: 1 addition & 0 deletions app/utils/client/lib/RestApiClient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export declare const APIClient: {
delete<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
get<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
post<P, B, R = any>(endpoint: string, params?: Serialized<P>, body?: B): Promise<Serialized<R>>;
put<P, B, R = any>(endpoint: string, params?: Serialized<P>, body?: B): Promise<Serialized<R>>;
upload<P, B, R = any>(
endpoint: string,
params?: Serialized<P>,
Expand Down

0 comments on commit 6d7752f

Please sign in to comment.