Skip to content

Commit

Permalink
Jitsi calls opening in external tabs (fix #197)
Browse files Browse the repository at this point in the history
* All Jitsi call tabs presented with two opening options:
  in this window (as usual) and in external browser tab (new)
* meet.jit.si automatically disables the embedding option
  with an explanation (#197)
* Automatic re-use of external browser tabs when switching rooms
  • Loading branch information
edemaine committed Apr 24, 2023
1 parent 06214b2 commit 1afffd2
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 13 deletions.
109 changes: 97 additions & 12 deletions client/TabJitsi.coffee
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React, {useEffect, useRef, useState} from 'react'
import {useTracker} from 'meteor/react-meteor-data'
import {ReactiveVar} from 'meteor/reactive-var'
import useScript from 'react-script-hook'
import Alert from 'react-bootstrap/Alert'
import Button from 'react-bootstrap/Button'
import Card from 'react-bootstrap/Card'
import Row from 'react-bootstrap/Row'
import Col from 'react-bootstrap/Col'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faRedoAlt} from '@fortawesome/free-solid-svg-icons/faRedoAlt'
import {faTimes} from '@fortawesome/free-solid-svg-icons/faTimes'
Expand All @@ -18,12 +21,37 @@ import {Tabs} from '/lib/tabs'
resetJitsiStatusAfter = 30 # seconds
defaultJitsiStatus = ->
joined: false
external: false
audioMuted: false
videoMuted: false
lastJitsiStatus = defaultJitsiStatus()
timeoutJitsiStatus = null
jitsiStartsJoined = (url, possible) ->
if lastJitsiStatus.external and externalWindow? and not externalWindow.closed
# prefer to re-use external tab if already open
externalOpen url
jitsiStatusReset()
false
else if lastJitsiStatus.joined
if possible # don't reset joined state if impossible to actually join
jitsiStatusReset()
true
jitsiStatusReset = ->
jitsiStatusCheckStop()
clean = defaultJitsiStatus()
for key in ['joined', 'external']
lastJitsiStatus[key] = clean[key]
return
numJitsi = 0 # number of joined Jitsi calls

timeoutJitsiStatus = null
jitsiStatusCheckStart = ->
return unless resetJitsiStatusAfter?
jitsiStatusCheckStop()
timeoutJitsiStatus = setTimeout jitsiStatusReset, resetJitsiStatusAfter * 1000
jitsiStatusCheckStop = ->
clearTimeout timeoutJitsiStatus if timeoutJitsiStatus?
timeoutJitsiStatus = null

parseJitsiUrl = (url) ->
return {} unless url?
parsed = new URL url
Expand All @@ -34,17 +62,42 @@ parseJitsiUrl = (url) ->
script = parsed.toString()
{host, roomName, script}

## When Jitsi is opened in an external tab, these are both set.
externalURL = new ReactiveVar() # url for active Jitsi call
externalWindow = null # Window object (for checking .closed)
externalInterval = null # setInterval checking for closed

## Open Jitsi call in external tab
externalOpen = (url) ->
externalURL.set url
externalWindow = window.open url + '#config.prejoinPageEnabled=false', 'jitsi'
externalInterval ?= setInterval ->
externalClosed() if externalWindow.closed
, 1000
return
externalClose = ->
externalWindow?.close()
externalClosed()
externalClosed = ->
clearInterval externalInterval
externalInterval = null
externalWindow = null
externalURL.set null
jitsiStatusCheckStart()

export TabJitsi = React.memo ({tabId, room}) ->
tab = useTracker ->
Tabs.findOne tabId
, [tabId]
{host, roomName, script} = parseJitsiUrl tab?.url
embeddable = (host != 'meet.jit.si')
[loading, error] = useScript
src: script
checkForExisting: true

ref = useRef() # div container for Jitsi iframe
[joined, setJoined] = useState lastJitsiStatus.joined # joined call?
[joined, setJoined] = useState -> # joined call?
jitsiStartsJoined tab?.url, embeddable
[ready, setReady] = useState false # connected to API?
[api, setApi] = useState() # JitsiMeetExternalAPI object
name = useNameWithPronouns()
Expand Down Expand Up @@ -122,19 +175,20 @@ export TabJitsi = React.memo ({tabId, room}) ->
useEffect ->
numJitsi++ if joined
lastJitsiStatus.joined = (numJitsi > 0)
if timeoutJitsiStatus?
clearTimeout timeoutJitsiStatus
timeoutJitsiStatus = null
jitsiStatusCheckStop()
->
numJitsi-- if joined
if numJitsi == 0 and resetJitsiStatusAfter?
timeoutJitsiStatus = setTimeout ->
timeoutJitsiStatus = null
#lastJitsiStatus = defaultJitsiStatus()
lastJitsiStatus.joined = defaultJitsiStatus().joined
, resetJitsiStatusAfter * 1000
jitsiStatusCheckStart() if numJitsi == 0
, [joined]

external = useTracker => (externalURL.get() == tab.url)
useEffect ->
-> # when tab closed
if externalURL.get() == tab.url
# encourage external join in next Jitsi tab
lastJitsiStatus.external = true
, []

return null unless tab
<div ref={ref}>
{if error
Expand All @@ -143,14 +197,45 @@ export TabJitsi = React.memo ({tabId, room}) ->
Is <a href={'https://' + host} target="_blank" rel="noopener">{host}</a> a valid Jitsi server?
</Alert>
else if not joined
join = if external then 'Rejoin' else 'Join'
<Card>
<Card.Body>
<Card.Title>Jitsi Meeting</Card.Title>
<p>
<b>Server:</b> <a href={'https://' + host} target="_blank" rel="noopener"><code>{host}</code></a><br/>
<b>Room ID:</b> <code>{roomName}</code>
</p>
<Button block onClick={-> setJoined true}>Join Call</Button>
<Row>
<Col xs={4}>
<Button block disabled={not embeddable}
onClick={-> externalClose(); setJoined true}>
{join} Call Here
</Button>
</Col>
<Col xs={8}>
{if embeddable
<p>{join} the call within this window.</p>
else
<p>The <code>meet.jit.si</code> server <a href="https://community.jitsi.org/t/important-embedding-meet-jit-si-in-your-web-app-will-no-longer-be-supported-please-use-jaas/123003">no longer supports embedding</a> into Comingle. Instead use:</p>
}
</Col>
</Row>
<Row>
<Col xs={4}>
{if external
<Button block variant="danger" onClick={externalClose}>Leave Call</Button>
else
<Button block onClick={-> externalOpen tab.url}>Join Call in External Tab</Button>
}
</Col>
<Col xs={8}>
{if external
<p>Close the separate browser tab.</p>
else
<p>Join the call within a separate browser tab.</p>
}
</Col>
</Row>
<p>When joining, you may need to grant access to your microphone and/or camera. If you want to try again, select the <FontAwesomeIcon icon={faRedoAlt}/> &ldquo;Reload Tab&rdquo; button at the top of this tab.</p>
<p>If you hang up on the call and receive an ad, click the <FontAwesomeIcon icon={faTimes}/> button (at the top right of the ad) to fully leave the call, and prevent Comingle from automatically joining future Jitsi calls.</p>
</Card.Body>
Expand Down
1 change: 1 addition & 0 deletions client/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ $code-color: #17a2b8; // $cyan
@import "{}/node_modules/bootstrap/scss/type";
//@import "{}/node_modules/bootstrap/scss/images";
@import "{}/node_modules/bootstrap/scss/code";
// We actually use Row and Col but looks better without them
//@import "{}/node_modules/bootstrap/scss/grid";
@import "{}/node_modules/bootstrap/scss/tables";
@import "{}/node_modules/bootstrap/scss/forms";
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"author": {
"name": "Erik Demaine",
"email": "edemaine@mit.edu",
"url": "http://erikdemaine.org/"
"url": "https://erikdemaine.org/"
},
"license": "MIT",
"bugs": {
Expand Down

0 comments on commit 1afffd2

Please sign in to comment.