diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js
index c7994105d68a2ee66b3bb3cb2b2920928e716de1..6fb62970b3f4f2dcce11cae2b3752fd5e7433834 100644
--- a/apps/meteor/.mocharc.api.js
+++ b/apps/meteor/.mocharc.api.js
@@ -14,5 +14,6 @@ module.exports = {
 		'tests/end-to-end/api/*.js',
 		'tests/end-to-end/api/*.ts',
 		'tests/end-to-end/apps/*.js',
+		'tests/end-to-end/apps/*.ts',
 	],
 };
diff --git a/apps/meteor/app/api/server/v1/videoConference.ts b/apps/meteor/app/api/server/v1/videoConference.ts
index 34a78fdf09da9096518ff6dcf04d0118381c135e..1f76fa4b736648a1b569fe81267f2321370651ce 100644
--- a/apps/meteor/app/api/server/v1/videoConference.ts
+++ b/apps/meteor/app/api/server/v1/videoConference.ts
@@ -1,25 +1,182 @@
-import { Meteor } from 'meteor/meteor';
+import type { VideoConference } from '@rocket.chat/core-typings';
+import {
+	isVideoConfStartProps,
+	isVideoConfJoinProps,
+	isVideoConfCancelProps,
+	isVideoConfInfoProps,
+	isVideoConfListProps,
+} from '@rocket.chat/rest-typings';
 
-import { Rooms } from '../../../models/server';
 import { API } from '../api';
+import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
+import { VideoConf } from '../../../../server/sdk';
+import { videoConfProviders } from '../../../../server/lib/videoConfProviders';
+import { availabilityErrors } from '../../../../lib/videoConference/constants';
 
 API.v1.addRoute(
-	'video-conference/jitsi.update-timeout',
-	{ authRequired: true },
+	'video-conference.start',
+	{ authRequired: true, validateParams: isVideoConfStartProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60000 } },
 	{
-		post() {
-			const { roomId, joiningNow = true } = this.bodyParams;
-			if (!roomId) {
-				return API.v1.failure('The "roomId" parameter is required!');
+		async post() {
+			const { roomId, title, allowRinging } = this.bodyParams;
+			const { userId } = this;
+			if (!userId || !(await canAccessRoomIdAsync(roomId, userId))) {
+				return API.v1.failure('invalid-params');
 			}
 
-			const room = Rooms.findOneById(roomId, { fields: { _id: 1 } });
-			if (!room) {
-				return API.v1.failure('Room does not exist!');
+			try {
+				const providerName = videoConfProviders.getActiveProvider();
+
+				if (!providerName) {
+					throw new Error(availabilityErrors.NOT_ACTIVE);
+				}
+
+				return API.v1.success({
+					data: {
+						...(await VideoConf.start(userId, roomId, { title, allowRinging: Boolean(allowRinging) })),
+						providerName,
+					},
+				});
+			} catch (e) {
+				return API.v1.failure(await VideoConf.diagnoseProvider(userId, roomId));
+			}
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.join',
+	{ authOrAnonRequired: true, validateParams: isVideoConfJoinProps, rateLimiterOptions: { numRequestsAllowed: 2, intervalTimeInMS: 5000 } },
+	{
+		async post() {
+			const { callId, state } = this.bodyParams;
+			const { userId } = this;
+
+			const call = await VideoConf.get(callId);
+			if (!call) {
+				return API.v1.failure('invalid-params');
+			}
+
+			if (!(await canAccessRoomIdAsync(call.rid, userId))) {
+				return API.v1.failure('invalid-params');
+			}
+
+			let url: string | undefined;
+
+			try {
+				url = await VideoConf.join(userId, callId, {
+					...(state?.cam !== undefined ? { cam: state.cam } : {}),
+					...(state?.mic !== undefined ? { mic: state.mic } : {}),
+				});
+			} catch (e) {
+				if (userId) {
+					return API.v1.failure(await VideoConf.diagnoseProvider(userId, call.rid, call.providerName));
+				}
+			}
+
+			if (!url) {
+				return API.v1.failure('failed-to-get-url');
 			}
 
-			const jitsiTimeout = Meteor.runAsUser(this.userId, () => Meteor.call('jitsi:updateTimeout', roomId, Boolean(joiningNow)));
-			return API.v1.success({ jitsiTimeout });
+			return API.v1.success({
+				url,
+				providerName: call.providerName,
+			});
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.cancel',
+	{ authRequired: true, validateParams: isVideoConfCancelProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 60000 } },
+	{
+		async post() {
+			const { callId } = this.bodyParams;
+			const { userId } = this;
+
+			const call = await VideoConf.get(callId);
+			if (!call) {
+				return API.v1.failure('invalid-params');
+			}
+
+			if (!userId || !(await canAccessRoomIdAsync(call.rid, userId))) {
+				return API.v1.failure('invalid-params');
+			}
+
+			await VideoConf.cancel(userId, callId);
+			return API.v1.success();
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.info',
+	{ authRequired: true, validateParams: isVideoConfInfoProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
+	{
+		async get() {
+			const { callId } = this.queryParams;
+			const { userId } = this;
+
+			const call = await VideoConf.get(callId);
+			if (!call) {
+				return API.v1.failure('invalid-params');
+			}
+
+			if (!userId || !(await canAccessRoomIdAsync(call.rid, userId))) {
+				return API.v1.failure('invalid-params');
+			}
+
+			const capabilities = await VideoConf.listProviderCapabilities(call.providerName);
+
+			return API.v1.success({
+				...(call as VideoConference),
+				capabilities,
+			});
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.list',
+	{ authRequired: true, validateParams: isVideoConfListProps, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
+	{
+		async get() {
+			const { roomId } = this.queryParams;
+			const { userId } = this;
+
+			const { offset, count } = this.getPaginationItems();
+
+			if (!userId || !(await canAccessRoomIdAsync(roomId, userId))) {
+				return API.v1.failure('invalid-params');
+			}
+
+			const data = await VideoConf.list(roomId, { offset, count });
+
+			return API.v1.success(data);
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.providers',
+	{ authRequired: true, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
+	{
+		async get() {
+			const data = await VideoConf.listProviders();
+
+			return API.v1.success({ data });
+		},
+	},
+);
+
+API.v1.addRoute(
+	'video-conference.capabilities',
+	{ authRequired: true, rateLimiterOptions: { numRequestsAllowed: 3, intervalTimeInMS: 1000 } },
+	{
+		async get() {
+			const data = await VideoConf.listCapabilities();
+
+			return API.v1.success(data);
 		},
 	},
 );
diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js
index d03160e02d0fcd76ffb5b0a41cc4d4b935d0dcea..828be594640d2f7f918df1596bc2cb3f0d961059 100644
--- a/apps/meteor/app/apps/server/bridges/bridges.js
+++ b/apps/meteor/app/apps/server/bridges/bridges.js
@@ -18,6 +18,7 @@ import { AppLivechatBridge } from './livechat';
 import { AppUploadBridge } from './uploads';
 import { UiInteractionBridge } from './uiInteraction';
 import { AppSchedulerBridge } from './scheduler';
+import { AppVideoConferenceBridge } from './videoConferences';
 
 export class RealAppBridges extends AppBridges {
 	constructor(orch) {
@@ -41,6 +42,7 @@ export class RealAppBridges extends AppBridges {
 		this._uiInteractionBridge = new UiInteractionBridge(orch);
 		this._schedulerBridge = new AppSchedulerBridge(orch);
 		this._cloudWorkspaceBridge = new AppCloudBridge(orch);
+		this._videoConfBridge = new AppVideoConferenceBridge(orch);
 	}
 
 	getCommandBridge() {
@@ -114,4 +116,8 @@ export class RealAppBridges extends AppBridges {
 	getCloudWorkspaceBridge() {
 		return this._cloudWorkspaceBridge;
 	}
+
+	getVideoConferenceBridge() {
+		return this._videoConfBridge;
+	}
 }
diff --git a/apps/meteor/app/apps/server/bridges/videoConferences.ts b/apps/meteor/app/apps/server/bridges/videoConferences.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9d7bf245e73bcba907825ef28146c5fa7521235e
--- /dev/null
+++ b/apps/meteor/app/apps/server/bridges/videoConferences.ts
@@ -0,0 +1,70 @@
+import { VideoConferenceBridge } from '@rocket.chat/apps-engine/server/bridges/VideoConferenceBridge';
+import { AppVideoConference, VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences';
+import { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders';
+
+import { VideoConf } from '../../../../server/sdk';
+import { AppServerOrchestrator } from '../orchestrator';
+import { videoConfProviders } from '../../../../server/lib/videoConfProviders';
+import type { AppVideoConferencesConverter } from '../converters/videoConferences';
+
+export class AppVideoConferenceBridge extends VideoConferenceBridge {
+	// eslint-disable-next-line no-empty-function
+	constructor(private readonly orch: AppServerOrchestrator) {
+		super();
+	}
+
+	protected async getById(callId: string, appId: string): Promise<VideoConference> {
+		this.orch.debugLog(`The App ${appId} is getting the video conference byId: "${callId}"`);
+
+		return this.orch.getConverters()?.get('videoConferences').convertById(callId);
+	}
+
+	protected async create(call: AppVideoConference, appId: string): Promise<string> {
+		this.orch.debugLog(`The App ${appId} is creating a video conference.`);
+
+		return (
+			await VideoConf.create({
+				type: 'videoconference',
+				...call,
+			})
+		).callId;
+	}
+
+	protected async update(call: VideoConference, appId: string): Promise<void> {
+		this.orch.debugLog(`The App ${appId} is updating a video conference.`);
+
+		const oldData = call._id && (await VideoConf.getUnfiltered(call._id));
+		if (!oldData) {
+			throw new Error('A video conference must exist to update.');
+		}
+
+		const data = (this.orch.getConverters()?.get('videoConferences') as AppVideoConferencesConverter).convertAppVideoConference(call);
+		await VideoConf.setProviderData(call._id, data.providerData);
+
+		for (const { _id, ts } of data.users) {
+			if (oldData.users.find((user) => user._id === _id)) {
+				continue;
+			}
+
+			VideoConf.addUser(call._id, _id, ts);
+		}
+
+		if (data.endedBy && data.endedBy._id !== oldData.endedBy?._id) {
+			await VideoConf.setEndedBy(call._id, data.endedBy._id);
+		} else if (data.endedAt) {
+			await VideoConf.setEndedAt(call._id, data.endedAt);
+		}
+
+		if (data.status > oldData.status) {
+			await VideoConf.setStatus(call._id, data.status);
+		}
+	}
+
+	protected async registerProvider(info: IVideoConfProvider): Promise<void> {
+		videoConfProviders.registerProvider(info.name, info.capabilities || {});
+	}
+
+	protected async unRegisterProvider(info: IVideoConfProvider): Promise<void> {
+		videoConfProviders.unRegisterProvider(info.name);
+	}
+}
diff --git a/apps/meteor/app/apps/server/converters/index.js b/apps/meteor/app/apps/server/converters/index.js
index edb1bcf57cb16c1bce79b59c00bf54db6bf2ba4f..d5fe67636dc01c2b536acd0b02583a008227ac24 100644
--- a/apps/meteor/app/apps/server/converters/index.js
+++ b/apps/meteor/app/apps/server/converters/index.js
@@ -2,5 +2,6 @@ import { AppMessagesConverter } from './messages';
 import { AppRoomsConverter } from './rooms';
 import { AppSettingsConverter } from './settings';
 import { AppUsersConverter } from './users';
+import { AppVideoConferencesConverter } from './videoConferences';
 
-export { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter };
+export { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter, AppVideoConferencesConverter };
diff --git a/apps/meteor/app/apps/server/converters/videoConferences.ts b/apps/meteor/app/apps/server/converters/videoConferences.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd4e2c113b6f4d7493d5c033bc93af74b1641953
--- /dev/null
+++ b/apps/meteor/app/apps/server/converters/videoConferences.ts
@@ -0,0 +1,36 @@
+import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences';
+import type { IVideoConference } from '@rocket.chat/core-typings';
+
+import { VideoConf } from '../../../../server/sdk';
+import type { AppServerOrchestrator } from '../orchestrator';
+
+export class AppVideoConferencesConverter {
+	// @ts-ignore
+	private orch: AppServerOrchestrator;
+
+	constructor(orch: AppServerOrchestrator) {
+		this.orch = orch;
+	}
+
+	async convertById(callId: string): Promise<VideoConference | undefined> {
+		const call = await VideoConf.getUnfiltered(callId);
+
+		return this.convertVideoConference(call);
+	}
+
+	convertVideoConference(call: IVideoConference | null): VideoConference | undefined {
+		if (!call) {
+			return;
+		}
+
+		return {
+			...call,
+		} as VideoConference;
+	}
+
+	convertAppVideoConference(call: VideoConference): IVideoConference {
+		return {
+			...call,
+		} as IVideoConference;
+	}
+}
diff --git a/apps/meteor/app/apps/server/orchestrator.js b/apps/meteor/app/apps/server/orchestrator.js
index 85a46fdc4a16ee9a9434db95050fbde1e49c3b9c..9d3bdf7a35a990cc804041ec6ba34c7de2018e62 100644
--- a/apps/meteor/app/apps/server/orchestrator.js
+++ b/apps/meteor/app/apps/server/orchestrator.js
@@ -8,7 +8,13 @@ import { AppsLogsModel, AppsModel, AppsPersistenceModel } from '../../models/ser
 import { settings, settingsRegistry } from '../../settings/server';
 import { RealAppBridges } from './bridges';
 import { AppMethods, AppServerNotifier, AppsRestApi, AppUIKitInteractionApi } from './communication';
-import { AppMessagesConverter, AppRoomsConverter, AppSettingsConverter, AppUsersConverter } from './converters';
+import {
+	AppMessagesConverter,
+	AppRoomsConverter,
+	AppSettingsConverter,
+	AppUsersConverter,
+	AppVideoConferencesConverter,
+} from './converters';
 import { AppDepartmentsConverter } from './converters/departments';
 import { AppUploadsConverter } from './converters/uploads';
 import { AppVisitorsConverter } from './converters/visitors';
@@ -54,6 +60,7 @@ export class AppServerOrchestrator {
 		this._converters.set('visitors', new AppVisitorsConverter(this));
 		this._converters.set('departments', new AppDepartmentsConverter(this));
 		this._converters.set('uploads', new AppUploadsConverter(this));
+		this._converters.set('videoConferences', new AppVideoConferencesConverter(this));
 
 		this._bridges = new RealAppBridges(this);
 
diff --git a/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js b/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js
deleted file mode 100644
index 8cb3f4d447c46455f97d4635328ca653d094b2cd..0000000000000000000000000000000000000000
--- a/apps/meteor/app/bigbluebutton/server/bigbluebutton-api.js
+++ /dev/null
@@ -1,188 +0,0 @@
-/* eslint-disable */
-import crypto from 'crypto';
-import { SystemLogger } from '../../../server/lib/logger/system';
-
-var BigBlueButtonApi, filterCustomParameters, include, noChecksumMethods,
-	__indexOf = [].indexOf || function (item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
-
-BigBlueButtonApi = (function () {
-	function BigBlueButtonApi(url, salt, debug, opts) {
-		var _base;
-		if (opts == null) {
-			opts = {};
-		}
-		this.url = url;
-		this.salt = salt;
-		this.opts = opts;
-		if ((_base = this.opts).shaType == null) {
-			_base.shaType = 'sha1';
-		}
-	}
-
-	BigBlueButtonApi.prototype.availableApiCalls = function () {
-		return ['/', 'create', 'join', 'isMeetingRunning', 'getMeetingInfo', 'end', 'getMeetings', 'getDefaultConfigXML', 'setConfigXML', 'enter', 'configXML', 'signOut', 'getRecordings', 'publishRecordings', 'deleteRecordings', 'updateRecordings', 'hooks/create'];
-	};
-
-	BigBlueButtonApi.prototype.urlParamsFor = function (param) {
-		switch (param) {
-			case "create":
-				return [["meetingID", true], ["name", true], ["attendeePW", false], ["moderatorPW", false], ["welcome", false], ["dialNumber", false], ["voiceBridge", false], ["webVoice", false], ["logoutURL", false], ["maxParticipants", false], ["record", false], ["duration", false], ["moderatorOnlyMessage", false], ["autoStartRecording", false], ["allowStartStopRecording", false], [/meta_\w+/, false]];
-			case "join":
-				return [["fullName", true], ["meetingID", true], ["password", true], ["createTime", false], ["userID", false], ["webVoiceConf", false], ["configToken", false], ["avatarURL", false], ["redirect", false], ["clientURL", false]];
-			case "isMeetingRunning":
-				return [["meetingID", true]];
-			case "end":
-				return [["meetingID", true], ["password", true]];
-			case "getMeetingInfo":
-				return [["meetingID", true], ["password", true]];
-			case "getRecordings":
-				return [["meetingID", false], ["recordID", false], ["state", false], [/meta_\w+/, false]];
-			case "publishRecordings":
-				return [["recordID", true], ["publish", true]];
-			case "deleteRecordings":
-				return [["recordID", true]];
-			case "updateRecordings":
-				return [["recordID", true], [/meta_\w+/, false]];
-			case "hooks/create":
-				return [["callbackURL", false], ["meetingID", false]];
-		}
-	};
-
-	BigBlueButtonApi.prototype.filterParams = function (params, method) {
-		var filters, r;
-		filters = this.urlParamsFor(method);
-		if ((filters == null) || filters.length === 0) {
-			({});
-		} else {
-			r = include(params, function (key, value) {
-				var filter, _i, _len;
-				for (_i = 0, _len = filters.length; _i < _len; _i++) {
-					filter = filters[_i];
-					if (filter[0] instanceof RegExp) {
-						if (key.match(filter[0]) || key.match(/^custom_/)) {
-							return true;
-						}
-					} else {
-						if (key.match("^" + filter[0] + "$") || key.match(/^custom_/)) {
-							return true;
-						}
-					}
-				}
-				return false;
-			});
-		}
-		return filterCustomParameters(r);
-	};
-
-	BigBlueButtonApi.prototype.urlFor = function (method, params, filter) {
-		var checksum, key, keys, param, paramList, property, query, sep, url, _i, _len;
-		if (filter == null) {
-			filter = true;
-		}
-		SystemLogger.debug("Generating URL for", method);
-		if (filter) {
-			params = this.filterParams(params, method);
-		} else {
-			params = filterCustomParameters(params);
-		}
-		url = this.url;
-		paramList = [];
-		if (params != null) {
-			keys = [];
-			for (property in params) {
-				keys.push(property);
-			}
-			keys = keys.sort();
-			for (_i = 0, _len = keys.length; _i < _len; _i++) {
-				key = keys[_i];
-				if (key != null) {
-					param = params[key];
-				}
-				if (param != null) {
-					paramList.push("" + (this.encodeForUrl(key)) + "=" + (this.encodeForUrl(param)));
-				}
-			}
-			if (paramList.length > 0) {
-				query = paramList.join("&");
-			}
-		} else {
-			query = '';
-		}
-		checksum = this.checksum(method, query);
-		if (paramList.length > 0) {
-			query = "" + method + "?" + query;
-			sep = '&';
-		} else {
-			if (method !== '/') {
-				query = method;
-			}
-			sep = '?';
-		}
-		if (__indexOf.call(noChecksumMethods(), method) < 0) {
-			query = "" + query + sep + "checksum=" + checksum;
-		}
-		return "" + url + "/" + query;
-	};
-
-	BigBlueButtonApi.prototype.checksum = function (method, query) {
-		var c, shaObj, str;
-		query || (query = "");
-		SystemLogger.debug("- Calculating the checksum using: '" + method + "', '" + query + "', '" + this.salt + "'");
-		str = method + query + this.salt;
-		if (this.opts.shaType === 'sha256') {
-			shaObj = crypto.createHash('sha256', "TEXT")
-		} else {
-			shaObj = crypto.createHash('sha1', "TEXT")
-		}
-		shaObj.update(str);
-		c = shaObj.digest('hex');
-		SystemLogger.debug("- Checksum calculated:", c);
-		return c;
-	};
-
-	BigBlueButtonApi.prototype.encodeForUrl = function (value) {
-		return encodeURIComponent(value).replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
-	};
-
-	BigBlueButtonApi.prototype.setMobileProtocol = function (url) {
-		return url.replace(/http[s]?\:\/\//, "bigbluebutton://");
-	};
-
-	return BigBlueButtonApi;
-
-})();
-
-include = function (input, _function) {
-	var key, value, _match, _obj;
-	_obj = new Object;
-	_match = null;
-	for (key in input) {
-		value = input[key];
-		if (_function.call(input, key, value)) {
-			_obj[key] = value;
-		}
-	}
-	return _obj;
-};
-
-export default BigBlueButtonApi;
-
-filterCustomParameters = function (params) {
-	var key, v;
-	for (key in params) {
-		v = params[key];
-		if (key.match(/^custom_/)) {
-			params[key.replace(/^custom_/, "")] = v;
-		}
-	}
-	for (key in params) {
-		if (key.match(/^custom_/)) {
-			delete params[key];
-		}
-	}
-	return params;
-};
-
-noChecksumMethods = function () {
-	return ['setConfigXML', '/', 'enter', 'configXML', 'signOut'];
-};
diff --git a/apps/meteor/app/bigbluebutton/server/index.js b/apps/meteor/app/bigbluebutton/server/index.js
deleted file mode 100644
index b6be696a20bd9ef3346f0e3c8fab595835a11565..0000000000000000000000000000000000000000
--- a/apps/meteor/app/bigbluebutton/server/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './bigbluebutton-api';
diff --git a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js
index 07c83303cb326a444895db4d31be5c5f5208c96c..29a838d5bd3aee8c19ea4ce4f3e1c58efc4d9e73 100644
--- a/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js
+++ b/apps/meteor/app/custom-sounds/client/lib/CustomSounds.js
@@ -10,6 +10,7 @@ const getCustomSoundId = (sound) => `custom-sound-${sound}`;
 class CustomSoundsClass {
 	constructor() {
 		this.list = new ReactiveVar({});
+		this.add({ _id: 'calling', name: 'Calling', extension: 'mp3', src: getURL('sounds/calling.mp3') });
 		this.add({ _id: 'chime', name: 'Chime', extension: 'mp3', src: getURL('sounds/chime.mp3') });
 		this.add({ _id: 'door', name: 'Door', extension: 'mp3', src: getURL('sounds/door.mp3') });
 		this.add({ _id: 'beep', name: 'Beep', extension: 'mp3', src: getURL('sounds/beep.mp3') });
@@ -108,6 +109,12 @@ class CustomSoundsClass {
 			audio.currentTime = 0;
 		}
 	};
+
+	isPlaying = (sound) => {
+		const audio = document.querySelector(`#${getCustomSoundId(sound)}`);
+
+		return audio && audio.duration > 0 && !audio.paused;
+	};
 }
 
 export const CustomSounds = new CustomSoundsClass();
diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts
index 903d942dba19a531d13db902a6ae4d8a5fbdb4fb..db6b3eed7889b6877502a9d164fa8630e1808b69 100644
--- a/apps/meteor/app/lib/server/functions/createRoom.ts
+++ b/apps/meteor/app/lib/server/functions/createRoom.ts
@@ -59,7 +59,7 @@ export const createRoom = function <T extends RoomType>(
 
 	const now = new Date();
 
-	const roomProps: Omit<IRoom, '_id' | '_updatedAt' | 'uids' | 'jitsiTimeout' | 'autoTranslateLanguage'> = {
+	const roomProps: Omit<IRoom, '_id' | '_updatedAt' | 'uids' | 'autoTranslateLanguage'> = {
 		fname: name,
 		...extraData,
 		name: getValidRoomName(name.trim(), undefined, {
diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
index 7ba891c38a357f64df8c4b37bbad4a4b8d93b69e..470b13b3d82ec448bef96f03452a2052bca83b09 100644
--- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
+++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts
@@ -1,5 +1,6 @@
 /* eslint-disable @typescript-eslint/explicit-function-return-type */
 import type { IMessage } from '@rocket.chat/core-typings';
+import { VideoConference } from '@rocket.chat/models';
 
 import { _setUsername } from './setUsername';
 import { _setRealName } from './setRealName';
@@ -87,6 +88,9 @@ export function saveUserIdentity({
 
 			// update name and fname of group direct messages
 			updateGroupDMsName(user);
+
+			// update name and username of users on video conferences
+			Promise.await(VideoConference.updateUserReferences(user._id, username || previousUsername, name || previousName));
 		}
 	}
 
diff --git a/apps/meteor/app/livechat/client/index.js b/apps/meteor/app/livechat/client/index.js
index 19a5118b809f4d5660292ac0052e286f19315f51..23da09e08a29012b9c77fbc653f05568e1ab4536 100644
--- a/apps/meteor/app/livechat/client/index.js
+++ b/apps/meteor/app/livechat/client/index.js
@@ -6,4 +6,3 @@ import './startup/notifyUnreadRooms';
 import './views/app/dialog/closeRoom';
 import './stylesheets/livechat.css';
 import './externalFrame';
-import './lib/messageTypes';
diff --git a/apps/meteor/app/livechat/client/lib/messageTypes.js b/apps/meteor/app/livechat/client/lib/messageTypes.js
deleted file mode 100644
index 9cf21362d9e139c205e887511a2e4bda0e6f44d1..0000000000000000000000000000000000000000
--- a/apps/meteor/app/livechat/client/lib/messageTypes.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { actionLinks } from '../../../action-links/client';
-
-actionLinks.register('createLivechatCall', function (message, params, instance) {
-	instance.tabBar.open('video');
-});
diff --git a/apps/meteor/app/livechat/server/api/lib/livechat.js b/apps/meteor/app/livechat/server/api/lib/livechat.js
index 92de8a4ffa9b1782db45a6859f59a60fbff2c604..507f06d687ede5dd6c20138de1cbeb1ece23af18 100644
--- a/apps/meteor/app/livechat/server/api/lib/livechat.js
+++ b/apps/meteor/app/livechat/server/api/lib/livechat.js
@@ -121,7 +121,7 @@ export async function settings({ businessUnit = '' } = {}) {
 			nameFieldRegistrationForm: initSettings.Livechat_name_field_registration_form,
 			emailFieldRegistrationForm: initSettings.Livechat_email_field_registration_form,
 			displayOfflineForm: initSettings.Livechat_display_offline_form,
-			videoCall: initSettings.Omnichannel_call_provider === 'Jitsi' && initSettings.Jitsi_Enabled === true,
+			videoCall: initSettings.Omnichannel_call_provider === 'Jitsi',
 			fileUpload: initSettings.Livechat_fileupload_enabled && initSettings.FileUpload_Enabled,
 			language: initSettings.Language,
 			transcript: initSettings.Livechat_enable_transcript,
@@ -155,8 +155,8 @@ export async function settings({ businessUnit = '' } = {}) {
 					},
 				],
 				jitsi: [
-					{ icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall' },
-					{ icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall' },
+					{ icon: 'icon-videocam', i18nLabel: 'Accept' },
+					{ icon: 'icon-cancel', i18nLabel: 'Decline' },
 				],
 			},
 		},
diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.js
index 60a26a7f4682d20b8ae2c14e174d7a12d96e3787..ca13d3278bcd5e2b03b800c720129b728190b67a 100644
--- a/apps/meteor/app/livechat/server/api/v1/videoCall.js
+++ b/apps/meteor/app/livechat/server/api/v1/videoCall.js
@@ -1,76 +1,17 @@
 import { Meteor } from 'meteor/meteor';
 import { Match, check } from 'meteor/check';
-import { Random } from 'meteor/random';
 import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-import { OmnichannelSourceType } from '@rocket.chat/core-typings';
 
 import { Messages, Rooms, Settings } from '../../../../models';
 import { settings as rcSettings } from '../../../../settings/server';
 import { API } from '../../../../api/server';
-import { findGuest, getRoom, settings } from '../lib/livechat';
+import { settings } from '../lib/livechat';
 import { hasPermission, canSendMessage } from '../../../../authorization';
 import { Livechat } from '../../lib/Livechat';
 import { Logger } from '../../../../logger';
 
 const logger = new Logger('LivechatVideoCallApi');
 
-API.v1.addRoute('livechat/video.call/:token', {
-	async get() {
-		try {
-			check(this.urlParams, {
-				token: String,
-			});
-
-			check(this.queryParams, {
-				rid: Match.Maybe(String),
-			});
-
-			const { token } = this.urlParams;
-
-			const guest = await findGuest(token);
-			if (!guest) {
-				throw new Meteor.Error('invalid-token');
-			}
-
-			const rid = this.queryParams.rid || Random.id();
-			const roomInfo = {
-				jitsiTimeout: new Date(Date.now() + 3600 * 1000),
-				source: {
-					type: OmnichannelSourceType.API,
-					alias: 'video-call',
-				},
-			};
-			const { room } = await getRoom({ guest, rid, roomInfo });
-			const config = await settings();
-			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.jitsi,
-			});
-			let rname;
-			if (rcSettings.get('Jitsi_URL_Room_Hash')) {
-				rname = rcSettings.get('uniqueID') + rid;
-			} else {
-				rname = encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name);
-			}
-			const videoCall = {
-				rid,
-				domain: rcSettings.get('Jitsi_Domain'),
-				provider: 'jitsi',
-				room: rcSettings.get('Jitsi_URL_Room_Prefix') + rname + rcSettings.get('Jitsi_URL_Room_Suffix'),
-				timeout: new Date(Date.now() + 3600 * 1000),
-			};
-
-			return API.v1.success(this.deprecationWarning({ videoCall }));
-		} catch (e) {
-			logger.error(e);
-			return API.v1.failure(e);
-		}
-	},
-});
-
 API.v1.addRoute(
 	'livechat/webrtc.call',
 	{ authRequired: true },
diff --git a/apps/meteor/app/livechat/server/index.js b/apps/meteor/app/livechat/server/index.js
index e681d8010ae49f8306c9888fbf3808cc02e68523..c44390cd6de6840cf2dae68a65817f5e4663d669 100644
--- a/apps/meteor/app/livechat/server/index.js
+++ b/apps/meteor/app/livechat/server/index.js
@@ -59,7 +59,6 @@ import './methods/sendFileLivechatMessage';
 import './methods/sendOfflineMessage';
 import './methods/setCustomField';
 import './methods/setDepartmentForVisitor';
-import './methods/startVideoCall';
 import './methods/transfer';
 import './methods/webhookTest';
 import './methods/setUpConnection';
@@ -81,7 +80,6 @@ import './sendMessageBySMS';
 import './api';
 import './api/rest';
 import './externalFrame';
-import './lib/messageTypes';
 import './methods/saveBusinessHour';
 
 export { Livechat } from './lib/Livechat';
diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js
index 424847dddb709a24bf1774ad852e5d88b831bd4c..d365e7c95fd0be0068e92790cb7fa401172702bd 100644
--- a/apps/meteor/app/livechat/server/lib/Livechat.js
+++ b/apps/meteor/app/livechat/server/lib/Livechat.js
@@ -42,6 +42,7 @@ import { businessHourManager } from '../business-hour';
 import notifications from '../../../notifications/server/lib/Notifications';
 import { addUserRoles } from '../../../../server/lib/roles/addUserRoles';
 import { removeUserFromRoles } from '../../../../server/lib/roles/removeUserFromRoles';
+import { VideoConf } from '../../../../server/sdk';
 
 const logger = new Logger('Livechat');
 
@@ -553,7 +554,6 @@ export const Livechat = {
 			'Livechat_offline_form_unavailable',
 			'Livechat_display_offline_form',
 			'Omnichannel_call_provider',
-			'Jitsi_Enabled',
 			'Language',
 			'Livechat_enable_transcript',
 			'Livechat_transcript_message',
@@ -1413,6 +1413,10 @@ export const Livechat = {
 	updateCallStatus(callId, rid, status, user) {
 		Rooms.setCallStatus(rid, status);
 		if (status === 'ended' || status === 'declined') {
+			if (Promise.await(VideoConf.declineLivechatCall(callId))) {
+				return;
+			}
+
 			return updateMessage({ _id: callId, msg: status, actionLinks: [], webRtcCallEndTs: new Date() }, user);
 		}
 	},
diff --git a/apps/meteor/app/livechat/server/lib/messageTypes.js b/apps/meteor/app/livechat/server/lib/messageTypes.js
deleted file mode 100644
index dadb7e69594cb598ae749d21095bb31fe4377b6e..0000000000000000000000000000000000000000
--- a/apps/meteor/app/livechat/server/lib/messageTypes.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-
-import { actionLinks } from '../../../action-links/server';
-import { api } from '../../../../server/sdk/api';
-import { Messages, LivechatRooms } from '../../../models/server';
-import { settings } from '../../../settings/server';
-import { Livechat } from './Livechat';
-
-actionLinks.register('denyLivechatCall', function (message /* , params*/) {
-	const user = Meteor.user();
-
-	Messages.createWithTypeRoomIdMessageAndUser('command', message.rid, 'endCall', user);
-	api.broadcast('notify.deleteMessage', message.rid, { _id: message._id });
-
-	const language = user.language || settings.get('Language') || 'en';
-
-	Livechat.closeRoom({
-		user,
-		room: LivechatRooms.findOneById(message.rid),
-		comment: TAPi18n.__('Videocall_declined', { lng: language }),
-	});
-	Meteor.defer(() => {
-		Messages.setHiddenById(message._id);
-	});
-});
diff --git a/apps/meteor/app/livechat/server/methods/startVideoCall.js b/apps/meteor/app/livechat/server/methods/startVideoCall.js
deleted file mode 100644
index b50e24729cf9957db492b9e7e88cf842b465883a..0000000000000000000000000000000000000000
--- a/apps/meteor/app/livechat/server/methods/startVideoCall.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Random } from 'meteor/random';
-import { OmnichannelSourceType } from '@rocket.chat/core-typings';
-
-import { Messages } from '../../../models/server';
-import { settings } from '../../../settings/server';
-import { Livechat } from '../lib/Livechat';
-import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
-
-Meteor.methods({
-	async 'livechat:startVideoCall'(roomId) {
-		methodDeprecationLogger.warn('livechat:startVideoCall will be deprecated in future versions of Rocket.Chat');
-		if (!Meteor.userId()) {
-			throw new Meteor.Error('error-not-authorized', 'Not authorized', {
-				method: 'livechat:closeByVisitor',
-			});
-		}
-
-		const guest = Meteor.user();
-
-		const message = {
-			_id: Random.id(),
-			rid: roomId || Random.id(),
-			msg: '',
-			ts: new Date(),
-		};
-
-		const roomInfo = {
-			jitsiTimeout: new Date(Date.now() + 3600 * 1000),
-			source: {
-				type: OmnichannelSourceType.API,
-				alias: 'video-call',
-			},
-		};
-
-		const room = await Livechat.getRoom(guest, message, roomInfo);
-		message.rid = room._id;
-
-		Messages.createWithTypeRoomIdMessageAndUser('livechat_video_call', room._id, '', guest, {
-			actionLinks: [
-				{ icon: 'icon-videocam', i18nLabel: 'Accept', method_id: 'createLivechatCall', params: '' },
-				{ icon: 'icon-cancel', i18nLabel: 'Decline', method_id: 'denyLivechatCall', params: '' },
-			],
-		});
-
-		let rname;
-		if (settings.get('Jitsi_URL_Room_Hash')) {
-			rname = settings.get('uniqueID') + roomId;
-		} else {
-			rname = encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name);
-		}
-		return {
-			roomId: room._id,
-			domain: settings.get('Jitsi_Domain'),
-			jitsiRoom: settings.get('Jitsi_URL_Room_Prefix') + rname + settings.get('Jitsi_URL_Room_Suffix'),
-		};
-	},
-});
diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js
index 66f8e8366f037f7d62209867353d674b0b413be6..e0c6df1e814b560885dc233b7e6a2050157b3967 100644
--- a/apps/meteor/app/models/server/models/Rooms.js
+++ b/apps/meteor/app/models/server/models/Rooms.js
@@ -51,20 +51,6 @@ export class Rooms extends Base {
 		return this.findOne(query, options);
 	}
 
-	setJitsiTimeout(_id, time) {
-		const query = {
-			_id,
-		};
-
-		const update = {
-			$set: {
-				jitsiTimeout: time,
-			},
-		};
-
-		return this.update(query, update);
-	}
-
 	setCallStatus(_id, status) {
 		const query = {
 			_id,
diff --git a/apps/meteor/app/settings/server/functions/validateSetting.ts b/apps/meteor/app/settings/server/functions/validateSetting.ts
index 09db6d2cb74a20f9e1b94a5cad849689102d6c11..659bb12225c4d1d0af4c881163c1e695436b99b2 100644
--- a/apps/meteor/app/settings/server/functions/validateSetting.ts
+++ b/apps/meteor/app/settings/server/functions/validateSetting.ts
@@ -37,8 +37,9 @@ export const validateSetting = <T extends ISetting>(_id: T['_id'], type: T['type
 			}
 			break;
 		case 'select':
+		case 'lookup':
 			if (typeof value !== 'string' && typeof value !== 'number') {
-				throw new Error(`Setting ${_id} is of type select but got ${typeof value}`);
+				throw new Error(`Setting ${_id} is of type ${type} but got ${typeof value}`);
 			}
 			break;
 		case 'date':
diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts
index 61efbfc4ceff966f499154ceaf95d0def65ed5d0..26cd858fd9caec59c9d275b0d9958fc012a28e74 100644
--- a/apps/meteor/app/statistics/server/lib/statistics.ts
+++ b/apps/meteor/app/statistics/server/lib/statistics.ts
@@ -33,7 +33,7 @@ import { getAppsStatistics } from './getAppsStatistics';
 import { getImporterStatistics } from './getImporterStatistics';
 import { getServicesStatistics } from './getServicesStatistics';
 import { getStatistics as getEnterpriseStatistics } from '../../../../ee/app/license/server';
-import { Analytics, Team } from '../../../../server/sdk';
+import { Analytics, Team, VideoConf } from '../../../../server/sdk';
 import { getSettingsStatistics } from '../../../../server/lib/statistics/getSettingsStatistics';
 
 const wizardFields = ['Organization_Type', 'Industry', 'Size', 'Country', 'Language', 'Server_Type', 'Register_Server'];
@@ -400,6 +400,7 @@ export const statistics = {
 		statistics.apps = getAppsStatistics();
 		statistics.services = getServicesStatistics();
 		statistics.importer = getImporterStatistics();
+		statistics.videoConf = await VideoConf.getStatistics();
 
 		// If getSettingsStatistics() returns an error, save as empty object.
 		statsPms.push(
diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js
index e5d289134f6306236ffe223b8e2d78cd244a5b5d..422f4a2fdda7c4aadde7b97799e8fd8fabfd17db 100644
--- a/apps/meteor/app/ui-sidenav/client/roomList.js
+++ b/apps/meteor/app/ui-sidenav/client/roomList.js
@@ -135,9 +135,9 @@ const mergeSubRoom = (subscription) => {
 			uids: 1,
 			streamingOptions: 1,
 			usernames: 1,
+			usersCount: 1,
 			topic: 1,
 			encrypted: 1,
-			jitsiTimeout: 1,
 			// autoTranslate: 1,
 			// autoTranslateLanguage: 1,
 			description: 1,
@@ -189,8 +189,7 @@ const mergeSubRoom = (subscription) => {
 		teamMain,
 		uids,
 		usernames,
-		jitsiTimeout,
-
+		usersCount,
 		v,
 		transcriptRequest,
 		servedBy,
@@ -228,8 +227,7 @@ const mergeSubRoom = (subscription) => {
 		teamMain,
 		uids,
 		usernames,
-		jitsiTimeout,
-
+		usersCount,
 		v,
 		transcriptRequest,
 		servedBy,
@@ -272,8 +270,7 @@ const mergeRoomSub = (room) => {
 		teamMain,
 		uids,
 		usernames,
-		jitsiTimeout,
-
+		usersCount,
 		v,
 		transcriptRequest,
 		servedBy,
@@ -310,6 +307,7 @@ const mergeRoomSub = (room) => {
 				retention,
 				uids,
 				usernames,
+				usersCount,
 				lastMessage,
 				streamingOptions,
 				teamId,
@@ -327,7 +325,6 @@ const mergeRoomSub = (room) => {
 				priorityId,
 				livechatData,
 				departmentId,
-				jitsiTimeout,
 				ts,
 				source,
 				queuedAt,
diff --git a/apps/meteor/app/videobridge/client/actionLink.js b/apps/meteor/app/videobridge/client/actionLink.js
deleted file mode 100644
index 8825a64f109d76b49f1ee8eea2aaeee8baa17583..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/client/actionLink.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Session } from 'meteor/session';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-
-import { actionLinks } from '../../action-links/client';
-import { Rooms } from '../../models/client';
-import { dispatchToastMessage } from '../../../client/lib/toast';
-import { APIClient } from '../../utils/client';
-
-actionLinks.register('joinJitsiCall', function (message, params, instance) {
-	const rid = Session.get('openedRoom');
-	if (!rid) {
-		return;
-	}
-
-	const room = Rooms.findOne({ _id: rid });
-	const username = Meteor.user()?.username;
-
-	if (!room) {
-		dispatchToastMessage({ type: 'info', message: TAPi18n.__('Call Already Ended', '') });
-		return;
-	}
-
-	if (room?.muted?.includes(username)) {
-		dispatchToastMessage({ type: 'error', message: TAPi18n.__('You_have_been_muted', '') });
-		return;
-	}
-
-	const clickTime = new Date();
-	const jitsiTimeout = new Date(room.jitsiTimeout);
-
-	APIClient.post('/v1/statistics.telemetry', {
-		params: [{ eventName: 'updateCounter', timestamp: Date.now(), settingsId: 'Jitsi_Click_To_Join_Count' }],
-	});
-
-	if (jitsiTimeout > clickTime) {
-		if (instance instanceof Function) {
-			instance('video');
-		} else {
-			instance.tabBar.open('video');
-		}
-
-		return;
-	}
-
-	// Get updated room info from the server to check if the call is still happening
-	Meteor.call('getRoomById', rid, (err, result) => {
-		if (err) {
-			throw err;
-		}
-
-		// If the openedRoom has changed, abort
-		if (rid !== Session.get('openedRoom')) {
-			return;
-		}
-
-		if (result?.jitsiTimeout && result.jitsiTimeout instanceof Date && result.jitsiTimeout > clickTime) {
-			if (instance instanceof Function) {
-				instance('video');
-			} else {
-				instance.tabBar.open('video');
-			}
-			return;
-		}
-
-		dispatchToastMessage({ type: 'info', message: TAPi18n.__('Call Already Ended', '') });
-	});
-});
diff --git a/apps/meteor/app/videobridge/client/index.js b/apps/meteor/app/videobridge/client/index.js
deleted file mode 100644
index e1d57007880e29e1629357c24ce54c7115637731..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/client/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import './views/bbbLiveView.html';
-import './tabBar';
-import './actionLink';
-import '../lib/messageType';
diff --git a/apps/meteor/app/videobridge/client/index.ts b/apps/meteor/app/videobridge/client/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd95da0a7d921332b73621568d29bd01513bf085
--- /dev/null
+++ b/apps/meteor/app/videobridge/client/index.ts
@@ -0,0 +1 @@
+import './tabBar';
diff --git a/apps/meteor/app/videobridge/client/tabBar.tsx b/apps/meteor/app/videobridge/client/tabBar.tsx
index 06f85ca80614f7d2661f5c74c3f29ff1d274a7ce..8590bf74377e7ec767dbd4dabe99c6258baa0a0d 100644
--- a/apps/meteor/app/videobridge/client/tabBar.tsx
+++ b/apps/meteor/app/videobridge/client/tabBar.tsx
@@ -1,119 +1,90 @@
-import React, { useMemo, lazy, ReactNode } from 'react';
-import { useStableArray } from '@rocket.chat/fuselage-hooks';
-import { Option, Badge } from '@rocket.chat/fuselage';
-import { useUser, useSetting, useTranslation } from '@rocket.chat/ui-contexts';
+import { useMemo, lazy } from 'react';
+import { useStableArray, useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useSetting, useUser } from '@rocket.chat/ui-contexts';
 
+import { useVideoConfDispatchOutgoing, useVideoConfIsCalling, useVideoConfIsRinging } from '../../../client/contexts/VideoConfContext';
 import { addAction, ToolboxActionConfig } from '../../../client/views/room/lib/Toolbox';
-import Header from '../../../client/components/Header';
+import { VideoConfManager } from '../../../client/lib/VideoConfManager';
+import { useVideoConfWarning } from '../../../client/views/room/contextualBar/VideoConference/useVideoConfWarning';
+import { useHasLicenseModule } from '../../../ee/client/hooks/useHasLicenseModule';
 
-const templateBBB = lazy(() => import('../../../client/views/room/contextualBar/VideoConference/BBB'));
-
-addAction('bbb_video', ({ room }) => {
-	const enabled = useSetting('bigbluebutton_Enabled');
-	const t = useTranslation();
-
-	const live = room?.streamingOptions && room.streamingOptions.type === 'call';
-
-	const enabledDirect = useSetting('bigbluebutton_enable_d');
-	const enabledGroup = useSetting('bigbluebutton_enable_p');
-	const enabledChannel = useSetting('bigbluebutton_enable_c');
-	const enabledTeams = useSetting('bigbluebutton_enable_teams');
-
-	const groups = useStableArray(
-		[enabledDirect && 'direct', 'direct_multiple', enabledGroup && 'group', enabledTeams && 'team', enabledChannel && 'channel'].filter(
-			Boolean,
-		) as ToolboxActionConfig['groups'],
-	);
-	const user = useUser();
-	const username = user ? user.username : '';
-	const enableOption = enabled && (!username || !room.muted?.includes(username));
+addAction('calls', () => {
+	const hasLicense = useHasLicenseModule('videoconference-enterprise');
 
 	return useMemo(
 		() =>
-			enableOption
+			hasLicense
 				? {
-						groups,
-						id: 'bbb_video',
-						title: 'BBB_Video_Call',
+						groups: ['channel', 'group', 'team'],
+						id: 'calls',
 						icon: 'phone',
-						template: templateBBB,
-						order: live ? -1 : 4,
-						renderAction: (props): ReactNode => (
-							<Header.ToolBoxAction {...props}>
-								{live ? (
-									<Header.Badge title={t('Started_a_video_call')} variant='primary'>
-										!
-									</Header.Badge>
-								) : null}
-							</Header.ToolBoxAction>
-						),
-						renderOption: ({ label: { title, icon }, ...props }: any): ReactNode => (
-							<Option label={title} title={title} icon={icon} {...props}>
-								<Badge title={t('Started_a_video_call')} variant='primary'>
-									!
-								</Badge>
-							</Option>
-						),
+						title: 'Calls',
+						template: lazy(() => import('../../../client/views/room/contextualBar/VideoConference/VideoConfList')),
+						order: 999,
 				  }
 				: null,
-		[enableOption, groups, live, t],
+		[hasLicense],
 	);
 });
 
-const templateJitsi = lazy(() => import('../../../client/views/room/contextualBar/VideoConference/Jitsi'));
+addAction('start-call', ({ room }) => {
+	const user = useUser();
+	const dispatchWarning = useVideoConfWarning();
+	const dispatchPopup = useVideoConfDispatchOutgoing();
+	const isCalling = useVideoConfIsCalling();
+	const isRinging = useVideoConfIsRinging();
 
-addAction('video', ({ room }) => {
-	const enabled = useSetting('Jitsi_Enabled');
-	const t = useTranslation();
+	const ownUser = room.uids && room.uids.length === 1;
 
-	const enabledChannel = useSetting('Jitsi_Enable_Channels');
-	const enabledTeams = useSetting('Jitsi_Enable_Teams');
+	// Only disable video conf if the settings are explicitly FALSE - any falsy value counts as true
+	const enabledDMs = useSetting('VideoConf_Enable_DMs') !== false;
+	const enabledChannel = useSetting('VideoConf_Enable_Channels') !== false;
+	const enabledTeams = useSetting('VideoConf_Enable_Teams') !== false;
+	const enabledGroups = useSetting('VideoConf_Enable_Groups') !== false;
 	const enabledLiveChat = useSetting('Omnichannel_call_provider') === 'Jitsi';
 
+	const live = room?.streamingOptions && room.streamingOptions.type === 'call';
+	const enabled = enabledDMs || enabledChannel || enabledTeams || enabledGroups || enabledLiveChat;
+
+	const enableOption = enabled && (!user?.username || !room.muted?.includes(user.username));
+
 	const groups = useStableArray(
-		['direct', 'direct_multiple', 'group', enabledLiveChat && 'live', enabledTeams && 'team', enabledChannel && 'channel'].filter(
-			Boolean,
-		) as ToolboxActionConfig['groups'],
+		[
+			enabledDMs && 'direct',
+			enabledDMs && 'direct_multiple',
+			enabledGroups && 'group',
+			enabledLiveChat && 'live',
+			enabledTeams && 'team',
+			enabledChannel && 'channel',
+		].filter(Boolean) as ToolboxActionConfig['groups'],
 	);
 
-	const currentTime = new Date().getTime();
-	const jitsiTimeout = new Date(room?.jitsiTimeout || currentTime).getTime();
-	const live = jitsiTimeout > currentTime || null;
-	const user = useUser();
-	const username = user ? user.username : '';
-	const enableOption = enabled && (!username || !room.muted?.includes(username));
+	const handleOpenVideoConf = useMutableCallback(async (): Promise<void> => {
+		if (isCalling || isRinging) {
+			return;
+		}
+
+		try {
+			await VideoConfManager.loadCapabilities();
+			dispatchPopup({ rid: room._id });
+		} catch (error) {
+			dispatchWarning(error.error);
+		}
+	});
 
 	return useMemo(
 		() =>
-			enableOption
+			enableOption && !ownUser
 				? {
 						groups,
-						id: 'video',
+						id: 'start-call',
 						title: 'Call',
 						icon: 'phone',
-						template: templateJitsi,
+						action: handleOpenVideoConf,
 						full: true,
 						order: live ? -1 : 4,
-						renderAction: (props): ReactNode => (
-							<Header.ToolBoxAction {...props}>
-								{live && (
-									<Header.Badge title={t('Started_a_video_call')} variant='primary'>
-										!
-									</Header.Badge>
-								)}
-							</Header.ToolBoxAction>
-						),
-						renderOption: ({ label: { title, icon }, ...props }: any): ReactNode => (
-							<Option label={title} title={title} icon={icon} {...props}>
-								{live && (
-									<Badge title={t('Started_a_video_call')} variant='primary'>
-										!
-									</Badge>
-								)}
-							</Option>
-						),
 				  }
 				: null,
-		[enableOption, groups, live, t],
+		[groups, enableOption, live, handleOpenVideoConf, ownUser],
 	);
 });
diff --git a/apps/meteor/app/videobridge/client/views/bbbLiveView.html b/apps/meteor/app/videobridge/client/views/bbbLiveView.html
deleted file mode 100644
index 47c0f4fcd0a3bde855495a53f96f8ac3536aec42..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/client/views/bbbLiveView.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<template name="bbbLiveView">
-	<iframe allowfullscreen="true" webkitallowfullscreen="true" mozallowfullscreen="true"  allow="microphone *; camera *; display-capture *" src="{{source}}" width="380" height="400" frameborder="0"></iframe>
-</template>
diff --git a/apps/meteor/app/videobridge/constants.js b/apps/meteor/app/videobridge/constants.js
deleted file mode 100644
index 14cec201fbcbf231a5b507f07cb1497f61a4d28a..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/constants.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export const TIMEOUT = 30 * 1000;
-export const HEARTBEAT = TIMEOUT / 3;
-export const DEBOUNCE = HEARTBEAT / 2;
diff --git a/apps/meteor/app/videobridge/lib/messageType.js b/apps/meteor/app/videobridge/lib/messageType.js
deleted file mode 100644
index 8f77cb61e9f6d1292ff0a1d0ec3007df50f3db78..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/lib/messageType.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-
-import { MessageTypes } from '../../ui-utils';
-
-Meteor.startup(function () {
-	MessageTypes.registerType({
-		id: 'jitsi_call_started',
-		system: true,
-		message: TAPi18n.__('Started_a_video_call'),
-	});
-});
diff --git a/apps/meteor/app/videobridge/server/actionLink.js b/apps/meteor/app/videobridge/server/actionLink.js
deleted file mode 100644
index 4eedf13dbcc2a0b37dcc9c51a1b0706b3bde9069..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/actionLink.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { actionLinks } from '../../action-links/server';
-
-actionLinks.register('joinJitsiCall', function (/* message, params*/) {});
diff --git a/apps/meteor/app/videobridge/server/index.js b/apps/meteor/app/videobridge/server/index.js
deleted file mode 100644
index ee24f5157b783cf9aa8d9ff1361057e282a7746b..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/index.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import '../lib/messageType';
-import './settings';
-import './methods/jitsiSetTimeout';
-import './methods/jitsiGenerateToken';
-import './methods/bbb';
-import './actionLink';
diff --git a/apps/meteor/app/videobridge/server/methods/bbb.js b/apps/meteor/app/videobridge/server/methods/bbb.js
deleted file mode 100644
index 3ec8cc0b03fbda38974d60b46bb1bc6a9181a90a..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/methods/bbb.js
+++ /dev/null
@@ -1,187 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { HTTP } from 'meteor/http';
-import { check } from 'meteor/check';
-import xml2js from 'xml2js';
-
-import BigBlueButtonApi from '../../../bigbluebutton/server';
-import { SystemLogger } from '../../../../server/lib/logger/system';
-import { settings } from '../../../settings/server';
-import { Rooms, Users } from '../../../models/server';
-import { saveStreamingOptions } from '../../../channel-settings/server';
-import { canAccessRoom, canAccessRoomId } from '../../../authorization/server';
-import { API } from '../../../api/server';
-
-const parser = new xml2js.Parser({
-	explicitRoot: true,
-});
-
-const parseString = Meteor.wrapAsync(parser.parseString);
-
-const getBBBAPI = () => {
-	const url = settings.get('bigbluebutton_server');
-	const secret = settings.get('bigbluebutton_sharedSecret');
-	const api = new BigBlueButtonApi(`${url}/bigbluebutton/api`, secret);
-	return { api, url };
-};
-
-Meteor.methods({
-	bbbJoin({ rid }) {
-		check(rid, String);
-
-		if (!this.userId) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' });
-		}
-
-		if (!rid) {
-			throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbJoin' });
-		}
-
-		const user = Users.findOneById(this.userId);
-		if (!user) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' });
-		}
-
-		const room = Rooms.findOneById(rid);
-		if (!room) {
-			throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbJoin' });
-		}
-
-		if (!canAccessRoom(room, user)) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' });
-		}
-
-		if (!settings.get('bigbluebutton_Enabled')) {
-			throw new Meteor.Error('error-not-allowed', 'Not Allowed', { method: 'bbbJoin' });
-		}
-
-		const { api } = getBBBAPI();
-		const meetingID = settings.get('uniqueID') + rid;
-		const createUrl = api.urlFor('create', {
-			name: room.t === 'd' ? 'Direct' : room.name,
-			meetingID,
-			attendeePW: 'ap',
-			moderatorPW: 'mp',
-			welcome: '<br>Welcome to <b>%%CONFNAME%%</b>!',
-			meta_html5chat: false,
-			meta_html5navbar: false,
-			meta_html5autoswaplayout: true,
-			meta_html5autosharewebcam: false,
-			meta_html5hidepresentation: true,
-		});
-
-		const createResult = HTTP.get(createUrl);
-		const doc = parseString(createResult.content);
-
-		if (doc.response.returncode[0]) {
-			const hookApi = api.urlFor('hooks/create', {
-				meetingID,
-				callbackURL: Meteor.absoluteUrl(`api/v1/videoconference.bbb.update/${meetingID}`),
-			});
-
-			const hookResult = HTTP.get(hookApi);
-
-			if (hookResult.statusCode !== 200) {
-				// TODO improve error logging
-				SystemLogger.error(hookResult);
-				return;
-			}
-
-			saveStreamingOptions(rid, {
-				type: 'call',
-			});
-
-			return {
-				url: api.urlFor('join', {
-					password: 'mp', // mp if moderator ap if attendee
-					meetingID,
-					fullName: user.username,
-					userID: user._id,
-					joinViaHtml5: true,
-					avatarURL: Meteor.absoluteUrl(`avatar/${user.username}`),
-					// clientURL: `${ url }/html5client/join`,
-				}),
-			};
-		}
-	},
-
-	bbbEnd({ rid }) {
-		check(rid, String);
-
-		if (!this.userId) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbEnd' });
-		}
-
-		if (!rid) {
-			throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbEnd' });
-		}
-
-		if (!canAccessRoomId(rid, this.userId)) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbEnd' });
-		}
-
-		if (!settings.get('bigbluebutton_Enabled')) {
-			throw new Meteor.Error('error-not-allowed', 'Not Allowed', { method: 'bbbEnd' });
-		}
-
-		const { api } = getBBBAPI();
-		const meetingID = settings.get('uniqueID') + rid;
-		const endApi = api.urlFor('end', {
-			meetingID,
-			password: 'mp', // mp if moderator ap if attendee
-		});
-
-		const endApiResult = HTTP.get(endApi);
-
-		if (endApiResult.statusCode !== 200) {
-			saveStreamingOptions(rid, {});
-			throw new Meteor.Error(endApiResult);
-		}
-		const doc = parseString(endApiResult.content);
-
-		if (['SUCCESS', 'FAILED'].includes(doc.response.returncode[0])) {
-			saveStreamingOptions(rid, {});
-		}
-	},
-});
-
-API.v1.addRoute(
-	'videoconference.bbb.update/:id',
-	{ authRequired: false },
-	{
-		post() {
-			// TODO check checksum
-			const event = JSON.parse(this.bodyParams.event)[0];
-			const eventType = event.data.id;
-			const meetingID = event.data.attributes.meeting['external-meeting-id'];
-			const rid = meetingID.replace(settings.get('uniqueID'), '');
-
-			SystemLogger.debug(eventType, rid);
-
-			if (eventType === 'meeting-ended') {
-				saveStreamingOptions(rid, {});
-			}
-
-			// if (eventType === 'user-left') {
-			// 	const { api } = getBBBAPI();
-
-			// 	const getMeetingInfoApi = api.urlFor('getMeetingInfo', {
-			// 		meetingID
-			// 	});
-
-			// 	const getMeetingInfoResult = HTTP.get(getMeetingInfoApi);
-
-			// 	if (getMeetingInfoResult.statusCode !== 200) {
-			// 		// TODO improve error logging
-			// 		SystemLogger.error({ getMeetingInfoResult });
-			// 	}
-
-			// 	const doc = parseString(getMeetingInfoResult.content);
-
-			// 	if (doc.response.returncode[0]) {
-			// 		const participantCount = parseInt(doc.response.participantCount[0]);
-			// 		SystemLogger.debug(participantCount);
-			// 	}
-			// }
-		},
-	},
-);
diff --git a/apps/meteor/app/videobridge/server/methods/jitsiGenerateToken.js b/apps/meteor/app/videobridge/server/methods/jitsiGenerateToken.js
deleted file mode 100644
index 2aabfe83dffce75455cd0f96d103544003f58473..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/methods/jitsiGenerateToken.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { jws } from 'jsrsasign';
-
-import { Rooms } from '../../../models/server';
-import { settings } from '../../../settings/server';
-import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom';
-import { getUserEmailAddress } from '../../../../lib/getUserEmailAddress';
-
-Meteor.methods({
-	'jitsi:generateAccessToken': (rid) => {
-		if (!Meteor.userId()) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', {
-				method: 'jitsi:generateToken',
-			});
-		}
-
-		const room = Rooms.findOneById(rid);
-
-		if (!canAccessRoom(room, Meteor.user())) {
-			throw new Meteor.Error('error-not-allowed', 'not allowed', { method: 'jitsi:generateToken' });
-		}
-
-		let rname;
-		if (settings.get('Jitsi_URL_Room_Hash')) {
-			rname = settings.get('uniqueID') + rid;
-		} else {
-			rname = encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name);
-		}
-		const jitsiRoom = settings.get('Jitsi_URL_Room_Prefix') + rname + settings.get('Jitsi_URL_Room_Suffix');
-		const jitsiDomain = settings.get('Jitsi_Domain');
-		const jitsiApplicationId = settings.get('Jitsi_Application_ID');
-		const jitsiApplicationSecret = settings.get('Jitsi_Application_Secret');
-		const jitsiLimitTokenToRoom = settings.get('Jitsi_Limit_Token_To_Room');
-
-		function addUserContextToPayload(payload) {
-			const user = Meteor.user();
-			payload.context = {
-				user: {
-					name: user.name,
-					email: getUserEmailAddress(user),
-					avatar: Meteor.absoluteUrl(`avatar/${user.username}`),
-					id: user._id,
-				},
-			};
-
-			return payload;
-		}
-
-		const JITSI_OPTIONS = {
-			jitsi_domain: jitsiDomain,
-			jitsi_lifetime_token: '1hour', // only 1 hour (for security reasons)
-			jitsi_application_id: jitsiApplicationId,
-			jitsi_application_secret: jitsiApplicationSecret,
-		};
-
-		const HEADER = {
-			typ: 'JWT',
-			alg: 'HS256',
-		};
-
-		const commonPayload = {
-			iss: JITSI_OPTIONS.jitsi_application_id,
-			sub: JITSI_OPTIONS.jitsi_domain,
-			iat: jws.IntDate.get('now'),
-			nbf: jws.IntDate.get('now'),
-			exp: jws.IntDate.get(`now + ${JITSI_OPTIONS.jitsi_lifetime_token}`),
-			aud: 'RocketChat',
-			room: jitsiLimitTokenToRoom ? jitsiRoom : '*',
-			context: '', // first empty
-		};
-
-		const header = JSON.stringify(HEADER);
-		const payload = JSON.stringify(addUserContextToPayload(commonPayload));
-
-		return jws.JWS.sign(HEADER.alg, header, payload, {
-			rstr: JITSI_OPTIONS.jitsi_application_secret,
-		});
-	},
-});
diff --git a/apps/meteor/app/videobridge/server/methods/jitsiSetTimeout.js b/apps/meteor/app/videobridge/server/methods/jitsiSetTimeout.js
deleted file mode 100644
index acc51845ac79bb295f88672f85a6d078c52273fc..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/methods/jitsiSetTimeout.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
-
-import { Rooms, Messages, Users } from '../../../models/server';
-import { callbacks } from '../../../../lib/callbacks';
-import { metrics } from '../../../metrics/server';
-import * as CONSTANTS from '../../constants';
-import { canSendMessage } from '../../../authorization/server';
-import { SystemLogger } from '../../../../server/lib/logger/system';
-import { settings } from '../../../settings/server';
-
-// TODO: Access Token missing. This is just a partial solution, it doesn't handle access token generation logic as present in this file - client/views/room/contextualBar/Call/Jitsi/CallJitsWithData.js
-const resolveJitsiCallUrl = (room) => {
-	const rname = settings.get('Jitsi_URL_Room_Hash')
-		? settings.get('uniqueID') + room._id
-		: encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name);
-	return `${settings.get('Jitsi_SSL') ? 'https://' : 'http://'}${settings.get('Jitsi_Domain')}/${settings.get(
-		'Jitsi_URL_Room_Prefix',
-	)}${rname}${settings.get('Jitsi_URL_Room_Suffix')}`;
-};
-
-Meteor.methods({
-	'jitsi:updateTimeout': (rid, joiningNow = true) => {
-		if (!Meteor.userId()) {
-			throw new Meteor.Error('error-invalid-user', 'Invalid user', {
-				method: 'jitsi:updateTimeout',
-			});
-		}
-
-		const uid = Meteor.userId();
-
-		const user = Users.findOneById(uid, {
-			fields: {
-				username: 1,
-				type: 1,
-			},
-		});
-
-		try {
-			const room = canSendMessage(rid, { uid, username: user.username, type: user.type });
-
-			const currentTime = new Date().getTime();
-
-			const jitsiTimeout = room.jitsiTimeout && new Date(room.jitsiTimeout).getTime();
-
-			const nextTimeOut = new Date(currentTime + CONSTANTS.TIMEOUT);
-
-			if (!jitsiTimeout || currentTime > jitsiTimeout - CONSTANTS.TIMEOUT / 2) {
-				Rooms.setJitsiTimeout(rid, nextTimeOut);
-			}
-
-			if (joiningNow && (!jitsiTimeout || currentTime > jitsiTimeout)) {
-				metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736
-
-				const message = Messages.createWithTypeRoomIdMessageAndUser('jitsi_call_started', rid, '', Meteor.user(), {
-					actionLinks: [
-						{
-							icon: 'icon-videocam',
-							label: TAPi18n.__('Click_to_join'),
-							i18nLabel: 'Click_to_join',
-							method_id: 'joinJitsiCall',
-							params: '',
-						},
-					],
-					customFields: {
-						...(room.customFields && { ...room.customFields }),
-						...(room.t === 'l' && { jitsiCallUrl: resolveJitsiCallUrl(room) }), // Note: this is just a temporary solution for the jitsi calls to work in Livechat. In future we wish to create specific events for specific to livechat calls (eg: start, accept, decline, end, etc) and this url info will be passed via there
-					},
-				});
-				message.msg = TAPi18n.__('Started_a_video_call');
-				callbacks.run('afterSaveMessage', message, {
-					...room,
-					jitsiTimeout: currentTime + CONSTANTS.TIMEOUT,
-				});
-			}
-
-			return jitsiTimeout || nextTimeOut;
-		} catch (err) {
-			SystemLogger.error({ msg: 'Error starting video call:', err });
-
-			throw new Meteor.Error('error-starting-video-call', err.message, {
-				method: 'jitsi:updateTimeout',
-			});
-		}
-	},
-});
diff --git a/apps/meteor/app/videobridge/server/settings.ts b/apps/meteor/app/videobridge/server/settings.ts
deleted file mode 100644
index b8ac7f97352ae3282bec6f36ad1488a2a3d9c2a4..0000000000000000000000000000000000000000
--- a/apps/meteor/app/videobridge/server/settings.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import { settingsRegistry } from '../../settings/server';
-
-settingsRegistry.addGroup('Video Conference', function () {
-	this.section('BigBlueButton', function () {
-		this.add('bigbluebutton_Enabled', false, {
-			type: 'boolean',
-			i18nLabel: 'Enabled',
-			alert: 'This Feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues',
-			public: true,
-		});
-
-		this.add('bigbluebutton_server', '', {
-			type: 'string',
-			i18nLabel: 'Domain',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-		});
-
-		this.add('bigbluebutton_sharedSecret', '', {
-			type: 'string',
-			i18nLabel: 'Shared_Secret',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-		});
-
-		this.add('bigbluebutton_Open_New_Window', false, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Always_open_in_new_window',
-			public: true,
-		});
-
-		this.add('bigbluebutton_enable_d', true, {
-			type: 'boolean',
-			i18nLabel: 'WebRTC_Enable_Direct',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-			public: true,
-		});
-
-		this.add('bigbluebutton_enable_p', true, {
-			type: 'boolean',
-			i18nLabel: 'WebRTC_Enable_Private',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-			public: true,
-		});
-
-		this.add('bigbluebutton_enable_c', false, {
-			type: 'boolean',
-			i18nLabel: 'WebRTC_Enable_Channel',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-			public: true,
-		});
-
-		this.add('bigbluebutton_enable_teams', false, {
-			type: 'boolean',
-			i18nLabel: 'BBB_Enable_Teams',
-			enableQuery: {
-				_id: 'bigbluebutton_Enabled',
-				value: true,
-			},
-			public: true,
-		});
-	});
-
-	this.section('Jitsi', function () {
-		this.add('Jitsi_Enabled', false, {
-			type: 'boolean',
-			i18nLabel: 'Enabled',
-			alert: 'This Feature is currently in beta! Please report bugs to github.com/RocketChat/Rocket.Chat/issues',
-			public: true,
-		});
-
-		this.add('Jitsi_Domain', 'meet.jit.si', {
-			type: 'string',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Domain',
-			public: true,
-		});
-
-		this.add('Jitsi_URL_Room_Prefix', 'RocketChat', {
-			type: 'string',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'URL_room_prefix',
-			public: true,
-		});
-
-		this.add('Jitsi_URL_Room_Suffix', '', {
-			type: 'string',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'URL_room_suffix',
-			public: true,
-		});
-
-		this.add('Jitsi_URL_Room_Hash', true, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'URL_room_hash',
-			i18nDescription: 'URL_room_hash_description',
-			public: true,
-		});
-
-		this.add('Jitsi_SSL', true, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'SSL',
-			public: true,
-		});
-
-		this.add('Jitsi_Open_New_Window', true, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Always_open_in_new_window',
-			public: true,
-		});
-
-		this.add('Jitsi_Enable_Channels', false, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Jitsi_Enable_Channels',
-			public: true,
-		});
-
-		this.add('Jitsi_Enable_Teams', false, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Jitsi_Enable_Teams',
-			public: true,
-		});
-
-		this.add('Jitsi_Chrome_Extension', 'nocfbnnmjnndkbipkabodnheejiegccf', {
-			type: 'string',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Jitsi_Chrome_Extension',
-			public: true,
-		});
-
-		this.add('Jitsi_Enabled_TokenAuth', false, {
-			type: 'boolean',
-			enableQuery: {
-				_id: 'Jitsi_Enabled',
-				value: true,
-			},
-			i18nLabel: 'Jitsi_Enabled_TokenAuth',
-			public: true,
-		});
-
-		this.add('Jitsi_Application_ID', '', {
-			type: 'string',
-			enableQuery: [
-				{ _id: 'Jitsi_Enabled', value: true },
-				{ _id: 'Jitsi_Enabled_TokenAuth', value: true },
-			],
-			i18nLabel: 'Jitsi_Application_ID',
-		});
-
-		this.add('Jitsi_Application_Secret', '', {
-			type: 'string',
-			enableQuery: [
-				{ _id: 'Jitsi_Enabled', value: true },
-				{ _id: 'Jitsi_Enabled_TokenAuth', value: true },
-			],
-			i18nLabel: 'Jitsi_Application_Secret',
-		});
-
-		this.add('Jitsi_Limit_Token_To_Room', true, {
-			type: 'boolean',
-			enableQuery: [
-				{ _id: 'Jitsi_Enabled', value: true },
-				{ _id: 'Jitsi_Enabled_TokenAuth', value: true },
-			],
-			i18nLabel: 'Jitsi_Limit_Token_To_Room',
-			public: true,
-		});
-		this.add('Jitsi_Click_To_Join_Count', 0, {
-			type: 'int',
-			hidden: true,
-		});
-		this.add('Jitsi_Start_SlashCommands_Count', 0, {
-			type: 'int',
-			hidden: true,
-		});
-	});
-});
diff --git a/apps/meteor/client/components/GenericModal.tsx b/apps/meteor/client/components/GenericModal.tsx
index 07c2b35c21cb378230dbefb499b5a91e082f9e42..94b6bf960aa019ba02776c4d1fee13762d39fcf2 100644
--- a/apps/meteor/client/components/GenericModal.tsx
+++ b/apps/meteor/client/components/GenericModal.tsx
@@ -9,8 +9,8 @@ type VariantType = 'danger' | 'warning' | 'info' | 'success';
 type GenericModalProps = RequiredModalProps & {
 	variant?: VariantType;
 	children?: ReactNode;
-	cancelText?: string;
-	confirmText?: string;
+	cancelText?: ReactNode;
+	confirmText?: ReactNode;
 	title?: string | ReactElement;
 	icon?: ComponentProps<typeof Icon>['name'] | ReactElement | null;
 	confirmDisabled?: boolean;
diff --git a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx
index 22bcd3cde2d34cdce2264e6ce1971b8d074797e1..2d81558b285d1708fa5c3923f874e5fda110f005 100644
--- a/apps/meteor/client/components/RoomIcon/RoomIcon.tsx
+++ b/apps/meteor/client/components/RoomIcon/RoomIcon.tsx
@@ -8,12 +8,18 @@ import { OmnichannelRoomIcon } from './OmnichannelRoomIcon';
 export const RoomIcon = ({
 	room,
 	size = 'x16',
+	isIncomingCall,
 	placement,
 }: {
 	room: IRoom;
 	size?: ComponentProps<typeof Icon>['size'];
+	isIncomingCall?: boolean;
 	placement: 'sidebar' | 'default';
 }): ReactElement | null => {
+	if (isIncomingCall) {
+		return <Icon name='phone' size={size} />;
+	}
+
 	if (room.prid) {
 		return <Icon name='baloons' size={size} />;
 	}
diff --git a/apps/meteor/client/components/modal/ModalPortal.tsx b/apps/meteor/client/components/modal/ModalPortal.tsx
index 7b5152ae6e4ec755729700e696c47ffe0c768d2e..bb3cf00dc8924d11d559e72a2ad08ddfbd9ca334 100644
--- a/apps/meteor/client/components/modal/ModalPortal.tsx
+++ b/apps/meteor/client/components/modal/ModalPortal.tsx
@@ -8,6 +8,9 @@ type ModalPortalProps = {
 	children?: ReactNode;
 };
 
+/**
+ * @todo: move to portals folder
+ */
 const ModalPortal = ({ children }: ModalPortalProps): ReactElement => {
 	const [modalRoot] = useState(() => createAnchor('modal-root'));
 	useEffect(() => (): void => deleteAnchor(modalRoot), [modalRoot]);
diff --git a/apps/meteor/client/contexts/VideoConfContext.ts b/apps/meteor/client/contexts/VideoConfContext.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3f71fc7b05f4ac8622ba2b1ce33795b7925a9f1e
--- /dev/null
+++ b/apps/meteor/client/contexts/VideoConfContext.ts
@@ -0,0 +1,74 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { createContext, useContext } from 'react';
+import { Subscription, useSubscription } from 'use-subscription';
+
+import { DirectCallParams, ProviderCapabilities, CallPreferences } from '../lib/VideoConfManager';
+
+export type VideoConfPopupPayload = {
+	id: string;
+	rid: IRoom['_id'];
+	isReceiving?: boolean;
+};
+
+export type VideoConfIncomingCall = {
+	callId: string;
+	uid: string;
+	rid: string;
+};
+
+type VideoConfContextValue = {
+	dispatchOutgoing: (options: Omit<VideoConfPopupPayload, 'id'>) => void;
+	dismissOutgoing: () => void;
+	startCall: (rid: IRoom['_id'], title?: string) => void;
+	acceptCall: (callId: string) => void;
+	joinCall: (callId: string) => void;
+	dismissCall: (callId: string) => void;
+	rejectIncomingCall: (callId: string) => void;
+	abortCall: () => void;
+	setPreferences: (prefs: { mic?: boolean; cam?: boolean }) => void;
+	queryIncomingCalls: Subscription<DirectCallParams[]>;
+	queryRinging: Subscription<boolean>;
+	queryCalling: Subscription<boolean>;
+	queryCapabilities: Subscription<ProviderCapabilities>;
+	queryPreferences: Subscription<CallPreferences>;
+};
+
+export const VideoConfContext = createContext<VideoConfContextValue | undefined>(undefined);
+const useVideoConfContext = (): VideoConfContextValue => {
+	const context = useContext(VideoConfContext);
+	if (!context) {
+		throw new Error('Must be running in VideoConf Context');
+	}
+
+	return context;
+};
+
+export const useVideoConfDispatchOutgoing = (): VideoConfContextValue['dispatchOutgoing'] => useVideoConfContext().dispatchOutgoing;
+export const useVideoConfDismissOutgoing = (): VideoConfContextValue['dismissOutgoing'] => useVideoConfContext().dismissOutgoing;
+export const useVideoConfStartCall = (): VideoConfContextValue['startCall'] => useVideoConfContext().startCall;
+export const useVideoConfAcceptCall = (): VideoConfContextValue['acceptCall'] => useVideoConfContext().acceptCall;
+export const useVideoConfJoinCall = (): VideoConfContextValue['joinCall'] => useVideoConfContext().joinCall;
+export const useVideoConfDismissCall = (): VideoConfContextValue['dismissCall'] => useVideoConfContext().dismissCall;
+export const useVideoConfAbortCall = (): VideoConfContextValue['abortCall'] => useVideoConfContext().abortCall;
+export const useVideoConfRejectIncomingCall = (): VideoConfContextValue['rejectIncomingCall'] => useVideoConfContext().rejectIncomingCall;
+export const useVideoConfIncomingCalls = (): DirectCallParams[] => {
+	const { queryIncomingCalls } = useVideoConfContext();
+	return useSubscription(queryIncomingCalls);
+};
+export const useVideoConfSetPreferences = (): VideoConfContextValue['setPreferences'] => useVideoConfContext().setPreferences;
+export const useVideoConfIsRinging = (): boolean => {
+	const { queryRinging } = useVideoConfContext();
+	return useSubscription(queryRinging);
+};
+export const useVideoConfIsCalling = (): boolean => {
+	const { queryCalling } = useVideoConfContext();
+	return useSubscription(queryCalling);
+};
+export const useVideoConfCapabilities = (): ProviderCapabilities => {
+	const { queryCapabilities } = useVideoConfContext();
+	return useSubscription(queryCapabilities);
+};
+export const useVideoConfPreferences = (): CallPreferences => {
+	const { queryPreferences } = useVideoConfContext();
+	return useSubscription(queryPreferences);
+};
diff --git a/apps/meteor/client/lib/VideoConfManager.ts b/apps/meteor/client/lib/VideoConfManager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c994660fb4b9dadd02b0924a501a3f32e98f9702
--- /dev/null
+++ b/apps/meteor/client/lib/VideoConfManager.ts
@@ -0,0 +1,680 @@
+import type { IRoom, IUser } from '@rocket.chat/core-typings';
+import { Emitter } from '@rocket.chat/emitter';
+import { Meteor } from 'meteor/meteor';
+import { Tracker } from 'meteor/tracker';
+
+import { Notifications } from '../../app/notifications/client';
+import { APIClient } from '../../app/utils/client';
+import { getConfig } from './utils/getConfig';
+
+const debug = !!(getConfig('debug') || getConfig('debug-VideoConf'));
+
+// The interval between attempts to call the remote user
+const CALL_INTERVAL = 3000;
+// How many attempts to call we're gonna make
+const CALL_ATTEMPT_LIMIT = 10;
+// The amount of time we'll assume an incoming call is still valid without any updates from the remote user
+const CALL_TIMEOUT = 10000;
+// How long are we gonna wait for a link after accepting an incoming call
+const ACCEPT_TIMEOUT = 5000;
+
+export type DirectCallParams = {
+	uid: IUser['_id'];
+	rid: IRoom['_id'];
+	callId: string;
+	dismissed?: boolean;
+	// 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 };
+
+export type CallPreferences = {
+	mic?: boolean;
+	cam?: boolean;
+};
+
+export type ProviderCapabilities = {
+	mic?: boolean;
+	cam?: boolean;
+	title?: boolean;
+};
+
+export type CurrentCallParams = {
+	callId: string;
+	url: string;
+};
+
+type VideoConfEvents = {
+	// We gave up on calling a remote user or they rejected our call
+	'direct/cancel': DirectCallParams;
+
+	// A remote user is calling us
+	'direct/ringing': DirectCallParams;
+
+	// An incoming call was lost, either by timeout or because the remote user canceled
+	'direct/lost': DirectCallParams;
+
+	// We tried to accept an incoming call but the process failed
+	'direct/failed': DirectCallParams;
+
+	// A remote user accepted our call
+	'direct/accepted': DirectCallParams;
+
+	// We stopped calling a remote user
+	'direct/stopped': DirectCallParams;
+
+	'preference/changed': { key: keyof CallPreferences; value: boolean };
+
+	// The list of incoming calls has changed in some way
+	'incoming/changed': void;
+
+	// The list of ringing incoming calls may have changed
+	'ringing/changed': void;
+
+	// The value of `isCalling` may have changed
+	'calling/changed': void;
+
+	'calling/ended': void;
+
+	// When join call
+	'call/join': CurrentCallParams;
+
+	'join/error': { error: string };
+
+	'start/error': { error: string };
+
+	'capabilities/changed': void;
+};
+export const VideoConfManager = new (class VideoConfManager extends Emitter<VideoConfEvents> {
+	private userId: string | undefined;
+
+	private currentCallHandler: ReturnType<typeof setTimeout> | undefined;
+
+	private currentCallData: (DirectCallParams & { joined?: boolean }) | undefined;
+
+	private startingNewCall = false;
+
+	private hooks: (() => void)[] = [];
+
+	private incomingDirectCalls: Map<string, IncomingDirectCall>;
+
+	private acceptingCallId: string | undefined;
+
+	private acceptingCallTimeout = 0;
+
+	private _preferences: CallPreferences;
+
+	private _capabilities: ProviderCapabilities;
+
+	public get preferences(): CallPreferences {
+		return this._preferences;
+	}
+
+	public get capabilities(): ProviderCapabilities {
+		return this._capabilities;
+	}
+
+	constructor() {
+		super();
+		this.incomingDirectCalls = new Map<string, IncomingDirectCall>();
+		this._preferences = { mic: true };
+		this._capabilities = {};
+	}
+
+	public isBusy(): boolean {
+		if (this.startingNewCall) {
+			return true;
+		}
+
+		return this.isCalling();
+	}
+
+	public isRinging(): boolean {
+		return ![...this.incomingDirectCalls.values()].every(({ dismissed }) => Boolean(dismissed));
+	}
+
+	public isCalling(): boolean {
+		if (this.currentCallHandler || (this.currentCallData && !this.currentCallData.joined)) {
+			return true;
+		}
+
+		return false;
+	}
+
+	public getIncomingDirectCalls(): DirectCallParams[] {
+		return [...this.incomingDirectCalls.values()].map(({ timeout: _, ...call }) => ({ ...call }));
+	}
+
+	public async startCall(roomId: IRoom['_id'], title?: string): Promise<void> {
+		if (!this.userId || this.isBusy()) {
+			throw new Error('Video manager is busy.');
+		}
+
+		debug && console.log(`[VideoConf] Starting new call on room ${roomId}`);
+		this.startingNewCall = true;
+		this.emit('calling/changed');
+
+		const { data } = await APIClient.post('/v1/video-conference.start', { roomId, title, allowRinging: true }).catch((e: any) => {
+			debug && console.error(`[VideoConf] Failed to start new call on room ${roomId}`);
+			this.startingNewCall = false;
+			this.emit('calling/changed');
+			this.emit('start/error', { error: e?.xhr?.responseJSON?.error || 'unknown-error' });
+
+			return Promise.reject(e);
+		});
+
+		this.startingNewCall = false;
+		this.emit('calling/changed');
+
+		if (data.type !== 'direct') {
+			this.emit('calling/ended');
+		}
+
+		switch (data.type) {
+			case 'direct':
+				return this.callUser({ uid: data.callee, rid: roomId, callId: data.callId });
+			case 'videoconference':
+				return this.joinCall(data.callId);
+			case 'livechat':
+				return this.joinCall(data.callId);
+		}
+	}
+
+	public acceptIncomingCall(callId: string): void {
+		const callData = this.incomingDirectCalls.get(callId);
+		if (!callData) {
+			throw new Error('Unable to find accepted call information.');
+		}
+
+		debug && console.log(`[VideoConf] Accepting incoming call.`);
+
+		if (callData.timeout) {
+			clearTimeout(callData.timeout);
+		}
+
+		// Mute this call Id so any lingering notifications don't trigger it again
+		this.dismissIncomingCall(callId);
+
+		this.acceptingCallId = callId;
+		this.acceptingCallTimeout = setTimeout(() => {
+			if (this.acceptingCallId !== callId) {
+				debug && console.warn(`[VideoConf] Accepting call timeout not properly cleared.`);
+				return;
+			}
+
+			debug && console.log(`[VideoConf] Attempt to accept call has timed out.`);
+			this.acceptingCallId = undefined;
+			this.acceptingCallTimeout = 0;
+
+			this.removeIncomingCall(callId);
+
+			this.emit('direct/failed', { callId, uid: callData.uid, rid: callData.rid });
+		}, ACCEPT_TIMEOUT) as unknown as number;
+
+		debug && console.log(`[VideoConf] Notifying user ${callData.uid} that we accept their call.`);
+		Notifications.notifyUser(callData.uid, 'video-conference.accepted', { callId, uid: this.userId, rid: callData.rid });
+	}
+
+	public rejectIncomingCall(callId: string): void {
+		this.dismissIncomingCall(callId);
+
+		const callData = this.incomingDirectCalls.get(callId);
+		if (!callData) {
+			return;
+		}
+
+		Notifications.notifyUser(callData.uid, 'video-conference.rejected', { callId, uid: this.userId, rid: callData.rid });
+		this.loseIncomingCall(callId);
+	}
+
+	public dismissedIncomingCalls(): void {
+		// Mute all calls that are currently ringing
+		if ([...this.incomingDirectCalls.keys()].some((callId) => this.dismissedIncomingCallHelper(callId))) {
+			this.emit('ringing/changed');
+			this.emit('incoming/changed');
+		}
+	}
+
+	public async loadCapabilities(): Promise<void> {
+		const { capabilities } = await APIClient.get('/v1/video-conference.capabilities').catch((e: any) => {
+			debug && console.error(`[VideoConf] Failed to load video conference capabilities`);
+
+			return Promise.reject(e);
+		});
+
+		this._capabilities = capabilities || {};
+		this.emit('capabilities/changed');
+	}
+
+	private dismissedIncomingCallHelper(callId: string): boolean {
+		// Muting will stop a callId from ringing, but it doesn't affect any part of the existing workflow
+		const callData = this.incomingDirectCalls.get(callId);
+		if (!callData) {
+			return false;
+		}
+		this.incomingDirectCalls.set(callId, { ...callData, dismissed: true });
+		setTimeout(() => {
+			const callData = this.incomingDirectCalls.get(callId);
+
+			if (!callData) {
+				return;
+			}
+			// eslint-disable-next-line @typescript-eslint/no-unused-vars
+			const { dismissed, ...rest } = callData;
+			this.incomingDirectCalls.set(callId, { ...rest });
+		}, CALL_TIMEOUT * 20);
+		return true;
+	}
+
+	public dismissIncomingCall(callId: string): boolean {
+		if (this.dismissedIncomingCallHelper(callId)) {
+			debug && console.log(`[VideoConf] Dismissed call ${callId}`);
+			this.emit('ringing/changed');
+			this.emit('incoming/changed');
+			return true;
+		}
+		debug && console.log(`[VideoConf] Failed to dismiss call ${callId}`);
+		return false;
+	}
+
+	public updateUser(): void {
+		const userId = Meteor.userId();
+
+		if (this.userId === userId) {
+			debug && console.log(`[VideoConf] Logged user has not changed, so we're not changing the hooks.`);
+			return;
+		}
+
+		debug && console.log(`[VideoConf] Logged user has changed.`);
+
+		if (this.userId) {
+			this.disconnect();
+		}
+
+		if (userId) {
+			this.connectUser(userId);
+		}
+	}
+
+	public changePreference(key: keyof CallPreferences, value: boolean): void {
+		this._preferences[key] = value;
+		this.emit('preference/changed', { key, value });
+	}
+
+	public setPreferences(prefs: Partial<CallPreferences>): void {
+		for (const key in prefs) {
+			if (prefs.hasOwnProperty(key)) {
+				const prefKey = key as keyof CallPreferences;
+				this.changePreference(prefKey, prefs[prefKey] as boolean);
+			}
+		}
+	}
+
+	public async joinCall(callId: string): Promise<void> {
+		debug && console.log(`[VideoConf] Joining call ${callId}.`);
+
+		if (this.acceptingCallTimeout && this.acceptingCallId === callId) {
+			clearTimeout(this.acceptingCallTimeout);
+			this.acceptingCallTimeout = 0;
+			this.acceptingCallId = undefined;
+		}
+
+		if (this.incomingDirectCalls.has(callId)) {
+			this.removeIncomingCall(callId);
+		}
+
+		const params = {
+			callId,
+			state: {
+				...(this._preferences.mic !== undefined ? { mic: this._preferences.mic } : {}),
+				...(this._preferences.cam !== undefined ? { cam: this._preferences.cam } : {}),
+			},
+		};
+
+		const { url } = await APIClient.post('/v1/video-conference.join', params).catch((e) => {
+			debug && console.error(`[VideoConf] Failed to join call ${callId}`);
+			this.emit('join/error', { error: e?.xhr?.responseJSON?.error || 'unknown-error' });
+
+			return Promise.reject(e);
+		});
+
+		if (!url) {
+			throw new Error('Failed to get video conference URL.');
+		}
+
+		debug && console.log(`[VideoConf] Opening ${url}.`);
+		this.emit('call/join', { url, callId });
+	}
+
+	public abortCall(): void {
+		if (!this.currentCallData) {
+			return;
+		}
+
+		this.giveUp(this.currentCallData);
+	}
+
+	private rejectIncomingCallsFromUser(userId: string): void {
+		for (const [, { callId, uid }] of this.incomingDirectCalls) {
+			if (userId === uid) {
+				debug && console.log(`[VideoConf] Rejecting old incoming call from user ${userId}`);
+				this.rejectIncomingCall(callId);
+			}
+		}
+	}
+
+	private async callUser({ uid, rid, callId }: DirectCallParams): Promise<void> {
+		if (this.currentCallHandler || this.currentCallData) {
+			throw new Error('Video Conference State Error.');
+		}
+
+		let attempt = 1;
+		this.currentCallData = { callId, rid, uid };
+		this.currentCallHandler = setInterval(() => {
+			if (!this.currentCallHandler) {
+				debug && console.warn(`[VideoConf] Ringing interval was not properly cleared.`);
+				return;
+			}
+
+			attempt++;
+
+			if (attempt > CALL_ATTEMPT_LIMIT) {
+				this.giveUp({ uid, rid, callId });
+				return;
+			}
+
+			debug && console.log(`[VideoConf] Ringing user ${uid}, attempt number ${attempt}.`);
+			Notifications.notifyUser(uid, 'video-conference.call', { uid: this.userId, rid, callId });
+		}, CALL_INTERVAL);
+		this.emit('calling/changed');
+
+		debug && console.log(`[VideoConf] Ringing user ${uid} for the first time.`);
+		Notifications.notifyUser(uid, 'video-conference.call', { uid: this.userId, rid, callId });
+	}
+
+	private async giveUp({ uid, rid, callId }: DirectCallParams): Promise<void> {
+		const joined = this.currentCallData?.joined;
+
+		debug && console.log(`[VideoConf] Stop ringing user ${uid}.`);
+		if (this.currentCallHandler) {
+			clearInterval(this.currentCallHandler);
+			this.currentCallHandler = undefined;
+			this.currentCallData = undefined;
+			this.emit('calling/changed');
+		}
+
+		debug && console.log(`[VideoConf] Notifying user ${uid} that we are no longer calling.`);
+		Notifications.notifyUser(uid, 'video-conference.canceled', { uid: this.userId, rid, callId });
+
+		this.emit('direct/cancel', { uid, rid, callId });
+		this.emit('direct/stopped', { uid, rid, callId });
+
+		if (joined) {
+			return;
+		}
+
+		APIClient.post('/v1/video-conference.cancel', { callId });
+	}
+
+	private disconnect(): void {
+		debug && console.log(`[VideoConf] disconnecting user ${this.userId}`);
+		for (const hook of this.hooks) {
+			hook();
+		}
+		this.hooks = [];
+
+		if (this.currentCallHandler) {
+			clearInterval(this.currentCallHandler);
+			this.currentCallHandler = undefined;
+		}
+
+		if (this.acceptingCallTimeout) {
+			clearTimeout(this.acceptingCallTimeout);
+			this.acceptingCallTimeout = 0;
+		}
+
+		this.incomingDirectCalls.forEach((call) => {
+			if (call.timeout) {
+				clearTimeout(call.timeout);
+			}
+		});
+		this.incomingDirectCalls.clear();
+		this.currentCallData = undefined;
+		this.acceptingCallId = undefined;
+		this._preferences = {};
+		this.emit('incoming/changed');
+		this.emit('ringing/changed');
+		this.emit('calling/changed');
+	}
+
+	private async hookNotification(eventName: string, cb: (...params: any[]) => void): Promise<void> {
+		this.hooks.push(await Notifications.onUser(eventName, cb));
+	}
+
+	private connectUser(userId: string): void {
+		debug && console.log(`[VideoConf] connecting user ${userId}`);
+		this.userId = userId;
+
+		this.hookNotification('video-conference.call', (params: DirectCallParams) => this.onDirectCall(params));
+		this.hookNotification('video-conference.canceled', (params: DirectCallParams) => this.onDirectCallCanceled(params));
+		this.hookNotification('video-conference.accepted', (params: DirectCallParams) => this.onDirectCallAccepted(params));
+		this.hookNotification('video-conference.rejected', (params: DirectCallParams) => this.onDirectCallRejected(params));
+		this.hookNotification('video-conference.confirmed', (params: DirectCallParams) => this.onDirectCallConfirmed(params));
+		this.hookNotification('video-conference.join', (params: DirectCallParams) => this.onDirectCallJoined(params));
+	}
+
+	private abortIncomingCall(callId: string): void {
+		// If we just accepted this call, then ignore the timeout
+		if (this.acceptingCallId === callId) {
+			return;
+		}
+
+		debug && console.log(`[VideoConf] Canceling call ${callId} due to ringing timeout.`);
+		this.loseIncomingCall(callId);
+	}
+
+	private loseIncomingCall(callId: string): void {
+		const lostCall = this.incomingDirectCalls.get(callId);
+		if (!lostCall) {
+			debug && console.warn(`[VideoConf] Unable to cancel ${callId} because we have no information about it.`);
+			return;
+		}
+
+		this.removeIncomingCall(callId);
+
+		debug && console.log(`[VideoConf] Call ${callId} from ${lostCall.uid} was lost.`);
+		this.emit('direct/lost', { callId, uid: lostCall.uid, rid: lostCall.rid });
+	}
+
+	private removeIncomingCall(callId: string): void {
+		if (!this.incomingDirectCalls.has(callId)) {
+			return;
+		}
+
+		const isRinging = this.isRinging();
+
+		const callData = this.incomingDirectCalls.get(callId);
+		if (callData?.timeout) {
+			clearTimeout(callData.timeout);
+		}
+
+		this.incomingDirectCalls.delete(callId);
+		this.emit('incoming/changed');
+
+		if (isRinging !== this.isRinging()) {
+			this.emit('ringing/changed');
+		}
+	}
+
+	private createAbortTimeout(callId: string): number {
+		return setTimeout(() => this.abortIncomingCall(callId), CALL_TIMEOUT) as unknown as number;
+	}
+
+	private startNewIncomingCall({ callId, uid, rid }: DirectCallParams): void {
+		if (this.isCallDismissed(callId)) {
+			debug && console.log(`[VideoConf] Ignoring dismissed call.`);
+			return;
+		}
+
+		// Reject any currently ringing call from the user before registering the new one.
+		this.rejectIncomingCallsFromUser(uid);
+
+		debug && console.log(`[VideoConf] Storing this new call information.`);
+		this.incomingDirectCalls.set(callId, {
+			callId,
+			uid,
+			rid,
+			timeout: this.createAbortTimeout(callId),
+		});
+
+		this.emit('incoming/changed');
+		this.emit('ringing/changed');
+		this.emit('direct/ringing', { callId, uid, rid });
+	}
+
+	private refreshExistingIncomingCall({ callId, uid, rid }: DirectCallParams): void {
+		const existingData = this.incomingDirectCalls.get(callId);
+		if (!existingData) {
+			throw new Error('Video Conference Manager State Error');
+		}
+
+		debug && console.log(`[VideoConf] Resetting call timeout.`);
+		if (existingData.timeout) {
+			clearTimeout(existingData.timeout);
+		}
+		existingData.timeout = this.createAbortTimeout(callId);
+
+		if (!this.isCallDismissed(callId)) {
+			this.emit('direct/ringing', { callId, uid, rid });
+		}
+	}
+
+	private onDirectCall({ callId, uid, rid }: DirectCallParams): void {
+		// If we already accepted this call, then don't ring again
+		if (this.acceptingCallId === callId) {
+			return;
+		}
+
+		debug && console.log(`[VideoConf] User ${uid} is ringing with call ${callId}.`);
+		if (this.incomingDirectCalls.has(callId)) {
+			this.refreshExistingIncomingCall({ callId, uid, rid });
+		} else {
+			this.startNewIncomingCall({ callId, uid, rid });
+		}
+	}
+
+	private onDirectCallCanceled({ callId }: DirectCallParams): void {
+		debug && console.log(`[VideoConf] Call ${callId} was canceled by the remote user.`);
+
+		// We had just accepted this call, but the remote user hang up before they got the notification, so cancel our acceptance
+		if (this.acceptingCallId === callId) {
+			if (this.acceptingCallTimeout) {
+				clearTimeout(this.acceptingCallTimeout);
+			}
+			this.acceptingCallTimeout = 0;
+		}
+
+		this.loseIncomingCall(callId);
+	}
+
+	private onDirectCallAccepted(params: DirectCallParams, skipConfirmation = false): void {
+		if (!params.callId || params.callId !== this.currentCallData?.callId) {
+			debug && console.log(`[VideoConf] User ${params.uid} has accepted a call ${params.callId} from us, but we're not calling.`);
+			return;
+		}
+
+		debug && console.log(`[VideoConf] User ${params.uid} has accepted our call ${params.callId}.`);
+
+		// Stop ringing
+		if (this.currentCallHandler) {
+			clearInterval(this.currentCallHandler);
+			this.currentCallHandler = undefined;
+		}
+
+		const callData = this.currentCallData;
+
+		this.emit('direct/accepted', params);
+		this.emit('direct/stopped', params);
+		this.currentCallData = undefined;
+		this.emit('calling/changed');
+
+		if (!callData.joined) {
+			this.joinCall(params.callId);
+		}
+
+		if (skipConfirmation) {
+			return;
+		}
+
+		debug && console.log(`[VideoConf] Notifying user ${callData.uid} that they can join the call now.`);
+		Notifications.notifyUser(callData.uid, 'video-conference.confirmed', { callId: callData.callId, uid: this.userId, rid: callData.rid });
+	}
+
+	private onDirectCallConfirmed(params: DirectCallParams): void {
+		if (!params.callId || params.callId !== this.acceptingCallId) {
+			debug && console.log(`[VideoConf] User ${params.uid} confirmed we can join ${params.callId} but we aren't trying to join it.`);
+			return;
+		}
+
+		this.joinCall(params.callId);
+	}
+
+	private onDirectCallJoined(params: DirectCallParams): void {
+		if (!params.callId) {
+			debug && console.log(`[VideoConf] Invalid 'video-conference.join' event received: ${params.callId}, ${params.uid}.`);
+			return;
+		}
+
+		if (params.uid === this.userId) {
+			if (this.currentCallData?.callId === params.callId) {
+				debug && console.log(`[VideoConf] We joined our own call (${this.userId}) from somewhere else. Flagging the call appropriatelly.`);
+				this.currentCallData.joined = true;
+				this.emit('calling/changed');
+				return;
+			}
+
+			if (this.incomingDirectCalls.has(params.callId)) {
+				debug && console.log(`[VideoConf] We joined the call ${params.callId} from somewhere else. Dismissing it.`);
+				this.dismissIncomingCall(params.callId);
+				this.loseIncomingCall(params.callId);
+			}
+			return;
+		}
+
+		debug && console.log(`[VideoConf] User ${params.uid} has joined a call we started ${params.callId}.`);
+		this.onDirectCallAccepted(params, true);
+	}
+
+	private onDirectCallRejected(params: DirectCallParams): void {
+		if (!params.callId || params.callId !== this.currentCallData?.callId) {
+			debug && console.log(`[VideoConf] User ${params.uid} has rejected a call ${params.callId} from us, but we're not calling.`);
+			return;
+		}
+
+		debug && console.log(`[VideoConf] User ${params.uid} has rejected our call ${params.callId}.`);
+
+		// Stop ringing
+		if (this.currentCallHandler) {
+			clearInterval(this.currentCallHandler);
+			this.currentCallHandler = undefined;
+		}
+
+		const { joined } = this.currentCallData;
+
+		this.emit('direct/cancel', params);
+		this.currentCallData = undefined;
+		this.emit('direct/stopped', params);
+		this.emit('calling/changed');
+
+		if (!joined) {
+			APIClient.post('/v1/video-conference.cancel', { callId: params.callId });
+		}
+	}
+
+	private isCallDismissed(callId: string): boolean {
+		return Boolean(this.incomingDirectCalls.get(callId)?.dismissed);
+	}
+})();
+
+Meteor.startup(() => Tracker.autorun(() => VideoConfManager.updateUser()));
diff --git a/apps/meteor/client/portals/VideoConfPopupPortal.ts b/apps/meteor/client/portals/VideoConfPopupPortal.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f6b6cff847a3c96fedb734906ec4337a024431de
--- /dev/null
+++ b/apps/meteor/client/portals/VideoConfPopupPortal.ts
@@ -0,0 +1,17 @@
+import { memo, useEffect, ReactElement, ReactNode, useState } from 'react';
+import { createPortal } from 'react-dom';
+
+import { createAnchor } from '../lib/utils/createAnchor';
+import { deleteAnchor } from '../lib/utils/deleteAnchor';
+
+type VideoConfPortalProps = {
+	children?: ReactNode;
+};
+
+const VideoConfPortal = ({ children }: VideoConfPortalProps): ReactElement => {
+	const [videoConfRoot] = useState(() => createAnchor('video-conf-root'));
+	useEffect(() => (): void => deleteAnchor(videoConfRoot), [videoConfRoot]);
+	return createPortal(children, videoConfRoot);
+};
+
+export default memo(VideoConfPortal);
diff --git a/apps/meteor/client/providers/CallProvider/CallProvider.tsx b/apps/meteor/client/providers/CallProvider/CallProvider.tsx
index 29dd13c4fde48ef3aa71dd7de564e36cbef91f1b..a0d3fd0b118c0971f57618263f06c447dddf9a68 100644
--- a/apps/meteor/client/providers/CallProvider/CallProvider.tsx
+++ b/apps/meteor/client/providers/CallProvider/CallProvider.tsx
@@ -34,7 +34,7 @@ import { OutgoingByeRequest } from 'sip.js/lib/core';
 
 import { CustomSounds } from '../../../app/custom-sounds/client';
 import { getUserPreference } from '../../../app/utils/client';
-import { useHasLicense } from '../../../ee/client/hooks/useHasLicense';
+import { useHasLicenseModule } from '../../../ee/client/hooks/useHasLicenseModule';
 import { WrapUpCallModal } from '../../../ee/client/voip/components/modals/WrapUpCallModal';
 import { CallContext, CallContextValue } from '../../contexts/CallContext';
 import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
@@ -70,7 +70,7 @@ export const CallProvider: FC = ({ children }) => {
 	const homeRoute = useRoute('home');
 	const setOutputMediaDevice = useSetOutputMediaDevice();
 	const setInputMediaDevice = useSetInputMediaDevice();
-	const isEnterprise = useHasLicense('voip-enterprise');
+	const isEnterprise = useHasLicenseModule('voip-enterprise');
 
 	const remoteAudioMediaRef = useRef<IExperimentalHTMLAudioElement>(null); // TODO: Create a dedicated file for the AUDIO and make the controls accessible
 
@@ -284,7 +284,7 @@ export const CallProvider: FC = ({ children }) => {
 		remoteAudioMediaRef.current && result.voipClient.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current });
 	}, [result.voipClient]);
 
-	const hasLicenseToMakeVoIPCalls = useHasLicense('voip-enterprise');
+	const hasLicenseToMakeVoIPCalls = useHasLicenseModule('voip-enterprise');
 
 	useEffect(() => {
 		if (!result.voipClient) {
diff --git a/apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts b/apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts
index c7cfcf0519c92b13f05f309eded6cc42a012d006..828b825721a69d65aba7442fd70afee6ee6f3181 100644
--- a/apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts
+++ b/apps/meteor/client/providers/CallProvider/hooks/useVoipClient.ts
@@ -4,7 +4,7 @@ import { useUser, useSetting, useEndpoint, useStream } from '@rocket.chat/ui-con
 import { KJUR } from 'jsrsasign';
 import { useEffect, useState } from 'react';
 
-import { useHasLicense } from '../../../../ee/client/hooks/useHasLicense';
+import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
 import { EEVoipClient } from '../../../../ee/client/lib/voip/EEVoipClient';
 import { VoIPUser } from '../../../lib/voip/VoIPUser';
 import { useWebRtcServers } from './useWebRtcServers';
@@ -32,7 +32,7 @@ export const useVoipClient = (): UseVoipClientResult => {
 	const iceServers = useWebRtcServers();
 	const [result, setResult] = useSafely(useState<UseVoipClientResult>({}));
 
-	const isEE = useHasLicense('voip-enterprise');
+	const isEE = useHasLicenseModule('voip-enterprise');
 
 	useEffect(() => {
 		const voipEnableEventHandler = (enabled: boolean): void => {
diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx
index c10dc215fda61bade75986b7d4a74bd3c3bc340d..ce10cc9ca1e16ea871d4b26bb1ff01259c1f965a 100644
--- a/apps/meteor/client/providers/MeteorProvider.tsx
+++ b/apps/meteor/client/providers/MeteorProvider.tsx
@@ -18,6 +18,7 @@ import ToastMessagesProvider from './ToastMessagesProvider';
 import TooltipProvider from './TooltipProvider';
 import TranslationProvider from './TranslationProvider';
 import UserProvider from './UserProvider';
+import VideoConfProvider from './VideoConfProvider';
 
 const MeteorProvider: FC = ({ children }) => (
 	<ConnectionStatusProvider>
@@ -34,13 +35,15 @@ const MeteorProvider: FC = ({ children }) => (
 												<UserProvider>
 													<ModalProvider>
 														<AuthorizationProvider>
-															<DeviceProvider>
-																<CallProvider>
-																	<OmnichannelProvider>
-																		<AttachmentProvider>{children}</AttachmentProvider>
-																	</OmnichannelProvider>
-																</CallProvider>
-															</DeviceProvider>
+															<VideoConfProvider>
+																<DeviceProvider>
+																	<CallProvider>
+																		<OmnichannelProvider>
+																			<AttachmentProvider>{children}</AttachmentProvider>
+																		</OmnichannelProvider>
+																	</CallProvider>
+																</DeviceProvider>
+															</VideoConfProvider>
 														</AuthorizationProvider>
 													</ModalProvider>
 												</UserProvider>
diff --git a/apps/meteor/client/providers/VideoConfProvider.tsx b/apps/meteor/client/providers/VideoConfProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..68492ddc0a14a1b934ef6aeb8c3313621d83e8e8
--- /dev/null
+++ b/apps/meteor/client/providers/VideoConfProvider.tsx
@@ -0,0 +1,82 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { useSetModal } from '@rocket.chat/ui-contexts';
+import React, { ReactElement, useState, ReactNode, useMemo, useEffect } from 'react';
+import { Unsubscribe } from 'use-subscription';
+
+import { VideoConfContext, VideoConfPopupPayload } from '../contexts/VideoConfContext';
+import { VideoConfManager, DirectCallParams, ProviderCapabilities, CallPreferences } from '../lib/VideoConfManager';
+import VideoConfBlockModal from '../views/room/contextualBar/VideoConference/VideoConfBlockModal';
+import VideoConfPopups from '../views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups';
+
+const VideoConfContextProvider = ({ children }: { children: ReactNode }): ReactElement => {
+	const [outgoing, setOutgoing] = useState<VideoConfPopupPayload | undefined>();
+	const setModal = useSetModal();
+
+	useEffect(
+		() =>
+			VideoConfManager.on('call/join', (props) => {
+				const open = (): void => {
+					const popup = window.open(props.url);
+
+					if (popup !== null) {
+						return;
+					}
+
+					setModal(<VideoConfBlockModal onClose={(): void => setModal(null)} onConfirm={open} />);
+				};
+				open();
+			}),
+		[setModal],
+	);
+
+	useEffect(() => {
+		VideoConfManager.on('direct/stopped', () => setOutgoing(undefined));
+		VideoConfManager.on('calling/ended', () => setOutgoing(undefined));
+	}, []);
+
+	const contextValue = useMemo(
+		() => ({
+			dispatchOutgoing: (option: Omit<VideoConfPopupPayload, 'id'>): void => setOutgoing({ ...option, id: option.rid }),
+			dismissOutgoing: (): void => setOutgoing(undefined),
+			startCall: (rid: IRoom['_id'], confTitle?: string): Promise<void> => VideoConfManager.startCall(rid, confTitle),
+			acceptCall: (callId: string): void => VideoConfManager.acceptIncomingCall(callId),
+			joinCall: (callId: string): Promise<void> => VideoConfManager.joinCall(callId),
+			dismissCall: (callId: string): void => {
+				VideoConfManager.dismissIncomingCall(callId);
+			},
+			rejectIncomingCall: (callId: string): void => VideoConfManager.rejectIncomingCall(callId),
+			abortCall: (): void => VideoConfManager.abortCall(),
+			setPreferences: (prefs: Partial<typeof VideoConfManager['preferences']>): void => VideoConfManager.setPreferences(prefs),
+			queryIncomingCalls: {
+				getCurrentValue: (): DirectCallParams[] => VideoConfManager.getIncomingDirectCalls(),
+				subscribe: (cb: () => void): Unsubscribe => VideoConfManager.on('incoming/changed', cb),
+			},
+			queryRinging: {
+				getCurrentValue: (): boolean => VideoConfManager.isRinging(),
+				subscribe: (cb: () => void): Unsubscribe => VideoConfManager.on('ringing/changed', cb),
+			},
+			queryCalling: {
+				getCurrentValue: (): boolean => VideoConfManager.isCalling(),
+				subscribe: (cb: () => void): Unsubscribe => VideoConfManager.on('calling/changed', cb),
+			},
+			queryCapabilities: {
+				getCurrentValue: (): ProviderCapabilities => VideoConfManager.capabilities,
+				subscribe: (cb: () => void): Unsubscribe => VideoConfManager.on('capabilities/changed', cb),
+			},
+			queryPreferences: {
+				getCurrentValue: (): CallPreferences => VideoConfManager.preferences,
+				subscribe: (cb: () => void): Unsubscribe => VideoConfManager.on('preference/changed', cb),
+			},
+		}),
+		[],
+	);
+
+	return (
+		<VideoConfContext.Provider value={contextValue}>
+			{children}
+			<VideoConfPopups>{outgoing}</VideoConfPopups>
+		</VideoConfContext.Provider>
+	);
+};
+
+export default VideoConfContextProvider;
diff --git a/apps/meteor/client/sidebar/RoomList/Row.tsx b/apps/meteor/client/sidebar/RoomList/Row.tsx
index 5cbe5a707e2affcf38803fbca7eba71127d2cb84..394c84f7783e23762dfc686434eabec34f38ab59 100644
--- a/apps/meteor/client/sidebar/RoomList/Row.tsx
+++ b/apps/meteor/client/sidebar/RoomList/Row.tsx
@@ -1,8 +1,9 @@
 import { IRoom, ISubscription } from '@rocket.chat/core-typings';
 import { SidebarSection } from '@rocket.chat/fuselage';
 import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { ComponentType, memo, ReactElement } from 'react';
+import React, { ComponentType, memo, useMemo, ReactElement } from 'react';
 
+import { useVideoConfAcceptCall, useVideoConfRejectIncomingCall, useVideoConfIncomingCalls } from '../../contexts/VideoConfContext';
 import { useAvatarTemplate } from '../hooks/useAvatarTemplate';
 import { useTemplateByViewMode } from '../hooks/useTemplateByViewMode';
 import OmnichannelSection from '../sections/OmnichannelSection';
@@ -27,6 +28,20 @@ type RoomListRowProps = {
 const Row = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRoom }): ReactElement => {
 	const { extended, t, SideBarItemTemplate, AvatarTemplate, openedRoom, sidebarViewMode } = data;
 
+	const acceptCall = useVideoConfAcceptCall();
+	const rejectCall = useVideoConfRejectIncomingCall();
+	const incomingCalls = useVideoConfIncomingCalls();
+	const currentCall = incomingCalls.find((call) => call.rid === item.rid);
+
+	const videoConfActions = useMemo(
+		() =>
+			currentCall && {
+				acceptCall: (): void => acceptCall(currentCall.callId),
+				rejectCall: (): void => rejectCall(currentCall.callId),
+			},
+		[acceptCall, rejectCall, currentCall],
+	);
+
 	if (typeof item === 'string') {
 		const Section = sections[item];
 		return Section ? (
@@ -37,6 +52,7 @@ const Row = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRo
 			</SidebarSection>
 		);
 	}
+
 	return (
 		<SideBarItemTemplateWithData
 			sidebarViewMode={sidebarViewMode}
@@ -46,6 +62,7 @@ const Row = ({ data, item }: { data: RoomListRowProps; item: ISubscription & IRo
 			extended={extended}
 			SideBarItemTemplate={SideBarItemTemplate}
 			AvatarTemplate={AvatarTemplate}
+			videoConfActions={currentCall && videoConfActions}
 		/>
 	);
 };
diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx
index 104a0b97496fab1cb343227c89cc58ee398e75b2..67889bc3a4fcd107dccfaa534a461466dd7bf197 100644
--- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx
+++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx
@@ -7,7 +7,7 @@ import {
 	isOmnichannelRoom,
 	ISubscription,
 } from '@rocket.chat/core-typings';
-import { Badge, Sidebar } from '@rocket.chat/fuselage';
+import { Badge, Sidebar, SidebarItemAction } from '@rocket.chat/fuselage';
 import { useLayout, useTranslation } from '@rocket.chat/ui-contexts';
 import React, { AllHTMLAttributes, ComponentType, memo, ReactElement, ReactNode } from 'react';
 
@@ -41,7 +41,7 @@ type RoomListRowProps = {
 			icon: ReactNode;
 			title: ReactNode;
 			avatar: ReactNode;
-			// actions: unknown;
+			actions: unknown;
 			href: string;
 			time?: Date;
 			menu?: ReactNode;
@@ -69,6 +69,9 @@ type RoomListRowProps = {
 	selected?: boolean;
 
 	sidebarViewMode?: unknown;
+	videoConfActions?: {
+		[action: string]: () => void;
+	};
 };
 
 function SideBarItemTemplateWithData({
@@ -76,13 +79,13 @@ function SideBarItemTemplateWithData({
 	id,
 	selected,
 	style,
-
 	extended,
 	SideBarItemTemplate,
 	AvatarTemplate,
 	t,
 	// sidebarViewMode,
 	isAnonymous,
+	videoConfActions,
 }: RoomListRowProps): ReactElement {
 	const { sidebar } = useLayout();
 
@@ -108,7 +111,7 @@ function SideBarItemTemplateWithData({
 	const icon = (
 		// TODO: Remove icon='at'
 		<Sidebar.Item.Icon highlighted={highlighted} icon='at'>
-			<RoomIcon room={room} placement='sidebar' />
+			<RoomIcon room={room} placement='sidebar' isIncomingCall={Boolean(videoConfActions)} />
 		</Sidebar.Item.Icon>
 	);
 
@@ -148,6 +151,14 @@ function SideBarItemTemplateWithData({
 			style={style}
 			badges={badges}
 			avatar={AvatarTemplate && <AvatarTemplate {...room} />}
+			actions={
+				videoConfActions && (
+					<>
+						<SidebarItemAction onClick={videoConfActions.acceptCall} secondary success icon='phone' />
+						<SidebarItemAction onClick={videoConfActions.rejectCall} secondary danger icon='phone-off' />
+					</>
+				)
+			}
 			menu={
 				!isAnonymous &&
 				!isQueued &&
diff --git a/apps/meteor/client/sidebar/footer/voip/index.tsx b/apps/meteor/client/sidebar/footer/voip/index.tsx
index c57eded2a3ba289dc4229afd8d5c8fb3ed980712..15323a9797e2454ad21740d0dea749324f53cb82 100644
--- a/apps/meteor/client/sidebar/footer/voip/index.tsx
+++ b/apps/meteor/client/sidebar/footer/voip/index.tsx
@@ -1,7 +1,7 @@
 import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
 import React, { ReactElement, useCallback, useMemo, useState } from 'react';
 
-import { useHasLicense } from '../../../../ee/client/hooks/useHasLicense';
+import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
 import {
 	useCallActions,
 	useCallCreateRoom,
@@ -29,7 +29,7 @@ export const VoipFooter = (): ReactElement | null => {
 
 	const [muted, setMuted] = useState(false);
 	const [paused, setPaused] = useState(false);
-	const isEE = useHasLicense('voip-enterprise');
+	const isEE = useHasLicenseModule('voip-enterprise');
 
 	const toggleMic = useCallback(
 		(state: boolean) => {
diff --git a/apps/meteor/client/sidebar/hooks/useRoomList.ts b/apps/meteor/client/sidebar/hooks/useRoomList.ts
index 202c8afb29d94a98ea6a0ebed7aaa9f10c502ffc..865cb9d2b2d4d11e627e47a76f7ec9d4f88a0be5 100644
--- a/apps/meteor/client/sidebar/hooks/useRoomList.ts
+++ b/apps/meteor/client/sidebar/hooks/useRoomList.ts
@@ -3,6 +3,7 @@ import { useDebouncedState } from '@rocket.chat/fuselage-hooks';
 import { useUserPreference, useUserSubscriptions, useSetting } from '@rocket.chat/ui-contexts';
 import { useEffect } from 'react';
 
+import { useVideoConfIncomingCalls } from '../../contexts/VideoConfContext';
 import { useOmnichannelEnabled } from '../../hooks/omnichannel/useOmnichannelEnabled';
 import { useQueuedInquiries } from '../../hooks/omnichannel/useQueuedInquiries';
 import { useQueryOptions } from './useQueryOptions';
@@ -26,6 +27,8 @@ export const useRoomList = (): Array<ISubscription & IRoom> => {
 
 	const inquiries = useQueuedInquiries();
 
+	const incomingCalls = useVideoConfIncomingCalls();
+
 	let queue: IRoom[] = emptyQueue;
 	if (inquiries.enabled) {
 		queue = inquiries.queue;
@@ -33,6 +36,7 @@ export const useRoomList = (): Array<ISubscription & IRoom> => {
 
 	useEffect(() => {
 		setRoomList(() => {
+			const incomingCall = new Set();
 			const favorite = new Set();
 			const team = new Set();
 			const omnichannel = new Set();
@@ -44,6 +48,10 @@ export const useRoomList = (): Array<ISubscription & IRoom> => {
 			const onHold = new Set();
 
 			rooms.forEach((room) => {
+				if (incomingCalls.find((call) => call.rid === room.rid)) {
+					return incomingCall.add(room);
+				}
+
 				if (sidebarShowUnread && (room.alert || room.unread) && !room.hideUnreadStatus) {
 					return unread.add(room);
 				}
@@ -81,6 +89,7 @@ export const useRoomList = (): Array<ISubscription & IRoom> => {
 
 			const groups = new Map();
 			showOmnichannel && groups.set('Omnichannel', []);
+			incomingCall.size && groups.set('Incoming Calls', incomingCall);
 			showOmnichannel && inquiries.enabled && queue.length && groups.set('Incoming_Livechats', queue);
 			showOmnichannel && omnichannel.size && groups.set('Open_Livechats', omnichannel);
 			showOmnichannel && onHold.size && groups.set('On_Hold_Chats', onHold);
@@ -96,6 +105,7 @@ export const useRoomList = (): Array<ISubscription & IRoom> => {
 	}, [
 		rooms,
 		showOmnichannel,
+		incomingCalls,
 		inquiries.enabled,
 		queue,
 		sidebarShowUnread,
diff --git a/apps/meteor/client/views/admin/apps/AppDetailsHeader.tsx b/apps/meteor/client/views/admin/apps/AppDetailsHeader.tsx
index 93ff4c3f17be3444f69a27f095c9875d0ff13a16..c99413cd9867ba7fa3b6e5803f49429a858dd18e 100644
--- a/apps/meteor/client/views/admin/apps/AppDetailsHeader.tsx
+++ b/apps/meteor/client/views/admin/apps/AppDetailsHeader.tsx
@@ -35,12 +35,18 @@ const AppDetailsHeader = ({ app }: { app: App }): ReactElement => {
 					<Box fontScale='p2m' mie='x16'>
 						{t('By_author', { author: author?.name })}
 					</Box>
-					| <Box mi='x16'>{t('Version_version', { version })}</Box> |{' '}
-					<Box mis='x16'>
-						{t('Marketplace_app_last_updated', {
-							lastUpdated,
-						})}
-					</Box>
+					<Box is='span'> | </Box>
+					<Box mi='x16'>{t('Version_version', { version })}</Box>
+					{lastUpdated && (
+						<>
+							<Box is='span'> | </Box>
+							<Box mis='x16'>
+								{t('Marketplace_app_last_updated', {
+									lastUpdated,
+								})}
+							</Box>
+						</>
+					)}
 				</Box>
 			</Box>
 		</Box>
diff --git a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx
index 479206cb31314b813320a29933977a3be98029ae..c6057ec5459cdc86790daf1311a541486d8d497c 100644
--- a/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx
+++ b/apps/meteor/client/views/admin/info/DeploymentCard.stories.tsx
@@ -231,6 +231,28 @@ export default {
 			totalEncryptedMessages: 0,
 			totalLinkInvitationUses: 0,
 			totalManuallyAddedUsers: 0,
+			videoConf: {
+				videoConference: {
+					started: 0,
+					ended: 0,
+				},
+				direct: {
+					calling: 0,
+					started: 0,
+					ended: 0,
+				},
+				livechat: {
+					started: 0,
+					ended: 0,
+				},
+				settings: {
+					provider: '',
+					dms: false,
+					channels: false,
+					groups: false,
+					teams: false,
+				},
+			},
 			totalSubscriptionRoles: 0,
 			totalUserRoles: 0,
 			totalWebRTCCalls: 0,
diff --git a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx
index 9be279660e3ba10875efa44d05c2199cd72e192c..be131afc8558a4becfc6fea14150b5577f8ada23 100644
--- a/apps/meteor/client/views/admin/info/InformationPage.stories.tsx
+++ b/apps/meteor/client/views/admin/info/InformationPage.stories.tsx
@@ -261,6 +261,28 @@ export default {
 			totalEncryptedMessages: 0,
 			totalLinkInvitationUses: 0,
 			totalManuallyAddedUsers: 0,
+			videoConf: {
+				videoConference: {
+					started: 0,
+					ended: 0,
+				},
+				direct: {
+					calling: 0,
+					started: 0,
+					ended: 0,
+				},
+				livechat: {
+					started: 0,
+					ended: 0,
+				},
+				settings: {
+					provider: '',
+					dms: false,
+					channels: false,
+					groups: false,
+					teams: false,
+				},
+			},
 			totalSubscriptionRoles: 0,
 			totalUserRoles: 0,
 			totalWebRTCCalls: 0,
diff --git a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx
index 154889e0fce47830c33faa0fca019253ef55197f..c4e5b04f201ca2339c92ef2f40418b5a79d4ae78 100644
--- a/apps/meteor/client/views/admin/info/UsageCard.stories.tsx
+++ b/apps/meteor/client/views/admin/info/UsageCard.stories.tsx
@@ -209,6 +209,28 @@ export default {
 			totalEncryptedMessages: 0,
 			totalLinkInvitationUses: 0,
 			totalManuallyAddedUsers: 0,
+			videoConf: {
+				videoConference: {
+					started: 0,
+					ended: 0,
+				},
+				direct: {
+					calling: 0,
+					started: 0,
+					ended: 0,
+				},
+				livechat: {
+					started: 0,
+					ended: 0,
+				},
+				settings: {
+					provider: '',
+					dms: false,
+					channels: false,
+					groups: false,
+					teams: false,
+				},
+			},
 			totalSubscriptionRoles: 0,
 			totalUserRoles: 0,
 			totalWebRTCCalls: 0,
diff --git a/apps/meteor/client/views/admin/info/UsageCard.tsx b/apps/meteor/client/views/admin/info/UsageCard.tsx
index dd06bd8276ecf3804fd0885c526a8015b89afedf..2825b56a54741ab3f10f1c143df9ed58f395ca6b 100644
--- a/apps/meteor/client/views/admin/info/UsageCard.tsx
+++ b/apps/meteor/client/views/admin/info/UsageCard.tsx
@@ -4,7 +4,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
 import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
 import React, { memo, ReactElement } from 'react';
 
-import { useHasLicense } from '../../../../ee/client/hooks/useHasLicense';
+import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule';
 import Card from '../../../components/Card';
 import { UserStatus } from '../../../components/UserStatus';
 import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize';
@@ -25,7 +25,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => {
 		router.push();
 	});
 
-	const canViewEngagement = useHasLicense('engagement-dashboard');
+	const canViewEngagement = useHasLicenseModule('engagement-dashboard');
 
 	return (
 		<Card data-qa-id='usage-card'>
diff --git a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx
index 446958d390d2bc57e7baebefb5a2e5d83369f1bc..7f077645343dc4c55bd30578786031484a7b10e8 100644
--- a/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx
+++ b/apps/meteor/client/views/admin/settings/MemoizedSetting.tsx
@@ -11,6 +11,7 @@ import FontSettingInput from './inputs/FontSettingInput';
 import GenericSettingInput from './inputs/GenericSettingInput';
 import IntSettingInput from './inputs/IntSettingInput';
 import LanguageSettingInput from './inputs/LanguageSettingInput';
+import LookupSettingInput from './inputs/LookupSettingInput';
 import MultiSelectSettingInput from './inputs/MultiSelectSettingInput';
 import PasswordSettingInput from './inputs/PasswordSettingInput';
 import RelativeUrlSettingInput from './inputs/RelativeUrlSettingInput';
@@ -36,6 +37,7 @@ const inputsByType: Record<ISettingBase['type'], ElementType<any>> = {
 	asset: AssetSettingInput,
 	roomPick: RoomPickSettingInput,
 	timezone: SelectTimezoneSettingInput,
+	lookup: LookupSettingInput,
 	date: GenericSettingInput, // @todo: implement
 	group: GenericSettingInput, // @todo: implement
 };
diff --git a/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..be8db62c1dcb03a0a447b40c634345cc69b31f4a
--- /dev/null
+++ b/apps/meteor/client/views/admin/settings/inputs/LookupSettingInput.tsx
@@ -0,0 +1,70 @@
+import { Box, Field, Flex, Select } from '@rocket.chat/fuselage';
+import type { PathFor } from '@rocket.chat/rest-typings';
+import React, { ReactElement } from 'react';
+
+import { AsyncState } from '../../../../hooks/useAsyncState';
+import { useEndpointData } from '../../../../hooks/useEndpointData';
+import ResetSettingButton from '../ResetSettingButton';
+
+type LookupSettingInputProps = {
+	_id: string;
+	label: string;
+	value?: string;
+	lookupEndpoint: PathFor<'GET'>;
+	placeholder?: string;
+	readonly?: boolean;
+	autocomplete?: boolean;
+	disabled?: boolean;
+	hasResetButton?: boolean;
+	onChangeValue?: (value: string) => void;
+	onResetButtonClick?: () => void;
+};
+
+function LookupSettingInput({
+	_id,
+	label,
+	value,
+	placeholder,
+	readonly,
+	autocomplete,
+	disabled,
+	lookupEndpoint,
+	hasResetButton,
+	onChangeValue,
+	onResetButtonClick,
+}: LookupSettingInputProps): ReactElement {
+	const handleChange = (value: string): void => {
+		onChangeValue?.(value);
+	};
+
+	const { value: options } = useEndpointData(lookupEndpoint) as AsyncState<{ data: { key: string; label: string }[] }>;
+	const values = options?.data || [];
+
+	return (
+		<>
+			<Flex.Container>
+				<Box>
+					<Field.Label htmlFor={_id} title={_id}>
+						{label}
+					</Field.Label>
+					{hasResetButton && <ResetSettingButton data-qa-reset-setting-id={_id} onClick={onResetButtonClick} />}
+				</Box>
+			</Flex.Container>
+			<Field.Row>
+				<Select
+					data-qa-setting-id={_id}
+					id={_id}
+					value={value}
+					placeholder={placeholder}
+					disabled={disabled}
+					readOnly={readonly}
+					autoComplete={autocomplete === false ? 'off' : undefined}
+					onChange={handleChange}
+					options={values.map(({ key, label }) => [key, label])}
+				/>
+			</Field.Row>
+		</>
+	);
+}
+
+export default LookupSettingInput;
diff --git a/apps/meteor/client/views/blocks/MessageBlock.js b/apps/meteor/client/views/blocks/MessageBlock.js
index 85e6fc577ab10fa6f027717dfabaad3fb3e31e40..1b9c194350ca75f8ed01c6c6332de1d95a0baf81 100644
--- a/apps/meteor/client/views/blocks/MessageBlock.js
+++ b/apps/meteor/client/views/blocks/MessageBlock.js
@@ -4,6 +4,7 @@ import React from 'react';
 
 import * as ActionManager from '../../../app/ui-message/client/ActionManager';
 import { useBlockRendered } from '../../components/message/hooks/useBlockRendered';
+import { useVideoConfJoinCall, useVideoConfSetPreferences } from '../../contexts/VideoConfContext';
 import { renderMessageBody } from '../../lib/utils/renderMessageBody';
 import './textParsers';
 
@@ -13,15 +14,25 @@ const mrkdwn = ({ text } = {}) => text && <span dangerouslySetInnerHTML={{ __htm
 messageParser.mrkdwn = mrkdwn;
 function MessageBlock({ mid: _mid, rid, blocks, appId }) {
 	const { ref, className } = useBlockRendered();
+	const joinCall = useVideoConfJoinCall();
+	const setPreferences = useVideoConfSetPreferences();
+
 	const context = {
-		action: ({ actionId, value, blockId, mid = _mid }) => {
+		action: ({ actionId, value, blockId, mid = _mid, appId }, event) => {
+			if (appId === 'videoconf-core' && actionId === 'join') {
+				event.preventDefault();
+				setPreferences({ mic: true, cam: false });
+				joinCall(blockId);
+				return;
+			}
+
 			ActionManager.triggerBlockAction({
 				blockId,
 				actionId,
 				value,
 				mid,
 				rid,
-				appId: blocks[0].appId,
+				appId,
 				container: {
 					type: UIKitIncomingInteractionContainerType.MESSAGE,
 					id: mid,
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/CallBBB.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/CallBBB.tsx
deleted file mode 100644
index 2c79b6bf353a7049009f4fef4bea3ce882e0a34a..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/CallBBB.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { FC } from 'react';
-
-import VerticalBar from '../../../../../components/VerticalBar';
-
-type CallBBBProps = {
-	startCall: () => void;
-	endCall: () => void;
-	handleClose: () => void;
-	canManageCall: boolean;
-	live: boolean;
-	openNewWindow: boolean;
-};
-
-const CallBBB: FC<CallBBBProps> = ({ handleClose, canManageCall, live, startCall, endCall, openNewWindow, ...props }) => {
-	const t = useTranslation();
-	return (
-		<>
-			<VerticalBar.Header>
-				<VerticalBar.Icon name='phone' />
-				<VerticalBar.Text>{t('Call')}</VerticalBar.Text>
-				{handleClose && <VerticalBar.Close onClick={handleClose} />}
-			</VerticalBar.Header>
-			<VerticalBar.ScrollableContent {...(props as any)}>
-				{openNewWindow ? (
-					<>
-						<Box fontScale='p2m'>{t('Video_Conference')}</Box>
-						<Box fontScale='p2' color='neutral-700'>
-							{t('Opened_in_a_new_window')}
-						</Box>
-					</>
-				) : null}
-				<ButtonGroup stretch>
-					{live && (
-						<Button primary onClick={startCall}>
-							{t('BBB_Join_Meeting')}
-						</Button>
-					)}
-					{live && canManageCall && (
-						<Button secondary danger onClick={endCall}>
-							{t('BBB_End_Meeting')}
-						</Button>
-					)}
-					{!live && canManageCall && (
-						<Button primary onClick={startCall}>
-							{t('BBB_Start_Meeting')}
-						</Button>
-					)}
-					{!live && !canManageCall && <Button primary>{t('BBB_You_have_no_permission_to_start_a_call')}</Button>}
-				</ButtonGroup>
-			</VerticalBar.ScrollableContent>
-		</>
-	);
-};
-
-export default CallBBB;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/D.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/D.tsx
deleted file mode 100644
index ef4cd74957075b9529f2fc4b2e5658dc588d6a0e..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/D.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import type { IRoom } from '@rocket.chat/core-typings';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useSetting, usePermission, useMethod } from '@rocket.chat/ui-contexts';
-import React, { FC, useEffect } from 'react';
-
-import { popout } from '../../../../../../app/ui-utils/client';
-import { useRoom } from '../../../contexts/RoomContext';
-import { useTabBarClose } from '../../../providers/ToolboxProvider';
-import CallBBB from './CallBBB';
-
-type DProps = {
-	rid: IRoom['_id'];
-};
-
-const D: FC<DProps> = ({ rid }) => {
-	const handleClose = useTabBarClose();
-	const openNewWindow = !!useSetting('bigbluebutton_Open_New_Window');
-	const hasCallManagement = usePermission('call-management', rid);
-	const room = useRoom();
-	const join = useMethod('bbbJoin');
-	const end = useMethod('bbbEnd');
-
-	const endCall = useMutableCallback(() => {
-		end({ rid });
-	});
-
-	const startCall = useMutableCallback(async () => {
-		const result = await join({ rid });
-		if (!result) {
-			return;
-		}
-		if (openNewWindow) {
-			return window.open(result.url);
-		}
-		popout.open({
-			content: 'bbbLiveView',
-			data: {
-				source: result.url,
-				streamingOptions: result,
-				canOpenExternal: true,
-				showVideoControls: false,
-			},
-			onCloseCallback: () => false,
-		});
-	});
-
-	useEffect(() => {
-		if (room?.streamingOptions?.type !== 'call' || openNewWindow || popout.context) {
-			return;
-		}
-		startCall();
-		return (): void => {
-			popout.close();
-		};
-	}, [room?.streamingOptions?.type, startCall, openNewWindow]);
-
-	const canManageCall = room?.t === 'd' || hasCallManagement;
-
-	return (
-		<CallBBB
-			handleClose={handleClose as () => void}
-			openNewWindow={openNewWindow}
-			live={room?.streamingOptions?.type === 'call'}
-			endCall={endCall}
-			startCall={startCall}
-			canManageCall={canManageCall}
-		/>
-	);
-};
-
-export default D;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/index.ts
deleted file mode 100644
index cf6c5685d491214ee0709efad7dc08c947e55441..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/BBB/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './D';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.stories.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.stories.tsx
deleted file mode 100644
index 6c433788cf60dbcb37f61e6eaab9e4dfd226df27..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.stories.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
-import React from 'react';
-
-import VerticalBar from '../../../../../components/VerticalBar';
-import CallJitsi from './CallJitsi';
-
-export default {
-	title: 'Room/Contextual Bar/CallJitsi',
-	component: CallJitsi,
-	parameters: {
-		layout: 'fullscreen',
-	},
-	decorators: [(fn) => <VerticalBar height='100vh'>{fn()}</VerticalBar>],
-} as ComponentMeta<typeof CallJitsi>;
-
-export const Default: ComponentStory<typeof CallJitsi> = (args) => <CallJitsi {...args} />;
-Default.storyName = 'CallJitsi';
-Default.args = {
-	openNewWindow: true,
-};
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.tsx
deleted file mode 100644
index 49ef48a2fe10cf93ef805ac790aacc35f636777a..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsi.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { ISetting } from '@rocket.chat/core-typings';
-import { Box } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { ReactElement, ReactNode, RefObject } from 'react';
-
-import VerticalBar from '../../../../../components/VerticalBar';
-
-type CallJitsiProps = {
-	handleClose: () => void;
-	openNewWindow: ISetting['value'];
-	refContent: RefObject<HTMLDivElement>;
-	children: ReactNode;
-};
-
-const CallJitsi = ({ handleClose, openNewWindow, refContent, children }: CallJitsiProps): ReactElement => {
-	const t = useTranslation();
-
-	const content = openNewWindow ? (
-		<>
-			<Box fontScale='p2m'>{t('Video_Conference')}</Box>
-			<Box fontScale='p2' color='neutral-700'>
-				{t('Opened_in_a_new_window')}
-			</Box>
-		</>
-	) : (
-		<div ref={refContent} />
-	);
-
-	return (
-		<>
-			<VerticalBar.Header>
-				<VerticalBar.Icon name='phone' />
-				<VerticalBar.Text>{t('Call')}</VerticalBar.Text>
-				{handleClose && <VerticalBar.Close onClick={handleClose} />}
-			</VerticalBar.Header>
-			<VerticalBar.ScrollableContent>
-				{content}
-				{children}
-			</VerticalBar.ScrollableContent>
-		</>
-	);
-};
-
-export default CallJitsi;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsiWithData.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsiWithData.tsx
deleted file mode 100644
index 56ba9d82926024d3924b5ca4e643c3b31558743e..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/CallJitsiWithData.tsx
+++ /dev/null
@@ -1,235 +0,0 @@
-import { IRoom } from '@rocket.chat/core-typings';
-import { Skeleton, Icon, Box } from '@rocket.chat/fuselage';
-import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks';
-import { clear } from '@rocket.chat/memo';
-import {
-	useConnectionStatus,
-	useSetModal,
-	useToastMessageDispatch,
-	useUser,
-	useSettings,
-	useMethod,
-	useTranslation,
-} from '@rocket.chat/ui-contexts';
-import React, { ReactElement, useRef, useEffect, useState, useMemo, useLayoutEffect, memo } from 'react';
-
-import { Subscriptions } from '../../../../../../app/models/client';
-import { HEARTBEAT, TIMEOUT, DEBOUNCE } from '../../../../../../app/videobridge/constants';
-import GenericModal from '../../../../../components/GenericModal';
-import { useRoom } from '../../../contexts/RoomContext';
-import { useTabBarClose } from '../../../providers/ToolboxProvider';
-import CallJitsi from './CallJitsi';
-import { JitsiBridge } from './lib/JitsiBridge';
-
-const querySettings = {
-	_id: [
-		'Jitsi_Open_New_Window',
-		'Jitsi_Domain',
-		'Jitsi_URL_Room_Hash',
-		'uniqueID',
-		'Jitsi_URL_Room_Prefix',
-		'Jitsi_URL_Room_Suffix',
-		'Jitsi_Chrome_Extension',
-		'Jitsi_SSL',
-		'Jitsi_Enabled_TokenAuth',
-	],
-};
-
-const CallJitsiWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => {
-	const user = useUser();
-	const { connected } = useConnectionStatus();
-	const [accessToken, setAccessToken] = useSafely(useState());
-	const [accepted, setAccepted] = useState(false);
-	const room = useRoom();
-	const ref = useRef<HTMLDivElement>(null);
-	const setModal = useSetModal();
-	const handleClose = useTabBarClose();
-	const closeModal = useMutableCallback(() => setModal(null));
-	const generateAccessToken = useMethod('jitsi:generateAccessToken');
-	const updateTimeout = useMethod('jitsi:updateTimeout');
-	const joinRoom = useMethod('joinRoom');
-	const dispatchToastMessage = useToastMessageDispatch();
-	const t = useTranslation();
-
-	const handleCancel = useMutableCallback(() => {
-		closeModal();
-		handleClose();
-	});
-
-	const {
-		Jitsi_Open_New_Window: openNewWindow,
-		Jitsi_Domain: domain,
-		Jitsi_SSL: ssl,
-		Jitsi_Chrome_Extension: desktopSharingChromeExtId,
-		Jitsi_URL_Room_Hash: useHashName,
-		uniqueID,
-		Jitsi_URL_Room_Prefix: prefix,
-		Jitsi_URL_Room_Suffix: sufix,
-		Jitsi_Enabled_TokenAuth: isEnabledTokenAuth,
-	} = Object.fromEntries(useSettings(querySettings).map(({ _id, value }) => [_id, value]));
-
-	useEffect(() => {
-		let ignore = false;
-		if (!isEnabledTokenAuth) {
-			setAccessToken(undefined);
-			return;
-		}
-
-		(async (): Promise<void> => {
-			const accessToken = await generateAccessToken(rid);
-			!ignore && setAccessToken(accessToken);
-		})();
-
-		return (): void => {
-			ignore = true;
-		};
-	}, [generateAccessToken, isEnabledTokenAuth, rid, setAccessToken]);
-
-	useLayoutEffect(() => {
-		if (!connected) {
-			handleClose();
-		}
-	}, [connected, handleClose]);
-
-	const rname = useHashName ? uniqueID + rid : encodeURIComponent((room?.t === 'd' ? room.usernames?.join('x') : room.name) ?? '');
-
-	const jitsi = useMemo(() => {
-		if (isEnabledTokenAuth && !accessToken) {
-			return;
-		}
-
-		const jitsiRoomName = prefix + rname + sufix;
-
-		return new JitsiBridge(
-			{
-				openNewWindow,
-				ssl,
-				domain,
-				jitsiRoomName,
-				accessToken,
-				desktopSharingChromeExtId,
-				name: user?.name || user?.username,
-			},
-			HEARTBEAT,
-		);
-	}, [
-		accessToken,
-		desktopSharingChromeExtId,
-		domain,
-		isEnabledTokenAuth,
-		openNewWindow,
-		prefix,
-		rname,
-		ssl,
-		sufix,
-		user?.name,
-		user?.username,
-	]);
-
-	const testAndHandleTimeout = useMutableCallback(() => {
-		if (jitsi?.openNewWindow) {
-			if (jitsi?.window?.closed) {
-				return jitsi.dispose();
-			}
-
-			try {
-				return updateTimeout(rid, false);
-			} catch (error) {
-				dispatchToastMessage({ type: 'error', message: t(error.reason) });
-				clear(() => undefined);
-				handleClose();
-				return jitsi.dispose();
-			}
-		}
-
-		if (new Date().valueOf() - new Date(room.jitsiTimeout ?? '').valueOf() > TIMEOUT) {
-			return jitsi?.dispose();
-		}
-
-		if (new Date().valueOf() - new Date(room.jitsiTimeout ?? '').valueOf() + TIMEOUT > DEBOUNCE) {
-			try {
-				return updateTimeout(rid, false);
-			} catch (error) {
-				dispatchToastMessage({ type: 'error', message: t(error.reason) });
-				clear(() => undefined);
-				handleClose();
-				return jitsi?.dispose();
-			}
-		}
-	});
-
-	useEffect(() => {
-		if (!accepted || !jitsi) {
-			return;
-		}
-
-		const clear = (): void => {
-			jitsi.off('HEARTBEAT', testAndHandleTimeout);
-			jitsi.dispose();
-		};
-
-		try {
-			if (jitsi.needsStart) {
-				jitsi.start(ref.current);
-				updateTimeout(rid, true);
-			} else {
-				updateTimeout(rid, false);
-			}
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: t(error.reason) });
-			clear();
-			handleClose();
-		}
-		jitsi.on('HEARTBEAT', testAndHandleTimeout);
-
-		return (): void => {
-			if (!jitsi.openNewWindow) clear();
-		};
-	}, [accepted, jitsi, rid, testAndHandleTimeout, updateTimeout, dispatchToastMessage, handleClose, t]);
-
-	const handleConfirm = useMutableCallback(() => {
-		if (jitsi) {
-			jitsi.needsStart = true;
-		}
-
-		setAccepted(true);
-		const sub = Subscriptions.findOne({ rid, 'u._id': user?._id });
-		if (!sub) {
-			joinRoom(rid);
-		}
-
-		if (openNewWindow) {
-			handleClose();
-		}
-	});
-
-	useLayoutEffect(() => {
-		if (!accepted) {
-			return setModal(() => (
-				<GenericModal
-					variant='warning'
-					title={t('Video_Conference')}
-					confirmText={t('Yes')}
-					onClose={handleCancel}
-					onCancel={handleCancel}
-					onConfirm={handleConfirm}
-				>
-					<Box display='flex' flexDirection='column' alignItems='center'>
-						<Icon name='modal-warning' size='x128' color='warning-500' />
-						<Box fontScale='h4'>{t('Start_video_call')}</Box>
-					</Box>
-				</GenericModal>
-			));
-		}
-
-		closeModal();
-	}, [accepted, closeModal, handleCancel, handleConfirm, setModal, t]);
-
-	return (
-		<CallJitsi handleClose={handleClose} openNewWindow={openNewWindow} refContent={ref}>
-			{!accepted && <Skeleton />}
-		</CallJitsi>
-	);
-};
-
-export default memo(CallJitsiWithData);
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/index.ts
deleted file mode 100644
index cab1d32f1aea58f6b0139e290fc2b5e4108d3eb3..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from './CallJitsiWithData';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/Jitsi.js b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/Jitsi.js
deleted file mode 100644
index 33f19651cfcc562ec2d84e550321ff0d1fd11469..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/Jitsi.js
+++ /dev/null
@@ -1,375 +0,0 @@
-/**
- * Implements API class that embeds Jitsi Meet in external applications.
- */
-
-import postis from 'postis';
-
-/**
- * The minimum width for the Jitsi Meet frame
- * @type {number}
- */
-const MIN_WIDTH = 200;
-// var MIN_WIDTH = 790;
-
-/**
- * The minimum height for the Jitsi Meet frame
- * @type {number}
- */
-const MIN_HEIGHT = 300;
-
-/**
- * Last id of api object
- * @type {number}
- */
-let id = 0;
-
-/**
- * Maps the names of the commands expected by the API with the name of the
- * commands expected by jitsi-meet
- */
-const commands = {
-	displayName: 'display-name',
-	toggleAudio: 'toggle-audio',
-	toggleVideo: 'toggle-video',
-	toggleFilmStrip: 'toggle-film-strip',
-	toggleChat: 'toggle-chat',
-	toggleContactList: 'toggle-contact-list',
-	toggleShareScreen: 'toggle-share-screen',
-};
-
-/**
- * Maps the names of the events expected by the API with the name of the
- * events expected by jitsi-meet
- */
-const events = {
-	incomingMessage: 'incoming-message',
-	outgoingMessage: 'outgoing-message',
-	displayNameChange: 'display-name-change',
-	participantJoined: 'participant-joined',
-	participantLeft: 'participant-left',
-	videoConferenceJoined: 'video-conference-joined',
-	videoConferenceLeft: 'video-conference-left',
-};
-
-/**
- * Sends the passed object to Jitsi Meet
- * @param postis {Postis object} the postis instance that is going to be used
- * to send the message
- * @param object the object to be sent
- * - method {sting}
- * - params {object}
- */
-function sendMessage(postis, object) {
-	postis.send(object);
-}
-
-/**
- * Sends message for event enable/disable status change.
- * @param postis {Postis object} the postis instance that is going to be used.
- * @param event {string} the name of the event
- * @param status {boolean} true - enabled; false - disabled;
- */
-function changeEventStatus(postis, event, status) {
-	if (!(event in events)) {
-		console.error('Not supported event name.');
-		return;
-	}
-	sendMessage(postis, {
-		method: 'jitsiSystemMessage',
-		params: { type: 'eventStatus', name: events[event], value: status },
-	});
-}
-
-/**
- * Constructs new API instance. Creates iframe element that loads
- * Jitsi Meet.
- * @param domain the domain name of the server that hosts the conference
- * @param room_name the name of the room to join
- * @param width width of the iframe
- * @param height height of the iframe
- * @param parent_node the node that will contain the iframe
- * @param filmStripOnly if the value is true only the small videos will be
- * visible.
- * @param noSsl if the value is true https won't be used
- * @param token if you need token authentication, then pass the token
- * @constructor
- */
-export function JitsiMeetExternalAPI(
-	domain,
-	room_name,
-	width,
-	height,
-	parentNode,
-	configOverwrite,
-	interfaceConfigOverwrite,
-	noSsl,
-	token,
-) {
-	if (!width || width < MIN_WIDTH) {
-		width = MIN_WIDTH;
-	}
-	if (!height || height < MIN_HEIGHT) {
-		height = MIN_HEIGHT;
-	}
-
-	this.parentNode = null;
-	if (parentNode) {
-		this.parentNode = parentNode;
-	} else {
-		const scriptTag = document.scripts[document.scripts.length - 1];
-		this.parentNode = scriptTag.parentNode;
-	}
-
-	this.iframeHolder = this.parentNode.appendChild(document.createElement('div'));
-	this.iframeHolder.id = `jitsiConference${id}`;
-	if (width) {
-		this.iframeHolder.style.width = `${width}px`;
-	}
-	if (height) {
-		this.iframeHolder.style.height = `${height}px`;
-	}
-	this.frameName = `jitsiConferenceFrame${id}`;
-	this.url = `${noSsl ? 'http' : 'https'}://${domain}/`;
-	if (room_name) {
-		this.url += room_name;
-	}
-	if (token) {
-		this.url += `?jwt=${token}`;
-	}
-	this.url += `#jitsi_meet_external_api_id=${id}`;
-
-	let key;
-	if (configOverwrite) {
-		for (key in configOverwrite) {
-			if (!configOverwrite.hasOwnProperty(key) || typeof key !== 'string') {
-				continue;
-			}
-			this.url += `&config.${key}=${configOverwrite[key]}`;
-		}
-	}
-
-	if (interfaceConfigOverwrite) {
-		for (key in interfaceConfigOverwrite) {
-			if (!interfaceConfigOverwrite.hasOwnProperty(key) || typeof key !== 'string') {
-				continue;
-			}
-			this.url += `&interfaceConfig.${key}=${interfaceConfigOverwrite[key]}`;
-		}
-	}
-
-	this.frame = document.createElement('iframe');
-	this.frame.src = this.url;
-	this.frame.name = this.frameName;
-	this.frame.id = this.frameName;
-	this.frame.width = '100%';
-	this.frame.height = '100%';
-	this.frame.setAttribute('allowFullScreen', 'true');
-	this.frame.setAttribute('allow', 'microphone; camera');
-	this.frame = this.iframeHolder.appendChild(this.frame);
-	this.postis = postis({
-		window: this.frame.contentWindow,
-		scope: `jitsi_meet_external_api_${id}`,
-	});
-
-	this.eventHandlers = {};
-
-	id++;
-}
-
-/**
- * Executes command. The available commands are:
- * displayName - sets the display name of the local participant to the value
- * passed in the arguments array.
- * toggleAudio - mutes / unmutes audio with no arguments
- * toggleVideo - mutes / unmutes video with no arguments
- * filmStrip - hides / shows the film strip with no arguments
- * If the command doesn't require any arguments the parameter should be set
- * to empty array or it may be omitted.
- * @param name the name of the command
- * @param arguments array of arguments
- */
-JitsiMeetExternalAPI.prototype.executeCommand = function (name, argumentsList) {
-	if (!(name in commands)) {
-		console.error('Not supported command name.');
-		return;
-	}
-	let argumentsArray = argumentsList;
-	if (!argumentsArray) {
-		argumentsArray = [];
-	}
-	sendMessage(this.postis, { method: commands[name], params: argumentsArray });
-};
-
-/**
- * Executes commands. The available commands are:
- * displayName - sets the display name of the local participant to the value
- * passed in the arguments array.
- * toggleAudio - mutes / unmutes audio. no arguments
- * toggleVideo - mutes / unmutes video. no arguments
- * filmStrip - hides / shows the film strip. no arguments
- * toggleChat - hides / shows chat. no arguments.
- * toggleContactList - hides / shows contact list. no arguments.
- * toggleShareScreen - starts / stops screen sharing. no arguments.
- * @param object the object with commands to be executed. The keys of the
- * object are the commands that will be executed and the values are the
- * arguments for the command.
- */
-JitsiMeetExternalAPI.prototype.executeCommands = function (object) {
-	Object.entries(object).forEach(([key, value]) => this.executeCommand(key, value));
-};
-
-/**
- * Adds event listeners to Meet Jitsi. The object key should be the name of
- * the event and value - the listener.
- * Currently we support the following
- * events:
- * incomingMessage - receives event notifications about incoming
- * messages. The listener will receive object with the following structure:
- * {{
- *  "from": from,//JID of the user that sent the message
- *  "nick": nick,//the nickname of the user that sent the message
- *  "message": txt//the text of the message
- * }}
- * outgoingMessage - receives event notifications about outgoing
- * messages. The listener will receive object with the following structure:
- * {{
- *  "message": txt//the text of the message
- * }}
- * displayNameChanged - receives event notifications about display name
- * change. The listener will receive object with the following structure:
- * {{
- * jid: jid,//the JID of the participant that changed his display name
- * displayname: displayName //the new display name
- * }}
- * participantJoined - receives event notifications about new participant.
- * The listener will receive object with the following structure:
- * {{
- * jid: jid //the jid of the participant
- * }}
- * participantLeft - receives event notifications about the participant that
- * left the room.
- * The listener will receive object with the following structure:
- * {{
- * jid: jid //the jid of the participant
- * }}
- * video-conference-joined - receives event notifications about the local user
- * has successfully joined the video conference.
- * The listener will receive object with the following structure:
- * {{
- * roomName: room //the room name of the conference
- * }}
- * video-conference-left - receives event notifications about the local user
- * has left the video conference.
- * The listener will receive object with the following structure:
- * {{
- * roomName: room //the room name of the conference
- * }}
- * @param object
- */
-JitsiMeetExternalAPI.prototype.addEventListeners = function (object) {
-	Object.entries(object).forEach(([key, value]) => this.addEventListener(key, value));
-};
-
-/**
- * Adds event listeners to Meet Jitsi. Currently we support the following
- * events:
- * incomingMessage - receives event notifications about incoming
- * messages. The listener will receive object with the following structure:
- * {{
- *  "from": from,//JID of the user that sent the message
- *  "nick": nick,//the nickname of the user that sent the message
- *  "message": txt//the text of the message
- * }}
- * outgoingMessage - receives event notifications about outgoing
- * messages. The listener will receive object with the following structure:
- * {{
- *  "message": txt//the text of the message
- * }}
- * displayNameChanged - receives event notifications about display name
- * change. The listener will receive object with the following structure:
- * {{
- * jid: jid,//the JID of the participant that changed his display name
- * displayname: displayName //the new display name
- * }}
- * participantJoined - receives event notifications about new participant.
- * The listener will receive object with the following structure:
- * {{
- * jid: jid //the jid of the participant
- * }}
- * participantLeft - receives event notifications about participant the that
- * left the room.
- * The listener will receive object with the following structure:
- * {{
- * jid: jid //the jid of the participant
- * }}
- * video-conference-joined - receives event notifications fired when the local
- * user has joined the video conference.
- * The listener will receive object with the following structure:
- * {{
- * roomName: room //the room name of the conference
- * }}
- * video-conference-left - receives event notifications fired when the local
- * user has joined the video conference.
- * The listener will receive object with the following structure:
- * {{
- * roomName: room //the room name of the conference
- * }}
- * @param event the name of the event
- * @param listener the listener
- */
-JitsiMeetExternalAPI.prototype.addEventListener = function (event, listener) {
-	if (!(event in events)) {
-		console.error('Not supported event name.');
-		return;
-	}
-	// We cannot remove listeners from postis that's why we are handling the
-	// callback that way.
-	if (!(event in this.eventHandlers)) {
-		this.postis.listen(events[event], (data) => {
-			if (event in this.eventHandlers && typeof this.eventHandlers[event] === 'function') {
-				this.eventHandlers[event].call(null, data);
-			}
-		});
-	}
-	this.eventHandlers[event] = listener;
-	changeEventStatus(this.postis, event, true);
-};
-
-/**
- * Removes event listener.
- * @param event the name of the event.
- */
-JitsiMeetExternalAPI.prototype.removeEventListener = function (event) {
-	if (!(event in this.eventHandlers)) {
-		console.error(`The event ${event} is not registered.`);
-		return;
-	}
-	delete this.eventHandlers[event];
-	changeEventStatus(this.postis, event, false);
-};
-
-/**
- * Removes event listeners.
- * @param events array with the names of the events.
- */
-JitsiMeetExternalAPI.prototype.removeEventListeners = function (events) {
-	for (let i = 0; i < events.length; i++) {
-		this.removeEventListener(events[i]);
-	}
-};
-
-/**
- * Removes the listeners and removes the Jitsi Meet frame.
- */
-JitsiMeetExternalAPI.prototype.dispose = function () {
-	this.postis.destroy();
-	const frame = document.getElementById(this.frameName);
-	if (frame) {
-		frame.src = 'about:blank';
-	}
-	const self = this;
-	window.setTimeout(() => {
-		self.iframeHolder.removeChild(self.frame);
-		self.iframeHolder.parentNode.removeChild(self.iframeHolder);
-	}, 10);
-};
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/JitsiBridge.js b/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/JitsiBridge.js
deleted file mode 100644
index 61c63954050407627d05ea60093014384809a98a..0000000000000000000000000000000000000000
--- a/apps/meteor/client/views/room/contextualBar/VideoConference/Jitsi/lib/JitsiBridge.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Emitter } from '@rocket.chat/emitter';
-
-import { JitsiMeetExternalAPI } from './Jitsi';
-
-export class JitsiBridge extends Emitter {
-	constructor({ openNewWindow, ssl, domain, jitsiRoomName, accessToken, desktopSharingChromeExtId, name }, heartbeat) {
-		super();
-
-		this.openNewWindow = openNewWindow;
-		this.ssl = ssl;
-		this.domain = domain;
-		this.jitsiRoomName = jitsiRoomName;
-		this.accessToken = accessToken;
-		this.desktopSharingChromeExtId = desktopSharingChromeExtId;
-		this.name = name;
-		this.heartbeat = heartbeat;
-		this.window = undefined;
-		this.needsStart = false;
-	}
-
-	start(domTarget) {
-		if (!this.needsStart) {
-			return;
-		}
-
-		this.needsStart = false;
-
-		const heartbeatTimer = setInterval(() => this.emit('HEARTBEAT', true), this.heartbeat);
-		this.once('dispose', () => clearTimeout(heartbeatTimer));
-
-		const { openNewWindow, ssl, domain, jitsiRoomName, accessToken, desktopSharingChromeExtId, name } = this;
-
-		const protocol = ssl ? 'https://' : 'http://';
-
-		const configOverwrite = {
-			desktopSharingChromeExtId,
-		};
-
-		const interfaceConfigOverwrite = {};
-
-		if (openNewWindow) {
-			const queryString = accessToken ? `?jwt=${accessToken}` : '';
-			const newWindow = window.open(`${protocol + domain}/${jitsiRoomName}${queryString}`, jitsiRoomName);
-
-			if (!newWindow) {
-				return;
-			}
-
-			const timer = setInterval(() => {
-				if (newWindow.closed) {
-					this.dispose();
-				}
-			}, 1000);
-
-			this.once('dispose', () => clearTimeout(timer));
-			this.window = newWindow;
-			return newWindow.focus();
-		}
-
-		const width = 'auto';
-		const height = 500;
-
-		const api = new JitsiMeetExternalAPI(
-			domain,
-			jitsiRoomName,
-			width,
-			height,
-			domTarget,
-			configOverwrite,
-			interfaceConfigOverwrite,
-			!ssl,
-			accessToken,
-		); // eslint-disable-line no-undef
-		api.executeCommand('displayName', [name]);
-		this.once('dispose', () => api.dispose());
-	}
-
-	dispose() {
-		clearInterval(this.timer);
-		this.emit('dispose', true);
-	}
-}
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfBlockModal.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfBlockModal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4114c00b8dc58a2abfb69b463de79a003d206e7f
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfBlockModal.tsx
@@ -0,0 +1,41 @@
+import { Box, Icon } from '@rocket.chat/fuselage';
+import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
+import React, { ReactElement } from 'react';
+
+import GenericModal from '../../../../components/GenericModal';
+
+const VideoConfBlockModal = ({ onClose, onConfirm }: { onClose: () => void; onConfirm: () => void }): ReactElement => {
+	const t = useTranslation();
+	const workspaceUrl = useSetting('Site_Url');
+
+	const confirmButtonContent = (
+		<Box>
+			<Icon mie='x8' size='x20' name='new-window' />
+			{t('Open_call')}
+		</Box>
+	);
+
+	return (
+		<GenericModal
+			icon={null}
+			variant='warning'
+			title={t('Open_call_in_new_tab')}
+			confirmText={confirmButtonContent}
+			onConfirm={onConfirm}
+			onCancel={onClose}
+			onClose={onClose}
+		>
+			<>
+				<Box mbe='x24'>{t('Your_web_browser_blocked_Rocket_Chat_from_opening_tab')}</Box>
+				<Box>
+					{t('To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL')}
+					<Box is='span' fontWeight={700}>
+						{workspaceUrl as string}
+					</Box>
+				</Box>
+			</>
+		</GenericModal>
+	);
+};
+
+export default VideoConfBlockModal;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b46ef94416c1e21137de7a4dbe69a12c58311d97
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfList.tsx
@@ -0,0 +1,82 @@
+import { IGroupVideoConference } from '@rocket.chat/core-typings';
+import { Box, States, StatesIcon, StatesTitle, StatesSubtitle } from '@rocket.chat/fuselage';
+import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import React, { ReactElement } from 'react';
+import { Virtuoso } from 'react-virtuoso';
+
+import ScrollableContentWrapper from '../../../../../components/ScrollableContentWrapper';
+import VerticalBar from '../../../../../components/VerticalBar';
+import { handleError } from '../../../../../lib/utils/handleError';
+import VideoConfListItem from './VideoConfListItem';
+
+type VideoConfListProps = {
+	onClose: () => void;
+	total: number;
+	videoConfs: IGroupVideoConference[];
+	loading: boolean;
+	error?: Error;
+	reload: () => void;
+	loadMoreItems: (min: number, max: number) => void;
+};
+
+const VideoConfList = ({ onClose, total, videoConfs, loading, error, reload, loadMoreItems }: VideoConfListProps): ReactElement => {
+	const t = useTranslation();
+
+	const { ref, contentBoxSize: { inlineSize = 378, blockSize = 1 } = {} } = useResizeObserver<HTMLElement>({
+		debounceDelay: 200,
+	});
+
+	if (loading) {
+		return <VerticalBar.Skeleton />;
+	}
+
+	return (
+		<>
+			<VerticalBar.Header>
+				<VerticalBar.Icon name='phone' />
+				<VerticalBar.Text>{t('Calls')}</VerticalBar.Text>
+				<VerticalBar.Close onClick={onClose} />
+			</VerticalBar.Header>
+
+			<VerticalBar.Content paddingInline={0} ref={ref}>
+				{(total === 0 || error) && (
+					<Box display='flex' flexDirection='column' justifyContent='center' height='100%'>
+						{error && (
+							<States>
+								<StatesIcon name='circle-exclamation' variation='danger' />
+								<StatesTitle>{t('Something_went_wrong')}</StatesTitle>
+								<StatesSubtitle>{handleError(error, false)}</StatesSubtitle>
+							</States>
+						)}
+						{!error && total === 0 && (
+							<States>
+								<StatesIcon name='video' />
+								<StatesTitle>{t('No_history')}</StatesTitle>
+								<StatesSubtitle>{t('There_is_no_video_conference_history_in_this_room')}</StatesSubtitle>
+							</States>
+						)}
+					</Box>
+				)}
+				{videoConfs.length > 0 && (
+					<Box flexGrow={1} flexShrink={1} overflow='hidden' display='flex'>
+						<Virtuoso
+							style={{
+								height: blockSize,
+								width: inlineSize,
+							}}
+							totalCount={total}
+							endReached={(start): unknown => loadMoreItems(start, Math.min(50, total - start))}
+							overscan={25}
+							data={videoConfs}
+							components={{ Scroller: ScrollableContentWrapper as any }}
+							itemContent={(_index, data): ReactElement => <VideoConfListItem videoConfData={data} reload={reload} />}
+						/>
+					</Box>
+				)}
+			</VerticalBar.Content>
+		</>
+	);
+};
+
+export default VideoConfList;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListItem.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListItem.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dccbe918973392e4784325097d12b1d99d92345c
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListItem.tsx
@@ -0,0 +1,108 @@
+import { IGroupVideoConference } from '@rocket.chat/core-typings';
+import { css } from '@rocket.chat/css-in-js';
+import { Button, Message, Box, Avatar } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import colors from '@rocket.chat/fuselage-tokens/colors';
+import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
+import React, { ReactElement } from 'react';
+
+import { VIDEOCONF_STACK_MAX_USERS } from '..';
+import UserAvatar from '../../../../../components/avatar/UserAvatar';
+import { useVideoConfJoinCall } from '../../../../../contexts/VideoConfContext';
+import { useTimeAgo } from '../../../../../hooks/useTimeAgo';
+
+const VideoConfListItem = ({
+	videoConfData,
+	className = [],
+	reload,
+	...props
+}: {
+	videoConfData: IGroupVideoConference;
+	className?: string[];
+	reload: () => void;
+}): ReactElement => {
+	const t = useTranslation();
+	const formatDate = useTimeAgo();
+	const joinCall = useVideoConfJoinCall();
+	const showRealName = Boolean(useSetting('UI_Use_Real_Name'));
+
+	const {
+		_id: callId,
+		createdBy: { name, username, _id },
+		users,
+		createdAt,
+		endedAt,
+	} = videoConfData;
+
+	const joinedUsers = users.filter((user) => user._id !== _id);
+
+	const hovered = css`
+		&:hover,
+		&:focus {
+			background: ${colors.n100};
+			.rcx-message {
+				background: ${colors.n100};
+			}
+		}
+		border-bottom: 2px solid ${colors.n300} !important;
+	`;
+
+	const handleJoinConference = useMutableCallback((): void => {
+		joinCall(callId);
+		return reload();
+	});
+
+	return (
+		<Box className={[...className, hovered].filter(Boolean)} pb='x8'>
+			<Message {...props}>
+				<Message.LeftContainer>
+					{username && <UserAvatar username={username} className='rcx-message__avatar' size='x36' />}
+				</Message.LeftContainer>
+				<Message.Container>
+					<Message.Header>
+						<Message.Name title={username}>{showRealName ? name : username}</Message.Name>
+						<Message.Timestamp>{formatDate(createdAt)}</Message.Timestamp>
+					</Message.Header>
+					<Message.Body clamp={2} />
+					<Box display='flex'></Box>
+					<Message.Block flexDirection='row' alignItems='center'>
+						<Button disabled={Boolean(endedAt)} small alignItems='center' display='flex' onClick={handleJoinConference}>
+							{endedAt ? t('Call_ended') : t('Join_call')}
+						</Button>
+						{joinedUsers.length > 0 && (
+							<Box mis='x8' fontScale='c1' display='flex' alignItems='center'>
+								<Avatar.Stack>
+									{joinedUsers.map(
+										(user, index) =>
+											user.username &&
+											index + 1 <= VIDEOCONF_STACK_MAX_USERS && (
+												<UserAvatar
+													data-tooltip={user.username}
+													key={user.username}
+													username={user.username}
+													etag={user.avatarETag}
+													size='x28'
+												/>
+											),
+									)}
+								</Avatar.Stack>
+								<Box mis='x4'>
+									{joinedUsers.length > VIDEOCONF_STACK_MAX_USERS
+										? t('__usersCount__members_joined', { usersCount: joinedUsers.length - VIDEOCONF_STACK_MAX_USERS })
+										: t('joined')}
+								</Box>
+							</Box>
+						)}
+						{joinedUsers.length === 0 && !endedAt && (
+							<Box mis='x8' fontScale='c1'>
+								{t('Be_the_first_to_join')}
+							</Box>
+						)}
+					</Message.Block>
+				</Message.Container>
+			</Message>
+		</Box>
+	);
+};
+
+export default VideoConfListItem;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..09ac842cf30b95586ef6e48e7d39b3d9dfc318c4
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListWithData.tsx
@@ -0,0 +1,29 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import React, { ReactElement, useMemo } from 'react';
+
+import { useRecordList } from '../../../../../hooks/lists/useRecordList';
+import { AsyncStatePhase } from '../../../../../hooks/useAsyncState';
+import { useTabBarClose } from '../../../providers/ToolboxProvider';
+import VideoConfList from './VideoConfList';
+import { useVideoConfList } from './useVideoConfList';
+
+const VideoConfListWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => {
+	const onClose = useTabBarClose();
+	const options = useMemo(() => ({ roomId: rid }), [rid]);
+	const { reload, videoConfList, loadMoreItems } = useVideoConfList(options);
+	const { phase, error, items: videoConfs, itemCount: totalItemCount } = useRecordList(videoConfList);
+
+	return (
+		<VideoConfList
+			onClose={onClose}
+			loadMoreItems={loadMoreItems}
+			loading={phase === AsyncStatePhase.LOADING}
+			total={totalItemCount}
+			error={error}
+			reload={reload}
+			videoConfs={videoConfs}
+		/>
+	);
+};
+
+export default VideoConfListWithData;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfRecordList.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfRecordList.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cf24301904f7cd837994886e5d57f6aaa10262fe
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfRecordList.ts
@@ -0,0 +1,9 @@
+import type { IGroupVideoConference } from '@rocket.chat/core-typings';
+
+import { RecordList } from '../../../../../lib/lists/RecordList';
+
+export class VideoConfRecordList extends RecordList<IGroupVideoConference> {
+	protected compare(a: IGroupVideoConference, b: IGroupVideoConference): number {
+		return b.createdAt.getTime() - a.createdAt.getTime();
+	}
+}
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..40e7a543a5eb5744c12e9f672100b9fbf7a0b583
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/index.ts
@@ -0,0 +1 @@
+export { default } from './VideoConfListWithData';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/useVideoConfList.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/useVideoConfList.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c676319b26c6859dab77a35cc46a98672e491b41
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/useVideoConfList.ts
@@ -0,0 +1,52 @@
+import type { IRoom } from '@rocket.chat/core-typings';
+import { useEndpoint } from '@rocket.chat/ui-contexts';
+import { useCallback, useState } from 'react';
+
+import { useScrollableRecordList } from '../../../../../hooks/lists/useScrollableRecordList';
+import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate';
+import { VideoConfRecordList } from './VideoConfRecordList';
+
+export const useVideoConfList = (options: {
+	roomId: IRoom['_id'];
+}): {
+	videoConfList: VideoConfRecordList;
+	initialItemCount: number;
+	reload: () => void;
+	loadMoreItems: (start: number, end: number) => void;
+} => {
+	const getVideoConfs = useEndpoint('GET', '/v1/video-conference.list');
+	const [videoConfList, setVideoConfList] = useState(() => new VideoConfRecordList());
+	const reload = useCallback(() => setVideoConfList(new VideoConfRecordList()), []);
+
+	useComponentDidUpdate(() => {
+		options && reload();
+	}, [options, reload]);
+
+	const fetchData = useCallback(
+		async (_start, _end) => {
+			const { data, total } = await getVideoConfs({
+				roomId: options.roomId,
+			});
+
+			return {
+				items: data.map((videoConf: any) => ({
+					...videoConf,
+					_updatedAt: new Date(videoConf._updatedAt),
+					createdAt: new Date(videoConf.createdAt),
+					endedAt: videoConf.endedAt ? new Date(videoConf.endedAt) : undefined,
+				})),
+				itemCount: total,
+			};
+		},
+		[getVideoConfs, options],
+	);
+
+	const { loadMoreItems, initialItemCount } = useScrollableRecordList(videoConfList, fetchData);
+
+	return {
+		reload,
+		videoConfList,
+		loadMoreItems,
+		initialItemCount,
+	};
+};
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/CallingPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/CallingPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1a98c00caee504e776b1742394a28b0d39b4a252
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/CallingPopup.tsx
@@ -0,0 +1,101 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { Box } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useUser } from '@rocket.chat/ui-contexts';
+import {
+	VideoConfPopup,
+	VideoConfPopupContent,
+	VideoConfPopupControllers,
+	VideoConfController,
+	useVideoConfControllers,
+	VideoConfButton,
+	VideoConfPopupFooter,
+	VideoConfPopupFooterButtons,
+	VideoConfPopupTitle,
+	VideoConfPopupUsername,
+} from '@rocket.chat/ui-video-conf';
+import React, { ReactElement } from 'react';
+
+import ReactiveUserStatus from '../../../../../../components/UserStatus/ReactiveUserStatus';
+import RoomAvatar from '../../../../../../components/avatar/RoomAvatar';
+import { useVideoConfSetPreferences, useVideoConfCapabilities, useVideoConfPreferences } from '../../../../../../contexts/VideoConfContext';
+
+type CallingPopupProps = {
+	id: string;
+	room: IRoom;
+	onClose: (id: string) => void;
+};
+
+const CallingPopup = ({ room, onClose, id }: CallingPopupProps): ReactElement => {
+	const t = useTranslation();
+	const user = useUser();
+	const userId = user?._id;
+	const directUserId = room.uids?.filter((uid) => uid !== userId).shift();
+	const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || [];
+
+	const videoConfPreferences = useVideoConfPreferences();
+	const setPreferences = useVideoConfSetPreferences();
+	const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences);
+	const capabilities = useVideoConfCapabilities();
+
+	const showCam = !!capabilities.cam;
+	const showMic = !!capabilities.mic;
+
+	const handleToggleMicPref = useMutableCallback(() => {
+		handleToggleMic();
+		setPreferences({ mic: !controllersConfig.mic });
+	});
+
+	const handleToggleCamPref = useMutableCallback(() => {
+		handleToggleCam();
+		setPreferences({ cam: !controllersConfig.cam });
+	});
+
+	return (
+		<VideoConfPopup>
+			<VideoConfPopupContent>
+				<RoomAvatar room={room} size='x40' />
+				<VideoConfPopupTitle text={t('Calling')} icon='phone-out' counter />
+				{directUserId && (
+					<Box display='flex' alignItems='center' mbs='x8'>
+						<ReactiveUserStatus uid={directUserId} />
+						{directUsername && <VideoConfPopupUsername username={directUsername} />}
+					</Box>
+				)}
+				{(showCam || showMic) && (
+					<VideoConfPopupControllers>
+						{showMic && (
+							<VideoConfController
+								active={controllersConfig.mic}
+								text={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								icon={controllersConfig.mic ? 'mic' : 'mic-off'}
+								onClick={handleToggleMicPref}
+							/>
+						)}
+						{showCam && (
+							<VideoConfController
+								active={controllersConfig.cam}
+								text={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								icon={controllersConfig.cam ? 'video' : 'video-off'}
+								onClick={handleToggleCamPref}
+							/>
+						)}
+					</VideoConfPopupControllers>
+				)}
+			</VideoConfPopupContent>
+			<VideoConfPopupFooter>
+				<VideoConfPopupFooterButtons>
+					{onClose && (
+						<VideoConfButton primary icon='phone-disabled' onClick={(): void => onClose(id)}>
+							{t('Cancel')}
+						</VideoConfButton>
+					)}
+				</VideoConfPopupFooterButtons>
+			</VideoConfPopupFooter>
+		</VideoConfPopup>
+	);
+};
+
+export default CallingPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3a336b632a045f6cab7e4281bdfa149b91fb5f99
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/ReceivingPopup.tsx
@@ -0,0 +1,111 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { Box, Skeleton } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useUser } from '@rocket.chat/ui-contexts';
+import {
+	VideoConfPopup,
+	VideoConfPopupContent,
+	VideoConfPopupControllers,
+	VideoConfController,
+	useVideoConfControllers,
+	VideoConfButton,
+	VideoConfPopupFooter,
+	VideoConfPopupFooterButtons,
+	VideoConfPopupTitle,
+	VideoConfPopupIndicators,
+	VideoConfPopupClose,
+	VideoConfPopupUsername,
+} from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, useMemo } from 'react';
+
+import ReactiveUserStatus from '../../../../../../components/UserStatus/ReactiveUserStatus';
+import RoomAvatar from '../../../../../../components/avatar/RoomAvatar';
+import { useVideoConfSetPreferences } from '../../../../../../contexts/VideoConfContext';
+import { AsyncStatePhase } from '../../../../../../hooks/useAsyncState';
+import { useEndpointData } from '../../../../../../hooks/useEndpointData';
+
+type ReceivingPopupProps = {
+	id: string;
+	room: IRoom;
+	position: number;
+	current: number;
+	total: number;
+	onClose: (id: string) => void;
+	onMute: (id: string) => void;
+	onConfirm: () => void;
+};
+
+const ReceivingPopup = ({ id, room, position, current, total, onClose, onMute, onConfirm }: ReceivingPopupProps): ReactElement => {
+	const t = useTranslation();
+	const user = useUser();
+	const userId = user?._id;
+	const directUserId = room.uids?.filter((uid) => uid !== userId).shift();
+	const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || [];
+
+	const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers();
+	const setPreferences = useVideoConfSetPreferences();
+
+	const params = useMemo(() => ({ callId: id }), [id]);
+	const { phase, value } = useEndpointData('/v1/video-conference.info', params);
+	const showMic = Boolean(value?.capabilities?.mic);
+	const showCam = Boolean(value?.capabilities?.cam);
+
+	const handleJoinCall = useMutableCallback(() => {
+		setPreferences(controllersConfig);
+		onConfirm();
+	});
+
+	return (
+		<VideoConfPopup position={position}>
+			<VideoConfPopupContent>
+				<VideoConfPopupClose title={t('Close')} onClick={(): void => onMute(id)} />
+				<RoomAvatar room={room} size='x40' />
+				{current && total ? <VideoConfPopupIndicators current={current} total={total} /> : null}
+				<VideoConfPopupTitle text='Incoming call from' icon='phone-in' />
+				{directUserId && (
+					<Box display='flex' alignItems='center' mbs='x8'>
+						<ReactiveUserStatus uid={directUserId} />
+						{directUsername && <VideoConfPopupUsername username={directUsername} />}
+					</Box>
+				)}
+				{phase === AsyncStatePhase.LOADING && <Skeleton />}
+				{phase === AsyncStatePhase.RESOLVED && (showMic || showCam) && (
+					<VideoConfPopupControllers>
+						{showMic && (
+							<VideoConfController
+								active={controllersConfig.mic}
+								text={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								icon={controllersConfig.mic ? 'mic' : 'mic-off'}
+								onClick={handleToggleMic}
+							/>
+						)}
+						{showCam && (
+							<VideoConfController
+								active={controllersConfig.cam}
+								text={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								icon={controllersConfig.cam ? 'video' : 'video-off'}
+								onClick={handleToggleCam}
+							/>
+						)}
+					</VideoConfPopupControllers>
+				)}
+			</VideoConfPopupContent>
+			<VideoConfPopupFooter>
+				<VideoConfPopupFooterButtons>
+					<VideoConfButton primary onClick={handleJoinCall}>
+						{t('Accept')}
+					</VideoConfButton>
+					{onClose && (
+						<VideoConfButton danger onClick={(): void => onClose(id)}>
+							{t('Decline')}
+						</VideoConfButton>
+					)}
+				</VideoConfPopupFooterButtons>
+			</VideoConfPopupFooter>
+		</VideoConfPopup>
+	);
+};
+
+export default ReceivingPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..debef4ba25f495bb25d2ec74c6dd13098aa1689d
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartCallPopup.tsx
@@ -0,0 +1,46 @@
+import { IRoom, isDirectMessageRoom, isMultipleDirectMessageRoom, isOmnichannelRoom } from '@rocket.chat/core-typings';
+import { useMutableCallback, useOutsideClick } from '@rocket.chat/fuselage-hooks';
+import { useUserId } from '@rocket.chat/ui-contexts';
+import { useVideoConfControllers } from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, useRef } from 'react';
+
+import { useVideoConfSetPreferences, useVideoConfPreferences } from '../../../../../../../contexts/VideoConfContext';
+import StartDirectCallPopup from './StartDirectCallPopup';
+import StartGroupCallPopup from './StartGroupCallPopup';
+import StartOmnichannelCallPopup from './StartOmnichannelCallPopup';
+
+type StartCallPopup = {
+	id: string;
+	room: IRoom;
+	onClose: () => void;
+	onConfirm: () => void;
+	loading: boolean;
+};
+
+const StartCallPopup = ({ loading, room, onClose, onConfirm }: StartCallPopup): ReactElement => {
+	const ref = useRef<HTMLDivElement>(null);
+	const userId = useUserId();
+	const directUserId = room.uids?.filter((uid) => uid !== userId).shift();
+	const videoConfPreferences = useVideoConfPreferences();
+	const setPreferences = useVideoConfSetPreferences();
+	const { controllersConfig } = useVideoConfControllers(videoConfPreferences);
+
+	useOutsideClick([ref], !loading ? onClose : (): void => undefined);
+
+	const handleStartCall = useMutableCallback(() => {
+		setPreferences(controllersConfig);
+		onConfirm();
+	});
+
+	if (isDirectMessageRoom(room) && !isMultipleDirectMessageRoom(room) && directUserId) {
+		return <StartDirectCallPopup loading={loading} ref={ref} room={room} onConfirm={handleStartCall} />;
+	}
+
+	if (isOmnichannelRoom(room)) {
+		return <StartOmnichannelCallPopup ref={ref} room={room} onConfirm={onConfirm} />;
+	}
+
+	return <StartGroupCallPopup loading={loading} ref={ref} room={room} onConfirm={handleStartCall} />;
+};
+
+export default StartCallPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2216b0d8cdb02406f1039d609e46138e5880a7e5
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartDirectCallPopup.tsx
@@ -0,0 +1,101 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { Box } from '@rocket.chat/fuselage';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useUser } from '@rocket.chat/ui-contexts';
+import {
+	VideoConfPopup,
+	VideoConfPopupContent,
+	VideoConfPopupControllers,
+	VideoConfController,
+	useVideoConfControllers,
+	VideoConfButton,
+	VideoConfPopupFooter,
+	VideoConfPopupTitle,
+	VideoConfPopupFooterButtons,
+	VideoConfPopupUsername,
+} from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, forwardRef, Ref } from 'react';
+
+import ReactiveUserStatus from '../../../../../../../components/UserStatus/ReactiveUserStatus';
+import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar';
+import {
+	useVideoConfSetPreferences,
+	useVideoConfCapabilities,
+	useVideoConfPreferences,
+} from '../../../../../../../contexts/VideoConfContext';
+
+type StartDirectCallPopup = {
+	room: IRoom;
+	onConfirm: () => void;
+	loading: boolean;
+};
+
+const StartDirectCallPopup = forwardRef(function StartDirectCallPopup(
+	{ room, onConfirm, loading }: StartDirectCallPopup,
+	ref: Ref<HTMLDivElement>,
+): ReactElement {
+	const t = useTranslation();
+	const user = useUser();
+	const userId = user?._id;
+	const directUserId = room.uids?.filter((uid) => uid !== userId).shift();
+	const [directUsername] = room.usernames?.filter((username) => username !== user?.username) || [];
+
+	const videoConfPreferences = useVideoConfPreferences();
+	const setPreferences = useVideoConfSetPreferences();
+	const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences);
+	const capabilities = useVideoConfCapabilities();
+
+	const showCam = !!capabilities.cam;
+	const showMic = !!capabilities.mic;
+
+	const handleStartCall = useMutableCallback(() => {
+		setPreferences(controllersConfig);
+		onConfirm();
+	});
+
+	return (
+		<VideoConfPopup ref={ref}>
+			<VideoConfPopupContent>
+				<RoomAvatar room={room} size='x40' />
+				<VideoConfPopupTitle text={t('Start_a_call')} />
+				{directUserId && (
+					<Box display='flex' alignItems='center' mbs='x8'>
+						<ReactiveUserStatus uid={directUserId} />
+						{directUsername && <VideoConfPopupUsername username={directUsername} />}
+					</Box>
+				)}
+				{(showCam || showMic) && (
+					<VideoConfPopupControllers>
+						{showMic && (
+							<VideoConfController
+								active={controllersConfig.mic}
+								text={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								icon={controllersConfig.mic ? 'mic' : 'mic-off'}
+								onClick={handleToggleMic}
+							/>
+						)}
+						{showCam && (
+							<VideoConfController
+								active={controllersConfig.cam}
+								text={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								icon={controllersConfig.cam ? 'video' : 'video-off'}
+								onClick={handleToggleCam}
+							/>
+						)}
+					</VideoConfPopupControllers>
+				)}
+			</VideoConfPopupContent>
+			<VideoConfPopupFooter>
+				<VideoConfPopupFooterButtons>
+					<VideoConfButton disabled={loading} primary onClick={handleStartCall}>
+						{t('Start_call')}
+					</VideoConfButton>
+				</VideoConfPopupFooterButtons>
+			</VideoConfPopupFooter>
+		</VideoConfPopup>
+	);
+});
+
+export default StartDirectCallPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..80950074c5418e076e5356b8607950db85cc01b0
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartGroupCallPopup.tsx
@@ -0,0 +1,97 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import {
+	VideoConfPopup,
+	VideoConfPopupContent,
+	VideoConfPopupControllers,
+	VideoConfController,
+	useVideoConfControllers,
+	VideoConfButton,
+	VideoConfPopupFooter,
+	VideoConfPopupTitle,
+	VideoConfPopupFooterButtons,
+} from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, forwardRef, Ref } from 'react';
+
+import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar';
+import {
+	useVideoConfSetPreferences,
+	useVideoConfCapabilities,
+	useVideoConfPreferences,
+} from '../../../../../../../contexts/VideoConfContext';
+
+type StartGroupCallPopup = {
+	room: IRoom;
+	onConfirm: () => void;
+	loading?: boolean;
+};
+
+const StartGroupCallPopup = forwardRef(function StartGroupCallPopup(
+	{ room, onConfirm, loading }: StartGroupCallPopup,
+	ref: Ref<HTMLDivElement>,
+): ReactElement {
+	const t = useTranslation();
+	const setPreferences = useVideoConfSetPreferences();
+	const videoConfPreferences = useVideoConfPreferences();
+	const { controllersConfig, handleToggleMic, handleToggleCam } = useVideoConfControllers(videoConfPreferences);
+	const capabilities = useVideoConfCapabilities();
+
+	const showCam = !!capabilities.cam;
+	const showMic = !!capabilities.mic;
+
+	const handleToggleMicPref = useMutableCallback(() => {
+		handleToggleMic();
+		setPreferences({ mic: !controllersConfig.mic });
+	});
+
+	const handleToggleCamPref = useMutableCallback(() => {
+		handleToggleCam();
+		setPreferences({ cam: !controllersConfig.cam });
+	});
+
+	const handleStartCall = useMutableCallback(() => {
+		setPreferences(controllersConfig);
+		onConfirm();
+	});
+
+	return (
+		<VideoConfPopup ref={ref}>
+			<VideoConfPopupContent>
+				<RoomAvatar room={room} size='x40' />
+				<VideoConfPopupTitle text={t('Start_a_call')} />
+				{(showCam || showMic) && (
+					<VideoConfPopupControllers>
+						{showMic && (
+							<VideoConfController
+								active={controllersConfig.mic}
+								text={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								title={controllersConfig.mic ? t('Mic_on') : t('Mic_off')}
+								icon={controllersConfig.mic ? 'mic' : 'mic-off'}
+								onClick={handleToggleMicPref}
+							/>
+						)}
+						{showCam && (
+							<VideoConfController
+								active={controllersConfig.cam}
+								text={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								title={controllersConfig.cam ? t('Cam_on') : t('Cam_off')}
+								icon={controllersConfig.cam ? 'video' : 'video-off'}
+								onClick={handleToggleCamPref}
+							/>
+						)}
+					</VideoConfPopupControllers>
+				)}
+			</VideoConfPopupContent>
+			<VideoConfPopupFooter>
+				<VideoConfPopupFooterButtons>
+					<VideoConfButton disabled={loading} primary onClick={handleStartCall}>
+						{t('Start_call')}
+					</VideoConfButton>
+				</VideoConfPopupFooterButtons>
+			</VideoConfPopupFooter>
+		</VideoConfPopup>
+	);
+});
+
+export default StartGroupCallPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a7527e7dfdfacfd2b3c1f583a68ab5422acb228
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/StartOmnichannelCallPopup.tsx
@@ -0,0 +1,44 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import {
+	VideoConfPopup,
+	VideoConfPopupContent,
+	VideoConfButton,
+	VideoConfPopupFooter,
+	VideoConfPopupTitle,
+	VideoConfPopupFooterButtons,
+} from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, forwardRef, Ref } from 'react';
+
+import RoomAvatar from '../../../../../../../components/avatar/RoomAvatar';
+
+type StartOmnichannelCallPopup = {
+	room: IRoom;
+	onConfirm: () => void;
+	loading?: boolean;
+};
+
+const StartOmnichannelCallPopup = forwardRef(function StartOmnichannelCallPopup(
+	{ room, onConfirm, loading }: StartOmnichannelCallPopup,
+	ref: Ref<HTMLDivElement>,
+): ReactElement {
+	const t = useTranslation();
+
+	return (
+		<VideoConfPopup ref={ref}>
+			<VideoConfPopupContent>
+				<RoomAvatar room={room} size='x40' />
+				<VideoConfPopupTitle text={t('Start_a_call')} />
+			</VideoConfPopupContent>
+			<VideoConfPopupFooter>
+				<VideoConfPopupFooterButtons>
+					<VideoConfButton disabled={loading} primary onClick={onConfirm}>
+						{t('Start_call')}
+					</VideoConfButton>
+				</VideoConfPopupFooterButtons>
+			</VideoConfPopupFooter>
+		</VideoConfPopup>
+	);
+});
+
+export default StartOmnichannelCallPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a2220d6af5f55651163fabd550ac1746a7ca396b
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/StartCallPopup/index.ts
@@ -0,0 +1 @@
+export { default } from './StartCallPopup';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..167cf56ac6e9b32d62ba52e01704970874261108
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/TimedVideoConfPopup.tsx
@@ -0,0 +1,94 @@
+import { IRoom } from '@rocket.chat/core-typings';
+import { useUserRoom } from '@rocket.chat/ui-contexts';
+import React, { ReactElement, useState } from 'react';
+
+import {
+	useVideoConfAcceptCall,
+	useVideoConfAbortCall,
+	useVideoConfRejectIncomingCall,
+	useVideoConfDismissCall,
+	useVideoConfStartCall,
+	useVideoConfDismissOutgoing,
+} from '../../../../../../contexts/VideoConfContext';
+import CallingPopup from './CallingPopup';
+import ReceivingPopup from './ReceivingPopup';
+import StartCallPopup from './StartCallPopup/StartCallPopup';
+
+export type TimedVideoConfPopupProps = {
+	id: string;
+	rid: IRoom['_id'];
+	isReceiving?: boolean;
+	isCalling?: boolean;
+	position: number;
+	current: number;
+	total: number;
+	onClose?: (id: string) => void;
+};
+
+const TimedVideoConfPopup = ({
+	id,
+	rid,
+	isReceiving = false,
+	isCalling = false,
+	position,
+	current,
+	total,
+}: TimedVideoConfPopupProps): ReactElement | null => {
+	const [starting, setStarting] = useState(false);
+	const acceptCall = useVideoConfAcceptCall();
+	const abortCall = useVideoConfAbortCall();
+	const rejectCall = useVideoConfRejectIncomingCall();
+	const dismissCall = useVideoConfDismissCall();
+	const startCall = useVideoConfStartCall();
+	const dismissOutgoing = useVideoConfDismissOutgoing();
+	const room = useUserRoom(rid);
+
+	if (!room) {
+		return null;
+	}
+
+	const handleConfirm = (): void => {
+		acceptCall(id);
+	};
+
+	const handleClose = (id: string): void => {
+		if (isReceiving) {
+			rejectCall(id);
+			return;
+		}
+
+		abortCall();
+	};
+
+	const handleMute = (): void => {
+		dismissCall(id);
+	};
+
+	const handleStartCall = async (): Promise<void> => {
+		setStarting(true);
+		startCall(rid);
+	};
+
+	if (isReceiving) {
+		return (
+			<ReceivingPopup
+				room={room}
+				id={id}
+				position={position}
+				current={current}
+				total={total}
+				onClose={handleClose}
+				onMute={handleMute}
+				onConfirm={handleConfirm}
+			/>
+		);
+	}
+
+	if (isCalling) {
+		return <CallingPopup room={room} id={id} onClose={handleClose} />;
+	}
+
+	return <StartCallPopup loading={starting} room={room} id={id} onClose={dismissOutgoing} onConfirm={handleStartCall} />;
+};
+
+export default TimedVideoConfPopup;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9c4e387f8af3669106561d5b7b84d626a0508634
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopup/index.ts
@@ -0,0 +1 @@
+export { default } from './TimedVideoConfPopup';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a4120bbe4452ba6de87476cb7023ea7fafb5241f
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/VideoConfPopups.tsx
@@ -0,0 +1,64 @@
+import { useCustomSound } from '@rocket.chat/ui-contexts';
+import { VideoConfPopupBackdrop } from '@rocket.chat/ui-video-conf';
+import React, { ReactElement, useEffect, useMemo } from 'react';
+
+import {
+	VideoConfPopupPayload,
+	useVideoConfIsCalling,
+	useVideoConfIsRinging,
+	useVideoConfIncomingCalls,
+} from '../../../../../contexts/VideoConfContext';
+import VideoConfPopupPortal from '../../../../../portals/VideoConfPopupPortal';
+import VideoConfPopup from './VideoConfPopup';
+
+const VideoConfPopups = ({ children }: { children?: VideoConfPopupPayload }): ReactElement => {
+	const incomingCalls = useVideoConfIncomingCalls();
+	const customSound = useCustomSound();
+	const isRinging = useVideoConfIsRinging();
+	const isCalling = useVideoConfIsCalling();
+
+	const popups = useMemo(
+		() =>
+			incomingCalls
+				.filter((incomingCall) => !incomingCall.dismissed)
+				.map((incomingCall) => ({ id: incomingCall.callId, rid: incomingCall.rid, isReceiving: true })),
+		[incomingCalls],
+	);
+
+	useEffect(() => {
+		if (!isRinging) {
+			return;
+		}
+
+		customSound.play('calling', { loop: true });
+
+		return (): void => {
+			customSound.pause('calling');
+		};
+	}, [customSound, isRinging]);
+
+	return (
+		<>
+			{(children || popups?.length > 0) && (
+				<VideoConfPopupPortal>
+					<VideoConfPopupBackdrop>
+						{(children ? [children, ...popups] : popups).map(({ id, rid, isReceiving }, index = 1) => (
+							<VideoConfPopup
+								key={id}
+								id={id}
+								rid={rid}
+								isReceiving={isReceiving}
+								isCalling={isCalling}
+								position={index * 10}
+								current={index}
+								total={popups.length}
+							/>
+						))}
+					</VideoConfPopupBackdrop>
+				</VideoConfPopupPortal>
+			)}
+		</>
+	);
+};
+
+export default VideoConfPopups;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..46bc3d08826eaefc2cda2c0fc6b537e08e032526
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfPopups/index.ts
@@ -0,0 +1 @@
+export { default } from './VideoConfPopups';
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/index.ts b/apps/meteor/client/views/room/contextualBar/VideoConference/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..46c8ccd2bb34bcbe773e0e1f548d40186f95fb27
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/index.ts
@@ -0,0 +1 @@
+export const VIDEOCONF_STACK_MAX_USERS = 6;
diff --git a/apps/meteor/client/views/room/contextualBar/VideoConference/useVideoConfWarning.tsx b/apps/meteor/client/views/room/contextualBar/VideoConference/useVideoConfWarning.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5d85a8c5f674297c703c00d75f172f938390f894
--- /dev/null
+++ b/apps/meteor/client/views/room/contextualBar/VideoConference/useVideoConfWarning.tsx
@@ -0,0 +1,78 @@
+import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
+import { useSetModal, useRoute, useSetting, useRole, useTranslation } from '@rocket.chat/ui-contexts';
+import React, { useMemo } from 'react';
+
+import { availabilityErrors } from '../../../../../lib/videoConference/constants';
+import GenericModal from '../../../../components/GenericModal';
+
+export const useVideoConfWarning = (): ((error: unknown) => void) => {
+	const t = useTranslation();
+	const setModal = useSetModal();
+	const videoConfSettingsRoute = useRoute('admin-settings');
+	const marketplaceRoute = useRoute('admin-marketplace');
+	const workspaceRegistered = useSetting('Cloud_Workspace_Client_Id');
+	const isAdmin = useRole('admin');
+
+	const handleClose = useMutableCallback(() => setModal(null));
+
+	const handleRedirectToConfiguration = useMutableCallback((error) => {
+		handleClose();
+		if (error === availabilityErrors.NOT_CONFIGURED) {
+			return marketplaceRoute.push({ context: 'installed' });
+		}
+
+		return videoConfSettingsRoute.push({
+			group: 'Video_Conference',
+		});
+	});
+
+	const handleOpenMarketplace = useMutableCallback(() => {
+		handleClose();
+		marketplaceRoute.push();
+	});
+
+	return useMemo(() => {
+		if (!isAdmin) {
+			return (): void =>
+				setModal(
+					<GenericModal icon={null} title={t('Video_conference_not_available')} onClose={handleClose} onConfirm={handleClose}>
+						{t('Video_conference_apps_can_be_installed')}
+					</GenericModal>,
+				);
+		}
+
+		return (error): void => {
+			if (error === availabilityErrors.NOT_CONFIGURED || error === availabilityErrors.NOT_ACTIVE) {
+				return setModal(
+					<GenericModal
+						icon={null}
+						variant='warning'
+						title={t('Configure_video_conference')}
+						onCancel={handleClose}
+						onClose={handleClose}
+						onConfirm={(): void => handleRedirectToConfiguration(error)}
+						confirmText={t('Open_settings')}
+					>
+						{t('Configure_video_conference_to_use')}
+					</GenericModal>,
+				);
+			}
+
+			if (error === availabilityErrors.NO_APP || !!workspaceRegistered) {
+				return setModal(
+					<GenericModal
+						icon={null}
+						variant='warning'
+						title={t('Video_conference_app_required')}
+						onCancel={handleClose}
+						onClose={handleClose}
+						onConfirm={handleOpenMarketplace}
+						confirmText={t('Explore_marketplace')}
+					>
+						{t('Video_conference_apps_available')}
+					</GenericModal>,
+				);
+			}
+		};
+	}, [handleClose, handleOpenMarketplace, handleRedirectToConfiguration, isAdmin, setModal, t, workspaceRegistered]);
+};
diff --git a/apps/meteor/client/views/room/lib/Toolbox/index.tsx b/apps/meteor/client/views/room/lib/Toolbox/index.tsx
index 113eacf7498bb515da90a1a06cd30c5584b46f68..e2a15ca2dec1581af34179a17a932807c87531e1 100644
--- a/apps/meteor/client/views/room/lib/Toolbox/index.tsx
+++ b/apps/meteor/client/views/room/lib/Toolbox/index.tsx
@@ -11,6 +11,7 @@ type ActionRendererProps = Omit<ToolboxActionConfig, 'renderAction' | 'groups' |
 	className: ComponentProps<typeof Box>['className'];
 	index: number;
 	title: string;
+	key: string;
 };
 
 export type ActionRenderer = (props: ActionRendererProps) => ReactNode;
diff --git a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
index a141f905b6d2ea8f0acb4122344d773551516c44..c9967a0cfbb129634fd0e424da7ec4b885f8857c 100644
--- a/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
+++ b/apps/meteor/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
@@ -41,8 +41,7 @@ export const useTeamsChannelList = (
 			});
 
 			return {
-				items: rooms.map(({ _updatedAt, lastMessage, lm, ts, jitsiTimeout, webRtcCallStartTime, ...room }) => ({
-					...(jitsiTimeout && { jitsiTimeout: new Date(jitsiTimeout) }),
+				items: rooms.map(({ _updatedAt, lastMessage, lm, ts, webRtcCallStartTime, ...room }) => ({
 					...(lm && { lm: new Date(lm) }),
 					...(ts && { ts: new Date(ts) }),
 					_updatedAt: new Date(_updatedAt),
diff --git a/apps/meteor/ee/app/canned-responses/client/tabBar.ts b/apps/meteor/ee/app/canned-responses/client/tabBar.ts
index f61890174e1efa4350221517ee6afcb4be59f79b..5124ca84eaa0605337bdd05896d995e99f41b32f 100644
--- a/apps/meteor/ee/app/canned-responses/client/tabBar.ts
+++ b/apps/meteor/ee/app/canned-responses/client/tabBar.ts
@@ -1,11 +1,11 @@
 import { lazy, useMemo } from 'react';
 import { useSetting } from '@rocket.chat/ui-contexts';
 
-import { useHasLicense } from '../../../client/hooks/useHasLicense';
+import { useHasLicenseModule } from '../../../client/hooks/useHasLicenseModule';
 import { addAction } from '../../../../client/views/room/lib/Toolbox';
 
 addAction('canned-responses', () => {
-	const hasLicense = useHasLicense('canned-responses');
+	const hasLicense = useHasLicenseModule('canned-responses');
 	const enabled = useSetting('Canned_Responses_Enable');
 
 	return useMemo(
diff --git a/apps/meteor/ee/app/license/server/bundles.ts b/apps/meteor/ee/app/license/server/bundles.ts
index 2b4d332dd2069a0200adc3605957792f7508d4c9..8aa0e90f9fcccb832280ea5f4e1503df16ec0cb5 100644
--- a/apps/meteor/ee/app/license/server/bundles.ts
+++ b/apps/meteor/ee/app/license/server/bundles.ts
@@ -10,7 +10,8 @@ export type BundleFeature =
 	| 'scalability'
 	| 'teams-mention'
 	| 'saml-enterprise'
-	| 'oauth-enterprise';
+	| 'oauth-enterprise'
+	| 'videoconference-enterprise';
 
 interface IBundle {
 	[key: string]: BundleFeature[];
@@ -30,6 +31,7 @@ const bundles: IBundle = {
 		'teams-mention',
 		'saml-enterprise',
 		'oauth-enterprise',
+		'videoconference-enterprise',
 	],
 	pro: [],
 };
diff --git a/apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx b/apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx
index 814f75efb2b857240100327cfa2b3d155fe1529f..16c00c9c8b58432baf2ebeb932a7ad1c9b5bce6c 100644
--- a/apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx
+++ b/apps/meteor/ee/client/hooks/useDevicesMenuOption.tsx
@@ -3,7 +3,7 @@ import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
 import React, { ReactNode } from 'react';
 
 import DeviceSettingsModal from '../voip/modals/DeviceSettingsModal';
-import { useHasLicense } from './useHasLicense';
+import { useHasLicenseModule } from './useHasLicenseModule';
 
 type DevicesMenuOption = {
 	type?: 'option' | 'heading' | 'divider';
@@ -12,7 +12,7 @@ type DevicesMenuOption = {
 };
 
 export const useDevicesMenuOption = (): DevicesMenuOption | null => {
-	const isEnterprise = useHasLicense('voip-enterprise');
+	const isEnterprise = useHasLicenseModule('voip-enterprise');
 	const t = useTranslation();
 	const setModal = useSetModal();
 
diff --git a/apps/meteor/ee/client/hooks/useHasLicense.ts b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts
similarity index 79%
rename from apps/meteor/ee/client/hooks/useHasLicense.ts
rename to apps/meteor/ee/client/hooks/useHasLicenseModule.ts
index 8aea5be9a279705bd126d6c2cea3abca52e09423..7c61f1d858795f264b343415b41398000f062e82 100644
--- a/apps/meteor/ee/client/hooks/useHasLicense.ts
+++ b/apps/meteor/ee/client/hooks/useHasLicenseModule.ts
@@ -3,7 +3,7 @@ import { useState, useEffect } from 'react';
 import { hasLicense } from '../../app/license/client';
 import { BundleFeature } from '../../app/license/server/bundles';
 
-export const useHasLicense = (licenseName: BundleFeature): 'loading' | boolean => {
+export const useHasLicenseModule = (licenseName: BundleFeature): 'loading' | boolean => {
 	const [license, setLicense] = useState<'loading' | boolean>('loading');
 
 	useEffect(() => {
diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.js b/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.js
index 0fd5f57ba687ab5e9559050a636b014d76e1b575..ce27ae47ae8c72cedc7064edd4c567e2fbca4b6f 100644
--- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.js
+++ b/apps/meteor/ee/client/omnichannel/monitors/MonitorsPageContainer.js
@@ -2,11 +2,11 @@ import React from 'react';
 
 import PageSkeleton from '../../../../client/components/PageSkeleton';
 import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage';
-import { useHasLicense } from '../../hooks/useHasLicense';
+import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
 import MonitorsPage from './MonitorsPage';
 
 const MonitorsPageContainer = () => {
-	const license = useHasLicense('livechat-enterprise');
+	const license = useHasLicenseModule('livechat-enterprise');
 
 	if (license === 'loading') {
 		return <PageSkeleton />;
diff --git a/apps/meteor/ee/server/configuration/index.ts b/apps/meteor/ee/server/configuration/index.ts
index a3def47a7891083231808800bd987290bcc3d18a..badf718504ecb59c74e0c284a9f3208b54685c67 100644
--- a/apps/meteor/ee/server/configuration/index.ts
+++ b/apps/meteor/ee/server/configuration/index.ts
@@ -1,3 +1,4 @@
 import './ldap';
 import './oauth';
 import './saml';
+import './videoConference';
diff --git a/apps/meteor/ee/server/configuration/videoConference.ts b/apps/meteor/ee/server/configuration/videoConference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f13776cee8bbbbb4a8c691879cca2c05fc3c90cd
--- /dev/null
+++ b/apps/meteor/ee/server/configuration/videoConference.ts
@@ -0,0 +1,52 @@
+import { Meteor } from 'meteor/meteor';
+import type { IRoom, IUser, VideoConference } from '@rocket.chat/core-typings';
+import { VideoConferenceStatus } from '@rocket.chat/core-typings';
+import { Rooms, Subscriptions } from '@rocket.chat/models';
+
+import { onLicense } from '../../app/license/server';
+import { videoConfTypes } from '../../../server/lib/videoConfTypes';
+import { addSettings } from '../settings/video-conference';
+import { callbacks } from '../../../lib/callbacks';
+import { VideoConf } from '../../../server/sdk';
+
+Meteor.startup(() =>
+	onLicense('videoconference-enterprise', () => {
+		addSettings();
+
+		videoConfTypes.registerVideoConferenceType(
+			{ type: 'direct', status: VideoConferenceStatus.CALLING },
+			async ({ _id, t }, allowRinging) => {
+				if (!allowRinging || t !== 'd') {
+					return false;
+				}
+
+				const room = await Rooms.findOneById<Pick<IRoom, 'uids'>>(_id, { projection: { uids: 1 } });
+
+				return Boolean(room && (!room.uids || room.uids.length === 2));
+			},
+		);
+
+		videoConfTypes.registerVideoConferenceType({ type: 'videoconference', ringing: true }, async ({ _id, t }, allowRinging) => {
+			if (!allowRinging || ['l', 'v'].includes(t)) {
+				return false;
+			}
+
+			if (t === 'd') {
+				const room = await Rooms.findOneById<Pick<IRoom, 'uids'>>(_id, { projection: { uids: 1 } });
+				if (room && (!room.uids || room.uids.length <= 2)) {
+					return false;
+				}
+			}
+
+			if ((await Subscriptions.findByRoomId(_id).count()) > 10) {
+				return false;
+			}
+
+			return true;
+		});
+
+		callbacks.add('onJoinVideoConference', async (callId: VideoConference['_id'], userId?: IUser['_id']) =>
+			VideoConf.addUser(callId, userId),
+		);
+	}),
+);
diff --git a/apps/meteor/ee/server/settings/index.ts b/apps/meteor/ee/server/settings/index.ts
index c5dd94f6bac586cb125d1cc641274aee1f694670..511cbcdf4cda06503e413524b341eef07b035eea 100644
--- a/apps/meteor/ee/server/settings/index.ts
+++ b/apps/meteor/ee/server/settings/index.ts
@@ -1,2 +1,3 @@
 import './ldap';
 import './saml';
+import './video-conference';
diff --git a/apps/meteor/ee/server/settings/video-conference.ts b/apps/meteor/ee/server/settings/video-conference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8564f9d0087f73dbf4b98bc31daadd6c18933bc6
--- /dev/null
+++ b/apps/meteor/ee/server/settings/video-conference.ts
@@ -0,0 +1,37 @@
+import { settingsRegistry } from '../../../app/settings/server';
+
+export function addSettings(): void {
+	settingsRegistry.addGroup('Video_Conference', function () {
+		this.with(
+			{
+				enterprise: true,
+				modules: ['videoconference-enterprise'],
+			},
+			function () {
+				this.add('VideoConf_Enable_DMs', true, {
+					type: 'boolean',
+					public: true,
+					invalidValue: true,
+				});
+
+				this.add('VideoConf_Enable_Channels', true, {
+					type: 'boolean',
+					public: true,
+					invalidValue: true,
+				});
+
+				this.add('VideoConf_Enable_Groups', true, {
+					type: 'boolean',
+					public: true,
+					invalidValue: true,
+				});
+
+				this.add('VideoConf_Enable_Teams', true, {
+					type: 'boolean',
+					public: true,
+					invalidValue: true,
+				});
+			},
+		);
+	});
+}
diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts
index f4408f7d94a99d5c86f22b10918211fb75a8108c..e2bc38df503722662444d6e841598828bd1ded33 100644
--- a/apps/meteor/lib/callbacks.ts
+++ b/apps/meteor/lib/callbacks.ts
@@ -11,6 +11,7 @@ import type {
 	OmnichannelAgentStatus,
 	ILivechatInquiryRecord,
 	ILivechatVisitor,
+	VideoConference,
 	ParsedUrl,
 	OEmbedMeta,
 	OEmbedUrlContent,
@@ -61,6 +62,7 @@ type EventLikeCallbackSignatures = {
 	'beforeCreateChannel': (owner: IUser, room: IRoom) => void;
 	'afterCreateRoom': (owner: IUser, room: IRoom) => void;
 	'onValidateLogin': (login: ILoginAttempt) => void;
+	'onJoinVideoConference': (callId: VideoConference['_id'], userId?: IUser['_id']) => Promise<void>;
 };
 
 /**
diff --git a/apps/meteor/lib/videoConference/constants.ts b/apps/meteor/lib/videoConference/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69e243115be98be34685191ce45a8c628a09c8b1
--- /dev/null
+++ b/apps/meteor/lib/videoConference/constants.ts
@@ -0,0 +1,5 @@
+export const availabilityErrors = {
+	NOT_CONFIGURED: 'video-conf-provider-not-configured',
+	NOT_ACTIVE: 'no-active-video-conf-provider',
+	NO_APP: 'no-videoconf-provider-app',
+};
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 9d3e05f337eb3b2d9439d0007dd3fdeac04c2afc..99a5853d82652697db3c6c29e46122fc6a991619 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -133,6 +133,7 @@
 		"@types/supertest": "^2.0.11",
 		"@types/ua-parser-js": "^0.7.36",
 		"@types/underscore.string": "0.0.38",
+		"@types/use-subscription": "^1.0.0",
 		"@types/use-sync-external-store": "^0.0.3",
 		"@types/uuid": "^8.3.4",
 		"@types/xml-crypto": "^1.4.1",
@@ -192,7 +193,7 @@
 		"@nivo/line": "0.62.0",
 		"@nivo/pie": "0.73.0",
 		"@rocket.chat/api-client": "workspace:^",
-		"@rocket.chat/apps-engine": "1.33.0-alpha.6456",
+		"@rocket.chat/apps-engine": "alpha",
 		"@rocket.chat/core-typings": "workspace:^",
 		"@rocket.chat/css-in-js": "~0.31.12",
 		"@rocket.chat/emitter": "~0.31.12",
@@ -220,6 +221,7 @@
 		"@rocket.chat/ui-client": "workspace:^",
 		"@rocket.chat/ui-contexts": "workspace:^",
 		"@rocket.chat/ui-kit": "~0.32.0-dev.0",
+		"@rocket.chat/ui-video-conf": "workspace:^",
 		"@slack/client": "^4.12.0",
 		"@slack/rtm-api": "^6.0.0",
 		"@types/cookie": "^0.5.1",
@@ -358,6 +360,7 @@
 		"underscore.string": "^3.3.6",
 		"universal-perf-hooks": "^1.0.1",
 		"url-polyfill": "^1.1.12",
+		"use-subscription": "~1.6.0",
 		"use-sync-external-store": "^1.2.0",
 		"uuid": "^8.3.2",
 		"vm2": "^3.9.9",
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 1110f3fe6d6e50f7b379536f4024004fdef758e9..9ba21649208971b0a6fa6d0d1168a928d01596af 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -3,6 +3,8 @@
   "500": "Internal Server Error",
   "__count__empty_rooms_will_be_removed_automatically": "__count__ empty rooms will be removed automatically.",
   "__count__empty_rooms_will_be_removed_automatically__rooms__": "__count__ empty rooms will be removed automatically:<br/> __rooms__.",
+  "__usersCount__members_joined": "+ __usersCount__ members joined",
+  "__usersCount__people_will_be_invited": "__usersCount__ people will be invited",
   "__username__is_no_longer__role__defined_by__user_by_": "__username__ is no longer __role__ by __user_by__",
   "__username__was_set__role__by__user_by_": "__username__ was set __role__ by __user_by__",
   "This_room_encryption_has_been_enabled_by__username_": "This room's encryption has been enabled by __username__",
@@ -301,6 +303,9 @@
   "additional_integrations_Zapier": "Are you looking to integrate other software and applications with Rocket.Chat but you don't have the time to manually do it? Then we suggest using Zapier which we fully support. Read more about it on our documentation. <a href='https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/' target='_blank'>https://rocket.chat/docs/administrator-guides/integrations/zapier/using-zaps/</a>",
   "Admin_disabled_encryption": "Your administrator did not enable E2E encryption.",
   "Admin_Info": "Admin Info",
+  "admin-no-active-video-conf-provider": "**Conference call not enabled**: Configure conference calls in order to make it available on this workspace.",
+  "admin-video-conf-provider-not-configured": "**Conference call not enabled**: Configure conference calls in order to make it available on this workspace.",
+  "admin-no-videoconf-provider-app": "**Conference call not enabled**: Conference call apps are available in the Rocket.Chat marketplace.",
   "Administration": "Administration",
   "Adult_images_are_not_allowed": "Adult images are not allowed",
   "Aerospace_and_Defense": "Aerospace & Defense",
@@ -666,6 +671,7 @@
   "BBB_Start_Meeting": "Start Meeting",
   "BBB_Video_Call": "BBB Video Call",
   "BBB_You_have_no_permission_to_start_a_call": "You have no permission to start a call",
+  "Be_the_first_to_join": "Be the first to join",
   "Belongs_To": "Belongs To",
   "Best_first_response_time": "Best first response time",
   "Beta_feature_Depends_on_Video_Conference_to_be_enabled": "Beta feature. Depends on Video Conference to be enabled.",
@@ -732,6 +738,8 @@
   "Calling": "Calling",
   "Call_Center": "Call Center",
   "Call_Center_Description": "Configure Rocket.Chat call center.",
+  "Call_ended": "Call ended",
+  "Calls": "Calls",
   "Calls_in_queue": "__calls__ call in queue",
   "Calls_in_queue_plural": "__calls__ calls in queue",
   "Calls_in_queue_empty": "Queue is empty",
@@ -743,6 +751,8 @@
   "call-management_description": "Permission to start a meeting",
   "Caller": "Caller",
   "Caller_Id": "Caller ID",
+  "Cam_on": "Cam On",
+  "Cam_off": "Cam Off",
   "Cancel": "Cancel",
   "Cancel_message_input": "Cancel",
   "Canceled": "Canceled",
@@ -996,6 +1006,7 @@
   "Commit_details": "Commit Details",
   "Completed": "Completed",
   "Computer": "Computer",
+  "Conference_name": "Conference name",
   "Configure_Incoming_Mail_IMAP": "Configure Incoming Mail (IMAP)",
   "Configure_Outgoing_Mail_SMTP": "Configure Outgoing Mail (SMTP)",
   "Confirm": "Confirm",
@@ -1004,6 +1015,8 @@
   "Confirm_New_Password_Placeholder": "Please re-enter new password...",
   "Confirm_password": "Confirm your password",
   "Confirmation": "Confirmation",
+  "Configure_video_conference": "Configure conference call",
+  "Configure_video_conference_to_use": "Configure conference calls in order to make it available on this workspace.",
   "Connect": "Connect",
   "Connected": "Connected",
   "Connect_SSL_TLS": "Connect with SSL/TLS",
@@ -1654,7 +1667,6 @@
   "Empty_title": "Empty title",
   "Enable_New_Message_Template": "Enable New Message Template",
   "Enable_New_Message_Template_alert": "This is a beta feature. It may not work as expected. Please report any issues you encounter.",
-  "See_on_Engagement_Dashboard": "See on Engagement Dashboard",
   "Enable": "Enable",
   "Enable_Auto_Away": "Enable Auto Away",
   "Enable_CSP": "Enable Content-Security-Policy",
@@ -2453,18 +2465,11 @@
   "italic": "Italic",
   "italics": "italics",
   "Items_per_page:": "Items per page:",
-  "Jitsi_Application_ID": "Application ID (iss)",
-  "Jitsi_Application_Secret": "Application Secret",
-  "Jitsi_Chrome_Extension": "Chrome Extension Id",
-  "Jitsi_Enable_Channels": "Enable in Channels",
-  "Jitsi_Enable_Teams": "Enable for Teams",
-  "Jitsi_Enabled_TokenAuth": "Enable JWT auth",
-  "Jitsi_Limit_Token_To_Room": "Limit token to Jitsi Room",
   "Job_Title": "Job Title",
-  "join": "Join",
-  "Join_call": "Join Call",
   "Join_audio_call": "Join audio call",
+  "Join_call": "Join call",
   "Join_Chat": "Join Chat",
+  "Join_conference": "Join conference",
   "Join_default_channels": "Join default channels",
   "Join_the_Community": "Join the Community",
   "Join_the_given_channel": "Join the given channel",
@@ -2473,6 +2478,7 @@
   "join-without-join-code": "Join Without Join Code",
   "join-without-join-code_description": "Permission to bypass the join code in channels with join code enabled",
   "Joined": "Joined",
+  "joined": "joined",
   "Joined_at": "Joined at",
   "JSON": "JSON",
   "Jump": "Jump",
@@ -3120,6 +3126,7 @@
   "meteor_status_try_now_waiting": "Try now",
   "meteor_status_waiting": "Waiting for server connection,",
   "Method": "Method",
+  "Mic_on": "Mic On",
   "Microphone": "Microphone",
   "Mic_off": "Mic Off",
   "Min_length_is": "Min length is %s",
@@ -3229,6 +3236,7 @@
   "Nickname": "Nickname",
   "Nickname_Placeholder": "Enter your nickname...",
   "No": "No",
+  "no-active-video-conf-provider": "**Conference call not enabled**: A workspace admin needs to enable the conference call feature first.",
   "No_available_agents_to_transfer": "No available agents to transfer",
   "No_app_matches": "No app matches",
   "No_app_matches_for": "No app matches for",
@@ -3247,6 +3255,7 @@
   "No_files_found": "No files found",
   "No_files_left_to_download": "No files left to download",
   "No_groups_yet": "You have no private groups yet.",
+  "No_history": "No history",
   "No_installed_app_matches": "No installed app matches",
   "No_integration_found": "No integration found by the provided id.",
   "No_Limit": "No Limit",
@@ -3265,6 +3274,7 @@
   "No_starred_messages": "No starred messages",
   "No_such_command": "No such command: `/__command__`",
   "No_Threads": "No threads found",
+  "no-videoconf-provider-app": "**Conference call not available**: Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.",
   "Nobody_available": "Nobody available",
   "Node_version": "Node Version",
   "None": "None",
@@ -3375,12 +3385,15 @@
   "Oops_page_not_found": "Oops, page not found",
   "Oops!": "Oops",
   "Open": "Open",
+  "Open_call": "Open call",
+  "Open_call_in_new_tab": "Open call in new tab",
   "Open_channel_user_search": "`%s` - Open Channel / User search",
   "Open_conversations": "Open Conversations",
   "Open_Days": "Open days",
   "Open_days_of_the_week": "Open Days of the Week",
   "Open_Livechats": "Chats in Progress",
   "Open_menu": "Open_menu",
+  "Open_settings": "Open settings",
   "Open_thread": "Open Thread",
   "Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code. You can also use one of your backup codes.",
   "Opened": "Opened",
@@ -3965,6 +3978,8 @@
   "Security": "Security",
   "Powered_by_RocketChat": "Powered by Rocket.Chat",
   "See_full_profile": "See full profile",
+  "See_history": "See history",
+  "See_on_Engagement_Dashboard": "See on Engagement Dashboard",
   "Select_a_department": "Select a department",
   "Select_a_room": "Select a room",
   "Select_a_user": "Select a user",
@@ -4154,6 +4169,7 @@
   "snippet-message_description": "Permission to create snippet message",
   "Snippeted_a_message": "Created a snippet __snippetLink__",
   "Social_Network": "Social Network",
+  "Something_went_wrong": "Something went wrong",
   "Sorry_page_you_requested_does_not_exist_or_was_deleted": "Sorry, page you requested does not exist or was deleted!",
   "Sort": "Sort",
   "Sort_By": "Sort by",
@@ -4168,12 +4184,16 @@
   "Star_Message": "Star Message",
   "Starred_Messages": "Starred Messages",
   "Start": "Start",
+  "Start_a_call": "Start a call",
+  "Start_a_call_with": "Start a call with",
   "Start_audio_call": "Start audio call",
+  "Start_call": "Start call",
   "Start_Chat": "Start Chat",
+  "Start_conference_call": "Start conference call",
   "Start_of_conversation": "Start of conversation",
   "Start_OTR": "Start OTR",
   "Start_video_call": "Start video call",
-  "Start_video_conference": "Start video conference?",
+  "Start_video_conference": "Start conference call?",
   "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s": "Start with <code class=\"inline\">%s</code> for user or <code class=\"inline\">%s</code> for channel. Eg: <code class=\"inline\">%s</code> or <code class=\"inline\">%s</code>",
   "start-discussion": "Start Discussion",
   "start-discussion_description": "Permission to start a discussion",
@@ -4420,6 +4440,7 @@
   "There_are_no_monitors_added_to_this_unit_yet": "There are no monitors added to this unit yet",
   "There_are_no_personal_access_tokens_created_yet": "There are no Personal Access Tokens created yet.",
   "There_are_no_users_in_this_role": "There are no users in this role.",
+  "There_is_no_video_conference_history_in_this_room": "There is no conference call history in this room",
   "There_is_one_or_more_apps_in_an_invalid_state_Click_here_to_review": "There is one or more apps in an invalid state. Click here to review.",
   "There_has_been_an_error_installing_the_app": "There has been an error installing the app",
   "These_notes_will_be_available_in_the_call_summary": "These notes will be available in the call summary",
@@ -4454,6 +4475,7 @@
   "To": "To",
   "To_additional_emails": "To additional emails",
   "To_install_RocketChat_Livechat_in_your_website_copy_paste_this_code_above_the_last_body_tag_on_your_site": "To install Rocket.Chat Livechat in your website, copy &amp; paste this code above the last <strong>&lt;/body&gt;</strong> tag on your site.",
+  "To_prevent_seeing_this_message_again_allow_popups_from_workspace_URL": "To prevent seeing this message again, make sure your browser settings allow pop-ups to be opened from the workspace URL: ",
   "to_see_more_details_on_how_to_integrate": "to see more details on how to integrate.",
   "To_users": "To Users",
   "Today": "Today",
@@ -4615,10 +4637,6 @@
   "Uploading_file": "Uploading file...",
   "Uptime": "Uptime",
   "URL": "URL",
-  "URL_room_hash": "Enable room name hash",
-  "URL_room_hash_description": "Recommended to enable if the Jitsi instance doesn't use any authentication mechanism.",
-  "URL_room_prefix": "URL room prefix",
-  "URL_room_suffix": "URL room suffix",
   "Usage": "Usage",
   "Use": "Use",
   "Use_account_preference": "Use account preference",
@@ -4768,13 +4786,34 @@
   "Verify_your_email_for_the_code_we_sent": "Verify your email for the code we sent",
   "Version": "Version",
   "Version_version": "Version __version__",
-  "Video Conference": "Video Conference",
-  "Video Conference_Description": "Configure video conferencing for your workspace.",
+  "Video_Conference_Description": "Configure conferencing calls for your workspace.",
   "Video_Chat_Window": "Video Chat",
-  "Video_Conference": "Video Conference",
+  "Video_Conference": "Conference Call",
+  "Video_Conferences": "Conference Calls",
+  "video-conf-provider-not-configured": "**Conference call not enabled**: A workspace admin needs to enable the conference calls feature first.",
+  "Video_conference_app_required": "Conference call app required",
+  "Video_conference_apps_available": "Conference call apps are available on the Rocket.Chat marketplace.",
+  "Video_conference_apps_can_be_installed": "Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.",
+  "Video_conference_not_available": "Conference call not available",
   "Video_message": "Video message",
   "Videocall_declined": "Video Call Declined.",
   "Video_and_Audio_Call": "Video and Audio Call",
+  "video_conference_started": "Started a call.",
+  "video_conference_ended": "Call ended.",
+  "video_conference_ended_by": "Ended a call.",
+  "video_livechat_started": "Started a video call.",
+  "video_livechat_missed": "Started a video call that wasn't answered.",
+  "video_direct_calling": "Is calling.",
+  "video_direct_ended": "Call ended.",
+  "video_direct_ended_by": "Ended a call.",
+  "video_direct_missed": "Started a call that wasn't answered.",
+  "video_direct_started": "Started a call.",
+  "VideoConf_Default_Provider": "Default Provider",
+  "VideoConf_Default_Provider_Description": "If you have multiple provider apps installed, select which one should be used for new conference calls.",
+  "VideoConf_Enable_Channels": "Enable in public channels",
+  "VideoConf_Enable_Groups": "Enable in private channels",
+  "VideoConf_Enable_DMs": "Enable in direct messages",
+  "VideoConf_Enable_Teams": "Enable in teams",
   "Videos": "Videos",
   "View_All": "View All Members",
   "View_channels": "View Channels",
@@ -5028,6 +5067,7 @@
   "Your_server_link": "Your server link",
   "Your_temporary_password_is_password": "Your temporary password is <strong>[password]</strong>.",
   "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.",
+  "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "Your web browser blocked Rocket.Chat from opening a new tab.",
   "Your_workspace_is_ready": "Your workspace is ready to use 🎉",
   "Zapier": "Zapier",
   "onboarding.component.form.steps": "Step {{currentStep}} of {{stepCount}}",
diff --git a/apps/meteor/public/sounds/calling.mp3 b/apps/meteor/public/sounds/calling.mp3
new file mode 100644
index 0000000000000000000000000000000000000000..6be6a7456bb07746dc802fb25d531679d7130927
Binary files /dev/null and b/apps/meteor/public/sounds/calling.mp3 differ
diff --git a/apps/meteor/server/cron/videoConferences.ts b/apps/meteor/server/cron/videoConferences.ts
new file mode 100644
index 0000000000000000000000000000000000000000..24a8c9b8c25d4235bcc3eb591c471f0f05dc13f1
--- /dev/null
+++ b/apps/meteor/server/cron/videoConferences.ts
@@ -0,0 +1,30 @@
+import type { SyncedCron } from 'meteor/littledata:synced-cron';
+import { VideoConference, VideoConferenceStatus } from '@rocket.chat/core-typings';
+import { VideoConference as VideoConferenceModel } from '@rocket.chat/models';
+
+import { VideoConf } from '../sdk';
+
+// 24 hours
+const VIDEO_CONFERENCE_TTL = 24 * 60 * 60 * 1000;
+
+async function runVideoConferences(): Promise<void> {
+	const minimum = new Date(new Date().valueOf() - VIDEO_CONFERENCE_TTL);
+
+	const calls = await (await VideoConferenceModel.findAllLongRunning(minimum))
+		.map(({ _id: callId }: Pick<VideoConference, '_id'>) => callId)
+		.toArray();
+
+	await Promise.all(calls.map((callId) => VideoConf.setStatus(callId, VideoConferenceStatus.EXPIRED)));
+}
+
+export function videoConferencesCron(syncedCron: typeof SyncedCron): void {
+	runVideoConferences();
+
+	syncedCron.add({
+		name: 'VideoConferences',
+		schedule(parser: any) {
+			return parser.cron('0 */3 * * *');
+		},
+		job: runVideoConferences,
+	});
+}
diff --git a/apps/meteor/server/importPackages.ts b/apps/meteor/server/importPackages.ts
index c5413917fa0191e34b21d6e24e746de38670a7ae..42a8f2a7f2eff93749d36ccd36a669f5c2a5120f 100644
--- a/apps/meteor/server/importPackages.ts
+++ b/apps/meteor/server/importPackages.ts
@@ -86,7 +86,6 @@ import '../app/tokenpass/server';
 import '../app/ui-master/server';
 import '../app/ui-vrecord/server';
 import '../app/user-data-download';
-import '../app/videobridge/server';
 import '../app/webdav/server';
 import '../app/webrtc/server';
 import '../app/wordpress/server';
@@ -96,7 +95,6 @@ import '../app/version-check/server';
 import '../app/search/server';
 import '../app/chatpal-search/server';
 import '../app/discussion/server';
-import '../app/bigbluebutton/server';
 import '../app/mail-messages/server';
 import '../app/user-status';
 import '../app/utils';
diff --git a/apps/meteor/server/lib/statistics/getSettingsStatistics.ts b/apps/meteor/server/lib/statistics/getSettingsStatistics.ts
index c43f4ef83717275137749d1d6d2478df233bf068..c1d99c590a2ed2bc43db14799133f13c1354a213 100644
--- a/apps/meteor/server/lib/statistics/getSettingsStatistics.ts
+++ b/apps/meteor/server/lib/statistics/getSettingsStatistics.ts
@@ -28,8 +28,6 @@ const setSettingsStatistics = async (settings: ISettingStatistics): Promise<ISet
 		otrEnable,
 		pushEnable,
 		threadsEnabled,
-		bigBlueButton,
-		jitsiEnabled,
 		webRTCEnableChannel,
 		webRTCEnablePrivate,
 		webRTCEnableDirect,
@@ -96,10 +94,6 @@ const setSettingsStatistics = async (settings: ISettingStatistics): Promise<ISet
 		threads: {
 			threadsEnabled,
 		},
-		videoConference: {
-			bigBlueButton,
-			jitsiEnabled,
-		},
 		webRTC: {
 			webRTCEnableChannel,
 			webRTCEnablePrivate,
@@ -135,8 +129,6 @@ export const getSettingsStatistics = async (): Promise<ISettingStatisticsObject>
 			{ key: 'OTR_Enable', alias: 'otrEnable' },
 			{ key: 'Push_enable', alias: 'pushEnable' },
 			{ key: 'Threads_enabled', alias: 'threadsEnabled' },
-			{ key: 'bigbluebutton_Enabled', alias: 'bigBlueButton' },
-			{ key: 'Jitsi_Enabled', alias: 'jitsiEnabled' },
 			{ key: 'WebRTC_Enable_Channel', alias: 'webRTCEnableChannel' },
 			{ key: 'WebRTC_Enable_Private', alias: 'webRTCEnablePrivate' },
 			{ key: 'WebRTC_Enable_Direct', alias: 'webRTCEnableDirect' },
diff --git a/apps/meteor/server/lib/videoConfProviders.ts b/apps/meteor/server/lib/videoConfProviders.ts
new file mode 100644
index 0000000000000000000000000000000000000000..353fa36fad8120945403b5a05881af57db4f52f4
--- /dev/null
+++ b/apps/meteor/server/lib/videoConfProviders.ts
@@ -0,0 +1,59 @@
+import { VideoConferenceCapabilities } from '@rocket.chat/core-typings';
+
+import { settings } from '../../app/settings/server';
+
+const providers = new Map<string, { capabilities: VideoConferenceCapabilities; label: string }>();
+
+export const videoConfProviders = {
+	registerProvider(providerName: string, capabilities: VideoConferenceCapabilities): void {
+		providers.set(providerName.toLowerCase(), { capabilities, label: providerName });
+	},
+
+	unRegisterProvider(providerName: string): void {
+		const key = providerName.toLowerCase();
+
+		if (providers.has(key)) {
+			providers.delete(key);
+		}
+	},
+
+	getActiveProvider(): string | undefined {
+		if (providers.size === 0) {
+			return;
+		}
+		const defaultProvider = settings.get<string>('VideoConf_Default_Provider');
+
+		if (defaultProvider) {
+			if (providers.has(defaultProvider)) {
+				return defaultProvider;
+			}
+
+			return;
+		}
+
+		if (providers.size === 1) {
+			const [[name]] = [...providers];
+			return name;
+		}
+	},
+
+	hasAnyProvider(): boolean {
+		return providers.size > 0;
+	},
+
+	getProviderList(): { key: string; label: string }[] {
+		return [...providers.keys()].map((key) => ({ key, label: providers.get(key)?.label || key }));
+	},
+
+	isProviderAvailable(name: string): boolean {
+		return providers.has(name);
+	},
+
+	getProviderCapabilities(name: string): VideoConferenceCapabilities | undefined {
+		if (!providers.has(name)) {
+			return;
+		}
+
+		return providers.get(name)?.capabilities;
+	},
+};
diff --git a/apps/meteor/server/lib/videoConfTypes.ts b/apps/meteor/server/lib/videoConfTypes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d899539c3ba778cc130791958fe634a6b791d281
--- /dev/null
+++ b/apps/meteor/server/lib/videoConfTypes.ts
@@ -0,0 +1,39 @@
+import type { AtLeast, IRoom, VideoConferenceCreateData, VideoConferenceType } from '@rocket.chat/core-typings';
+
+type RoomRequiredFields = AtLeast<IRoom, '_id' | 't'>;
+type VideoConferenceTypeCondition = (room: RoomRequiredFields, allowRinging: boolean) => Promise<boolean>;
+
+const typeConditions: {
+	data: VideoConferenceType | AtLeast<VideoConferenceCreateData, 'type'>;
+	condition: VideoConferenceTypeCondition;
+	priority: number;
+}[] = [];
+
+export const videoConfTypes = {
+	registerVideoConferenceType(
+		data: VideoConferenceType | AtLeast<VideoConferenceCreateData, 'type'>,
+		condition: VideoConferenceTypeCondition,
+		priority = 1,
+	): void {
+		typeConditions.push({ data, condition, priority });
+		typeConditions.sort(({ priority: prior1 }, { priority: prior2 }) => prior2 - prior1);
+	},
+
+	async getTypeForRoom(room: RoomRequiredFields, allowRinging: boolean): Promise<AtLeast<VideoConferenceCreateData, 'type'>> {
+		for await (const { data, condition } of typeConditions) {
+			if (await condition(room, allowRinging)) {
+				if (typeof data === 'string') {
+					return {
+						type: data,
+					};
+				}
+
+				return data;
+			}
+		}
+
+		return { type: 'videoconference' };
+	},
+};
+
+videoConfTypes.registerVideoConferenceType({ type: 'livechat' }, async ({ t }) => t === 'l');
diff --git a/apps/meteor/server/models/VideoConference.ts b/apps/meteor/server/models/VideoConference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c4a144b2e752fe16e74d1575a0bc4227a0f2c30
--- /dev/null
+++ b/apps/meteor/server/models/VideoConference.ts
@@ -0,0 +1,7 @@
+import { registerModel } from '@rocket.chat/models';
+
+import { trashCollection } from '../database/trash';
+import { db } from '../database/utils';
+import { VideoConferenceRaw } from './raw/VideoConference';
+
+registerModel('IVideoConferenceModel', new VideoConferenceRaw(db, trashCollection));
diff --git a/apps/meteor/server/models/raw/Messages.ts b/apps/meteor/server/models/raw/Messages.ts
index dfa4ecfaa71b808975a930488fe6fb46f86694e5..686c2f2fd3cb6e184c130fe0d3ce38827e8a418e 100644
--- a/apps/meteor/server/models/raw/Messages.ts
+++ b/apps/meteor/server/models/raw/Messages.ts
@@ -229,6 +229,35 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
 		);
 	}
 
+	async setBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void> {
+		await this.updateOne(
+			{ _id },
+			{
+				$set: {
+					blocks,
+				},
+			},
+		);
+	}
+
+	async addBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void> {
+		await this.updateOne({ _id }, { $addToSet: { blocks: { $each: blocks } } });
+	}
+
+	async removeVideoConfJoinButton(_id: IMessage['_id']): Promise<void> {
+		await this.updateOne(
+			{ _id },
+			{
+				$pull: {
+					blocks: {
+						appId: 'videoconf-core',
+						type: 'actions',
+					} as Required<IMessage>['blocks'][number],
+				},
+			},
+		);
+	}
+
 	async countRoomsWithStarredMessages(options: CollectionAggregationOptions): Promise<number> {
 		const queryResult = await this.col
 			.aggregate<{ _id: null; total: number }>(
diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts
index 28e7ed1b3221f5186e37e94ebd6fad88412bde7e..f4f432d7f6dd80a588539b8d4e24318a0d95ed76 100644
--- a/apps/meteor/server/models/raw/Subscriptions.ts
+++ b/apps/meteor/server/models/raw/Subscriptions.ts
@@ -47,6 +47,14 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
 		return this.find(query, options);
 	}
 
+	findByRoomId(roomId: string, options: FindOneOptions<ISubscription> = {}): Cursor<ISubscription> {
+		const query = {
+			rid: roomId,
+		};
+
+		return this.find(query, options);
+	}
+
 	findByRoomIdAndNotUserId(roomId: string, userId: string, options: FindOneOptions<ISubscription> = {}): Cursor<ISubscription> {
 		const query = {
 			'rid': roomId,
diff --git a/apps/meteor/server/models/raw/VideoConference.ts b/apps/meteor/server/models/raw/VideoConference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..94123025714128010e2fa4cc3ca8f686ddc59540
--- /dev/null
+++ b/apps/meteor/server/models/raw/VideoConference.ts
@@ -0,0 +1,273 @@
+import type {
+	Cursor,
+	UpdateOneOptions,
+	UpdateQuery,
+	UpdateWriteOpResult,
+	IndexSpecification,
+	Collection,
+	Db,
+	FindOneOptions,
+	FilterQuery,
+} from 'mongodb';
+import type {
+	VideoConference,
+	IGroupVideoConference,
+	ILivechatVideoConference,
+	IUser,
+	IRoom,
+	RocketChatRecordDeleted,
+} from '@rocket.chat/core-typings';
+import type { InsertionModel, IVideoConferenceModel } from '@rocket.chat/model-typings';
+import { VideoConferenceStatus } from '@rocket.chat/core-typings';
+import { getCollectionName } from '@rocket.chat/models';
+
+import { BaseRaw } from './BaseRaw';
+
+export class VideoConferenceRaw extends BaseRaw<VideoConference> implements IVideoConferenceModel {
+	constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<VideoConference>>) {
+		super(db, getCollectionName('video_conference'), trash);
+	}
+
+	protected modelIndexes(): IndexSpecification[] {
+		return [
+			{ key: { rid: 1, createdAt: 1 }, unique: false },
+			{ key: { type: 1, status: 1 }, unique: false },
+		];
+	}
+
+	public async findAllByRoomId(
+		rid: IRoom['_id'],
+		{ offset, count }: { offset?: number; count?: number } = {},
+	): Promise<Cursor<VideoConference>> {
+		return this.find(
+			{ rid },
+			{
+				sort: { createdAt: -1 },
+				skip: offset,
+				limit: count,
+				projection: {
+					providerData: 0,
+				},
+			},
+		);
+	}
+
+	public async findAllLongRunning(minDate: Date): Promise<Cursor<Pick<VideoConference, '_id'>>> {
+		return this.find(
+			{
+				createdAt: {
+					$lte: minDate,
+				},
+				endedAt: {
+					$exists: false,
+				},
+			},
+			{
+				projection: {
+					_id: 1,
+				},
+			},
+		);
+	}
+
+	public async countByTypeAndStatus(
+		type: VideoConference['type'],
+		status: VideoConferenceStatus,
+		options: FindOneOptions<VideoConference>,
+	): Promise<number> {
+		return this.find(
+			{
+				type,
+				status,
+			} as FilterQuery<VideoConference>,
+			options,
+		).count();
+	}
+
+	public async createDirect({
+		providerName,
+		...callDetails
+	}: Pick<VideoConference, 'rid' | 'createdBy' | 'providerName'>): Promise<string> {
+		const call: InsertionModel<VideoConference> = {
+			type: 'direct',
+			users: [],
+			messages: {},
+			status: VideoConferenceStatus.CALLING,
+			createdAt: new Date(),
+			providerName: providerName.toLowerCase(),
+			ringing: true,
+			...callDetails,
+		};
+
+		return (await this.insertOne(call)).insertedId;
+	}
+
+	public async createGroup({
+		providerName,
+		...callDetails
+	}: Required<Pick<IGroupVideoConference, 'rid' | 'title' | 'createdBy' | 'providerName' | 'ringing'>>): Promise<string> {
+		const call: InsertionModel<IGroupVideoConference> = {
+			type: 'videoconference',
+			users: [],
+			messages: {},
+			status: VideoConferenceStatus.STARTED,
+			anonymousUsers: 0,
+			createdAt: new Date(),
+			providerName: providerName.toLowerCase(),
+			...callDetails,
+		};
+
+		return (await this.insertOne(call)).insertedId;
+	}
+
+	public async createLivechat({
+		providerName,
+		...callDetails
+	}: Required<Pick<ILivechatVideoConference, 'rid' | 'createdBy' | 'providerName'>>): Promise<string> {
+		const call: InsertionModel<ILivechatVideoConference> = {
+			type: 'livechat',
+			users: [],
+			messages: {},
+			status: VideoConferenceStatus.STARTED,
+			createdAt: new Date(),
+			providerName: providerName.toLowerCase(),
+			...callDetails,
+		};
+
+		return (await this.insertOne(call)).insertedId;
+	}
+
+	public updateOneById(
+		_id: string,
+		update: UpdateQuery<VideoConference> | Partial<VideoConference>,
+		options?: UpdateOneOptions,
+	): Promise<UpdateWriteOpResult> {
+		return this.updateOne({ _id }, update, options);
+	}
+
+	public async setEndedById(callId: string, endedBy?: { _id: string; name: string; username: string }, endedAt?: Date): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: {
+				endedBy,
+				endedAt: endedAt || new Date(),
+			},
+		});
+	}
+
+	public async setDataById(callId: string, data: Partial<Omit<VideoConference, '_id'>>): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: data,
+		});
+	}
+
+	public async setRingingById(callId: string, ringing: boolean): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: {
+				ringing,
+			},
+		});
+	}
+
+	public async setStatusById(callId: string, status: VideoConference['status']): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: {
+				status,
+			},
+		});
+	}
+
+	public async setUrlById(callId: string, url: string): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: {
+				url,
+			},
+		});
+	}
+
+	public async setProviderDataById(callId: string, providerData: Record<string, any> | undefined): Promise<void> {
+		await this.updateOneById(callId, {
+			...(providerData
+				? {
+						$set: {
+							providerData,
+						},
+				  }
+				: {
+						$unset: {
+							providerData: 1,
+						},
+				  }),
+		});
+	}
+
+	public async addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void> {
+		await this.updateOneById(callId, {
+			$addToSet: {
+				users: {
+					_id: user._id,
+					username: user.username,
+					name: user.name,
+					avatarETag: user.avatarETag,
+					ts: user.ts || new Date(),
+				},
+			},
+		});
+	}
+
+	public async setMessageById(callId: string, messageType: keyof VideoConference['messages'], messageId: string): Promise<void> {
+		await this.updateOneById(callId, {
+			$set: {
+				[`messages.${messageType}`]: messageId,
+			},
+		});
+	}
+
+	public async updateUserReferences(userId: IUser['_id'], username: IUser['username'], name: IUser['name']): Promise<void> {
+		await this.updateMany(
+			{
+				'users._id': userId,
+			},
+			{
+				$set: {
+					'users.$.name': name,
+					'users.$.username': username,
+				},
+			},
+		);
+
+		await this.updateMany(
+			{
+				'createdBy._id': userId,
+			},
+			{
+				$set: {
+					'createdBy.name': name,
+					'createdBy.username': username,
+				},
+			},
+		);
+
+		await this.updateMany(
+			{
+				'endedBy._id': userId,
+			},
+			{
+				$set: {
+					'endedBy.name': name,
+					'endedBy.username': username,
+				},
+			},
+		);
+	}
+
+	public async increaseAnonymousCount(callId: IGroupVideoConference['_id']): Promise<void> {
+		await this.updateOne(
+			{ _id: callId },
+			{
+				$inc: {
+					anonymousUsers: 1,
+				},
+			},
+		);
+	}
+}
diff --git a/apps/meteor/server/models/startup.ts b/apps/meteor/server/models/startup.ts
index 0d2618b0251a3b95ee92429bb2b7ace89cb86037..0469db7747a4dde3e72f209bca1e130bcf6cd2c7 100644
--- a/apps/meteor/server/models/startup.ts
+++ b/apps/meteor/server/models/startup.ts
@@ -51,5 +51,6 @@ import './Uploads';
 import './UserDataFiles';
 import './Users';
 import './UsersSessions';
+import './VideoConference';
 import './VoipRoom';
 import './WebdavAccounts';
diff --git a/apps/meteor/server/modules/core-apps/videoconf.module.ts b/apps/meteor/server/modules/core-apps/videoconf.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87f0ca009da1d454d79348405529f2fbe6a4a6e1
--- /dev/null
+++ b/apps/meteor/server/modules/core-apps/videoconf.module.ts
@@ -0,0 +1,18 @@
+import { IUiKitCoreApp } from '../../sdk/types/IUiKitCoreApp';
+import { VideoConf } from '../../sdk';
+
+export class VideoConfModule implements IUiKitCoreApp {
+	appId = 'videoconf-core';
+
+	async blockAction(payload: any): Promise<any> {
+		const {
+			actionId,
+			payload: { blockId: callId },
+			user: { _id: userId },
+		} = payload;
+
+		if (actionId === 'join') {
+			VideoConf.join(userId, callId, {});
+		}
+	}
+}
diff --git a/apps/meteor/server/modules/watchers/publishFields.ts b/apps/meteor/server/modules/watchers/publishFields.ts
index 525b9259088be34e38730dd09df26e9274a7555f..6de9272a28ee30efa6e4b1b7985a3f84b15fca16 100644
--- a/apps/meteor/server/modules/watchers/publishFields.ts
+++ b/apps/meteor/server/modules/watchers/publishFields.ts
@@ -61,7 +61,6 @@ export const roomFields = {
 	unmuted: 1,
 	_updatedAt: 1,
 	archived: 1,
-	jitsiTimeout: 1,
 	description: 1,
 	default: 1,
 	customFields: 1,
diff --git a/apps/meteor/server/sdk/index.ts b/apps/meteor/server/sdk/index.ts
index b01171bb48c0881498f5893bca1a052db11c46ba..b63628b3e944b61608976d4d95ae5b23e60f6b43 100644
--- a/apps/meteor/server/sdk/index.ts
+++ b/apps/meteor/server/sdk/index.ts
@@ -16,6 +16,7 @@ import { IVoipService } from './types/IVoipService';
 import { IOmnichannelVoipService } from './types/IOmnichannelVoipService';
 import { IAnalyticsService } from './types/IAnalyticsService';
 import { ILDAPService } from './types/ILDAPService';
+import { IVideoConfService } from './types/IVideoConfService';
 import { ISAUMonitorService } from './types/ISAUMonitorService';
 import { FibersContextStore } from './lib/ContextStore';
 
@@ -36,6 +37,7 @@ export const LivechatVoip = proxifyWithWait<IOmnichannelVoipService>('omnichanne
 export const Analytics = proxifyWithWait<IAnalyticsService>('analytics');
 export const LDAP = proxifyWithWait<ILDAPService>('ldap');
 export const SAUMonitor = proxifyWithWait<ISAUMonitorService>('sau-monitor');
+export const VideoConf = proxifyWithWait<IVideoConfService>('video-conference');
 
 // Calls without wait. Means that the service is optional and the result may be an error
 // of service/method not available
diff --git a/apps/meteor/server/sdk/types/IVideoConfService.ts b/apps/meteor/server/sdk/types/IVideoConfService.ts
new file mode 100644
index 0000000000000000000000000000000000000000..031334aa55f0f42eb4533f667152703368ca41d5
--- /dev/null
+++ b/apps/meteor/server/sdk/types/IVideoConfService.ts
@@ -0,0 +1,36 @@
+import type {
+	IRoom,
+	IStats,
+	IUser,
+	VideoConference,
+	VideoConferenceCapabilities,
+	VideoConferenceCreateData,
+	VideoConferenceInstructions,
+} from '@rocket.chat/core-typings';
+import type { PaginatedResult } from '@rocket.chat/rest-typings';
+
+export type VideoConferenceJoinOptions = {
+	mic?: boolean;
+	cam?: boolean;
+};
+
+export interface IVideoConfService {
+	create(data: VideoConferenceCreateData): Promise<VideoConferenceInstructions>;
+	start(caller: IUser['_id'], rid: string, options: { title?: string; allowRinging?: boolean }): Promise<VideoConferenceInstructions>;
+	join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise<string>;
+	cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void>;
+	get(callId: VideoConference['_id']): Promise<Omit<VideoConference, 'providerData'> | null>;
+	getUnfiltered(callId: VideoConference['_id']): Promise<VideoConference | null>;
+	list(roomId: IRoom['_id'], pagination?: { offset?: number; count?: number }): Promise<PaginatedResult<{ data: VideoConference[] }>>;
+	setProviderData(callId: VideoConference['_id'], data: VideoConference['providerData'] | undefined): Promise<void>;
+	setEndedBy(callId: VideoConference['_id'], endedBy: IUser['_id']): Promise<void>;
+	setEndedAt(callId: VideoConference['_id'], endedAt: Date): Promise<void>;
+	setStatus(callId: VideoConference['_id'], status: VideoConference['status']): Promise<void>;
+	addUser(callId: VideoConference['_id'], userId?: IUser['_id'], ts?: Date): Promise<void>;
+	listProviders(): Promise<{ key: string; label: string }[]>;
+	listCapabilities(): Promise<{ providerName: string; capabilities: VideoConferenceCapabilities }>;
+	listProviderCapabilities(providerName: string): Promise<VideoConferenceCapabilities>;
+	declineLivechatCall(callId: VideoConference['_id']): Promise<boolean>;
+	diagnoseProvider(uid: string, rid: string, providerName?: string): Promise<string | undefined>;
+	getStatistics(): Promise<IStats['videoConf']>;
+}
diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts
index 4f9b27231dd55e3b97637ed24589944b57fb5dd6..b8ee0ff0a1183eb71d0d0962fd35cbedc414d09a 100644
--- a/apps/meteor/server/services/startup.ts
+++ b/apps/meteor/server/services/startup.ts
@@ -14,6 +14,7 @@ import { TeamService } from './team/service';
 import { UiKitCoreApp } from './uikit-core-app/service';
 import { OmnichannelVoipService } from './omnichannel-voip/service';
 import { VoipService } from './voip/service';
+import { VideoConfService } from './video-conference/service';
 import { isRunningMs } from '../lib/isRunningMs';
 import { PushService } from './push/service';
 
@@ -33,6 +34,7 @@ api.registerService(new OmnichannelVoipService());
 api.registerService(new TeamService());
 api.registerService(new UiKitCoreApp());
 api.registerService(new PushService());
+api.registerService(new VideoConfService());
 
 // if the process is running in micro services mode we don't need to register services that will run separately
 if (!isRunningMs()) {
diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..52f8cc870bc336b691cd1df75ffc0285001d56a8
--- /dev/null
+++ b/apps/meteor/server/services/video-conference/service.ts
@@ -0,0 +1,907 @@
+import { MongoInternals } from 'meteor/mongo';
+import type {
+	IDirectVideoConference,
+	IRoom,
+	IUser,
+	VideoConferenceInstructions,
+	DirectCallInstructions,
+	ConferenceInstructions,
+	LivechatInstructions,
+	AtLeast,
+	IGroupVideoConference,
+	IMessage,
+	IStats,
+	VideoConference,
+	VideoConferenceCapabilities,
+	VideoConferenceCreateData,
+	Optional,
+} from '@rocket.chat/core-typings';
+import {
+	VideoConferenceStatus,
+	isDirectVideoConference,
+	isGroupVideoConference,
+	isLivechatVideoConference,
+} from '@rocket.chat/core-typings';
+import type { MessageSurfaceLayout, ContextBlock } from '@rocket.chat/ui-kit';
+import type { AppVideoConfProviderManager } from '@rocket.chat/apps-engine/server/managers';
+import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
+import type { PaginatedResult } from '@rocket.chat/rest-typings';
+import { Users, VideoConference as VideoConferenceModel, Rooms, Messages, Subscriptions } from '@rocket.chat/models';
+
+import type { IVideoConfService, VideoConferenceJoinOptions } from '../../sdk/types/IVideoConfService';
+import { ServiceClassInternal } from '../../sdk/types/ServiceClass';
+import { Apps } from '../../../app/apps/server';
+import { sendMessage } from '../../../app/lib/server/functions/sendMessage';
+import { settings } from '../../../app/settings/server';
+import { getURL } from '../../../app/utils/server';
+import { videoConfProviders } from '../../lib/videoConfProviders';
+import { videoConfTypes } from '../../lib/videoConfTypes';
+import { updateCounter } from '../../../app/statistics/server/functions/updateStatsCounter';
+import { api } from '../../sdk/api';
+import { readSecondaryPreferred } from '../../database/readSecondaryPreferred';
+import { availabilityErrors } from '../../../lib/videoConference/constants';
+import { callbacks } from '../../../lib/callbacks';
+import { Notifications } from '../../../app/notifications/server';
+
+const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo;
+
+export class VideoConfService extends ServiceClassInternal implements IVideoConfService {
+	protected name = 'video-conference';
+
+	// VideoConference.create: Start a video conference using the type and provider specified as arguments
+	public async create({ type, rid, createdBy, providerName, ...data }: VideoConferenceCreateData): Promise<VideoConferenceInstructions> {
+		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't' | 'uids' | 'name' | 'fname'>>(rid, {
+			projection: { t: 1, uids: 1, name: 1, fname: 1 },
+		});
+
+		if (!room) {
+			throw new Error('invalid-room');
+		}
+
+		const user = await Users.findOneById<IUser>(createdBy, {});
+		if (!user) {
+			throw new Error('failed-to-load-own-data');
+		}
+
+		if (type === 'direct') {
+			if (room.t !== 'd' || !room.uids || room.uids.length > 2) {
+				throw new Error('type-and-room-not-compatible');
+			}
+
+			return this.startDirect(providerName, user, room, data);
+		}
+
+		if (type === 'livechat') {
+			return this.startLivechat(providerName, user, rid);
+		}
+
+		const title = (data as Partial<IGroupVideoConference>).title || room.fname || room.name || '';
+		return this.startGroup(providerName, user, room._id, title, data);
+	}
+
+	// VideoConference.start: Detect the desired type and provider then start a video conference using them
+	public async start(
+		caller: IUser['_id'],
+		rid: string,
+		{ title, allowRinging }: { title?: string; allowRinging?: boolean },
+	): Promise<VideoConferenceInstructions> {
+		const providerName = await this.getValidatedProvider();
+		const initialData = await this.getTypeForNewVideoConference(rid, Boolean(allowRinging));
+
+		const data = {
+			...initialData,
+			createdBy: caller,
+			rid,
+			providerName,
+		} as VideoConferenceCreateData;
+
+		if (data.type === 'videoconference') {
+			data.title = title;
+		}
+
+		return this.create(data);
+	}
+
+	public async join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise<string> {
+		const call = await VideoConferenceModel.findOneById(callId);
+		if (!call || call.endedAt) {
+			throw new Error('invalid-call');
+		}
+
+		let user: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> | null = null;
+
+		if (uid) {
+			user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>(uid, {
+				projection: { name: 1, username: 1, avatarETag: 1 },
+			});
+			if (!user) {
+				throw new Error('failed-to-load-own-data');
+			}
+		}
+
+		if (call.providerName === 'jitsi') {
+			updateCounter({ settingsId: 'Jitsi_Click_To_Join_Count' });
+		}
+
+		return this.joinCall(call, user || undefined, options);
+	}
+
+	public async cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void> {
+		const call = await VideoConferenceModel.findOneById(callId);
+		if (!call || !isDirectVideoConference(call)) {
+			throw new Error('invalid-call');
+		}
+
+		if (call.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {
+			throw new Error('invalid-call-status');
+		}
+
+		const user = await Users.findOneById(uid);
+		if (!user) {
+			throw new Error('failed-to-load-own-data');
+		}
+
+		if (call.messages.started) {
+			const text = TAPi18n.__('video_direct_missed', { username: call.createdBy.username as string });
+			await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]);
+		}
+
+		await VideoConferenceModel.setDataById(callId, {
+			ringing: false,
+			status: VideoConferenceStatus.DECLINED,
+			endedAt: new Date(),
+			endedBy: {
+				_id: user._id,
+				name: user.name,
+				username: user.username,
+			},
+		});
+	}
+
+	public async get(callId: VideoConference['_id']): Promise<Omit<VideoConference, 'providerData'> | null> {
+		return VideoConferenceModel.findOneById<Omit<VideoConference, 'providerData'>>(callId, { projection: { providerData: 0 } });
+	}
+
+	public async getUnfiltered(callId: VideoConference['_id']): Promise<VideoConference | null> {
+		return VideoConferenceModel.findOneById(callId);
+	}
+
+	public async list(
+		roomId: IRoom['_id'],
+		pagination: { offset?: number; count?: number } = {},
+	): Promise<PaginatedResult<{ data: VideoConference[] }>> {
+		const cursor = await VideoConferenceModel.findAllByRoomId(roomId, pagination);
+
+		const data = (await cursor.toArray()) as VideoConference[];
+		const total = await cursor.count();
+
+		return {
+			data,
+			offset: pagination.offset || 0,
+			count: data.length,
+			total,
+		};
+	}
+
+	public async setProviderData(callId: VideoConference['_id'], data: VideoConference['providerData'] | undefined): Promise<void> {
+		VideoConferenceModel.setProviderDataById(callId, data);
+	}
+
+	public async setEndedBy(callId: VideoConference['_id'], endedBy: IUser['_id']): Promise<void> {
+		const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name'>>>(endedBy, {
+			projection: { username: 1, name: 1 },
+		});
+		if (!user) {
+			throw new Error('Invalid User');
+		}
+
+		VideoConferenceModel.setEndedById(callId, {
+			_id: user._id,
+			username: user.username,
+			name: user.name,
+		});
+	}
+
+	public async setEndedAt(callId: VideoConference['_id'], endedAt: Date): Promise<void> {
+		VideoConferenceModel.setEndedById(callId, undefined, endedAt);
+	}
+
+	public async setStatus(callId: VideoConference['_id'], status: VideoConference['status']): Promise<void> {
+		switch (status) {
+			case VideoConferenceStatus.ENDED:
+				return this.endCall(callId);
+			case VideoConferenceStatus.EXPIRED:
+				return this.expireCall(callId);
+		}
+
+		VideoConferenceModel.setStatusById(callId, status);
+	}
+
+	public async addUser(callId: VideoConference['_id'], userId?: IUser['_id'], ts?: Date): Promise<void> {
+		const call = await this.get(callId);
+		if (!call) {
+			throw new Error('Invalid video conference');
+		}
+
+		if (!userId) {
+			if (call.type === 'videoconference') {
+				return this.addAnonymousUser(call as Omit<IGroupVideoConference, 'providerData'>);
+			}
+
+			throw new Error('Invalid User');
+		}
+
+		const user = await Users.findOneById<Required<Pick<IUser, '_id' | 'username' | 'name'>>>(userId, {
+			projection: { username: 1, name: 1 },
+		});
+		if (!user) {
+			throw new Error('Invalid User');
+		}
+
+		this.addUserToCall(call, {
+			_id: user._id,
+			username: user.username,
+			name: user.name,
+			ts: ts || new Date(),
+		});
+	}
+
+	public async listProviders(): Promise<{ key: string; label: string }[]> {
+		return videoConfProviders.getProviderList();
+	}
+
+	public async listProviderCapabilities(providerName: string): Promise<VideoConferenceCapabilities> {
+		return videoConfProviders.getProviderCapabilities(providerName) || {};
+	}
+
+	public async listCapabilities(): Promise<{ providerName: string; capabilities: VideoConferenceCapabilities }> {
+		const providerName = await this.getValidatedProvider();
+
+		return {
+			providerName,
+			capabilities: videoConfProviders.getProviderCapabilities(providerName) || {},
+		};
+	}
+
+	public async declineLivechatCall(callId: VideoConference['_id']): Promise<boolean> {
+		const call = await this.getUnfiltered(callId);
+
+		if (!isLivechatVideoConference(call)) {
+			return false;
+		}
+
+		if (call.messages.started) {
+			const text = TAPi18n.__('video_livechat_missed', { username: call.createdBy.username as string });
+			await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text)]);
+		}
+
+		await VideoConferenceModel.setDataById(call._id, {
+			status: VideoConferenceStatus.DECLINED,
+			endedAt: new Date(),
+		});
+
+		return true;
+	}
+
+	public async diagnoseProvider(uid: string, rid: string, providerName?: string): Promise<string | undefined> {
+		try {
+			if (providerName) {
+				await this.validateProvider(providerName);
+			} else {
+				await this.getValidatedProvider();
+			}
+		} catch (error: unknown) {
+			if (error instanceof Error) {
+				this.createEphemeralMessage(uid, rid, error.message);
+				return error.message;
+			}
+		}
+	}
+
+	public async getStatistics(): Promise<IStats['videoConf']> {
+		const options = {
+			readPreference: readSecondaryPreferred(db),
+		};
+
+		return {
+			videoConference: {
+				started: await VideoConferenceModel.countByTypeAndStatus('videoconference', VideoConferenceStatus.STARTED, options),
+				ended: await VideoConferenceModel.countByTypeAndStatus('videoconference', VideoConferenceStatus.ENDED, options),
+			},
+			direct: {
+				calling: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.CALLING, options),
+				started: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.STARTED, options),
+				ended: await VideoConferenceModel.countByTypeAndStatus('direct', VideoConferenceStatus.ENDED, options),
+			},
+			livechat: {
+				started: await VideoConferenceModel.countByTypeAndStatus('livechat', VideoConferenceStatus.STARTED, options),
+				ended: await VideoConferenceModel.countByTypeAndStatus('livechat', VideoConferenceStatus.ENDED, options),
+			},
+			settings: {
+				provider: settings.get<string>('VideoConf_Default_Provider'),
+				dms: settings.get<boolean>('VideoConf_Enable_DMs'),
+				channels: settings.get<boolean>('VideoConf_Enable_Channels'),
+				groups: settings.get<boolean>('VideoConf_Enable_Groups'),
+				teams: settings.get<boolean>('VideoConf_Enable_Teams'),
+			},
+		};
+	}
+
+	private async endCall(callId: VideoConference['_id']): Promise<void> {
+		const call = await this.getUnfiltered(callId);
+		if (!call) {
+			return;
+		}
+
+		await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.ENDED });
+		if (call.messages?.started) {
+			await this.removeJoinButton(call.messages.started);
+		}
+
+		switch (call.type) {
+			case 'direct':
+				return this.endDirectCall(call);
+			case 'videoconference':
+				return this.endGroupCall(call);
+		}
+	}
+
+	private async expireCall(callId: VideoConference['_id']): Promise<void> {
+		const call = await VideoConferenceModel.findOneById<Pick<VideoConference, '_id' | 'messages'>>(callId, { projection: { messages: 1 } });
+		if (!call) {
+			return;
+		}
+
+		await VideoConferenceModel.setDataById(call._id, { endedAt: new Date(), status: VideoConferenceStatus.EXPIRED });
+		if (call.messages?.started) {
+			return this.removeJoinButton(call.messages.started);
+		}
+	}
+
+	private async removeJoinButton(messageId: IMessage['_id']): Promise<void> {
+		await Messages.removeVideoConfJoinButton(messageId);
+	}
+
+	private async endDirectCall(call: IDirectVideoConference): Promise<void> {
+		if (!call.messages.ended) {
+			this.createDirectCallEndedMessage(call);
+		}
+	}
+
+	private async endGroupCall(call: IGroupVideoConference): Promise<void> {
+		if (!call.messages.ended) {
+			this.createGroupCallEndedMessage(call);
+		}
+	}
+
+	private async getTypeForNewVideoConference(
+		rid: IRoom['_id'],
+		allowRinging: boolean,
+	): Promise<AtLeast<VideoConferenceCreateData, 'type'>> {
+		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 't'>>(rid, {
+			projection: { t: 1 },
+		});
+
+		if (!room) {
+			throw new Error('invalid-room');
+		}
+
+		return videoConfTypes.getTypeForRoom(room, allowRinging);
+	}
+
+	private async createMessage(rid: IRoom['_id'], user: IUser, extraData: Partial<IMessage> = {}): Promise<IMessage['_id']> {
+		const record = {
+			msg: '',
+			groupable: false,
+			...extraData,
+		};
+
+		const room = await Rooms.findOneById(rid);
+
+		const message = sendMessage(user, record, room, false);
+		return message._id;
+	}
+
+	private async createDirectCallMessage(call: IDirectVideoConference, user: IUser): Promise<IMessage['_id']> {
+		const text = TAPi18n.__('video_direct_calling', {
+			username: user.username || '',
+		});
+
+		return this.createMessage(call.rid, user, {
+			blocks: [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)],
+		});
+	}
+
+	private async createGroupCallMessage(rid: IRoom['_id'], user: IUser, callId: string, title: string): Promise<IMessage['_id']> {
+		const text = TAPi18n.__('video_conference_started', {
+			conference: title,
+			username: user.username || '',
+		});
+
+		return this.createMessage(rid, user, {
+			blocks: [
+				this.buildMessageBlock(text),
+				this.buildJoinButtonBlock(callId, title),
+				{
+					type: 'context',
+					elements: [],
+				},
+			],
+		} as Partial<IMessage>);
+	}
+
+	private async validateProvider(providerName: string): Promise<void> {
+		const manager = await this.getProviderManager();
+		const configured = await manager.isFullyConfigured(providerName).catch(() => false);
+		if (!configured) {
+			throw new Error(availabilityErrors.NOT_CONFIGURED);
+		}
+	}
+
+	private async getValidatedProvider(): Promise<string> {
+		if (!videoConfProviders.hasAnyProvider()) {
+			throw new Error(availabilityErrors.NO_APP);
+		}
+
+		const providerName = videoConfProviders.getActiveProvider();
+		if (!providerName) {
+			throw new Error(availabilityErrors.NOT_ACTIVE);
+		}
+
+		await this.validateProvider(providerName);
+
+		return providerName;
+	}
+
+	private async createEphemeralMessage(uid: string, rid: string, i18nKey: string): Promise<void> {
+		const user = await Users.findOneById<Pick<IUser, 'language' | 'roles'>>(uid, { projection: { language: 1, roles: 1 } });
+		const language = user?.language || settings.get<string>('Language') || 'en';
+		const key = user?.roles.includes('admin') ? `admin-${i18nKey}` : i18nKey;
+		const msg = TAPi18n.__(key, {
+			lng: language,
+		});
+
+		api.broadcast('notify.ephemeralMessage', uid, rid, {
+			msg,
+		});
+	}
+
+	private async createDirectCallEndedMessage(call: IDirectVideoConference): Promise<IMessage['_id'] | undefined> {
+		const user = await Users.findOneById(call.endedBy?._id || call.createdBy._id);
+		if (!user) {
+			return;
+		}
+
+		const text =
+			user._id === call.endedBy?._id
+				? TAPi18n.__('video_direct_ended_by', {
+						username: user.username || '',
+				  })
+				: TAPi18n.__('video_direct_ended');
+
+		return this.createMessage(call.rid, user, {
+			blocks: [this.buildMessageBlock(text)],
+		} as Partial<IMessage>);
+	}
+
+	private async createGroupCallEndedMessage(call: IGroupVideoConference): Promise<IMessage['_id'] | undefined> {
+		const user = await Users.findOneById(call.endedBy?._id || call.createdBy._id);
+		if (!user) {
+			return;
+		}
+
+		const text =
+			user._id === call.endedBy?._id
+				? TAPi18n.__('video_conference_ended_by', {
+						conference: call.title,
+						username: user.username || '',
+				  })
+				: TAPi18n.__('video_conference_ended', {
+						conference: call.title,
+				  });
+
+		return this.createMessage(call.rid, user, {
+			blocks: [this.buildMessageBlock(text)],
+		} as Partial<IMessage>);
+	}
+
+	private async createLivechatMessage(rid: IRoom['_id'], user: IUser, callId: string, url: string): Promise<IMessage['_id']> {
+		const text = TAPi18n.__('video_livechat_started', {
+			username: user.username || '',
+		});
+
+		return this.createMessage(rid, user, {
+			blocks: [
+				this.buildMessageBlock(text),
+				{
+					type: 'actions',
+					appId: 'videoconf-core',
+					blockId: callId,
+					elements: [
+						{
+							appId: 'videoconf-core',
+							blockId: callId,
+							actionId: 'joinLivechat',
+							type: 'button',
+							text: {
+								type: 'plain_text',
+								text: TAPi18n.__('Join_call'),
+								emoji: true,
+							},
+							url,
+						},
+					],
+				},
+			],
+		});
+	}
+
+	private buildMessageBlock(text: string): MessageSurfaceLayout[number] {
+		return {
+			type: 'section',
+			appId: 'videoconf-core',
+			text: {
+				type: 'plain_text',
+				text,
+				emoji: true,
+			},
+		};
+	}
+
+	private buildJoinButtonBlock(callId: string, title = ''): MessageSurfaceLayout[number] {
+		return {
+			type: 'actions',
+			appId: 'videoconf-core',
+			elements: [
+				{
+					appId: 'videoconf-core',
+					blockId: callId,
+					actionId: 'join',
+					value: title,
+					type: 'button',
+					text: {
+						type: 'plain_text',
+						text: TAPi18n.__('Join_call'),
+						emoji: true,
+					},
+				},
+			],
+		};
+	}
+
+	private async startDirect(
+		providerName: string,
+		user: IUser,
+		{ _id: rid, uids }: AtLeast<IRoom, '_id' | 'uids'>,
+		extraData?: Partial<IDirectVideoConference>,
+	): Promise<DirectCallInstructions> {
+		const callee = uids?.filter((uid) => uid !== user._id).pop();
+		if (!callee) {
+			// Are you trying to call yourself?
+			throw new Error('invalid-call-target');
+		}
+
+		const callId = await VideoConferenceModel.createDirect({
+			...extraData,
+			rid,
+			createdBy: {
+				_id: user._id,
+				name: user.name,
+				username: user.username,
+			},
+			providerName,
+		});
+		const call = (await this.getUnfiltered(callId)) as IDirectVideoConference | null;
+		if (!call) {
+			throw new Error('failed-to-create-direct-call');
+		}
+
+		const url = await this.generateNewUrl(call);
+		VideoConferenceModel.setUrlById(callId, url);
+
+		const messageId = await this.createDirectCallMessage(call, user);
+		VideoConferenceModel.setMessageById(callId, 'started', messageId);
+
+		return {
+			type: 'direct',
+			callId,
+			callee,
+		};
+	}
+
+	private async notifyUsersOfRoom(rid: IRoom['_id'], uid: IUser['_id'], eventName: string, ...args: any[]): Promise<void> {
+		const subscriptions = Subscriptions.findByRoomIdAndNotUserId(rid, uid, {
+			projection: { 'u._id': 1, '_id': 0 },
+		});
+
+		await subscriptions.forEach(async (subscription) => Notifications.notifyUser(subscription.u._id, eventName, ...args));
+	}
+
+	private async startGroup(
+		providerName: string,
+		user: IUser,
+		rid: IRoom['_id'],
+		title: string,
+		extraData?: Partial<IGroupVideoConference>,
+	): Promise<ConferenceInstructions> {
+		const callId = await VideoConferenceModel.createGroup({
+			...extraData,
+			rid,
+			title,
+			createdBy: {
+				_id: user._id,
+				name: user.name,
+				username: user.username,
+			},
+			providerName,
+		});
+		const call = await this.getUnfiltered(callId);
+		if (!call) {
+			throw new Error('failed-to-create-group-call');
+		}
+
+		const url = await this.generateNewUrl(call);
+		VideoConferenceModel.setUrlById(callId, url);
+
+		call.url = url;
+
+		const messageId = await this.createGroupCallMessage(rid, user, callId, title);
+		VideoConferenceModel.setMessageById(callId, 'started', messageId);
+
+		if (call.ringing) {
+			await this.notifyUsersOfRoom(rid, user._id, 'video-conference.ring', { callId, title, createdBy: call.createdBy, providerName });
+		}
+
+		return {
+			type: 'videoconference',
+			callId,
+			rid,
+		};
+	}
+
+	private async startLivechat(providerName: string, user: IUser, rid: IRoom['_id']): Promise<LivechatInstructions> {
+		const callId = await VideoConferenceModel.createLivechat({
+			rid,
+			createdBy: {
+				_id: user._id,
+				name: user.name,
+				username: user.username,
+			},
+			providerName,
+		});
+
+		const call = await this.getUnfiltered(callId);
+		if (!call) {
+			throw new Error('failed-to-create-livechat-call');
+		}
+
+		const joinUrl = await this.getUrl(call);
+		const messageId = await this.createLivechatMessage(rid, user, callId, joinUrl);
+		await VideoConferenceModel.setMessageById(callId, 'started', messageId);
+
+		return {
+			type: 'livechat',
+			callId,
+		};
+	}
+
+	private async joinCall(
+		call: VideoConference,
+		user: AtLeast<IUser, '_id' | 'username' | 'name' | 'avatarETag'> | undefined,
+		options: VideoConferenceJoinOptions,
+	): Promise<string> {
+		await callbacks.runAsync('onJoinVideoConference', call._id, user?._id);
+
+		return this.getUrl(call, user, options);
+	}
+
+	private async getProviderManager(): Promise<AppVideoConfProviderManager> {
+		if (!Apps?.isLoaded()) {
+			throw new Error('apps-engine-not-loaded');
+		}
+
+		const manager = Apps.getManager()?.getVideoConfProviderManager();
+		if (!manager) {
+			throw new Error(availabilityErrors.NO_APP);
+		}
+
+		return manager;
+	}
+
+	private async getRoomName(rid: string): Promise<string> {
+		const room = await Rooms.findOneById<Pick<IRoom, '_id' | 'name' | 'fname'>>(rid, { projection: { name: 1, fname: 1 } });
+
+		return room?.fname || room?.name || rid;
+	}
+
+	private async generateNewUrl(call: VideoConference): Promise<string> {
+		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
+			throw new Error('video-conf-provider-unavailable');
+		}
+
+		const title = isGroupVideoConference(call) ? call.title || (await this.getRoomName(call.rid)) : '';
+
+		return (await this.getProviderManager())
+			.generateUrl(call.providerName, {
+				_id: call._id,
+				type: call.type,
+				rid: call.rid,
+				createdBy: call.createdBy as Required<VideoConference['createdBy']>,
+				title,
+				providerData: call.providerData,
+			})
+			.catch((e) => {
+				throw new Error(e);
+			});
+	}
+
+	private async getCallTitleForUser(call: VideoConference, userId?: IUser['_id']): Promise<string> {
+		if (call.type === 'videoconference' && call.title) {
+			return call.title;
+		}
+
+		if (userId) {
+			const subscription = await Subscriptions.findOneByRoomIdAndUserId(call.rid, userId, { projection: { fname: 1, name: 1 } });
+			if (subscription) {
+				return subscription.fname || subscription.name;
+			}
+		}
+
+		const room = await Rooms.findOneById(call.rid);
+		return room?.fname || room?.name || 'Rocket.Chat';
+	}
+
+	private async getCallTitle(call: VideoConference): Promise<string> {
+		if (call.type === 'videoconference') {
+			if (call.title) {
+				return call.title;
+			}
+		}
+
+		const room = await Rooms.findOneById(call.rid);
+		if (room) {
+			if (room.t === 'd') {
+				if (room.usernames?.length) {
+					return room.usernames.join(', ');
+				}
+			} else if (room.fname) {
+				return room.fname;
+			} else if (room.name) {
+				return room.name;
+			}
+		}
+
+		return 'Rocket.Chat';
+	}
+
+	private async getUrl(
+		call: VideoConference,
+		user?: AtLeast<IUser, '_id' | 'username' | 'name'>,
+		options: VideoConferenceJoinOptions = {},
+	): Promise<string> {
+		if (!videoConfProviders.isProviderAvailable(call.providerName)) {
+			throw new Error('video-conf-provider-unavailable');
+		}
+
+		if (!call.url) {
+			call.url = await this.generateNewUrl(call);
+			VideoConferenceModel.setUrlById(call._id, call.url);
+		}
+
+		const callData = {
+			_id: call._id,
+			type: call.type,
+			rid: call.rid,
+			url: call.url,
+			createdBy: call.createdBy as Required<VideoConference['createdBy']>,
+			providerData: {
+				...(call.providerData || {}),
+				...{ customCallTitle: await this.getCallTitleForUser(call, user?._id) },
+			},
+			title: await this.getCallTitle(call),
+		};
+
+		const userData = user && {
+			_id: user._id,
+			username: user.username as string,
+			name: user.name as string,
+		};
+
+		return (await this.getProviderManager()).customizeUrl(call.providerName, callData, userData, options).catch((e) => {
+			throw new Error(e);
+		});
+	}
+
+	private async addUserToCall(
+		call: Optional<VideoConference, 'providerData'>,
+		{ _id, username, name, avatarETag, ts }: AtLeast<IUser, '_id' | 'username' | 'name' | 'avatarETag'> & { ts?: Date },
+	): Promise<void> {
+		if (call.users.find((user) => user._id === _id)) {
+			return;
+		}
+
+		await VideoConferenceModel.addUserById(call._id, { _id, username, name, avatarETag, ts });
+
+		switch (call.type) {
+			case 'videoconference':
+				return this.updateGroupCallMessage(call as IGroupVideoConference, { _id, username, name });
+			case 'direct':
+				return this.updateDirectCall(call as IDirectVideoConference, _id);
+		}
+	}
+
+	private async addAnonymousUser(call: Optional<IGroupVideoConference, 'providerData'>): Promise<void> {
+		await VideoConferenceModel.increaseAnonymousCount(call._id);
+
+		if (!call.messages.started) {
+			return;
+		}
+
+		const imageUrl = getURL(`/avatar/@a`, { cdn: false, full: true });
+		return this.addAvatarToCallMessage(call.messages.started, imageUrl, TAPi18n.__('Anonymous'));
+	}
+
+	private async addAvatarToCallMessage(messageId: IMessage['_id'], imageUrl: string, altText: string): Promise<void> {
+		const message = await Messages.findOneById<Pick<IMessage, '_id' | 'blocks'>>(messageId, { projection: { blocks: 1 } });
+		if (!message) {
+			return;
+		}
+
+		const blocks = message.blocks || [];
+
+		const avatarsBlock = (blocks.find((block) => block.type === 'context') || { type: 'context', elements: [] }) as ContextBlock;
+		if (!blocks.includes(avatarsBlock)) {
+			blocks.push(avatarsBlock);
+		}
+
+		if (avatarsBlock.elements.find((el) => el.type === 'image' && el.imageUrl === imageUrl)) {
+			return;
+		}
+
+		avatarsBlock.elements = [
+			...avatarsBlock.elements,
+			{
+				type: 'image',
+				imageUrl,
+				altText,
+			},
+		];
+
+		await Messages.setBlocksById(message._id, blocks);
+	}
+
+	private async updateGroupCallMessage(
+		call: Optional<IGroupVideoConference, 'providerData'>,
+		user: Pick<IUser, '_id' | 'username' | 'name'>,
+	): Promise<void> {
+		if (!call.messages.started || !user.username) {
+			return;
+		}
+		const imageUrl = getURL(`/avatar/${user.username}`, { cdn: false, full: true });
+
+		return this.addAvatarToCallMessage(call.messages.started, imageUrl, user.name || user.username);
+	}
+
+	private async updateDirectCall(call: IDirectVideoConference, newUserId: IUser['_id']): Promise<void> {
+		// 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 });
+			if (newUserId !== call.createdBy._id) {
+				Notifications.notifyUser(newUserId, 'video-conference.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);
+			}
+		}
+
+		if (call.status !== VideoConferenceStatus.CALLING) {
+			return;
+		}
+
+		await VideoConferenceModel.setStatusById(call._id, VideoConferenceStatus.STARTED);
+
+		if (call.messages.started) {
+			const text = TAPi18n.__('video_direct_started', { username: call.createdBy.username || '' });
+			await Messages.setBlocksById(call.messages.started, [this.buildMessageBlock(text), this.buildJoinButtonBlock(call._id)]);
+		}
+	}
+}
diff --git a/apps/meteor/server/settings/index.ts b/apps/meteor/server/settings/index.ts
index 4a5af30485e1de6637d37851c9bcc2cef5d6abd9..8b75e04da050f66e4dcedb887a22a282b229a680 100644
--- a/apps/meteor/server/settings/index.ts
+++ b/apps/meteor/server/settings/index.ts
@@ -1 +1,2 @@
 import './ldap';
+import './video-conference';
diff --git a/apps/meteor/server/settings/video-conference.ts b/apps/meteor/server/settings/video-conference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae5fc03032d4b60be112634de02bc82c86c69c34
--- /dev/null
+++ b/apps/meteor/server/settings/video-conference.ts
@@ -0,0 +1,19 @@
+import { settingsRegistry } from '../../app/settings/server';
+
+settingsRegistry.addGroup('Video_Conference', function () {
+	this.add('VideoConf_Default_Provider', '', {
+		type: 'lookup',
+		lookupEndpoint: 'v1/video-conference.providers',
+		public: true,
+	});
+
+	// #ToDo: Those should probably be handled by the apps themselves
+	this.add('Jitsi_Click_To_Join_Count', 0, {
+		type: 'int',
+		hidden: true,
+	});
+	this.add('Jitsi_Start_SlashCommands_Count', 0, {
+		type: 'int',
+		hidden: true,
+	});
+});
diff --git a/apps/meteor/server/startup/coreApps.ts b/apps/meteor/server/startup/coreApps.ts
index 369ea2f89c2243ad69e5a473bf5291e3b01547d7..4aca84d8481170e48f5dc3efba03da27f2c3b835 100644
--- a/apps/meteor/server/startup/coreApps.ts
+++ b/apps/meteor/server/startup/coreApps.ts
@@ -1,6 +1,8 @@
 import { Nps } from '../modules/core-apps/nps.module';
 import { BannerModule } from '../modules/core-apps/banner.module';
+import { VideoConfModule } from '../modules/core-apps/videoconf.module';
 import { registerCoreApp } from '../services/uikit-core-app/service';
 
 registerCoreApp(new Nps());
 registerCoreApp(new BannerModule());
+registerCoreApp(new VideoConfModule());
diff --git a/apps/meteor/server/startup/cron.js b/apps/meteor/server/startup/cron.js
index 24bf029572c97e4f595652bd56d4596d465a9cf7..d91fd0180e1653488465c934a4a410cc1e73a668 100644
--- a/apps/meteor/server/startup/cron.js
+++ b/apps/meteor/server/startup/cron.js
@@ -6,6 +6,7 @@ import { oembedCron } from '../cron/oembed';
 import { statsCron } from '../cron/statistics';
 import { npsCron } from '../cron/nps';
 import { federationCron } from '../cron/federation';
+import { videoConferencesCron } from '../cron/videoConferences';
 
 const logger = new Logger('SyncedCron');
 
@@ -21,6 +22,7 @@ Meteor.defer(function () {
 	statsCron(SyncedCron, logger);
 	npsCron(SyncedCron);
 	federationCron(SyncedCron);
+	videoConferencesCron(SyncedCron);
 
 	SyncedCron.start();
 });
diff --git a/apps/meteor/server/startup/migrations/index.ts b/apps/meteor/server/startup/migrations/index.ts
index 6466e0d9ff279653e69a038517df20d32db02027..a58dd2b76127b53b5fb173896cd91252fc1817f5 100644
--- a/apps/meteor/server/startup/migrations/index.ts
+++ b/apps/meteor/server/startup/migrations/index.ts
@@ -98,4 +98,5 @@ import './v271';
 import './v272';
 import './v273';
 import './v274';
+import './v275';
 import './xrun';
diff --git a/apps/meteor/server/startup/migrations/v275.ts b/apps/meteor/server/startup/migrations/v275.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c69a8344f3e4904fd9be07a732b42b6e55b73a5
--- /dev/null
+++ b/apps/meteor/server/startup/migrations/v275.ts
@@ -0,0 +1,40 @@
+import { Settings } from '@rocket.chat/models';
+
+import { addMigration } from '../../lib/migrations';
+
+addMigration({
+	version: 275,
+	async up() {
+		await Settings.deleteMany({
+			_id: {
+				$in: [
+					'bigbluebutton_Enabled',
+					'bigbluebutton_server',
+					'bigbluebutton_sharedSecret',
+					'bigbluebutton_Open_New_Window',
+					'bigbluebutton_enable_d',
+					'bigbluebutton_enable_p',
+					'bigbluebutton_enable_c',
+					'bigbluebutton_enable_teams',
+					'Jitsi_Enabled',
+					'Jitsi_Domain',
+					'Jitsi_URL_Room_Prefix',
+					'Jitsi_URL_Room_Suffix',
+					'Jitsi_URL_Room_Hash',
+					'Jitsi_SSL',
+					'Jitsi_Open_New_Window',
+					'Jitsi_Enable_Channels',
+					'Jitsi_Enable_Teams',
+					'Jitsi_Chrome_Extension',
+					'Jitsi_Enabled_TokenAuth',
+					'Jitsi_Application_ID',
+					'Jitsi_Application_Secret',
+					'Jitsi_Limit_Token_To_Room',
+					'Video Conference',
+					'Jitsi',
+					'BigBlueButton',
+				],
+			},
+		});
+	},
+});
diff --git a/apps/meteor/tests/data/apps/apps-data.js b/apps/meteor/tests/data/apps/apps-data.js
index 560d076c9312575b4b6c597f0d806b36feb2ae2c..456cc4423a70fc61129c5a175776ca7118d9f343 100644
--- a/apps/meteor/tests/data/apps/apps-data.js
+++ b/apps/meteor/tests/data/apps/apps-data.js
@@ -1,4 +1,4 @@
-export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.2.zip?raw=true';
+export const APP_URL = 'https://github.com/RocketChat/Apps.RocketChat.Tester/blob/master/dist/appsrocketchattester_0.0.4.zip?raw=true';
 export const APP_NAME = 'Apps.RocketChat.Tester';
 export const APP_USERNAME = 'appsrocketchattester.bot';
 export const apps = (path = '') => `/api/apps${path}`;
diff --git a/apps/meteor/tests/e2e/04-main-elements-render.spec.ts b/apps/meteor/tests/e2e/04-main-elements-render.spec.ts
index 02c12b53632e90ec2eb10e98322de4ac0e9feb67..93e1fb71453668ae7114562dfe2cb3f2bf126ac0 100644
--- a/apps/meteor/tests/e2e/04-main-elements-render.spec.ts
+++ b/apps/meteor/tests/e2e/04-main-elements-render.spec.ts
@@ -173,8 +173,12 @@ test.describe('[Main Elements Render]', function () {
 			});
 
 			test('expect to show tab files content', async () => {
+				await flexTab.doOpenMoreOptionMenu();
 				await flexTab.btnTabFiles.click();
+
 				await expect(flexTab.contentTabFiles).toBeVisible();
+
+				await flexTab.doOpenMoreOptionMenu();
 				await flexTab.btnTabFiles.click();
 			});
 
diff --git a/apps/meteor/tests/e2e/pageobjects/FlexTab.ts b/apps/meteor/tests/e2e/pageobjects/FlexTab.ts
index 5a167704baf2bb9cdd3244b71d135e2570a8fa68..8ee728628a7e99761e89b2e6ebb1cb589b257250 100644
--- a/apps/meteor/tests/e2e/pageobjects/FlexTab.ts
+++ b/apps/meteor/tests/e2e/pageobjects/FlexTab.ts
@@ -36,7 +36,7 @@ export class FlexTab extends BasePage {
 	}
 
 	get btnTabFiles(): Locator {
-		return this.page.locator('[data-qa-id=ToolBoxAction-clip]');
+		return this.page.locator('//*[contains(@class, "rcx-option__content") and contains(text(), "Files")]');
 	}
 
 	get btnTabMentions(): Locator {
diff --git a/apps/meteor/tests/end-to-end/api/15-video-conference.js b/apps/meteor/tests/end-to-end/api/15-video-conference.js
deleted file mode 100644
index 95fa8f30e6814b41f29ca85c35a8956ff3743818..0000000000000000000000000000000000000000
--- a/apps/meteor/tests/end-to-end/api/15-video-conference.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { expect } from 'chai';
-
-import { getCredentials, api, request, credentials } from '../../data/api-data.js';
-
-describe('[Video Conference]', function () {
-	this.retries(0);
-
-	before((done) => getCredentials(done));
-
-	describe('POST [/video-conference/jitsi.update-timeout]', () => {
-		it('should return an error when call the endpoint without roomId required parameter', (done) => {
-			request
-				.post(api('video-conference/jitsi.update-timeout'))
-				.set(credentials)
-				.expect(400)
-				.expect((res) => {
-					expect(res.body).to.have.property('success', false);
-					expect(res.body.error).to.be.equal('The "roomId" parameter is required!');
-				})
-				.end(done);
-		});
-		it('should return an error when call the endpoint withan invalid roomId', (done) => {
-			request
-				.post(api('video-conference/jitsi.update-timeout'))
-				.set(credentials)
-				.send({
-					roomId: 'invalid-id',
-				})
-				.expect(400)
-				.expect((res) => {
-					expect(res.body).to.have.property('success', false);
-					expect(res.body.error).to.be.equal('Room does not exist!');
-				})
-				.end(done);
-		});
-		it('should return success when parameters are correct', (done) => {
-			request
-				.post(api('video-conference/jitsi.update-timeout'))
-				.set(credentials)
-				.send({
-					roomId: 'GENERAL',
-				})
-				.expect(200)
-				.expect((res) => {
-					expect(res.body).to.have.property('success', true);
-					expect(res.body).to.have.property('jitsiTimeout');
-				})
-				.end(done);
-		});
-	});
-});
diff --git a/apps/meteor/tests/end-to-end/apps/00-installation.js b/apps/meteor/tests/end-to-end/apps/00-installation.js
index a58e4fee2710bb338b92d7e1dfe5c7e69fa82773..292b51e6114ac9fd8a5b6f097003e574d0960dc9 100644
--- a/apps/meteor/tests/end-to-end/apps/00-installation.js
+++ b/apps/meteor/tests/end-to-end/apps/00-installation.js
@@ -107,5 +107,21 @@ describe('Apps - Installation', function () {
 					.end(done);
 			});
 		});
+		describe('Video Conf Provider registration', () => {
+			it('should have created two video conf provider successfully', (done) => {
+				request
+					.get(api('video-conference.providers'))
+					.set(credentials)
+					.expect('Content-Type', 'application/json')
+					.expect(200)
+					.expect((res) => {
+						expect(res.body).to.have.a.property('success', true);
+						expect(res.body).to.have.a.property('data').that.is.an('array').with.lengthOf(2);
+						expect(res.body.data[0]).to.be.an('object').with.a.property('key').equal('test');
+						expect(res.body.data[1]).to.be.an('object').with.a.property('key').equal('unconfigured');
+					})
+					.end(done);
+			});
+		});
 	});
 });
diff --git a/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts
new file mode 100644
index 0000000000000000000000000000000000000000..25971874f6c5dc2902d69baccc7cd1cbfd53c3f3
--- /dev/null
+++ b/apps/meteor/tests/end-to-end/apps/05-video-conferences.ts
@@ -0,0 +1,420 @@
+import { expect } from 'chai';
+import type { Response } from 'supertest';
+
+import { getCredentials, request, api, credentials } from '../../data/api-data.js';
+import { cleanupApps, installTestApp } from '../../data/apps/helper.js';
+import { createRoom } from '../../data/rooms.helper';
+import { updateSetting } from '../../data/permissions.helper';
+import { adminUsername } from '../../data/user';
+
+describe('Apps - Video Conferences', function () {
+	this.retries(0);
+
+	before((done) => getCredentials(done));
+
+	const roomName = `apps-e2etest-room-${Date.now()}-videoconf`;
+	let roomId: string | undefined;
+
+	before((done) => {
+		createRoom({
+			type: 'p',
+			name: roomName,
+		} as any).end((_err: unknown, createdRoom: any) => {
+			roomId = createdRoom.body.group._id;
+			done();
+		});
+	});
+
+	describe('[With No App]', () => {
+		before(async () => {
+			await cleanupApps();
+		});
+
+		it('should fail to load capabilities', (done) => {
+			request
+				.get(api('video-conference.capabilities'))
+				.set(credentials)
+				.send()
+				.expect(400)
+				.expect((res: Response) => {
+					expect(res.body.success).to.be.equal(false);
+					expect(res.body.error).to.be.equal('no-videoconf-provider-app');
+				})
+				.end(done);
+		});
+
+		it('should fail to start a call', (done) => {
+			request
+				.post(api('video-conference.start'))
+				.set(credentials)
+				.send({
+					roomId,
+				})
+				.expect(400)
+				.expect((res: Response) => {
+					expect(res.body.success).to.be.equal(false);
+					expect(res.body.error).to.be.equal('no-videoconf-provider-app');
+				})
+				.end(done);
+		});
+	});
+
+	describe('[With Test App]', () => {
+		before(async () => {
+			await cleanupApps();
+			await installTestApp();
+		});
+
+		describe('[/video-conference.capabilities]', () => {
+			it('should fail to load capabilities with no default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', '').then(() => {
+					request
+						.get(api('video-conference.capabilities'))
+						.set(credentials)
+						.send()
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('no-active-video-conf-provider');
+						})
+						.end(done);
+				});
+			});
+
+			it('should fail to load capabilities with an invalid default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'invalid').then(() => {
+					request
+						.get(api('video-conference.capabilities'))
+						.set(credentials)
+						.send()
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('no-active-video-conf-provider');
+						})
+						.end(done);
+				});
+			});
+
+			it('should fail to load capabilities with a default provider lacking configuration', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'unconfigured').then(() => {
+					request
+						.get(api('video-conference.capabilities'))
+						.set(credentials)
+						.send()
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('video-conf-provider-not-configured');
+						})
+						.end(done);
+				});
+			});
+
+			it('should load capabilities successfully with a valid default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.get(api('video-conference.capabilities'))
+						.set(credentials)
+						.send()
+						.expect(200)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(true);
+							expect(res.body.providerName).to.be.equal('test');
+							expect(res.body.capabilities).to.be.an('object');
+							expect(res.body.capabilities).to.have.a.property('mic').equal(true);
+							expect(res.body.capabilities).to.have.a.property('cam').equal(false);
+							expect(res.body.capabilities).to.have.a.property('title').equal(true);
+						})
+						.end(done);
+				});
+			});
+		});
+
+		describe('[/video-conference.start]', () => {
+			it('should fail to start a call with no default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', '').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('no-active-video-conf-provider');
+						})
+						.end(done);
+				});
+			});
+
+			it('should fail to start a call with an invalid default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'invalid').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('no-active-video-conf-provider');
+						})
+						.end(done);
+				});
+			});
+
+			it('should fail to start a call with a default provider lacking configuration', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'unconfigured').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.expect(400)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(false);
+							expect(res.body.error).to.be.equal('video-conf-provider-not-configured');
+						})
+						.end(done);
+				});
+			});
+
+			it('should start a call successfully with a valid default provider', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.expect(200)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(true);
+							expect(res.body.data).to.be.an('object');
+							expect(res.body.data).to.have.a.property('providerName').equal('test');
+							expect(res.body.data).to.have.a.property('type').equal('videoconference');
+							expect(res.body.data).to.have.a.property('callId').that.is.a('string');
+							// expect(res.body.data).to.have.a.property('rid').equal(roomId);
+							// expect(res.body.data).to.have.a.property('createdBy').that.is.an('object').with.a.property('username').equal(adminUsername);
+						})
+						.end(done);
+				});
+			});
+
+			it('should start a call successfully when sending a title', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+							title: 'Conference Title',
+						})
+						.expect(200)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(true);
+							expect(res.body.data).to.be.an('object');
+							expect(res.body.data).to.have.a.property('providerName').equal('test');
+							expect(res.body.data).to.have.a.property('type').equal('videoconference');
+							expect(res.body.data).to.have.a.property('callId').that.is.a('string');
+						})
+						.end(done);
+				});
+			});
+
+			it('should start a call successfully when sending the allowRinging attribute', (done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+							title: 'Conference Title',
+							allowRinging: true,
+						})
+						.expect(200)
+						.expect((res: Response) => {
+							expect(res.body.success).to.be.equal(true);
+							expect(res.body.data).to.be.an('object');
+							expect(res.body.data).to.have.a.property('providerName').equal('test');
+							expect(res.body.data).to.have.a.property('type').equal('videoconference');
+							expect(res.body.data).to.have.a.property('callId').that.is.a('string');
+						})
+						.end(done);
+				});
+			});
+		});
+
+		describe('[/video-conference.join]', () => {
+			let callId: string | undefined;
+
+			before((done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.then((res: Response) => {
+							callId = res.body.data.callId;
+							done();
+						});
+				});
+			});
+
+			it('should join a videoconference successfully', (done) => {
+				request
+					.post(api('video-conference.join'))
+					.set(credentials)
+					.send({
+						callId,
+					})
+					.expect(200)
+					.expect((res: Response) => {
+						expect(res.body.success).to.be.equal(true);
+						expect(res.body).to.have.a.property('providerName').equal('test');
+						const userId = credentials['X-User-Id'];
+						expect(res.body).to.have.a.property('url').equal(`test/videoconference/${callId}/${roomName}/${userId}`);
+					})
+					.end(done);
+			});
+
+			it('should join a videoconference using the specified state', (done) => {
+				request
+					.post(api('video-conference.join'))
+					.set(credentials)
+					.send({
+						callId,
+						state: {
+							mic: true,
+							cam: false,
+						},
+					})
+					.expect(200)
+					.expect((res: Response) => {
+						expect(res.body.success).to.be.equal(true);
+						expect(res.body).to.have.a.property('providerName').equal('test');
+						const userId = credentials['X-User-Id'];
+						expect(res.body).to.have.a.property('url').equal(`test/videoconference/${callId}/${roomName}/${userId}/mic`);
+					})
+					.end(done);
+			});
+		});
+
+		describe('[/video-conference.info]', () => {
+			let callId: string | undefined;
+
+			before((done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.then((res: Response) => {
+							callId = res.body.data.callId;
+							done();
+						});
+				});
+			});
+
+			it('should load the video conference data successfully', (done) => {
+				request
+					.get(api('video-conference.info'))
+					.set(credentials)
+					.query({
+						callId,
+					})
+					.expect(200)
+					.expect((res: Response) => {
+						expect(res.body.success).to.be.equal(true);
+						expect(res.body).to.have.a.property('providerName').equal('test');
+						expect(res.body).to.not.have.a.property('providerData');
+						expect(res.body).to.have.a.property('_id').equal(callId);
+						expect(res.body).to.have.a.property('url').equal(`test/videoconference/${callId}/${roomName}`);
+						expect(res.body).to.have.a.property('type').equal('videoconference');
+						expect(res.body).to.have.a.property('rid').equal(roomId);
+						expect(res.body).to.have.a.property('users').that.is.an('array').with.lengthOf(0);
+						expect(res.body).to.have.a.property('status').equal(1);
+						expect(res.body).to.have.a.property('title').equal(roomName);
+						expect(res.body).to.have.a.property('messages').that.is.an('object');
+						expect(res.body.messages).to.have.a.property('started').that.is.a('string');
+						expect(res.body).to.have.a.property('createdBy').that.is.an('object');
+						expect(res.body.createdBy).to.have.a.property('_id').equal(credentials['X-User-Id']);
+						expect(res.body.createdBy).to.have.a.property('username').equal(adminUsername);
+					})
+					.end(done);
+			});
+		});
+
+		describe('[/video-conference.list]', () => {
+			let callId1: string | undefined;
+			let callId2: string | undefined;
+
+			before((done) => {
+				updateSetting('VideoConf_Default_Provider', 'test').then(() => {
+					request
+						.post(api('video-conference.start'))
+						.set(credentials)
+						.send({
+							roomId,
+						})
+						.then((res: Response) => {
+							callId1 = res.body.data.callId;
+
+							request
+								.post(api('video-conference.start'))
+								.set(credentials)
+								.send({
+									roomId,
+								})
+								.then((res: Response) => {
+									callId2 = res.body.data.callId;
+									done();
+								});
+						});
+				});
+			});
+
+			it('should load the list of video conferences sorted by new', (done) => {
+				request
+					.get(api('video-conference.list'))
+					.set(credentials)
+					.query({
+						roomId,
+					})
+					.expect(200)
+					.expect((res: Response) => {
+						expect(res.body.success).to.be.equal(true);
+						expect(res.body).to.have.a.property('count').that.is.greaterThanOrEqual(2);
+						expect(res.body).to.have.a.property('data').that.is.an('array').with.lengthOf(res.body.count);
+
+						const call2 = res.body.data[0];
+						const call1 = res.body.data[1];
+
+						expect(call1).to.have.a.property('_id').equal(callId1);
+						expect(call1).to.have.a.property('url').equal(`test/videoconference/${callId1}/${roomName}`);
+						expect(call1).to.have.a.property('type').equal('videoconference');
+						expect(call1).to.have.a.property('rid').equal(roomId);
+						expect(call1).to.have.a.property('users').that.is.an('array').with.lengthOf(0);
+						expect(call1).to.have.a.property('status').equal(1);
+						expect(call1).to.have.a.property('title').equal(roomName);
+						expect(call1).to.have.a.property('messages').that.is.an('object');
+						expect(call1.messages).to.have.a.property('started').that.is.a('string');
+						expect(call1).to.have.a.property('createdBy').that.is.an('object');
+						expect(call1.createdBy).to.have.a.property('_id').equal(credentials['X-User-Id']);
+						expect(call1.createdBy).to.have.a.property('username').equal(adminUsername);
+
+						expect(call2).to.have.a.property('_id').equal(callId2);
+					})
+					.end(done);
+			});
+		});
+	});
+});
diff --git a/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfCancelProps.spec.ts b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfCancelProps.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e6c40d6ecfac48ff8de3a8f6abff54875236354a
--- /dev/null
+++ b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfCancelProps.spec.ts
@@ -0,0 +1,40 @@
+import { assert } from 'chai';
+import { isVideoConfCancelProps } from '@rocket.chat/rest-typings';
+
+describe('VideoConfCancelProps (definition/rest/v1)', () => {
+	describe('isVideoConfCancelProps', () => {
+		it('should be a function', () => {
+			assert.isFunction(isVideoConfCancelProps);
+		});
+		it('should return false when provided anything that is not an VideoConfCancelProps', () => {
+			assert.isFalse(isVideoConfCancelProps(undefined));
+			assert.isFalse(isVideoConfCancelProps(null));
+			assert.isFalse(isVideoConfCancelProps(''));
+			assert.isFalse(isVideoConfCancelProps(123));
+			assert.isFalse(isVideoConfCancelProps({}));
+			assert.isFalse(isVideoConfCancelProps([]));
+			assert.isFalse(isVideoConfCancelProps(new Date()));
+			assert.isFalse(isVideoConfCancelProps(new Error()));
+		});
+		it('should return false if callId is not provided to VideoConfCancelProps', () => {
+			assert.isFalse(isVideoConfCancelProps({}));
+		});
+
+		it('should accept a callId with nothing else', () => {
+			assert.isTrue(
+				isVideoConfCancelProps({
+					callId: 'callId',
+				}),
+			);
+		});
+
+		it('should return false when extra parameters are provided to VideoConfCancelProps', () => {
+			assert.isFalse(
+				isVideoConfCancelProps({
+					callId: 'callId',
+					extra: 'extra',
+				}),
+			);
+		});
+	});
+});
diff --git a/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfInfoProps.spec.ts b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfInfoProps.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d122c2cb53cea845ce82f6ed20fb0e00740fa2c1
--- /dev/null
+++ b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfInfoProps.spec.ts
@@ -0,0 +1,40 @@
+import { assert } from 'chai';
+import { isVideoConfInfoProps } from '@rocket.chat/rest-typings';
+
+describe('VideoConfInfoProps (definition/rest/v1)', () => {
+	describe('isVideoConfInfoProps', () => {
+		it('should be a function', () => {
+			assert.isFunction(isVideoConfInfoProps);
+		});
+		it('should return false when provided anything that is not an VideoConfInfoProps', () => {
+			assert.isFalse(isVideoConfInfoProps(undefined));
+			assert.isFalse(isVideoConfInfoProps(null));
+			assert.isFalse(isVideoConfInfoProps(''));
+			assert.isFalse(isVideoConfInfoProps(123));
+			assert.isFalse(isVideoConfInfoProps({}));
+			assert.isFalse(isVideoConfInfoProps([]));
+			assert.isFalse(isVideoConfInfoProps(new Date()));
+			assert.isFalse(isVideoConfInfoProps(new Error()));
+		});
+		it('should return false if callId is not provided to VideoConfInfoProps', () => {
+			assert.isFalse(isVideoConfInfoProps({}));
+		});
+
+		it('should accept a callId with nothing else', () => {
+			assert.isTrue(
+				isVideoConfInfoProps({
+					callId: 'callId',
+				}),
+			);
+		});
+
+		it('should return false when extra parameters are provided to VideoConfInfoProps', () => {
+			assert.isFalse(
+				isVideoConfInfoProps({
+					callId: 'callId',
+					extra: 'extra',
+				}),
+			);
+		});
+	});
+});
diff --git a/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfJoinProps.spec.ts b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfJoinProps.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8618052ea2978b236ec93bba555da8862a328f46
--- /dev/null
+++ b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfJoinProps.spec.ts
@@ -0,0 +1,45 @@
+import { assert } from 'chai';
+import { isVideoConfJoinProps } from '@rocket.chat/rest-typings';
+
+describe('VideoConfJoinProps (definition/rest/v1)', () => {
+	describe('isVideoConfJoinProps', () => {
+		it('should be a function', () => {
+			assert.isFunction(isVideoConfJoinProps);
+		});
+		it('should return false when provided anything that is not an VideoConfJoinProps', () => {
+			assert.isFalse(isVideoConfJoinProps(undefined));
+			assert.isFalse(isVideoConfJoinProps(null));
+			assert.isFalse(isVideoConfJoinProps(''));
+			assert.isFalse(isVideoConfJoinProps(123));
+			assert.isFalse(isVideoConfJoinProps({}));
+			assert.isFalse(isVideoConfJoinProps([]));
+			assert.isFalse(isVideoConfJoinProps(new Date()));
+			assert.isFalse(isVideoConfJoinProps(new Error()));
+		});
+		it('should return false if callId is not provided to VideoConfJoinProps', () => {
+			assert.isFalse(isVideoConfJoinProps({}));
+		});
+
+		it('should accept a callId with nothing else', () => {
+			assert.isTrue(
+				isVideoConfJoinProps({
+					callId: 'callId',
+				}),
+			);
+		});
+
+		it('should return false when provided with an invalid state', () => {
+			assert.isFalse(isVideoConfJoinProps({ callId: 'callId', state: 123 }));
+			assert.isFalse(isVideoConfJoinProps({ callId: 'callId', state: [] }));
+		});
+
+		it('should return false when extra parameters are provided to VideoConfJoinProps', () => {
+			assert.isFalse(
+				isVideoConfJoinProps({
+					callId: 'callId',
+					extra: 'extra',
+				}),
+			);
+		});
+	});
+});
diff --git a/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfListProps.spec.ts b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfListProps.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0a12106a3e4172f0b42d50fda220a09c6816f33a
--- /dev/null
+++ b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfListProps.spec.ts
@@ -0,0 +1,50 @@
+import { assert } from 'chai';
+import { isVideoConfListProps } from '@rocket.chat/rest-typings';
+
+describe('VideoConfListProps (definition/rest/v1)', () => {
+	describe('isVideoConfListProps', () => {
+		it('should be a function', () => {
+			assert.isFunction(isVideoConfListProps);
+		});
+		it('should return false when provided anything that is not an VideoConfListProps', () => {
+			assert.isFalse(isVideoConfListProps(undefined));
+			assert.isFalse(isVideoConfListProps(null));
+			assert.isFalse(isVideoConfListProps(''));
+			assert.isFalse(isVideoConfListProps(123));
+			assert.isFalse(isVideoConfListProps({}));
+			assert.isFalse(isVideoConfListProps([]));
+			assert.isFalse(isVideoConfListProps(new Date()));
+			assert.isFalse(isVideoConfListProps(new Error()));
+		});
+		it('should return false if roomId is not provided to VideoConfListProps', () => {
+			assert.isFalse(isVideoConfListProps({}));
+		});
+
+		it('should accept a roomId with nothing else', () => {
+			assert.isTrue(
+				isVideoConfListProps({
+					roomId: 'roomId',
+				}),
+			);
+		});
+
+		it('should accept the count and offset parameters', () => {
+			assert.isTrue(
+				isVideoConfListProps({
+					roomId: 'roomId',
+					offset: 50,
+					count: 25,
+				}),
+			);
+		});
+
+		it('should return false when extra parameters are provided to VideoConfListProps', () => {
+			assert.isFalse(
+				isVideoConfListProps({
+					roomId: 'roomId',
+					extra: 'extra',
+				}),
+			);
+		});
+	});
+});
diff --git a/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfStartProps.spec.ts b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfStartProps.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01af580226f39e97f44b22c83e5f8eedd3e6e105
--- /dev/null
+++ b/apps/meteor/tests/unit/definition/rest/v1/video-conference/VideoConfStartProps.spec.ts
@@ -0,0 +1,58 @@
+import { assert } from 'chai';
+import { isVideoConfStartProps } from '@rocket.chat/rest-typings';
+
+describe('VideoConfStartProps (definition/rest/v1)', () => {
+	describe('isVideoConfStartProps', () => {
+		it('should be a function', () => {
+			assert.isFunction(isVideoConfStartProps);
+		});
+		it('should return false when provided anything that is not an VideoConfStartProps', () => {
+			assert.isFalse(isVideoConfStartProps(undefined));
+			assert.isFalse(isVideoConfStartProps(null));
+			assert.isFalse(isVideoConfStartProps(''));
+			assert.isFalse(isVideoConfStartProps(123));
+			assert.isFalse(isVideoConfStartProps({}));
+			assert.isFalse(isVideoConfStartProps([]));
+			assert.isFalse(isVideoConfStartProps(new Date()));
+			assert.isFalse(isVideoConfStartProps(new Error()));
+		});
+		it('should return false if roomId is not provided to VideoConfStartProps', () => {
+			assert.isFalse(isVideoConfStartProps({}));
+		});
+
+		it('should accept a roomId with nothing else', () => {
+			assert.isTrue(
+				isVideoConfStartProps({
+					roomId: 'roomId',
+				}),
+			);
+		});
+
+		it('should accept the allowRinging parameter', () => {
+			assert.isTrue(
+				isVideoConfStartProps({
+					roomId: 'roomId',
+					allowRinging: true,
+				}),
+			);
+		});
+
+		it('should accept a roomId with a title', () => {
+			assert.isTrue(
+				isVideoConfStartProps({
+					roomId: 'roomId',
+					title: 'extra',
+				}),
+			);
+		});
+
+		it('should return false when extra parameters are provided to VideoConfStartProps', () => {
+			assert.isFalse(
+				isVideoConfStartProps({
+					roomId: 'roomId',
+					extra: 'extra',
+				}),
+			);
+		});
+	});
+});
diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts
index b374f9bfaa299689f5e61f69e95d9da4d706366d..00f359f8552ef7443a3e96c3f2df884ef16b4b26 100644
--- a/packages/core-typings/src/IRoom.ts
+++ b/packages/core-typings/src/IRoom.ts
@@ -37,7 +37,6 @@ export interface IRoom extends IRocketChatRecord {
 	lastMessage?: IMessage;
 	lm?: Date;
 	usersCount: number;
-	jitsiTimeout?: Date;
 	callStatus?: CallStatus;
 	webRtcCallStartTime?: Date;
 	servedBy?: {
diff --git a/packages/core-typings/src/ISetting.ts b/packages/core-typings/src/ISetting.ts
index 63236e56194bab9d1eaf8e6efce60748ba1783fb..c8aa1129b8a99759349cd10098be34445253b10c 100644
--- a/packages/core-typings/src/ISetting.ts
+++ b/packages/core-typings/src/ISetting.ts
@@ -42,7 +42,8 @@ export interface ISettingBase {
 		| 'asset'
 		| 'roomPick'
 		| 'group'
-		| 'date';
+		| 'date'
+		| 'lookup';
 	public: boolean;
 	env: boolean;
 	group?: GroupId;
@@ -73,6 +74,7 @@ export interface ISettingBase {
 	multiline?: boolean;
 	values?: Array<ISettingSelectOption>;
 	placeholder?: string;
+	lookupEndpoint?: string;
 	wizard?: {
 		step: number;
 		order: number;
@@ -168,8 +170,6 @@ export interface ISettingStatistics {
 	pushEnable?: boolean;
 	globalSearchEnabled?: boolean;
 	threadsEnabled?: boolean;
-	bigBlueButton?: boolean;
-	jitsiEnabled?: boolean;
 	webRTCEnableChannel?: boolean;
 	webRTCEnablePrivate?: boolean;
 	webRTCEnableDirect?: boolean;
diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts
index ee728569997d7085332e46775579d1c8963d3dab..feec8bd02bc817f93ac2ecf28bb0591838286506 100644
--- a/packages/core-typings/src/IStats.ts
+++ b/packages/core-typings/src/IStats.ts
@@ -170,6 +170,28 @@ export interface IStats {
 	totalEncryptedMessages: number;
 	totalLinkInvitationUses: number;
 	totalManuallyAddedUsers: number;
+	videoConf: {
+		videoConference: {
+			started: number;
+			ended: number;
+		};
+		direct: {
+			calling: number;
+			started: number;
+			ended: number;
+		};
+		livechat: {
+			started: number;
+			ended: number;
+		};
+		settings: {
+			provider: string;
+			dms: boolean;
+			channels: boolean;
+			groups: boolean;
+			teams: boolean;
+		};
+	};
 	totalSubscriptionRoles: number;
 	totalUserRoles: number;
 	totalWebRTCCalls: number;
diff --git a/packages/core-typings/src/IVideoConference.ts b/packages/core-typings/src/IVideoConference.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a4c0b57134716cbab7d16f8692f852bc593951ef
--- /dev/null
+++ b/packages/core-typings/src/IVideoConference.ts
@@ -0,0 +1,98 @@
+import type { AtLeast } from './utils';
+import type { IRocketChatRecord } from './IRocketChatRecord';
+import type { IRoom } from './IRoom';
+import type { IUser } from './IUser';
+import type { IMessage } from './IMessage';
+
+export enum VideoConferenceStatus {
+	CALLING = 0,
+	STARTED = 1,
+	EXPIRED = 2,
+	ENDED = 3,
+	DECLINED = 4,
+}
+
+export type DirectCallInstructions = {
+	type: 'direct';
+	callee: IUser['_id'];
+	callId: string;
+};
+
+export type ConferenceInstructions = {
+	type: 'videoconference';
+	callId: string;
+	rid: IRoom['_id'];
+};
+
+export type LivechatInstructions = {
+	type: 'livechat';
+	callId: string;
+};
+
+export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
+
+export interface IVideoConferenceUser extends Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> {
+	ts: Date;
+}
+
+export interface IVideoConference extends IRocketChatRecord {
+	type: VideoConferenceType;
+	rid: string;
+	users: IVideoConferenceUser[];
+	status: VideoConferenceStatus;
+	messages: {
+		started?: IMessage['_id'];
+		ended?: IMessage['_id'];
+	};
+	url?: string;
+
+	createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
+	createdAt: Date;
+
+	endedBy?: Pick<IUser, '_id' | 'username' | 'name'>;
+	endedAt?: Date;
+
+	providerName: string;
+	providerData?: Record<string, any>;
+
+	ringing?: boolean;
+}
+
+export interface IDirectVideoConference extends IVideoConference {
+	type: 'direct';
+}
+
+export interface IGroupVideoConference extends IVideoConference {
+	type: 'videoconference';
+	anonymousUsers: number;
+	title: string;
+}
+
+export interface ILivechatVideoConference extends IVideoConference {
+	type: 'livechat';
+}
+
+export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
+
+export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
+
+export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference => {
+	return call?.type === 'direct';
+};
+
+export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference => {
+	return call?.type === 'videoconference';
+};
+
+export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference => {
+	return call?.type === 'livechat';
+};
+
+type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
+type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
+type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
+
+export type VideoConferenceCreateData = AtLeast<
+	DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData,
+	'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData'
+>;
diff --git a/packages/core-typings/src/VideoConferenceCapabilities.ts b/packages/core-typings/src/VideoConferenceCapabilities.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0ff6ccb754afd517fd59694a409de0404c551ea7
--- /dev/null
+++ b/packages/core-typings/src/VideoConferenceCapabilities.ts
@@ -0,0 +1,5 @@
+export type VideoConferenceCapabilities = {
+	mic?: boolean;
+	cam?: boolean;
+	title?: boolean;
+};
diff --git a/packages/core-typings/src/VideoConferenceOptions.ts b/packages/core-typings/src/VideoConferenceOptions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4ed1a415e2a10f5f2633603f535ef77474a6bd6b
--- /dev/null
+++ b/packages/core-typings/src/VideoConferenceOptions.ts
@@ -0,0 +1,4 @@
+export type VideoConferenceOptions = {
+	mic?: boolean;
+	cam?: boolean;
+};
diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts
index ae31c6cb0f3f5fe9db0b0b45d775b7051248a0bd..696b276dae123dd222d09d9b02a58f4b90466b39 100644
--- a/packages/core-typings/src/index.ts
+++ b/packages/core-typings/src/index.ts
@@ -110,3 +110,6 @@ export * from './IInquiry';
 export * from './ILivechatPriority';
 
 export * from './IAutoTranslate';
+export * from './IVideoConference';
+export * from './VideoConferenceCapabilities';
+export * from './VideoConferenceOptions';
diff --git a/packages/livechat/src/components/Calls/CallNotification.js b/packages/livechat/src/components/Calls/CallNotification.js
index 68a36b3a0e6b1918cac070f413aee27b2989703f..e4fa4d06f6db45676893c703c8e559a9ea1a3138 100644
--- a/packages/livechat/src/components/Calls/CallNotification.js
+++ b/packages/livechat/src/components/Calls/CallNotification.js
@@ -44,7 +44,7 @@ const CallNotification = ({
 	const acceptClick = async () => {
 		setShow(!{ show });
 		switch (callProvider) {
-			case constants.jitsiCallStartedMessageType: {
+			case 'video-conference': {
 				window.open(url, rid);
 				await dispatch({
 					incomingCallAlert: { show: false, url, callProvider },
diff --git a/packages/livechat/src/components/Calls/JoinCallButton.js b/packages/livechat/src/components/Calls/JoinCallButton.js
index 9bc0560aa7b136534258f32474e10e1c14d4fed4..a0ea279480be9f43e51e8a91670a8ff3264171f2 100644
--- a/packages/livechat/src/components/Calls/JoinCallButton.js
+++ b/packages/livechat/src/components/Calls/JoinCallButton.js
@@ -15,7 +15,7 @@ export const JoinCallButton = ({ t, ...props }) => {
 
 	const clickJoinCall = () => {
 		switch (props.callProvider) {
-			case constants.jitsiCallStartedMessageType: {
+			case 'video-conference': {
 				window.open(props.url, room._id);
 				break;
 			}
diff --git a/packages/livechat/src/components/Messages/MessageList/index.js b/packages/livechat/src/components/Messages/MessageList/index.js
index 305c35ed3d3a27f9815e1ebb4a7d04bb0097076d..c5af1a423092f06906e1047ab130e78c43ba461a 100644
--- a/packages/livechat/src/components/Messages/MessageList/index.js
+++ b/packages/livechat/src/components/Messages/MessageList/index.js
@@ -98,6 +98,10 @@ export class MessageList extends MemoizedComponent {
 		window.removeEventListener('resize', this.handleResize);
 	}
 
+	isVideoConfMessage(message) {
+		return Boolean(message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat'));
+	}
+
 	renderItems = ({
 		attachmentResolver = getAttachmentUrl,
 		avatarResolver,
@@ -116,7 +120,7 @@ export class MessageList extends MemoizedComponent {
 			const message = messages[i];
 			const nextMessage = messages[i + 1];
 
-			if ((message.t === constants.webRTCCallStartedMessageType || message.t === constants.jitsiCallStartedMessageType)
+			if ((message.t === constants.webRTCCallStartedMessageType)
 				&& message.actionLinks && message.actionLinks.length
 				&& ongoingCall && isCallOngoing(ongoingCall.callStatus)
 				&& !message.webRtcCallEndTs) {
@@ -127,6 +131,14 @@ export class MessageList extends MemoizedComponent {
 				continue;
 			}
 
+			const videoConfJoinBlock = message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat');
+			if (videoConfJoinBlock) {
+				// If the call is not accepted yet, don't render the message.
+				if (!ongoingCall || !isCallOngoing(ongoingCall.callStatus)) {
+					continue;
+				}
+			}
+
 			const showDateSeparator = !previousMessage || !isSameDay(parseISO(message.ts), parseISO(previousMessage.ts));
 			if (showDateSeparator) {
 				items.push(
diff --git a/packages/livechat/src/components/Messages/constants.js b/packages/livechat/src/components/Messages/constants.js
index e4a0d92f536028fce60c93214159facd23123ce4..f48482a44b759688bea192f54f6b613be43acc74 100644
--- a/packages/livechat/src/components/Messages/constants.js
+++ b/packages/livechat/src/components/Messages/constants.js
@@ -7,5 +7,4 @@ export const MESSAGE_TYPE_WELCOME = 'wm';
 export const MESSAGE_TYPE_LIVECHAT_CLOSED = 'livechat-close';
 export const MESSAGE_TYPE_LIVECHAT_STARTED = 'livechat-started';
 export const MESSAGE_TYPE_LIVECHAT_TRANSFER_HISTORY = 'livechat_transfer_history';
-export const MESSAGE_JITSI_CALL = 'jitsi_call_started';
 export const MESSAGE_WEBRTC_CALL = 'livechat_webrtc_video_call';
diff --git a/packages/livechat/src/lib/constants.js b/packages/livechat/src/lib/constants.js
index 7171755465b086188bc16e96c8429ddcbf734a4a..6c968a707f3e755522f59c49900a03b76c7cb295 100644
--- a/packages/livechat/src/lib/constants.js
+++ b/packages/livechat/src/lib/constants.js
@@ -5,5 +5,4 @@ export default {
 	livechatDisconnectedAlertId: 'LIVECHAT_DISCONNECTED',
 	livechatQueueMessageId: 'LIVECHAT_QUEUE_MESSAGE',
 	webRTCCallStartedMessageType: 'livechat_webrtc_video_call',
-	jitsiCallStartedMessageType: 'jitsi_call_started',
 };
diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js
index 18e5be2b8282e660b1e4c42efcc76bf8d8421773..5c74ba6096fb4b98cad3fdbbf4809b558fe74e8c 100644
--- a/packages/livechat/src/lib/room.js
+++ b/packages/livechat/src/lib/room.js
@@ -34,19 +34,48 @@ export const closeChat = async ({ transcriptRequested } = {}) => {
 	route('/chat-finished');
 };
 
+const getVideoConfMessageData = (message) => message.blocks?.find(({ appId }) => appId === 'videoconf-core')?.elements?.find(({ actionId }) => actionId === 'joinLivechat');
+
+const isVideoCallMessage = (message) => {
+	if (message.t === constants.webRTCCallStartedMessageType) {
+		return true;
+	}
+
+	if (getVideoConfMessageData(message)) {
+		return true;
+	}
+
+	return false;
+};
+
+const findCallData = (message) => {
+	const videoConfJoinBlock = getVideoConfMessageData(message);
+	if (videoConfJoinBlock) {
+		return {
+			callId: videoConfJoinBlock.blockId,
+			url: videoConfJoinBlock.url,
+			callProvider: 'video-conference',
+		};
+	}
+
+	return { callId: message._id, url: '', callProvider: message.t };
+};
+
 // TODO: use a separate event to listen to call start event. Listening on the message type isn't a good solution
 export const processIncomingCallMessage = async (message) => {
 	const { alerts } = store.state;
 	try {
+		const { callId, url, callProvider } = findCallData(message);
+
 		await store.setState({
 			incomingCallAlert: {
 				show: true,
-				callProvider: message.t,
+				callProvider,
 				callerUsername: message.u.username,
 				rid: message.rid,
 				time: message.ts,
-				callId: message._id,
-				url: message.t === constants.jitsiCallStartedMessageType ? message.customFields.jitsiCallUrl : '',
+				callId,
+				url,
 			},
 			ongoingCall: {
 				callStatus: CallStatus.RINGING,
@@ -67,7 +96,7 @@ const processMessage = async (message) => {
 		commands[message.msg] && commands[message.msg]();
 	} else if (message.webRtcCallEndTs) {
 		await store.setState({ ongoingCall: { callStatus: CallStatus.ENDED, time: message.ts }, incomingCallAlert: null });
-	} else if (message.t === constants.webRTCCallStartedMessageType || message.t === constants.jitsiCallStartedMessageType) {
+	} else if (isVideoCallMessage(message)) {
 		await processIncomingCallMessage(message);
 	}
 };
@@ -194,7 +223,7 @@ Livechat.onMessage(async (message) => {
 });
 
 export const getGreetingMessages = (messages) => messages && messages.filter((msg) => msg.trigger);
-export const getLatestCallMessage = (messages) => messages && messages.filter((msg) => msg.t === constants.webRTCCallStartedMessageType || msg.t === constants.jitsiCallStartedMessageType).pop();
+export const getLatestCallMessage = (messages) => messages && messages.filter((msg) => isVideoCallMessage(msg)).pop();
 
 export const loadMessages = async () => {
 	const { ongoingCall } = store.state;
@@ -225,7 +254,8 @@ export const loadMessages = async () => {
 	if (!latestCallMessage) {
 		return;
 	}
-	if (latestCallMessage.t === constants.jitsiCallStartedMessageType) {
+	const videoConfJoinBlock = getVideoConfMessageData(latestCallMessage);
+	if (videoConfJoinBlock) {
 		await store.setState({
 			ongoingCall: {
 				callStatus: CallStatus.IN_PROGRESS_DIFFERENT_TAB,
@@ -235,7 +265,7 @@ export const loadMessages = async () => {
 				show: false,
 				callProvider:
 				latestCallMessage.t,
-				url: latestCallMessage.customFields.jitsiCallUrl,
+				url: videoConfJoinBlock.url,
 			},
 		});
 		return;
diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts
index cf73129041b5a1cca2562b75310c9858a2bd1163..c5854953d23bff145ad0163e9896fbf342dbaa0b 100644
--- a/packages/model-typings/src/index.ts
+++ b/packages/model-typings/src/index.ts
@@ -57,5 +57,6 @@ export * from './models/IUploadsModel';
 export * from './models/IUserDataFilesModel';
 export * from './models/IUsersModel';
 export * from './models/IUsersSessionsModel';
+export * from './models/IVideoConferenceModel';
 export * from './models/IVoipRoomModel';
 export * from './models/IWebdavAccountsModel';
diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts
index 7de672054095a8850b86882fc8efbf7206ad500d..e371534d32a6d8036b0678540730ec3ad98342f3 100644
--- a/packages/model-typings/src/models/IMessagesModel.ts
+++ b/packages/model-typings/src/models/IMessagesModel.ts
@@ -44,4 +44,10 @@ export interface IMessagesModel extends IBaseModel<IMessage> {
 	findPinned(options: WithoutProjection<FindOneOptions<IMessage>>): Cursor<IMessage>;
 
 	findStarred(options: WithoutProjection<FindOneOptions<IMessage>>): Cursor<IMessage>;
+
+	setBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void>;
+
+	addBlocksById(_id: string, blocks: Required<IMessage>['blocks']): Promise<void>;
+
+	removeVideoConfJoinButton(_id: IMessage['_id']): Promise<void>;
 }
diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts
index 042e0d5d49896e64ff5803332c73abba57ec7753..0cd59e12d0150adad56fa02ab3d99971b499c95e 100644
--- a/packages/model-typings/src/models/ISubscriptionsModel.ts
+++ b/packages/model-typings/src/models/ISubscriptionsModel.ts
@@ -10,6 +10,8 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {
 
 	findByUserIdAndRoomIds(userId: string, roomIds: Array<string>, options?: FindOneOptions<ISubscription>): Cursor<ISubscription>;
 
+	findByRoomId(roomId: string, options?: FindOneOptions<ISubscription>): Cursor<ISubscription>;
+
 	findByRoomIdAndNotUserId(roomId: string, userId: string, options?: FindOneOptions<ISubscription>): Cursor<ISubscription>;
 
 	findByLivechatRoomIdAndNotUserId(roomId: string, userId: string, options?: FindOneOptions<ISubscription>): Cursor<ISubscription>;
diff --git a/packages/model-typings/src/models/IVideoConferenceModel.ts b/packages/model-typings/src/models/IVideoConferenceModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ee6127ee2674d06e3ce0c755c987dbad060e8a74
--- /dev/null
+++ b/packages/model-typings/src/models/IVideoConferenceModel.ts
@@ -0,0 +1,61 @@
+import type { Cursor, UpdateOneOptions, UpdateQuery, UpdateWriteOpResult, FindOneOptions } from 'mongodb';
+import type {
+	IGroupVideoConference,
+	ILivechatVideoConference,
+	IRoom,
+	IUser,
+	VideoConference,
+	VideoConferenceStatus,
+} from '@rocket.chat/core-typings';
+
+import type { IBaseModel } from './IBaseModel';
+
+export interface IVideoConferenceModel extends IBaseModel<VideoConference> {
+	findAllByRoomId(rid: IRoom['_id'], { offset, count }: { offset?: number; count?: number }): Promise<Cursor<VideoConference>>;
+
+	findAllLongRunning(minDate: Date): Promise<Cursor<Pick<VideoConference, '_id'>>>;
+
+	countByTypeAndStatus(
+		type: VideoConference['type'],
+		status: VideoConferenceStatus,
+		options: FindOneOptions<VideoConference>,
+	): Promise<number>;
+
+	createDirect({ providerName, ...callDetails }: Pick<VideoConference, 'rid' | 'createdBy' | 'providerName'>): Promise<string>;
+
+	createGroup({
+		providerName,
+		...callDetails
+	}: Required<Pick<IGroupVideoConference, 'rid' | 'title' | 'createdBy' | 'providerName'>>): Promise<string>;
+
+	createLivechat({
+		providerName,
+		...callDetails
+	}: Required<Pick<ILivechatVideoConference, 'rid' | 'createdBy' | 'providerName'>>): Promise<string>;
+
+	updateOneById(
+		_id: string,
+		update: UpdateQuery<VideoConference> | Partial<VideoConference>,
+		options?: UpdateOneOptions,
+	): Promise<UpdateWriteOpResult>;
+
+	setDataById(callId: string, data: Partial<Omit<VideoConference, '_id'>>): Promise<void>;
+
+	setEndedById(callId: string, endedBy?: { _id: string; name: string; username: string }, endedAt?: Date): Promise<void>;
+
+	setRingingById(callId: string, ringing: boolean): Promise<void>;
+
+	setStatusById(callId: string, status: VideoConference['status']): Promise<void>;
+
+	setUrlById(callId: string, url: string): Promise<void>;
+
+	setProviderDataById(callId: string, providerData: Record<string, any> | undefined): Promise<void>;
+
+	addUserById(callId: string, user: Pick<IUser, '_id' | 'name' | 'username' | 'avatarETag'> & { ts?: Date }): Promise<void>;
+
+	setMessageById(callId: string, messageType: keyof VideoConference['messages'], messageId: string): Promise<void>;
+
+	updateUserReferences(userId: IUser['_id'], username: IUser['username'], name: IUser['name']): Promise<void>;
+
+	increaseAnonymousCount(callId: IGroupVideoConference['_id']): Promise<void>;
+}
diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts
index 30db255dc6c28986d6f6926daa9160449a5eb81a..e9e5c053e22c8d58d04ddbed024f728d075619ac 100644
--- a/packages/models/src/index.ts
+++ b/packages/models/src/index.ts
@@ -57,6 +57,7 @@ import type {
 	IUserDataFilesModel,
 	IUsersSessionsModel,
 	IUsersModel,
+	IVideoConferenceModel,
 	IVoipRoomModel,
 	IWebdavAccountsModel,
 } from '@rocket.chat/model-typings';
@@ -128,5 +129,6 @@ export const Users = proxify<IUsersModel>('IUsersModel');
 export const Uploads = proxify<IUploadsModel>('IUploadsModel');
 export const UserDataFiles = proxify<IUserDataFilesModel>('IUserDataFilesModel');
 export const UsersSessions = proxify<IUsersSessionsModel>('IUsersSessionsModel');
+export const VideoConference = proxify<IVideoConferenceModel>('IVideoConferenceModel');
 export const VoipRoom = proxify<IVoipRoomModel>('IVoipRoomModel');
 export const WebdavAccounts = proxify<IWebdavAccountsModel>('IWebdavAccountsModel');
diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts
index eef47cbfca8dc4e74ab3cb0771ca99ef0fdd74b3..375abbb11d4462c0da351015d2fe72f73a4f4085 100644
--- a/packages/rest-typings/src/index.ts
+++ b/packages/rest-typings/src/index.ts
@@ -148,6 +148,7 @@ export * from './v1/permissions';
 export * from './v1/roles';
 export * from './v1/settings';
 export * from './v1/teams';
+export * from './v1/videoConference';
 export * from './v1/assets';
 export * from './v1/channels/ChannelsAddAllProps';
 export * from './v1/channels/ChannelsArchiveProps';
diff --git a/packages/rest-typings/src/v1/videoConference.ts b/packages/rest-typings/src/v1/videoConference.ts
deleted file mode 100644
index d6a4717e5131acea9f7969cc5ae34bf1ebc62320..0000000000000000000000000000000000000000
--- a/packages/rest-typings/src/v1/videoConference.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { IRoom } from '@rocket.chat/core-typings';
-import Ajv from 'ajv';
-
-const ajv = new Ajv({
-	coerceTypes: true,
-});
-
-type VideoConferenceJitsiUpdateTimeout = { roomId: IRoom['_id']; joiningNow?: boolean };
-
-const VideoConferenceJitsiUpdateTimeoutSchema = {
-	type: 'object',
-	properties: {
-		roomId: {
-			type: 'string',
-		},
-		joiningNow: {
-			type: 'boolean',
-			nullable: true,
-		},
-	},
-	required: ['roomId'],
-	additionalProperties: false,
-};
-
-export const isVideoConferenceJitsiUpdateTimeoutProps = ajv.compile<VideoConferenceJitsiUpdateTimeout>(
-	VideoConferenceJitsiUpdateTimeoutSchema,
-);
-
-export type VideoConferenceEndpoints = {
-	'/v1/video-conference/jitsi.update-timeout': {
-		POST: (params: VideoConferenceJitsiUpdateTimeout) => {
-			jitsiTimeout: number;
-		};
-	};
-};
diff --git a/packages/rest-typings/src/v1/videoConference/VideoConfCancelProps.ts b/packages/rest-typings/src/v1/videoConference/VideoConfCancelProps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..656345a4ad3d7f36d9487299717f0b1d87820251
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/VideoConfCancelProps.ts
@@ -0,0 +1,21 @@
+import Ajv, { JSONSchemaType } from 'ajv';
+
+const ajv = new Ajv();
+
+export type VideoConfCancelProps = {
+	callId: string;
+};
+
+const videoConfCancelPropsSchema: JSONSchemaType<VideoConfCancelProps> = {
+	type: 'object',
+	properties: {
+		callId: {
+			type: 'string',
+			nullable: false,
+		},
+	},
+	required: ['callId'],
+	additionalProperties: false,
+};
+
+export const isVideoConfCancelProps = ajv.compile(videoConfCancelPropsSchema);
diff --git a/packages/rest-typings/src/v1/videoConference/VideoConfInfoProps.ts b/packages/rest-typings/src/v1/videoConference/VideoConfInfoProps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01c0d88163b1728b5e66921f47cace4ed1d4e7bd
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/VideoConfInfoProps.ts
@@ -0,0 +1,19 @@
+import Ajv, { JSONSchemaType } from 'ajv';
+
+const ajv = new Ajv();
+
+export type VideoConfInfoProps = { callId: string };
+
+const videoConfInfoPropsSchema: JSONSchemaType<VideoConfInfoProps> = {
+	type: 'object',
+	properties: {
+		callId: {
+			type: 'string',
+			nullable: false,
+		},
+	},
+	required: ['callId'],
+	additionalProperties: false,
+};
+
+export const isVideoConfInfoProps = ajv.compile(videoConfInfoPropsSchema);
diff --git a/packages/rest-typings/src/v1/videoConference/VideoConfJoinProps.ts b/packages/rest-typings/src/v1/videoConference/VideoConfJoinProps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae2abf0285c0da9d1a430cce8a687cffb49c7bde
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/VideoConfJoinProps.ts
@@ -0,0 +1,40 @@
+import Ajv, { JSONSchemaType } from 'ajv';
+
+const ajv = new Ajv();
+
+export type VideoConfJoinProps = {
+	callId: string;
+	state?: {
+		mic?: boolean;
+		cam?: boolean;
+	};
+};
+
+const videoConfJoinPropsSchema: JSONSchemaType<VideoConfJoinProps> = {
+	type: 'object',
+	properties: {
+		callId: {
+			type: 'string',
+			nullable: false,
+		},
+		state: {
+			type: 'object',
+			nullable: true,
+			properties: {
+				mic: {
+					type: 'boolean',
+					nullable: true,
+				},
+				cam: {
+					type: 'boolean',
+					nullable: true,
+				},
+			},
+			additionalProperties: false,
+		},
+	},
+	required: ['callId'],
+	additionalProperties: false,
+};
+
+export const isVideoConfJoinProps = ajv.compile(videoConfJoinPropsSchema);
diff --git a/packages/rest-typings/src/v1/videoConference/VideoConfListProps.ts b/packages/rest-typings/src/v1/videoConference/VideoConfListProps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b30c79cf58b408b570df5385937c00f9e3677b90
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/VideoConfListProps.ts
@@ -0,0 +1,25 @@
+import Ajv, { JSONSchemaType } from 'ajv';
+
+const ajv = new Ajv();
+
+export type VideoConfListProps = {
+	roomId: string;
+	count?: number;
+	offset?: number;
+};
+
+const videoConfListPropsSchema: JSONSchemaType<VideoConfListProps> = {
+	type: 'object',
+	properties: {
+		roomId: {
+			type: 'string',
+			nullable: false,
+		},
+		offset: { type: 'number', nullable: true },
+		count: { type: 'number', nullable: true },
+	},
+	required: ['roomId'],
+	additionalProperties: false,
+};
+
+export const isVideoConfListProps = ajv.compile(videoConfListPropsSchema);
diff --git a/packages/rest-typings/src/v1/videoConference/VideoConfStartProps.ts b/packages/rest-typings/src/v1/videoConference/VideoConfStartProps.ts
new file mode 100644
index 0000000000000000000000000000000000000000..795db6bfeb724c998eff7645ae87d5e523ae9f6a
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/VideoConfStartProps.ts
@@ -0,0 +1,27 @@
+import Ajv, { JSONSchemaType } from 'ajv';
+
+const ajv = new Ajv();
+
+export type VideoConfStartProps = { roomId: string; title?: string; allowRinging?: boolean };
+
+const videoConfStartPropsSchema: JSONSchemaType<VideoConfStartProps> = {
+	type: 'object',
+	properties: {
+		roomId: {
+			type: 'string',
+			nullable: false,
+		},
+		title: {
+			type: 'string',
+			nullable: true,
+		},
+		allowRinging: {
+			type: 'boolean',
+			nullable: true,
+		},
+	},
+	required: ['roomId'],
+	additionalProperties: false,
+};
+
+export const isVideoConfStartProps = ajv.compile(videoConfStartPropsSchema);
diff --git a/packages/rest-typings/src/v1/videoConference/index.ts b/packages/rest-typings/src/v1/videoConference/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3160c6bd7a5ae976615267d6cfaf63769284ba9d
--- /dev/null
+++ b/packages/rest-typings/src/v1/videoConference/index.ts
@@ -0,0 +1,44 @@
+import type { VideoConferenceInstructions, VideoConference, VideoConferenceCapabilities } from '@rocket.chat/core-typings';
+
+import type { VideoConfInfoProps } from './VideoConfInfoProps';
+import type { VideoConfListProps } from './VideoConfListProps';
+import type { VideoConfStartProps } from './VideoConfStartProps';
+import type { VideoConfJoinProps } from './VideoConfJoinProps';
+import type { VideoConfCancelProps } from './VideoConfCancelProps';
+import type { PaginatedResult } from '../../helpers/PaginatedResult';
+
+export * from './VideoConfInfoProps';
+export * from './VideoConfListProps';
+export * from './VideoConfStartProps';
+export * from './VideoConfJoinProps';
+export * from './VideoConfCancelProps';
+
+export type VideoConferenceEndpoints = {
+	'/v1/video-conference.start': {
+		POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } };
+	};
+
+	'/v1/video-conference.join': {
+		POST: (params: VideoConfJoinProps) => { url: string; providerName: string };
+	};
+
+	'/v1/video-conference.cancel': {
+		POST: (params: VideoConfCancelProps) => void;
+	};
+
+	'/v1/video-conference.info': {
+		GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
+	};
+
+	'/v1/video-conference.list': {
+		GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>;
+	};
+
+	'/v1/video-conference.capabilities': {
+		GET: () => { providerName: string; capabilities: VideoConferenceCapabilities };
+	};
+
+	'/v1/video-conference.providers': {
+		GET: () => { data: { key: string; label: string }[] };
+	};
+};
diff --git a/packages/ui-contexts/src/CustomSoundContext.ts b/packages/ui-contexts/src/CustomSoundContext.ts
index f2cb49cc5e065fbf751697035dcb9742eb133283..25942019c3ebbfdf6f240632854df8c600ab9aa9 100644
--- a/packages/ui-contexts/src/CustomSoundContext.ts
+++ b/packages/ui-contexts/src/CustomSoundContext.ts
@@ -3,10 +3,12 @@ import { createContext } from 'react';
 
 export type CustomSoundContextValue = {
 	play: (sound: string, options?: { volume?: number; loop?: boolean }) => void;
+	pause: (sound: string) => void;
 	getList: () => ICustomSound[] | undefined;
 };
 
 export const CustomSoundContext = createContext<CustomSoundContextValue>({
 	play: () => undefined,
+	pause: () => undefined,
 	getList: () => undefined,
 });
diff --git a/packages/ui-contexts/src/ServerContext/methods.ts b/packages/ui-contexts/src/ServerContext/methods.ts
index 825febf0e22cb6236875366378aa7e4aca3ef74d..36e62bdc6b5e8665d8962dde1ca653fff76fff5e 100644
--- a/packages/ui-contexts/src/ServerContext/methods.ts
+++ b/packages/ui-contexts/src/ServerContext/methods.ts
@@ -76,8 +76,6 @@ export interface ServerMethods {
 	'insertOrUpdateSound': (args: { previousName?: string; name?: string; _id?: string; extension: string }) => string;
 	'insertOrUpdateUserStatus': (...args: any[]) => any;
 	'instances/get': (...args: any[]) => any;
-	'jitsi:generateAccessToken': (...args: any[]) => any;
-	'jitsi:updateTimeout': (...args: any[]) => any;
 	'joinRoom': JoinRoomMethod;
 	'leaveRoom': (...args: any[]) => any;
 	'Mailer.sendMail': (from: string, subject: string, body: string, dryrun: boolean, query: string) => any;
diff --git a/packages/ui-video-conf/.eslintrc b/packages/ui-video-conf/.eslintrc
new file mode 100644
index 0000000000000000000000000000000000000000..afaf8e438ea9df53431468f67aad76366892a19c
--- /dev/null
+++ b/packages/ui-video-conf/.eslintrc
@@ -0,0 +1,73 @@
+{
+	"extends": ["@rocket.chat/eslint-config"],
+	"plugins": ["react", "react-hooks"],
+	"parser": "@babel/eslint-parser",
+	"rules": {
+		"jsx-quotes": ["error", "prefer-single"],
+		"react/display-name": "error",
+		"react/self-closing-comp": "error",
+		"react/jsx-uses-react": "error",
+		"react/jsx-uses-vars": "error",
+		"react/jsx-no-undef": "error",
+		"react/jsx-fragments": ["error", "syntax"],
+		"react/no-multi-comp": "error",
+		"react/react-in-jsx-scope": "error",
+		"react-hooks/rules-of-hooks": "error",
+		"react-hooks/exhaustive-deps": "warn"
+	},
+	"settings": {
+		"import/resolver": {
+			"node": {
+				"extensions": [".js", ".jsx", ".ts", ".tsx"]
+			}
+		},
+		"react": {
+			"version": "detect"
+		}
+	},
+	"env": {
+		"browser": true,
+		"es6": true
+	},
+	"overrides": [
+		{
+			"files": ["**/*.ts", "**/*.tsx"],
+			"extends": "../typescript",
+			"parser": "@typescript-eslint/parser",
+			"plugins": ["react", "react-hooks"],
+			"rules": {
+				"jsx-quotes": ["error", "prefer-single"],
+				"react/display-name": "error",
+				"react/jsx-uses-react": "error",
+				"react/jsx-uses-vars": "error",
+				"react/jsx-no-undef": "error",
+				"react/jsx-fragments": ["error", "syntax"],
+				"react/no-multi-comp": "error",
+				"react/react-in-jsx-scope": "error",
+				"react-hooks/rules-of-hooks": "error",
+				"react-hooks/exhaustive-deps": "warn"
+			},
+			"env": {
+				"browser": true,
+				"es6": true
+			},
+			"settings": {
+				"import/resolver": {
+					"node": {
+						"extensions": [".js", ".jsx", ".ts", ".tsx"]
+					}
+				},
+				"react": {
+					"version": "detect"
+				}
+			}
+		},
+		{
+			"files": ["**/*.stories.js", "**/*.stories.jsx", "**/*.stories.ts", "**/*.stories.tsx"],
+			"rules": {
+				"react/display-name": "off",
+				"react/no-multi-comp": "off"
+			}
+		}
+	]
+}
diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..c94a2280801e7e5f925848376566b191207850f6
--- /dev/null
+++ b/packages/ui-video-conf/package.json
@@ -0,0 +1,37 @@
+{
+	"name": "@rocket.chat/ui-video-conf",
+	"version": "0.0.1",
+	"private": true,
+	"devDependencies": {
+		"@rocket.chat/css-in-js": "~0.31.14",
+		"@rocket.chat/eslint-config": "workspace:^",
+		"@rocket.chat/fuselage": "~0.31.14",
+		"@rocket.chat/fuselage-hooks": "~0.31.14",
+		"@rocket.chat/styled": "~0.31.14",
+		"@types/jest": "^27.4.1",
+		"eslint": "^8.12.0",
+		"jest": "^27.5.1",
+		"ts-jest": "^27.1.4",
+		"typescript": "~4.3.5"
+	},
+	"peerDependencies": {
+		"@rocket.chat/css-in-js": "*",
+		"@rocket.chat/fuselage": "*",
+		"@rocket.chat/fuselage-hooks": "*",
+		"@rocket.chat/styled": "*",
+		"react": "^17.0.2",
+		"react-dom": "^17.0.2"
+	},
+	"scripts": {
+		"eslint": "eslint --ext .js,.jsx,.ts,.tsx .",
+		"eslint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
+		"jest": "jest",
+		"build": "tsc -p tsconfig.json",
+		"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
+	},
+	"main": "./dist/index.js",
+	"typings": "./dist/index.d.ts",
+	"files": [
+		"/dist"
+	]
+}
diff --git a/packages/ui-video-conf/src/VideoConfButton.tsx b/packages/ui-video-conf/src/VideoConfButton.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3a0b4f1afe5f38e65bd580925cb2ccb5c4bf398b
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfButton.tsx
@@ -0,0 +1,21 @@
+import type { ReactNode, ReactElement, ButtonHTMLAttributes } from 'react';
+import { Button, Icon, IconProps } from '@rocket.chat/fuselage';
+
+type VideoConfButtonProps = {
+	icon?: IconProps['name'];
+	primary?: boolean;
+	danger?: boolean;
+	disabled?: boolean;
+	children: ReactNode;
+} & Omit<ButtonHTMLAttributes<HTMLElement>, 'ref' | 'is' | 'className' | 'size' | 'elevation'>;
+
+const VideoConfButton = ({ primary, danger, disabled, icon, children, ...props }: VideoConfButtonProps): ReactElement => {
+	return (
+		<Button width='100%' primary={primary} danger={danger} disabled={disabled} {...props}>
+			{icon && <Icon mie='x4' size='x20' name={icon} />}
+			{children}
+		</Button>
+	);
+};
+
+export default VideoConfButton;
diff --git a/packages/ui-video-conf/src/VideoConfController.tsx b/packages/ui-video-conf/src/VideoConfController.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..da9b9e051e9a8fad2b2ca446b1f7c05c71bfc502
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfController.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import type { ReactElement, ButtonHTMLAttributes } from 'react';
+
+import { IconButton } from '@rocket.chat/fuselage';
+import type { IconProps } from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+
+type VideoConfControllerProps = {
+	icon: IconProps['name'];
+	active?: boolean;
+	text: string;
+} & Omit<ButtonHTMLAttributes<HTMLElement>, 'ref' | 'is' | 'className' | 'size' | 'elevation'>;
+
+const VideoConfController = ({ active, text, icon, ...props }: VideoConfControllerProps): ReactElement => {
+  const id = useUniqueId();
+
+  return <IconButton icon={icon} id={id} info={active} square secondary {...props} />
+}
+
+export default VideoConfController;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c9e49384ad5a1b9536451c3f80c015e104ec380d
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopup.tsx
@@ -0,0 +1,31 @@
+import React, { forwardRef } from 'react';
+import type { ReactNode, ReactElement, HTMLAttributes, Ref } from 'react';
+import styled from '@rocket.chat/styled';
+import { Box } from '@rocket.chat/fuselage';
+
+export const VideoConfPopupContainer = styled(
+  'div',
+  ({ position: _position, ...props }: { position?: number }) =>
+    props
+)`
+	width: 100%;
+	position: absolute;
+	box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.15);
+	top: ${(p) => p.position ? `${p.position}px` : '0'};
+	left: -${(p) => p.position ? `${p.position}px` : '0'};
+`;
+
+type VideoConfPopupProps = {
+	children: ReactNode; 
+	position?: number;
+} & HTMLAttributes<HTMLElement>;
+
+const VideoConfPopup = forwardRef(function VideoConfPopup({ children, position }: VideoConfPopupProps, ref: Ref<HTMLDivElement>): ReactElement {
+	return (
+		<VideoConfPopupContainer ref={ref} position={position}>
+			<Box p='x24' maxWidth='x276' backgroundColor='white'>{children}</Box>
+		</VideoConfPopupContainer>
+	);
+});
+
+export default VideoConfPopup;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..64a891406e7e939753950351355c261286c74bd8
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupBackdrop.tsx
@@ -0,0 +1,15 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+import { Box } from '@rocket.chat/fuselage';
+import { css } from '@rocket.chat/css-in-js';
+
+const backdropStyle = css`
+  position: fixed;
+  top: 0;
+  right: 0;
+  min-width: 276px;
+`;
+
+const VideoConfPopupBackdrop = ({ children }: { children: ReactNode }) => <Box m='x40' className={backdropStyle}>{children}</Box>
+
+export default VideoConfPopupBackdrop;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..46920741856e920883e2331420af1699698854cc
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupClose.tsx
@@ -0,0 +1,11 @@
+import { Box, IconButton } from '@rocket.chat/fuselage';
+import type { ReactElement } from 'react';
+import React from 'react';
+
+const VideoConfPopupClose = ({ onClick, title }: { onClick: () => void; title?: string }): ReactElement => (
+	<Box display='flex' justifyContent='end' width='100%'> 
+    <IconButton mini onClick={onClick} icon='cross' title={title} />
+  </Box>
+);
+
+export default VideoConfPopupClose;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0a9e577c34d342d9ec59c998d7499aeba7138f02
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupContent.tsx
@@ -0,0 +1,11 @@
+import { Box } from '@rocket.chat/fuselage';
+import type { ReactNode, ReactElement } from 'react';
+import React from 'react';
+
+const VideoConfPopupContent = ({ children }: { children: ReactNode }): ReactElement => (
+	<Box display='flex' flexDirection='column' alignItems='center'>
+    {children}
+  </Box>
+);
+
+export default VideoConfPopupContent;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..21a0107fc20302b1770f744f86c1bca57edc3c46
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupControllers.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import type { ReactNode, ReactElement } from 'react';
+import { ButtonGroup } from '@rocket.chat/fuselage';
+
+const VideoConfPopupControllers = ({ children }: { children: ReactNode }): ReactElement => <ButtonGroup mbs='x16'>{children}</ButtonGroup>;
+
+export default VideoConfPopupControllers;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ed7c1aa44400f0fa42784cfc71a4373ffeac51c4
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooter.tsx
@@ -0,0 +1,7 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+import { Margins } from '@rocket.chat/fuselage';
+
+const VideoConfPopupFooter = ({ children }: { children: ReactNode }) => <Margins blockStart='x16'>{children}</Margins>
+
+export default VideoConfPopupFooter;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7bf0cf6537b4172003fc5142901965cf243ee3ea
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupFooterButtons.tsx
@@ -0,0 +1,7 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+import { ButtonGroup } from '@rocket.chat/fuselage';
+
+const VideoConfPopupFooterButtons = ({ children }: { children: ReactNode }) => <ButtonGroup width='full' stretch>{children}</ButtonGroup>
+
+export default VideoConfPopupFooterButtons;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9c30f2cc27795671feedadab8aaf7c3abaea5188
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupIndicators.tsx
@@ -0,0 +1,7 @@
+import React from 'react';
+import { Box } from '@rocket.chat/fuselage';
+
+const VideoConfPopupIndicators = ({ current, total }: { current: number; total: number }) => 
+  <Box mbs='x8' fontScale='micro' color='neutral-700'>{current} of {total}</Box>
+
+export default VideoConfPopupIndicators;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e565cfe45d471e1d940de586bbc103b3ae0843cd
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupTitle.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import type { ComponentProps } from 'react';
+import { Box, Icon, Throbber } from '@rocket.chat/fuselage';
+
+type VideoConfPopupTitleProps = {
+  text: string;
+  counter?: boolean; 
+  icon?: ComponentProps<typeof Icon>['name'];
+};
+
+const VideoConfPopupTitle = ({ text, counter = false, icon }: VideoConfPopupTitleProps) => {
+  return (
+    <Box mbs='x8' display='flex' alignItems='center'>
+      {icon && <Icon size='x20' name={icon} />}
+      <Box mis='x4' fontScale='p1b'>
+        {text} 
+      </Box>
+      {counter && <Throbber size='x8' mis='x4' inheritColor />}
+    </Box>
+  );
+}
+
+export default VideoConfPopupTitle;
diff --git a/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..809688803e00eaf9708b5be9e8d99162d4895389
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/VideoConfPopupUsername.tsx
@@ -0,0 +1,18 @@
+import { Box } from '@rocket.chat/fuselage';
+import type { ReactElement } from 'react';
+import React from 'react';
+
+const VideoConfPopupUsername = ({ name, username }: { name?: string; username: string }): ReactElement => (
+	<Box mis='x8' display='flex'>
+    <Box>{name || username}</Box>
+    {name && (
+      <Box mis='x4' color='neutral-600'>
+        {`(${username})`}
+      </Box>
+    )}
+  </Box>
+);
+
+export default VideoConfPopupUsername;
+
+
diff --git a/packages/ui-video-conf/src/VideoConfPopup/index.ts b/packages/ui-video-conf/src/VideoConfPopup/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba9488dd26d4de90c8b519d439838981f54fc97e
--- /dev/null
+++ b/packages/ui-video-conf/src/VideoConfPopup/index.ts
@@ -0,0 +1,23 @@
+import VideoConfPopup from './VideoConfPopup';
+import VideoConfPopupContent from './VideoConfPopupContent';
+import VideoConfPopupBackdrop from './VideoConfPopupBackdrop';
+import VideoConfPopupControllers from './VideoConfPopupControllers';
+import VideoConfPopupFooter from './VideoConfPopupFooter';
+import VideoConfPopupFooterButtons from './VideoConfPopupFooterButtons';
+import VideoConfPopupTitle from './VideoConfPopupTitle';
+import VideoConfPopupIndicators from './VideoConfPopupIndicators';
+import VideoConfPopupClose from './VideoConfPopupClose';
+import VideoConfPopupUsername from './VideoConfPopupUsername';
+
+export {
+  VideoConfPopup,
+  VideoConfPopupContent,
+  VideoConfPopupTitle,
+  VideoConfPopupBackdrop,
+  VideoConfPopupControllers,
+  VideoConfPopupIndicators,
+  VideoConfPopupClose,
+  VideoConfPopupUsername,
+  VideoConfPopupFooter,
+  VideoConfPopupFooterButtons,
+}
diff --git a/packages/ui-video-conf/src/hooks/index.ts b/packages/ui-video-conf/src/hooks/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60256b341935701d6127dd4fda6bf9d93c83e330
--- /dev/null
+++ b/packages/ui-video-conf/src/hooks/index.ts
@@ -0,0 +1 @@
+export * from './useVideoConfControllers';
diff --git a/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts b/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e44a35938f18a030d187a895ab76a4653a1116b
--- /dev/null
+++ b/packages/ui-video-conf/src/hooks/useVideoConfControllers.ts
@@ -0,0 +1,17 @@
+import { useCallback, useState } from 'react';
+
+export const useVideoConfControllers = (initialPreferences: { mic?: boolean; cam?: boolean } = { mic: true, cam: false }) => {
+  const [controllersConfig, setControllersConfig] = useState(initialPreferences);
+
+  const handleToggleMic = useCallback((): void => {
+    setControllersConfig((prevState) => ({ ...prevState, mic: !prevState.mic }));
+  }, []);
+
+  const handleToggleCam = useCallback((): void => {
+    setControllersConfig((prevState) => ({ ...prevState, cam: !prevState.cam }));
+  }, []);
+
+  return {
+    controllersConfig, handleToggleMic, handleToggleCam
+  }
+}
diff --git a/packages/ui-video-conf/src/index.ts b/packages/ui-video-conf/src/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02d4cb2b515564b104a3dfd4e844bbebce8dd7d3
--- /dev/null
+++ b/packages/ui-video-conf/src/index.ts
@@ -0,0 +1,6 @@
+import VideoConfButton from './VideoConfButton';
+import VideoConfController from './VideoConfController';
+
+export * from './VideoConfPopup';
+export * from './hooks';
+export { VideoConfButton, VideoConfController }
diff --git a/packages/ui-video-conf/tsconfig.json b/packages/ui-video-conf/tsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..455edb8149c4a529e5abc6b2a3f4c381b8c96be6
--- /dev/null
+++ b/packages/ui-video-conf/tsconfig.json
@@ -0,0 +1,8 @@
+{
+	"extends": "../../tsconfig.base.json",
+	"compilerOptions": {
+		"rootDir": "./src",
+		"outDir": "./dist"
+	},
+	"include": ["./src/**/*"]
+}
diff --git a/yarn.lock b/yarn.lock
index 007abe31ae09abd157ba779e2cf1b3e1f48c62f0..04175e85b2396332dcbdc3d59089d463c0f566c6 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3327,20 +3327,6 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@rocket.chat/apps-engine@npm:1.33.0-alpha.6456":
-  version: 1.33.0-alpha.6456
-  resolution: "@rocket.chat/apps-engine@npm:1.33.0-alpha.6456"
-  dependencies:
-    adm-zip: ^0.5.9
-    cryptiles: ^4.1.3
-    lodash.clonedeep: ^4.5.0
-    semver: ^5.7.1
-    stack-trace: 0.0.10
-    uuid: ^3.4.0
-  checksum: 18a27f4573affb687ec3c0befe2d6282920929f5283f376eddcdbeea30cef385af3a9f0b9d7a7d1bf89545dda4a76ae079d521d604dc0db5d9e3dfd82b63ccc6
-  languageName: node
-  linkType: hard
-
 "@rocket.chat/apps-engine@npm:^1.31.0":
   version: 1.32.0
   resolution: "@rocket.chat/apps-engine@npm:1.32.0"
@@ -3355,6 +3341,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rocket.chat/apps-engine@npm:alpha":
+  version: 1.33.0-alpha.6487
+  resolution: "@rocket.chat/apps-engine@npm:1.33.0-alpha.6487"
+  dependencies:
+    adm-zip: ^0.5.9
+    cryptiles: ^4.1.3
+    lodash.clonedeep: ^4.5.0
+    semver: ^5.7.1
+    stack-trace: 0.0.10
+    uuid: ^3.4.0
+  checksum: 218833239b0b3fcd4ec5d185489b910f4376da9cfef3ce19e4cf121bd156c2f23eef0b1c226c6fabae22a7e84d3a196b3a9fdb26fe3d31ab4c404f1785ce5fe5
+  languageName: node
+  linkType: hard
+
 "@rocket.chat/core-typings@workspace:^, @rocket.chat/core-typings@workspace:packages/core-typings":
   version: 0.0.0-use.local
   resolution: "@rocket.chat/core-typings@workspace:packages/core-typings"
@@ -3384,7 +3384,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@rocket.chat/css-in-js@npm:^0.31.14":
+"@rocket.chat/css-in-js@npm:^0.31.14, @rocket.chat/css-in-js@npm:~0.31.14":
   version: 0.31.14
   resolution: "@rocket.chat/css-in-js@npm:0.31.14"
   dependencies:
@@ -3616,6 +3616,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rocket.chat/fuselage-hooks@npm:~0.31.14":
+  version: 0.31.14
+  resolution: "@rocket.chat/fuselage-hooks@npm:0.31.14"
+  dependencies:
+    use-sync-external-store: ~1.2.0
+  peerDependencies:
+    "@rocket.chat/fuselage-tokens": "*"
+    react: ^17.0.2
+  checksum: 7f298d7094f9afacc48357db0d472d4d4f6f875554f51e5c41e4e202b24e1e3a1ed00c5ec6b8a281fbfde1fd9d4ce7a57e20bad7e87d81fe0bfe942e8e113653
+  languageName: node
+  linkType: hard
+
 "@rocket.chat/fuselage-hooks@npm:~0.31.14-dev.1":
   version: 0.31.14-dev.1
   resolution: "@rocket.chat/fuselage-hooks@npm:0.31.14-dev.1"
@@ -3738,7 +3750,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@rocket.chat/fuselage@npm:^0.31.14":
+"@rocket.chat/fuselage@npm:^0.31.14, @rocket.chat/fuselage@npm:~0.31.14":
   version: 0.31.14
   resolution: "@rocket.chat/fuselage@npm:0.31.14"
   dependencies:
@@ -4018,7 +4030,7 @@ __metadata:
     "@nivo/pie": 0.73.0
     "@playwright/test": ^1.21.1
     "@rocket.chat/api-client": "workspace:^"
-    "@rocket.chat/apps-engine": 1.33.0-alpha.6456
+    "@rocket.chat/apps-engine": alpha
     "@rocket.chat/core-typings": "workspace:^"
     "@rocket.chat/css-in-js": ~0.31.12
     "@rocket.chat/emitter": ~0.31.12
@@ -4048,6 +4060,7 @@ __metadata:
     "@rocket.chat/ui-client": "workspace:^"
     "@rocket.chat/ui-contexts": "workspace:^"
     "@rocket.chat/ui-kit": ~0.32.0-dev.0
+    "@rocket.chat/ui-video-conf": "workspace:^"
     "@settlin/spacebars-loader": ^1.0.9
     "@slack/client": ^4.12.0
     "@slack/rtm-api": ^6.0.0
@@ -4121,6 +4134,7 @@ __metadata:
     "@types/supertest": ^2.0.11
     "@types/ua-parser-js": ^0.7.36
     "@types/underscore.string": 0.0.38
+    "@types/use-subscription": ^1.0.0
     "@types/use-sync-external-store": ^0.0.3
     "@types/uuid": ^8.3.4
     "@types/xml-crypto": ^1.4.1
@@ -4297,6 +4311,7 @@ __metadata:
     underscore.string: ^3.3.6
     universal-perf-hooks: ^1.0.1
     url-polyfill: ^1.1.12
+    use-subscription: ~1.6.0
     use-sync-external-store: ^1.2.0
     uuid: ^8.3.2
     vm2: ^3.9.9
@@ -4431,7 +4446,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@rocket.chat/styled@npm:^0.31.14":
+"@rocket.chat/styled@npm:^0.31.14, @rocket.chat/styled@npm:~0.31.14":
   version: 0.31.14
   resolution: "@rocket.chat/styled@npm:0.31.14"
   dependencies:
@@ -4588,6 +4603,30 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@rocket.chat/ui-video-conf@workspace:^, @rocket.chat/ui-video-conf@workspace:packages/ui-video-conf":
+  version: 0.0.0-use.local
+  resolution: "@rocket.chat/ui-video-conf@workspace:packages/ui-video-conf"
+  dependencies:
+    "@rocket.chat/css-in-js": ~0.31.14
+    "@rocket.chat/eslint-config": "workspace:^"
+    "@rocket.chat/fuselage": ~0.31.14
+    "@rocket.chat/fuselage-hooks": ~0.31.14
+    "@rocket.chat/styled": ~0.31.14
+    "@types/jest": ^27.4.1
+    eslint: ^8.12.0
+    jest: ^27.5.1
+    ts-jest: ^27.1.4
+    typescript: ~4.3.5
+  peerDependencies:
+    "@rocket.chat/css-in-js": "*"
+    "@rocket.chat/fuselage": "*"
+    "@rocket.chat/fuselage-hooks": "*"
+    "@rocket.chat/styled": "*"
+    react: ^17.0.2
+    react-dom: ^17.0.2
+  languageName: unknown
+  linkType: soft
+
 "@selderee/plugin-htmlparser2@npm:^0.6.0":
   version: 0.6.0
   resolution: "@selderee/plugin-htmlparser2@npm:0.6.0"
@@ -8341,6 +8380,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/use-subscription@npm:^1.0.0":
+  version: 1.0.0
+  resolution: "@types/use-subscription@npm:1.0.0"
+  checksum: 47fff868682692ecda7110bd04ba4c5b1324854c0bcccc765606a42d4bd9be475207413c8829a883b98e7edd801100df53876da0ff89ac21a8f964e440636ef2
+  languageName: node
+  linkType: hard
+
 "@types/use-sync-external-store@npm:^0.0.3":
   version: 0.0.3
   resolution: "@types/use-sync-external-store@npm:0.0.3"
@@ -9302,7 +9348,7 @@ __metadata:
     human-interval: ~1.0.0
     moment-timezone: ~0.5.27
     mongodb: ~3.5.0
-  checksum: cc8c1bbba7545628d9d039c58e701ff65cf07f241f035b731716eec0d5ef906ce09d60c3b321bbfb9e6c641994d1afd23aaeb92d645b33bf7be9942f13574173
+  checksum: acb4ebb7e7356f6e53e810d821eb6aa3d88bbfb9e85183e707517bee6d1eea1f189f38bdf0dd2b91360492ab7643134d510c320d2523d86596498ab98e59735b
   languageName: node
   linkType: hard
 
@@ -32960,6 +33006,15 @@ __metadata:
   languageName: node
   linkType: hard
 
+"use-subscription@npm:~1.6.0":
+  version: 1.6.0
+  resolution: "use-subscription@npm:1.6.0"
+  peerDependencies:
+    react: ^18.0.0
+  checksum: 0ce15a3ed1f66f78cb2d7a83ff4980e2ac93054b0bcbdc4e492e77fed599233372aaaf9c9fd9b370484c491c5aee0b980859db1108ad63ee4cdba0f468c4f5d1
+  languageName: node
+  linkType: hard
+
 "use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:~1.2.0":
   version: 1.2.0
   resolution: "use-sync-external-store@npm:1.2.0"