From 8d205dd767849bfad3a7460c5a562ded662f4835 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Thu, 3 Dec 2020 00:22:46 -0300 Subject: [PATCH 1/7] fix storybook problem --- client/components/basic/ScrollableContentWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/basic/ScrollableContentWrapper.tsx b/client/components/basic/ScrollableContentWrapper.tsx index c14c8cca4b4c..e5b123271f35 100644 --- a/client/components/basic/ScrollableContentWrapper.tsx +++ b/client/components/basic/ScrollableContentWrapper.tsx @@ -15,7 +15,7 @@ type CustomScrollbarsProps = { const ScrollableContentWrapper: FC = React.memo(React.forwardRef(({ onScroll, children }, ref) => { const dir = useDir(); - return ; + return ; })); export default ScrollableContentWrapper; From b26e33ec54e1cbfb624b0dda1324ece3f1b6ef69 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Thu, 3 Dec 2020 23:45:03 -0300 Subject: [PATCH 2/7] [WIP] implement call component --- app/videobridge/client/tabBar.js | 4 +- client/channel/Call/Jitsi/CallJitsi.js | 119 ++++++++++++++++++ .../channel/Call/Jitsi/CallJitsi.stories.js | 16 +++ client/channel/Call/Jitsi/index.js | 3 + client/channel/adapters.js | 8 ++ 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 client/channel/Call/Jitsi/CallJitsi.js create mode 100644 client/channel/Call/Jitsi/CallJitsi.stories.js create mode 100644 client/channel/Call/Jitsi/index.js diff --git a/app/videobridge/client/tabBar.js b/app/videobridge/client/tabBar.js index 99f9ef9f75ed..fb805fc0d9ac 100644 --- a/app/videobridge/client/tabBar.js +++ b/app/videobridge/client/tabBar.js @@ -32,6 +32,7 @@ Meteor.startup(function() { icon: 'phone', iconColor: 'red', template: 'videoFlexTabBbb', + full: true, width: 600, order: live ? -1 : 0, class: () => live && 'live', @@ -46,7 +47,8 @@ Meteor.startup(function() { i18nTitle: 'Call', icon: 'phone', iconColor: 'red', - template: 'videoFlexTab', + template: 'CallJitsi', + full: true, width: 600, order: 0, }); diff --git a/client/channel/Call/Jitsi/CallJitsi.js b/client/channel/Call/Jitsi/CallJitsi.js new file mode 100644 index 000000000000..bd18440a1de0 --- /dev/null +++ b/client/channel/Call/Jitsi/CallJitsi.js @@ -0,0 +1,119 @@ +import React, { useRef, useEffect } from 'react'; +import { Meteor } from 'meteor/meteor'; +import { Box } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; + +import { useUserSubscription } from '../../../contexts/UserContext'; +import { useTranslation } from '../../../contexts/TranslationContext'; +import { useSettings } from '../../../contexts/SettingsContext'; +import VerticalBar from '../../../components/basic/VerticalBar'; + + +export const CallJitsi = ({ + handleClose, + handleWindow, + refContent, +}) => { + const t = useTranslation(); + + let content; + + if (handleWindow) { + content = <> + {t('Video_Conference')} + {t('Opened_in_a_new_window')} + ; + } else { + content = ; + } + + return <> + + + {t('Call')} + {handleClose && } + + + {content} + + ; +}; + +export default React.memo(({ tabBar, rid }) => { + const handleClose = useMutableCallback(() => tabBar && tabBar.close()); + + const ref = useRef(); + const settings = useSettings({ + _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', + ], + }).map((setting) => { + const mapped = {}; + mapped[setting._id] = setting.value; + return mapped; + }).reduce((acc, cur) => ({ ...acc, ...cur })); + const subscription = useUserSubscription(); + + const isOpenNewWindow = settings.Jitsi_Open_New_Window; + + const domain = settings.Jitsi_Domain; + let rname = settings.uniqueID + rid; + + if (settings.Jitsi_URL_Room_Hash) { + rname = settings.uniqueID + rid; + } else { + rname = encodeURIComponent(subscription.t === 'd' ? subscription.usernames.join(' x ') : subscription.name); + } + + const jitsiRoom = settings.Jitsi_URL_Room_Prefix + rname + settings.Jitsi_URL_Room_Suffix; + const width = 'auto'; + const height = 500; + const configOverwrite = { + desktopSharingChromeExtId: settings.Jitsi_Chrome_Extension, + }; + const interfaceConfigOverwrite = {}; + const noSsl = !settings.Jitsi_SSL; + const isEnabledTokenAuth = settings.Jitsi_Enabled_TokenAuth; + const accessToken = isEnabledTokenAuth && Meteor.call('jitsi:generateAccessToken', rid); + + + useEffect(() => { + if (isOpenNewWindow) { + const queryString = accessToken ? `?jwt=${ accessToken }` : ''; + const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }${ queryString }`, jitsiRoom); + + if (newWindow) { + const closeInterval = setInterval(() => { + if (newWindow.closed === false) { + return; + } + + tabBar.close(); + clearInterval(closeInterval); + }, 300); + return newWindow.focus(); + } + } + + if (typeof JitsiMeetExternalAPI !== 'undefined') { + const api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, ref.current, configOverwrite, interfaceConfigOverwrite, noSsl, accessToken); // eslint-disable-line no-undef + api.executeCommand('displayName', [rname]); + } + }); + + return ( + + ); +}); diff --git a/client/channel/Call/Jitsi/CallJitsi.stories.js b/client/channel/Call/Jitsi/CallJitsi.stories.js new file mode 100644 index 000000000000..2ad551b10b06 --- /dev/null +++ b/client/channel/Call/Jitsi/CallJitsi.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { Box } from '@rocket.chat/fuselage'; + +import VerticalBar from '../../../components/basic/VerticalBar'; +import { CallJitsi } from './CallJitsi'; + +export default { + title: 'components/basic/CallJitsi', + component: CallJitsi, +}; + +export const Default = () => + + + +; diff --git a/client/channel/Call/Jitsi/index.js b/client/channel/Call/Jitsi/index.js new file mode 100644 index 000000000000..c9e4bb897407 --- /dev/null +++ b/client/channel/Call/Jitsi/index.js @@ -0,0 +1,3 @@ +import CallJitsi from './CallJitsi'; + +export default CallJitsi; diff --git a/client/channel/adapters.js b/client/channel/adapters.js index c7a958e3f551..14db7d944c88 100644 --- a/client/channel/adapters.js +++ b/client/channel/adapters.js @@ -41,3 +41,11 @@ createTemplateForComponent('EditInvite', () => import('../views/room/RoomMembers createTemplateForComponent('OTR', () => import('../views/room/ContextualBar/OTR'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap }); + +createTemplateForComponent('CallJitsi', () => import('./Call/Jitsi'), { + renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap +}); + +// createTemplateForComponent('CallBbb', () => import('./Call/Bbb'), { +// renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap +// }); From 91aa553203ee891e8b68c6b606a114ce0382b26e Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Mon, 7 Dec 2020 09:16:56 -0300 Subject: [PATCH 3/7] Jitsi Call component --- app/videobridge/client/index.js | 2 - .../client/views/videoFlexTab.html | 16 -- app/videobridge/client/views/videoFlexTab.js | 181 ------------------ client/channel/Call/Jitsi/CallJitsi.js | 42 +++- .../Call/Jitsi/components/CallModal.js | 28 +++ 5 files changed, 64 insertions(+), 205 deletions(-) delete mode 100644 app/videobridge/client/views/videoFlexTab.html delete mode 100644 app/videobridge/client/views/videoFlexTab.js create mode 100644 client/channel/Call/Jitsi/components/CallModal.js diff --git a/app/videobridge/client/index.js b/app/videobridge/client/index.js index 1e30d9f29985..cc0c244027a3 100644 --- a/app/videobridge/client/index.js +++ b/app/videobridge/client/index.js @@ -1,8 +1,6 @@ import './stylesheets/video.css'; -import './views/videoFlexTab.html'; import './views/bbbLiveView.html'; import './views/videoFlexTabBbb.html'; -import './views/videoFlexTab'; import './views/videoFlexTabBbb'; import './tabBar'; import './actionLink'; diff --git a/app/videobridge/client/views/videoFlexTab.html b/app/videobridge/client/views/videoFlexTab.html deleted file mode 100644 index c81e0f8b31e4..000000000000 --- a/app/videobridge/client/views/videoFlexTab.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/app/videobridge/client/views/videoFlexTab.js b/app/videobridge/client/views/videoFlexTab.js deleted file mode 100644 index a89b2041074b..000000000000 --- a/app/videobridge/client/views/videoFlexTab.js +++ /dev/null @@ -1,181 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Tracker } from 'meteor/tracker'; -import { Session } from 'meteor/session'; -import { Template } from 'meteor/templating'; -import { TimeSync } from 'meteor/mizzao:timesync'; - -import { settings } from '../../../settings'; -import { modal, TabBar, call } from '../../../ui-utils/client'; -import { t } from '../../../utils/client'; -import { Users, Rooms } from '../../../models'; -import * as CONSTANTS from '../../constants'; - -Template.videoFlexTab.helpers({ - openInNewWindow() { - return settings.get('Jitsi_Open_New_Window'); - }, -}); - -Template.videoFlexTab.onCreated(function() { - this.tabBar = Template.currentData().tabBar; -}); -Template.videoFlexTab.onDestroyed(function() { - return this.stop && this.stop(); -}); - -Template.videoFlexTab.onRendered(function() { - this.api = null; - - const rid = Session.get('openedRoom'); - - const width = 'auto'; - const height = 500; - - const configOverwrite = { - desktopSharingChromeExtId: settings.get('Jitsi_Chrome_Extension'), - }; - const interfaceConfigOverwrite = {}; - - let jitsiRoomActive = null; - - const closePanel = () => { - // Reset things. Should probably be handled better in closeFlex() - $('.flex-tab').css('max-width', ''); - $('.main-content').css('right', ''); - - this.tabBar.close(); - - TabBar.updateButton('video', { class: '' }); - }; - - const stop = () => { - if (this.intervalHandler) { - Meteor.defer(() => this.api && this.api.dispose()); - clearInterval(this.intervalHandler); - } - }; - - this.stop = stop; - - const update = async () => { - const { jitsiTimeout } = Rooms.findOne({ _id: rid }, { fields: { jitsiTimeout: 1 } }); - - if (jitsiTimeout && (TimeSync.serverTime() - new Date(jitsiTimeout) + CONSTANTS.TIMEOUT < CONSTANTS.DEBOUNCE)) { - return; - } - if (Meteor.status().connected) { - return call('jitsi:updateTimeout', rid); - } - closePanel(); - return this.stop(); - }; - - const start = async () => { - try { - const jitsiTimeout = await update(); - if (!jitsiTimeout) { - return; - } - clearInterval(this.intervalHandler); - this.intervalHandler = setInterval(update, CONSTANTS.HEARTBEAT); - TabBar.updateButton('video', { class: 'red' }); - return jitsiTimeout; - } catch (error) { - console.error(error); - closePanel(); - throw error; - } - }; - - modal.open({ - title: t('Video_Conference'), - text: t('Start_video_call'), - type: 'warning', - showCancelButton: true, - confirmButtonText: t('Yes'), - cancelButtonText: t('Cancel'), - html: false, - }, (dismiss) => { - if (!dismiss) { - return closePanel(); - } - this.intervalHandler = null; - this.autorun(async () => { - if (!settings.get('Jitsi_Enabled')) { - return closePanel(); - } - - if (this.tabBar.getState() !== 'opened') { - TabBar.updateButton('video', { class: '' }); - return stop(); - } - - const domain = settings.get('Jitsi_Domain'); - let rname; - if (settings.get('Jitsi_URL_Room_Hash')) { - rname = settings.get('uniqueID') + rid; - } else { - const room = Rooms.findOne({ _id: rid }); - 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 noSsl = !settings.get('Jitsi_SSL'); - const isEnabledTokenAuth = settings.get('Jitsi_Enabled_TokenAuth'); - - if (jitsiRoomActive !== null && jitsiRoomActive !== jitsiRoom) { - jitsiRoomActive = null; - - closePanel(); - - return stop(); - } - - const accessToken = isEnabledTokenAuth && await call('jitsi:generateAccessToken', rid); - - jitsiRoomActive = jitsiRoom; - - if (settings.get('Jitsi_Open_New_Window')) { - return Tracker.nonreactive(async () => { - await start(); - - const queryString = accessToken ? `?jwt=${ accessToken }` : ''; - - const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }${ queryString }`, jitsiRoom); - if (newWindow) { - const closeInterval = setInterval(() => { - if (newWindow.closed === false) { - return; - } - closePanel(); - stop(); - clearInterval(closeInterval); - }, 300); - return newWindow.focus(); - } - }); - } - - if (typeof JitsiMeetExternalAPI !== 'undefined') { - // Keep it from showing duplicates when re-evaluated on variable change. - const name = Users.findOne(Meteor.userId(), { fields: { name: 1 } }); - if (!$('[id^=jitsiConference]').length) { - Tracker.nonreactive(async () => { - await start(); - - this.api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, this.$('.video-container').get(0), configOverwrite, interfaceConfigOverwrite, noSsl, accessToken); - - /* - * Hack to send after frame is loaded. - * postMessage converts to events in the jitsi meet iframe. - * For some reason those aren't working right. - */ - setTimeout(() => this.api.executeCommand('displayName', [name]), 5000); - }); - } - - // Execute any commands that might be reactive. Like name changing. - this.api && this.api.executeCommand('displayName', [name]); - } - }); - }); -}); diff --git a/client/channel/Call/Jitsi/CallJitsi.js b/client/channel/Call/Jitsi/CallJitsi.js index bd18440a1de0..14f997a51587 100644 --- a/client/channel/Call/Jitsi/CallJitsi.js +++ b/client/channel/Call/Jitsi/CallJitsi.js @@ -1,8 +1,10 @@ -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useCallback, useEffect } from 'react'; import { Meteor } from 'meteor/meteor'; import { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { CallModal } from './components/CallModal'; +import { useSetModal } from '../../../contexts/ModalContext'; import { useUserSubscription } from '../../../contexts/UserContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useSettings } from '../../../contexts/SettingsContext'; @@ -41,6 +43,8 @@ export const CallJitsi = ({ export default React.memo(({ tabBar, rid }) => { const handleClose = useMutableCallback(() => tabBar && tabBar.close()); + const setModal = useSetModal(); + const closeModal = useCallback(() => setModal(null), [setModal]); const ref = useRef(); const settings = useSettings({ @@ -76,16 +80,18 @@ export default React.memo(({ tabBar, rid }) => { const jitsiRoom = settings.Jitsi_URL_Room_Prefix + rname + settings.Jitsi_URL_Room_Suffix; const width = 'auto'; const height = 500; - const configOverwrite = { - desktopSharingChromeExtId: settings.Jitsi_Chrome_Extension, - }; - const interfaceConfigOverwrite = {}; const noSsl = !settings.Jitsi_SSL; const isEnabledTokenAuth = settings.Jitsi_Enabled_TokenAuth; const accessToken = isEnabledTokenAuth && Meteor.call('jitsi:generateAccessToken', rid); + const handleYes = useCallback(() => { + closeModal(); + + const configOverwrite = { + desktopSharingChromeExtId: settings.Jitsi_Chrome_Extension, + }; + const interfaceConfigOverwrite = {}; - useEffect(() => { if (isOpenNewWindow) { const queryString = accessToken ? `?jwt=${ accessToken }` : ''; const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }${ queryString }`, jitsiRoom); @@ -107,6 +113,30 @@ export default React.memo(({ tabBar, rid }) => { const api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, ref.current, configOverwrite, interfaceConfigOverwrite, noSsl, accessToken); // eslint-disable-line no-undef api.executeCommand('displayName', [rname]); } + }, [ + accessToken, + domain, + jitsiRoom, + noSsl, + rname, + isOpenNewWindow, + tabBar, + settings.Jitsi_Chrome_Extension, + closeModal, + ]); + + const handleCancel = useCallback(() => { + closeModal(); + tabBar.close(); + }, [closeModal, tabBar]); + + useEffect(() => { + setModal(() => + , + ); }); return ( diff --git a/client/channel/Call/Jitsi/components/CallModal.js b/client/channel/Call/Jitsi/components/CallModal.js new file mode 100644 index 000000000000..880134a9d18b --- /dev/null +++ b/client/channel/Call/Jitsi/components/CallModal.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { Box, Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; + +import { useTranslation } from '../../../../contexts/TranslationContext'; + +export const CallModal = ({ + handleYes, + handleCancel, +}) => { + const t = useTranslation(); + + return + + {t('Video_Conference')} + + + + + {t('Start_video_call')} + + + + + + + + ; +}; From 1fe5f2baee1fb11e7c2f7675f5ee82bd4e8afc15 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 10 Dec 2020 01:48:26 -0300 Subject: [PATCH 4/7] Fix Review --- app/videobridge/.eslintrc | 1 - app/videobridge/client/tabBar.js | 6 - client/views/room/adapters.js | 2 +- .../contextualBar/Call/Jitsi/CallJitsi.js | 239 +++++---- .../Call/Jitsi/CallJitsi.stories.js | 7 +- .../contextualBar/Call/Jitsi/lib/Jitsi.js | 458 +++++++++++++++++ .../Call/Jitsi/lib/JitsiBridge.js | 79 +++ package-lock.json | 5 + package.json | 1 + .../client/public/external_api.js | 462 ------------------ 10 files changed, 684 insertions(+), 576 deletions(-) create mode 100644 client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js create mode 100644 client/views/room/contextualBar/Call/Jitsi/lib/JitsiBridge.js delete mode 100644 public/packages/rocketchat_videobridge/client/public/external_api.js diff --git a/app/videobridge/.eslintrc b/app/videobridge/.eslintrc index 826a8e171654..7073f846c0d7 100644 --- a/app/videobridge/.eslintrc +++ b/app/videobridge/.eslintrc @@ -1,5 +1,4 @@ { "globals": { - "JitsiMeetExternalAPI": false } } \ No newline at end of file diff --git a/app/videobridge/client/tabBar.js b/app/videobridge/client/tabBar.js index fb805fc0d9ac..bc14faa82755 100644 --- a/app/videobridge/client/tabBar.js +++ b/app/videobridge/client/tabBar.js @@ -67,12 +67,6 @@ Meteor.startup(function() { Tracker.autorun(function() { if (settings.get('Jitsi_Enabled')) { - // Load from the jitsi meet instance. - if (typeof JitsiMeetExternalAPI === 'undefined') { - const prefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; - $.getScript(`${ prefix }/packages/rocketchat_videobridge/client/public/external_api.js`); - } - // Compare current time to call started timeout. If its past then call is probably over. if (Session.get('openedRoom')) { const rid = Session.get('openedRoom'); diff --git a/client/views/room/adapters.js b/client/views/room/adapters.js index d359dd322d2f..f53fa7bb8527 100644 --- a/client/views/room/adapters.js +++ b/client/views/room/adapters.js @@ -42,7 +42,7 @@ createTemplateForComponent('OTR', () => import('./contextualBar/OTR'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap }); -createTemplateForComponent('CallJitsi', () => import('./Call/Jitsi'), { +createTemplateForComponent('CallJitsi', () => import('./contextualBar/Call/Jitsi'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap }); diff --git a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js index 0e476b51b829..7ba71ed4110e 100644 --- a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js +++ b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js @@ -1,33 +1,30 @@ -import React, { useRef, useCallback, useEffect } from 'react'; -import { Meteor } from 'meteor/meteor'; -import { Box } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import React, { useRef, useEffect, useState, useMemo, useLayoutEffect } from 'react'; +import { Box, Skeleton } from '@rocket.chat/fuselage'; +import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks'; import { CallModal } from './components/CallModal'; import { useSetModal } from '../../../../../contexts/ModalContext'; -import { useUserSubscription } from '../../../../../contexts/UserContext'; +import { useUserRoom, useUser } from '../../../../../contexts/UserContext'; import { useTranslation } from '../../../../../contexts/TranslationContext'; import { useSettings } from '../../../../../contexts/SettingsContext'; import VerticalBar from '../../../../../components/VerticalBar'; - +import { HEARTBEAT, TIMEOUT, DEBOUNCE } from '../../../../../../app/videobridge/constants'; +import { useMethod } from '../../../../../contexts/ServerContext'; +import { useConnectionStatus } from '../../../../../contexts/ConnectionStatusContext'; +import { JitsiBridge } from './lib/JitsiBridge'; export const CallJitsi = ({ handleClose, - handleWindow, + openNewWindow, refContent, + children, }) => { const t = useTranslation(); - let content; - - if (handleWindow) { - content = <> - {t('Video_Conference')} - {t('Opened_in_a_new_window')} - ; - } else { - content = ; - } + const content = openNewWindow ? <> + {t('Video_Conference')} + {t('Opened_in_a_new_window')} + :
; return <> @@ -37,113 +34,145 @@ export const CallJitsi = ({ {content} + {children} ; }; +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', + ], +}; + export default React.memo(({ tabBar, rid }) => { - const handleClose = useMutableCallback(() => tabBar && tabBar.close()); + const user = useUser(); + const { connected } = useConnectionStatus(); + const [accessToken, setAccessToken] = useSafely(useState()); + const [accepted, setAccepted] = useState(false); + const room = useUserRoom(rid); const setModal = useSetModal(); - const closeModal = useCallback(() => setModal(null), [setModal]); + const handleClose = useMutableCallback(() => tabBar && tabBar.close()); + const closeModal = useMutableCallback(() => setModal(null)); + const generateAccessToken = useMethod('jitsi:generateAccessToken'); + const updateTimeout = useMethod('jitsi:updateTimeout'); - const ref = useRef(); - const settings = useSettings({ - _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', - ], - }).map((setting) => { - const mapped = {}; - mapped[setting._id] = setting.value; - return mapped; - }).reduce((acc, cur) => ({ ...acc, ...cur })); - const subscription = useUserSubscription(); - - const isOpenNewWindow = settings.Jitsi_Open_New_Window; - - const domain = settings.Jitsi_Domain; - let rname = settings.uniqueID + rid; - - if (settings.Jitsi_URL_Room_Hash) { - rname = settings.uniqueID + rid; - } else { - rname = encodeURIComponent(subscription.t === 'd' ? subscription.usernames.join(' x ') : subscription.name); - } - - const jitsiRoom = settings.Jitsi_URL_Room_Prefix + rname + settings.Jitsi_URL_Room_Suffix; - const width = 'auto'; - const height = 500; - const noSsl = !settings.Jitsi_SSL; - const isEnabledTokenAuth = settings.Jitsi_Enabled_TokenAuth; - const accessToken = isEnabledTokenAuth && Meteor.call('jitsi:generateAccessToken', rid); - - const handleYes = useCallback(() => { + const handleCancel = useMutableCallback(() => { closeModal(); + handleClose(); + }); - const configOverwrite = { - desktopSharingChromeExtId: settings.Jitsi_Chrome_Extension, - }; - const interfaceConfigOverwrite = {}; - - if (isOpenNewWindow) { - const queryString = accessToken ? `?jwt=${ accessToken }` : ''; - const newWindow = window.open(`${ (noSsl ? 'http://' : 'https://') + domain }/${ jitsiRoom }${ queryString }`, jitsiRoom); - - if (newWindow) { - const closeInterval = setInterval(() => { - if (newWindow.closed === false) { - return; - } - - tabBar.close(); - clearInterval(closeInterval); - }, 300); - return newWindow.focus(); - } + const ref = useRef(); + + 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(); + return; } + (async () => { + const accessToken = await generateAccessToken(rid); + !ignore && setAccessToken(accessToken); + })(); + return () => { ignore = true; }; + }, [generateAccessToken, isEnabledTokenAuth, rid, setAccessToken]); - if (typeof JitsiMeetExternalAPI !== 'undefined') { - const api = new JitsiMeetExternalAPI(domain, jitsiRoom, width, height, ref.current, configOverwrite, interfaceConfigOverwrite, noSsl, accessToken); // eslint-disable-line no-undef - api.executeCommand('displayName', [rname]); + useEffect(() => { + if (!connected) { + handleClose(); } - }, [ - accessToken, - domain, - jitsiRoom, - noSsl, - rname, - isOpenNewWindow, - tabBar, - settings.Jitsi_Chrome_Extension, - closeModal, - ]); - - const handleCancel = useCallback(() => { - closeModal(); - tabBar.close(); - }, [closeModal, tabBar]); + }, [connected, handleClose]); + + const jitsi = useMemo(() => { + if (isEnabledTokenAuth && ! accessToken) { + return; + } + + const rname = useHashName ? uniqueID + rid : encodeURIComponent(room.t === 'd' ? room.usernames.join(' x ') : room.name); + 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, rid, room.name, room.t, room.usernames, ssl, sufix, uniqueID, useHashName, user.name, user.username]); + + const testAndHandleTimeout = useMutableCallback(() => { + if (new Date() - new Date(room.jitsiTimeout) > TIMEOUT) { + return jitsi.dispose(); + } + + if (new Date() - new Date(room.jitsiTimeout) + TIMEOUT > DEBOUNCE) { + return updateTimeout(rid); + } + }); useEffect(() => { - setModal(() => - , - ); + if (!accepted || !jitsi) { + return; + } + jitsi.start(ref.current); + + updateTimeout(rid); + + jitsi.on('HEARTBEAT', testAndHandleTimeout); + + return () => { + jitsi.off('HEARTBEAT', testAndHandleTimeout); + jitsi.dispose(); + }; + }, [accepted, jitsi, rid, testAndHandleTimeout, updateTimeout]); + + const handleYes = useMutableCallback(() => { + setAccepted(true); }); + useLayoutEffect(() => { + if (!accepted) { + setModal(() => + , + ); + return; + } + closeModal(); + }, [accepted, closeModal, handleCancel, handleYes, setModal]); + return ( + > + {!accepted && } + ); }); diff --git a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.stories.js b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.stories.js index 2b8362f2d3e8..9a25d1edcec1 100644 --- a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.stories.js +++ b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.stories.js @@ -3,6 +3,7 @@ import { Box } from '@rocket.chat/fuselage'; import VerticalBar from '../../../../../components/VerticalBar'; import { CallJitsi } from './CallJitsi'; +import { CallModal } from './components/CallModal'; export default { title: 'components/basic/CallJitsi', @@ -11,6 +12,10 @@ export default { export const Default = () => - + ; + +export const Modal = () => + +; diff --git a/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js b/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js new file mode 100644 index 000000000000..f0f35ac3fb01 --- /dev/null +++ b/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js @@ -0,0 +1,458 @@ +/** + * 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], function(data) { + if (event in this.eventHandlers && typeof this.eventHandlers[event] === 'function') { this.eventHandlers[event].call(null, data); } + }.bind(this)); + } + 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(function() { + self.iframeHolder.removeChild(self.frame); + self.iframeHolder.parentNode.removeChild(self.iframeHolder); + }, 10); +}; + +// function Postis(options) { +// const { scope } = options; +// const targetWindow = options.window; +// const windowForEventListening = options.windowForEventListening || window; +// const listeners = {}; +// let sendBuffer = []; +// const listenBuffer = {}; +// let ready = false; +// const readyMethod = '__ready__'; +// let readynessCheck; + +// const listener = function(event) { +// let data; +// try { +// data = JSON.parse(event.data); +// } catch (e) { +// return; +// } + +// if (data.postis && data.scope === scope) { +// const listenersForMethod = listeners[data.method]; +// if (listenersForMethod) { +// for (let i = 0; i < listenersForMethod.length; i++) { +// listenersForMethod[i].call(null, data.params); +// } +// } else { +// listenBuffer[data.method] = listenBuffer[data.method] || []; +// listenBuffer[data.method].push(data.params); +// } +// } +// }; + +// windowForEventListening.addEventListener('message', listener, false); + +// const postis = { +// listen(method, callback) { +// listeners[method] = listeners[method] || []; +// listeners[method].push(callback); + +// const listenBufferForMethod = listenBuffer[method]; +// if (listenBufferForMethod) { +// const listenersForMethod = listeners[method]; +// for (let i = 0; i < listenersForMethod.length; i++) { +// for (let j = 0; j < listenBufferForMethod.length; j++) { +// listenersForMethod[i].call(null, listenBufferForMethod[j]); +// } +// } +// } +// delete listenBuffer[method]; +// }, + +// send(opts) { +// const { method } = opts; + +// if ((ready || opts.method === readyMethod) && (targetWindow && typeof targetWindow.postMessage === 'function')) { +// targetWindow.postMessage(JSON.stringify({ +// postis: true, +// scope, +// method, +// params: opts.params, +// }), '*'); +// } else { +// sendBuffer.push(opts); +// } +// }, + +// ready(callback) { +// if (ready) { +// callback(); +// } else { +// setTimeout(function() { postis.ready(callback); }, 50); +// } +// }, + +// destroy(callback) { +// clearInterval(readynessCheck); +// ready = false; +// if (windowForEventListening && typeof windowForEventListening.removeEventListener === 'function') { +// windowForEventListening.removeEventListener('message', listener); +// } +// callback && callback(); +// }, +// }; + +// const readyCheckID = `${ +new Date() + Math.random() }`; + +// readynessCheck = setInterval(function() { +// postis.send({ +// method: readyMethod, +// params: readyCheckID, +// }); +// }, 50); + +// postis.listen(readyMethod, function(id) { +// if (id === readyCheckID) { +// clearInterval(readynessCheck); +// ready = true; + +// for (let i = 0; i < sendBuffer.length; i++) { +// postis.send(sendBuffer[i]); +// } +// sendBuffer = []; +// } else { +// postis.send({ +// method: readyMethod, +// params: id, +// }); +// } +// }); + +// return postis; +// } diff --git a/client/views/room/contextualBar/Call/Jitsi/lib/JitsiBridge.js b/client/views/room/contextualBar/Call/Jitsi/lib/JitsiBridge.js new file mode 100644 index 000000000000..8f586a35b9b7 --- /dev/null +++ b/client/views/room/contextualBar/Call/Jitsi/lib/JitsiBridge.js @@ -0,0 +1,79 @@ +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; + } + + start(domTarget) { + 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)); + + 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/package-lock.json b/package-lock.json index e03875d34918..aeb738a3a125 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27789,6 +27789,11 @@ } } }, + "postis": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postis/-/postis-2.2.0.tgz", + "integrity": "sha1-3F4yN2WYXd/cv9r8MUGpVprvdak=" + }, "preact": { "version": "10.5.5", "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.5.tgz", diff --git a/package.json b/package.json index c0922b1aec9b..63c7b77f64ae 100644 --- a/package.json +++ b/package.json @@ -232,6 +232,7 @@ "pdfjs-dist": "^2.4.456", "photoswipe": "^4.1.3", "poplib": "^0.1.7", + "postis": "^2.2.0", "prom-client": "^12.0.0", "prometheus-gc-stats": "^0.6.2", "querystring": "^0.2.0", diff --git a/public/packages/rocketchat_videobridge/client/public/external_api.js b/public/packages/rocketchat_videobridge/client/public/external_api.js deleted file mode 100644 index 9acc01d2cb9b..000000000000 --- a/public/packages/rocketchat_videobridge/client/public/external_api.js +++ /dev/null @@ -1,462 +0,0 @@ -/** - * Implements API class that embeds Jitsi Meet in external applications. - */ - -// var postisInit = require("postis"); - -/** - * The minimum width for the Jitsi Meet frame - * @type {number} - */ -var MIN_WIDTH = 200; -// var MIN_WIDTH = 790; - -/** - * The minimum height for the Jitsi Meet frame - * @type {number} - */ -var MIN_HEIGHT = 300; - -/** - * Last id of api object - * @type {number} - */ -var id = 0; - -/** - * Maps the names of the commands expected by the API with the name of the - * commands expected by jitsi-meet - */ -var 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 - */ -var 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 } - }); -} - -var JitsiMeetExternalAPI; - -/** - * 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 - */ -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 { - var 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; - - var 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; - } - var 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) { - for (var key in object) { - this.executeCommand(key, object[key]); - } -}; - -/** - * 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) { - for (var i in object) { - this.addEventListener(i, object[i]); - } -}; - -/** - * 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], function (data) { - if (event in this.eventHandlers && typeof this.eventHandlers[event] === "function") this.eventHandlers[event].call(null, data); - }.bind(this)); - 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 (var 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(); - var frame = document.getElementById(this.frameName); - if (frame) frame.src = 'about:blank'; - var self = this; - window.setTimeout(function () { - self.iframeHolder.removeChild(self.frame); - self.iframeHolder.parentNode.removeChild(self.iframeHolder); - }, 10); -}; - -function Postis(options) { - var scope = options.scope; - var targetWindow = options.window; - var windowForEventListening = options.windowForEventListening || window; - var listeners = {}; - var sendBuffer = []; - var listenBuffer = {}; - var ready = false; - var readyMethod = "__ready__"; - var readynessCheck; - - var listener = function(event) { - var data; - try { - data = JSON.parse(event.data); - } catch (e) { - return; - } - - if (data.postis && data.scope === scope) { - var listenersForMethod = listeners[data.method]; - if (listenersForMethod) { - for (var i = 0; i < listenersForMethod.length; i++) { - listenersForMethod[i].call(null, data.params); - } - } else { - listenBuffer[data.method] = listenBuffer[data.method] || []; - listenBuffer[data.method].push(data.params); - } - } - }; - - windowForEventListening.addEventListener("message", listener, false); - - var postis = { - listen: function (method, callback) { - listeners[method] = listeners[method] || []; - listeners[method].push(callback); - - var listenBufferForMethod = listenBuffer[method]; - if (listenBufferForMethod) { - var listenersForMethod = listeners[method]; - for (var i = 0; i < listenersForMethod.length; i++) { - for (var j = 0; j < listenBufferForMethod.length; j++) { - listenersForMethod[i].call(null, listenBufferForMethod[j]); - } - } - } - delete listenBuffer[method]; - }, - - send: function (opts) { - var method = opts.method; - - if ((ready || opts.method === readyMethod) && (targetWindow && typeof targetWindow.postMessage === "function")) { - targetWindow.postMessage(JSON.stringify({ - postis: true, - scope: scope, - method: method, - params: opts.params - }), "*"); - } else { - sendBuffer.push(opts); - } - }, - - ready: function (callback) { - if (ready) { - callback(); - } else { - setTimeout(function () { postis.ready(callback); }, 50); - } - }, - - destroy: function (callback) { - clearInterval(readynessCheck); - ready = false; - if (windowForEventListening && typeof windowForEventListening.removeEventListener === "function") { - windowForEventListening.removeEventListener("message", listener); - } - callback && callback(); - } - }; - - var readyCheckID = +new Date() + Math.random() + ""; - - readynessCheck = setInterval(function () { - postis.send({ - method: readyMethod, - params: readyCheckID - }); - }, 50); - - postis.listen(readyMethod, function (id) { - if (id === readyCheckID) { - clearInterval(readynessCheck); - ready = true; - - for (var i = 0; i < sendBuffer.length; i++) { - postis.send(sendBuffer[i]); - } - sendBuffer = []; - } else { - postis.send({ - method: readyMethod, - params: id - }); - } - }); - - return postis; -} From c00aea6fd4c1eff226bb5cfa6a1e01e4e9e47df7 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 10 Dec 2020 02:15:11 -0300 Subject: [PATCH 5/7] Remove dangling .eslintrc file --- app/videobridge/.eslintrc | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 app/videobridge/.eslintrc diff --git a/app/videobridge/.eslintrc b/app/videobridge/.eslintrc deleted file mode 100644 index 7073f846c0d7..000000000000 --- a/app/videobridge/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "globals": { - } -} \ No newline at end of file From a0cde66656086d99abcd7e952e23a0824eb492d4 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 10 Dec 2020 10:16:54 -0300 Subject: [PATCH 6/7] Remove Commented Postis --- .../contextualBar/Call/Jitsi/CallJitsi.js | 8 +- .../contextualBar/Call/Jitsi/lib/Jitsi.js | 113 ------------------ 2 files changed, 5 insertions(+), 116 deletions(-) diff --git a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js index 7ba71ed4110e..831d3f1280a3 100644 --- a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js +++ b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js @@ -53,7 +53,7 @@ const querySettings = { ], }; -export default React.memo(({ tabBar, rid }) => { +const CallJitsWithData = ({ tabBar, rid }) => { const user = useUser(); const { connected } = useConnectionStatus(); const [accessToken, setAccessToken] = useSafely(useState()); @@ -98,7 +98,7 @@ export default React.memo(({ tabBar, rid }) => { return () => { ignore = true; }; }, [generateAccessToken, isEnabledTokenAuth, rid, setAccessToken]); - useEffect(() => { + useLayoutEffect(() => { if (!connected) { handleClose(); } @@ -175,4 +175,6 @@ export default React.memo(({ tabBar, rid }) => { {!accepted && } ); -}); +}; + +export default React.memo(CallJitsWithData); diff --git a/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js b/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js index f0f35ac3fb01..8313af0fef0c 100644 --- a/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js +++ b/client/views/room/contextualBar/Call/Jitsi/lib/Jitsi.js @@ -343,116 +343,3 @@ JitsiMeetExternalAPI.prototype.dispose = function() { self.iframeHolder.parentNode.removeChild(self.iframeHolder); }, 10); }; - -// function Postis(options) { -// const { scope } = options; -// const targetWindow = options.window; -// const windowForEventListening = options.windowForEventListening || window; -// const listeners = {}; -// let sendBuffer = []; -// const listenBuffer = {}; -// let ready = false; -// const readyMethod = '__ready__'; -// let readynessCheck; - -// const listener = function(event) { -// let data; -// try { -// data = JSON.parse(event.data); -// } catch (e) { -// return; -// } - -// if (data.postis && data.scope === scope) { -// const listenersForMethod = listeners[data.method]; -// if (listenersForMethod) { -// for (let i = 0; i < listenersForMethod.length; i++) { -// listenersForMethod[i].call(null, data.params); -// } -// } else { -// listenBuffer[data.method] = listenBuffer[data.method] || []; -// listenBuffer[data.method].push(data.params); -// } -// } -// }; - -// windowForEventListening.addEventListener('message', listener, false); - -// const postis = { -// listen(method, callback) { -// listeners[method] = listeners[method] || []; -// listeners[method].push(callback); - -// const listenBufferForMethod = listenBuffer[method]; -// if (listenBufferForMethod) { -// const listenersForMethod = listeners[method]; -// for (let i = 0; i < listenersForMethod.length; i++) { -// for (let j = 0; j < listenBufferForMethod.length; j++) { -// listenersForMethod[i].call(null, listenBufferForMethod[j]); -// } -// } -// } -// delete listenBuffer[method]; -// }, - -// send(opts) { -// const { method } = opts; - -// if ((ready || opts.method === readyMethod) && (targetWindow && typeof targetWindow.postMessage === 'function')) { -// targetWindow.postMessage(JSON.stringify({ -// postis: true, -// scope, -// method, -// params: opts.params, -// }), '*'); -// } else { -// sendBuffer.push(opts); -// } -// }, - -// ready(callback) { -// if (ready) { -// callback(); -// } else { -// setTimeout(function() { postis.ready(callback); }, 50); -// } -// }, - -// destroy(callback) { -// clearInterval(readynessCheck); -// ready = false; -// if (windowForEventListening && typeof windowForEventListening.removeEventListener === 'function') { -// windowForEventListening.removeEventListener('message', listener); -// } -// callback && callback(); -// }, -// }; - -// const readyCheckID = `${ +new Date() + Math.random() }`; - -// readynessCheck = setInterval(function() { -// postis.send({ -// method: readyMethod, -// params: readyCheckID, -// }); -// }, 50); - -// postis.listen(readyMethod, function(id) { -// if (id === readyCheckID) { -// clearInterval(readynessCheck); -// ready = true; - -// for (let i = 0; i < sendBuffer.length; i++) { -// postis.send(sendBuffer[i]); -// } -// sendBuffer = []; -// } else { -// postis.send({ -// method: readyMethod, -// params: id, -// }); -// } -// }); - -// return postis; -// } From e754845b8d2fd97549f1eef4dc41155dc978e3a9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 9 Feb 2021 23:12:59 -0300 Subject: [PATCH 7/7] BBB --- app/videobridge/client/index.js | 3 - app/videobridge/client/stylesheets/video.css | 18 ---- app/videobridge/client/tabBar.tsx | 8 +- .../client/views/videoFlexTabBbb.html | 16 --- .../client/views/videoFlexTabBbb.js | 64 ----------- app/videobridge/server/methods/bbb.js | 8 +- app/videobridge/server/settings.js | 12 +++ client/views/room/adapters.js | 7 -- .../room/contextualBar/Call/BBB/CallBBB.tsx | 102 ++++++++++++++++++ .../room/contextualBar/Call/BBB/index.js | 3 + .../contextualBar/Call/Jitsi/CallJitsi.js | 5 +- packages/rocketchat-i18n/i18n/en.i18n.json | 4 + 12 files changed, 132 insertions(+), 118 deletions(-) delete mode 100644 app/videobridge/client/stylesheets/video.css delete mode 100644 app/videobridge/client/views/videoFlexTabBbb.html delete mode 100644 app/videobridge/client/views/videoFlexTabBbb.js create mode 100644 client/views/room/contextualBar/Call/BBB/CallBBB.tsx create mode 100644 client/views/room/contextualBar/Call/BBB/index.js diff --git a/app/videobridge/client/index.js b/app/videobridge/client/index.js index cc0c244027a3..e1d57007880e 100644 --- a/app/videobridge/client/index.js +++ b/app/videobridge/client/index.js @@ -1,7 +1,4 @@ -import './stylesheets/video.css'; import './views/bbbLiveView.html'; -import './views/videoFlexTabBbb.html'; -import './views/videoFlexTabBbb'; import './tabBar'; import './actionLink'; import '../lib/messageType'; diff --git a/app/videobridge/client/stylesheets/video.css b/app/videobridge/client/stylesheets/video.css deleted file mode 100644 index f9f00d45035e..000000000000 --- a/app/videobridge/client/stylesheets/video.css +++ /dev/null @@ -1,18 +0,0 @@ -.flex-tab { - .video-chat { - ul { - li { - margin-bottom: 20px; - } - } - } -} - -.video-chat { - .main-video { - iframe { - width: 100%; - min-height: 299px; - } - } -} diff --git a/app/videobridge/client/tabBar.tsx b/app/videobridge/client/tabBar.tsx index 0369a25c018f..55a591c2d6d8 100644 --- a/app/videobridge/client/tabBar.tsx +++ b/app/videobridge/client/tabBar.tsx @@ -7,6 +7,8 @@ import { addAction, ToolboxActionConfig } from '../../../client/views/room/lib/T import { useTranslation } from '../../../client/contexts/TranslationContext'; import Header from '../../../client/components/Header'; +const templateBBB = lazy(() => import('../../../client/views/room/contextualBar/Call/BBB')); + addAction('bbb_video', ({ room }) => { const enabled = useSetting('bigbluebutton_Enabled'); const t = useTranslation(); @@ -28,7 +30,7 @@ addAction('bbb_video', ({ room }) => { id: 'bbb_video', title: 'BBB Video Call', icon: 'phone', - template: 'videoFlexTabBbb', + template: templateBBB, order: live ? -1 : 0, renderAction: (props): React.ReactNode => {live ? ! : null} @@ -37,7 +39,7 @@ addAction('bbb_video', ({ room }) => { } : null), [enabled, groups, live, t]); }); -const template = lazy(() => import('../../../client/views/room/contextualBar/Call/Jitsi')); +const templateJitsi = lazy(() => import('../../../client/views/room/contextualBar/Call/Jitsi')); addAction('video', ({ room }) => { const enabled = useSetting('Jitsi_Enabled'); @@ -61,7 +63,7 @@ addAction('video', ({ room }) => { id: 'video', title: 'Call', icon: 'phone', - template, + template: templateJitsi, full: true, order: live ? -1 : 0, renderAction: (props): React.ReactNode => diff --git a/app/videobridge/client/views/videoFlexTabBbb.html b/app/videobridge/client/views/videoFlexTabBbb.html deleted file mode 100644 index 5a6939e5dc42..000000000000 --- a/app/videobridge/client/views/videoFlexTabBbb.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/app/videobridge/client/views/videoFlexTabBbb.js b/app/videobridge/client/views/videoFlexTabBbb.js deleted file mode 100644 index a12581da3604..000000000000 --- a/app/videobridge/client/views/videoFlexTabBbb.js +++ /dev/null @@ -1,64 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Template } from 'meteor/templating'; - -import { settings } from '../../../settings'; -import { Rooms } from '../../../models'; -import { hasAllPermission } from '../../../authorization'; -import { popout } from '../../../ui-utils'; - -Template.videoFlexTabBbb.helpers({ - openInNewWindow() { - return settings.get('Jitsi_Open_New_Window'); - }, - - live() { - const isLive = Rooms.findOne({ _id: this.rid, 'streamingOptions.type': 'call' }, { fields: { streamingOptions: 1 } }) != null; - - if (isLive === false && popout.context) { - popout.close(); - } - - return isLive; - }, - - callManagement() { - const type = Rooms.findOne({ _id: this.rid }).t; - return type === 'd' || hasAllPermission('call-management') || hasAllPermission('call-management', this.rid); - }, -}); - -Template.videoFlexTabBbb.onCreated(function() { - this.tabBar = Template.currentData().tabBar; -}); - -Template.videoFlexTabBbb.events({ - 'click .js-join-meeting'(e) { - $(e.currentTarget).prop('disabled', true); - Meteor.call('bbbJoin', { rid: this.rid }, (err, result) => { - $(e.currentTarget).prop('disabled', false); - console.log(err, result); - if (result) { - popout.open({ - content: 'bbbLiveView', - data: { - source: result.url, - streamingOptions: result, - canOpenExternal: true, - showVideoControls: false, - }, - onCloseCallback: () => console.log('bye popout'), - }); - } - }); - // Get the link and open the iframe - }, - - 'click .js-end-meeting'(e) { - $(e.currentTarget).prop('disabled', true); - Meteor.call('bbbEnd', { rid: this.rid }, (err, result) => { - // $(e.currentTarget).prop('disabled', false); - console.log(err, result); - }); - // Get the link and open the iframe - }, -}); diff --git a/app/videobridge/server/methods/bbb.js b/app/videobridge/server/methods/bbb.js index b06045271d54..3695cc389cd3 100644 --- a/app/videobridge/server/methods/bbb.js +++ b/app/videobridge/server/methods/bbb.js @@ -111,14 +111,12 @@ Meteor.methods({ const endApiResult = HTTP.get(endApi); if (endApiResult.statusCode !== 200) { - // TODO improve error logging - console.log({ endApiResult }); - return; + saveStreamingOptions(rid, {}); + throw new Meteor.Error(endApiResult); } - const doc = parseString(endApiResult.content); - if (doc.response.returncode[0] === 'FAILED') { + if (['SUCCESS', 'FAILED'].includes(doc.response.returncode[0])) { saveStreamingOptions(rid, {}); } }, diff --git a/app/videobridge/server/settings.js b/app/videobridge/server/settings.js index 17b9daafaef9..ba5bb41cf06d 100644 --- a/app/videobridge/server/settings.js +++ b/app/videobridge/server/settings.js @@ -30,6 +30,18 @@ Meteor.startup(function() { }, }); + + 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', diff --git a/client/views/room/adapters.js b/client/views/room/adapters.js index ec75fed65bbb..b6543c92c417 100644 --- a/client/views/room/adapters.js +++ b/client/views/room/adapters.js @@ -73,13 +73,6 @@ createTemplateForComponent('OTR', () => import('./contextualBar/OTR'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap }); -createTemplateForComponent('CallJitsi', () => import('./contextualBar/Call/Jitsi'), { - renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap -}); - -// createTemplateForComponent('CallBbb', () => import('./Call/Bbb'), { -// renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap -// }); createTemplateForComponent('EditRoomInfo', () => import('./contextualBar/Info/EditRoomInfo'), { renderContainerView: () => HTML.DIV({ class: 'contextual-bar' }), // eslint-disable-line new-cap }); diff --git a/client/views/room/contextualBar/Call/BBB/CallBBB.tsx b/client/views/room/contextualBar/Call/BBB/CallBBB.tsx new file mode 100644 index 000000000000..6c11a7e1573c --- /dev/null +++ b/client/views/room/contextualBar/Call/BBB/CallBBB.tsx @@ -0,0 +1,102 @@ +import React, { FC, useEffect } from 'react'; +import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; + +import { useTranslation } from '../../../../../contexts/TranslationContext'; +import VerticalBar from '../../../../../components/VerticalBar'; +import { useTabBarClose } from '../../../providers/ToolboxProvider'; +import { useSetting } from '../../../../../contexts/SettingsContext'; +import { useRoom } from '../../../providers/RoomProvider'; +import { usePermission } from '../../../../../contexts/AuthorizationContext'; +import { IRoom } from '../../../../../../definition/IRoom'; +import { useMethod } from '../../../../../contexts/ServerContext'; +import { popout } from '../../../../../../app/ui-utils/client'; + + +export const CallBBB: FC <{ + startCall: () => void; + endCall: () => void; + handleClose: () => void; + canManageCall: boolean; + live: boolean; + openNewWindow: boolean; +}> = ({ + handleClose, + canManageCall, + live, + startCall, + endCall, + openNewWindow, + ...props +}) => { + const t = useTranslation(); + return <> + + + {t('Call')} + {handleClose && } + + + { openNewWindow ? <> + {t('Video_Conference')} + {t('Opened_in_a_new_window')} + : null } + + { live && } + { live && canManageCall && } + { !live && canManageCall && } + { !live && !canManageCall && } + + + ; +}; + + +const D: FC<{ rid: IRoom['_id'] }> = ({ 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' || popout.context) { + return; + } + startCall(); + return (): void => { + popout.close(); + }; + }, [room?.streamingOptions?.type, startCall]); + + const canManageCall = room?.t === 'd' || hasCallManagement; + + return void} openNewWindow={openNewWindow} live={room?.streamingOptions?.type === 'call'} endCall={endCall} startCall={startCall} canManageCall={canManageCall} />; +}; + +export default D; diff --git a/client/views/room/contextualBar/Call/BBB/index.js b/client/views/room/contextualBar/Call/BBB/index.js new file mode 100644 index 000000000000..7645698cb045 --- /dev/null +++ b/client/views/room/contextualBar/Call/BBB/index.js @@ -0,0 +1,3 @@ +import CallBBB from './CallBBB'; + +export default CallBBB; diff --git a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js index 831d3f1280a3..697a48ebd264 100644 --- a/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js +++ b/client/views/room/contextualBar/Call/Jitsi/CallJitsi.js @@ -12,6 +12,7 @@ import { HEARTBEAT, TIMEOUT, DEBOUNCE } from '../../../../../../app/videobridge/ import { useMethod } from '../../../../../contexts/ServerContext'; import { useConnectionStatus } from '../../../../../contexts/ConnectionStatusContext'; import { JitsiBridge } from './lib/JitsiBridge'; +import { useTabBarClose } from '../../../providers/ToolboxProvider'; export const CallJitsi = ({ handleClose, @@ -53,14 +54,14 @@ const querySettings = { ], }; -const CallJitsWithData = ({ tabBar, rid }) => { +const CallJitsWithData = ({ rid }) => { const user = useUser(); const { connected } = useConnectionStatus(); const [accessToken, setAccessToken] = useSafely(useState()); const [accepted, setAccepted] = useState(false); const room = useUserRoom(rid); const setModal = useSetModal(); - const handleClose = useMutableCallback(() => tabBar && tabBar.close()); + const handleClose = useTabBarClose(); const closeModal = useMutableCallback(() => setModal(null)); const generateAccessToken = useMethod('jitsi:generateAccessToken'); const updateTimeout = useMethod('jitsi:updateTimeout'); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 313056147a80..f7688c1fdf7a 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -592,6 +592,10 @@ "Backup_codes": "Backup codes", "ban-user": "Ban User", "ban-user_description": "Permission to ban a user from a channel", + "BBB_End_Meeting": "End Meeting", + "BBB_Join_Meeting": "Join Meeting", + "BBB_Start_Meeting": "Start Meeting", + "BBB_You_have_no_permission_to_start_a_call": "You have no permission to start a call", "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.", "Better": "Better",