mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-05 22:32:31 +00:00
Compare commits
34 Commits
android-sd
...
4296
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
335b43036d | ||
|
|
b3ca51c7d0 | ||
|
|
f9d545c531 | ||
|
|
5f5468995f | ||
|
|
bf7aa39947 | ||
|
|
ad948bdbe2 | ||
|
|
29366a0029 | ||
|
|
0bec7c7ab7 | ||
|
|
29805edd02 | ||
|
|
11fd5363ce | ||
|
|
1790c71c80 | ||
|
|
36d95ed51f | ||
|
|
ef0af1a8c0 | ||
|
|
b85cd2348f | ||
|
|
035f720a50 | ||
|
|
c8444a9a0d | ||
|
|
7f5751b918 | ||
|
|
fc6bd3667c | ||
|
|
2c42dd0773 | ||
|
|
1891ce0b24 | ||
|
|
e34c5673b2 | ||
|
|
41ba55a6a9 | ||
|
|
758b60f92b | ||
|
|
fcc69b92bb | ||
|
|
a697caea03 | ||
|
|
5b89709483 | ||
|
|
e4ce3928dc | ||
|
|
b0188a7184 | ||
|
|
53281c2d42 | ||
|
|
3da1b65757 | ||
|
|
0e5091adba | ||
|
|
f376542441 | ||
|
|
bd65108692 | ||
|
|
546b0abe32 |
@@ -411,6 +411,10 @@ function disconnect() {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (!connection) {
|
||||
return onDisconnected();
|
||||
}
|
||||
|
||||
return connection.disconnect().then(onDisconnected, onDisconnected);
|
||||
}
|
||||
|
||||
|
||||
16
config.js
16
config.js
@@ -111,6 +111,11 @@ var config = {
|
||||
// participants and to enable it back a reload is needed.
|
||||
// startSilent: false
|
||||
|
||||
// Sets the preferred target bitrate for the Opus audio codec by setting its
|
||||
// 'maxaveragebitrate' parameter. Currently not available in p2p mode.
|
||||
// Valid values are in the range 6000 to 510000
|
||||
// opusMaxAvgBitrate: 20000,
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
@@ -401,6 +406,15 @@ var config = {
|
||||
// The Amplitude APP Key:
|
||||
// amplitudeAPPKey: '<APP_KEY>'
|
||||
|
||||
// Configuration for the rtcstats server:
|
||||
// In order to enable rtcstats one needs to provide a endpoint url.
|
||||
// rtcstatsEndpoint: wss://rtcstats-server-pilot.jitsi.net/,
|
||||
|
||||
// The interval at which rtcstats will poll getStats, defaults to 1000ms.
|
||||
// If the value is set to 0 getStats won't be polled and the rtcstats client
|
||||
// will only send data related to RTCPeerConnection events.
|
||||
// rtcstatsPolIInterval: 1000
|
||||
|
||||
// Array of script URLs to load as lib-jitsi-meet "analytics handlers".
|
||||
// scriptURLs: [
|
||||
// "libs/analytics-ga.min.js", // google-analytics
|
||||
@@ -508,7 +522,7 @@ var config = {
|
||||
/**
|
||||
External API url used to receive branding specific information.
|
||||
If there is no url set or there are missing fields, the defaults are applied.
|
||||
None of the fieds are mandatory and the response must have the shape:
|
||||
None of the fields are mandatory and the response must have the shape:
|
||||
{
|
||||
// The hex value for the colour used as background
|
||||
backgroundColor: '#fff',
|
||||
|
||||
@@ -33,6 +33,26 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AtlasKit sets a default margin on the rendered modals, so
|
||||
* when the shift-right class is set when the chat opens, we
|
||||
* pad the modal container in order for the modals to be centered
|
||||
* while also taking the chat size into consideration.
|
||||
*/
|
||||
@media (min-width: 480px + $sidebarWidth) {
|
||||
.shift-right [class^="Modal__FillScreen"] {
|
||||
padding-left: $sidebarWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Similarly, we offset the notifications when the chat is open by
|
||||
* padding the container.
|
||||
*/
|
||||
.shift-right [class^="styledFlagGroup-"] {
|
||||
padding-left: $sidebarWidth;
|
||||
}
|
||||
|
||||
.jitsi-icon svg {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
@@ -4,16 +4,11 @@
|
||||
color: #FFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
height: 100%;
|
||||
left: -$sidebarWidth;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transition: left 0.5s;
|
||||
width: $sidebarWidth;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
|
||||
@@ -36,13 +36,7 @@
|
||||
}
|
||||
|
||||
&-checkbox-container {
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
justify-content: center;
|
||||
line-height: 20px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
/**
|
||||
* Shared style for full screen local track based dialogs/modals.
|
||||
*/
|
||||
.premeeting-screen {
|
||||
.premeeting-screen,
|
||||
.preview-overlay {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.premeeting-screen {
|
||||
align-items: stretch;
|
||||
background: #1C2025;
|
||||
bottom: 0;
|
||||
background: radial-gradient(50% 50% at 50% 50%, #5D95C7 0%, #376288 100%), #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 1.3em;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: $toolbarZ + 1;
|
||||
|
||||
.action-btn {
|
||||
@@ -74,9 +78,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.preview-overlay {
|
||||
background-image: linear-gradient(transparent, black);
|
||||
z-index: $toolbarZ + 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
align-items: center;
|
||||
background-image: linear-gradient(transparent, black);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -189,9 +197,16 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preview-avatar-container {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background: #A4B8D1;
|
||||
margin: 200px auto 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
@@ -201,3 +216,66 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin flex-centered() {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin icon-container($bg, $fill) {
|
||||
.toggle-button-icon-container {
|
||||
background: $bg;
|
||||
|
||||
svg {
|
||||
fill: $fill
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
margin: 0 auto;
|
||||
width: 320px;
|
||||
|
||||
@include flex-centered();
|
||||
|
||||
svg {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #1C2025;
|
||||
|
||||
@include icon-container(#A4B8D1, #1C2025);
|
||||
}
|
||||
|
||||
&-container {
|
||||
position: relative;
|
||||
|
||||
@include flex-centered();
|
||||
}
|
||||
|
||||
&-icon-container {
|
||||
border-radius: 50%;
|
||||
left: -22px;
|
||||
padding: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&--toggled {
|
||||
background: #75757A;
|
||||
|
||||
&:hover {
|
||||
background: #75757A;
|
||||
|
||||
@include icon-container(#A4B8D1, #75757A);
|
||||
}
|
||||
|
||||
@include icon-container(#A4B8D1, #75757A);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
|
||||
.toolbox-background {
|
||||
background-image: linear-gradient(to top, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
||||
transition: bottom .3s ease-in;
|
||||
|
||||
@@ -181,6 +181,13 @@
|
||||
visibility: hidden;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.shift-right {
|
||||
&#largeVideoContainer {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
|
||||
@@ -46,7 +46,16 @@
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: $filmstripVideosZ
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
&.shift-right {
|
||||
margin-left: $sidebarWidth;
|
||||
width: calc(100% - #{$sidebarWidth});
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
width: calc(100vw - #{$sidebarWidth});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
font-size: 14px;
|
||||
color: #6FB1EA;
|
||||
}
|
||||
|
||||
& > :first-child:not(:last-child) {
|
||||
margin-right: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-edit-field,
|
||||
.settings-sub-pane {
|
||||
.profile-edit-field {
|
||||
flex: 1;
|
||||
}
|
||||
.settings-sub-pane {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.profile-edit-field {
|
||||
margin-right: 20px;
|
||||
|
||||
29
debian/jitsi-meet-turnserver.postinst
vendored
29
debian/jitsi-meet-turnserver.postinst
vendored
@@ -87,9 +87,36 @@ case "$1" in
|
||||
if [[ -f $TURN_CONFIG ]] ; then
|
||||
echo "------------------------------------------------"
|
||||
echo ""
|
||||
echo "turnserver is already configured on this machine, skipping."
|
||||
echo "turnserver is already configured on this machine."
|
||||
echo ""
|
||||
echo "------------------------------------------------"
|
||||
|
||||
if grep -q "jitsi-meet coturn config" "$TURN_CONFIG" && ! grep -q "jitsi-meet coturn relay disable config" "$TURN_CONFIG" ; then
|
||||
echo "Updating coturn config"
|
||||
echo "# jitsi-meet coturn relay disable config. Do not modify this line
|
||||
no-multicast-peers
|
||||
no-cli
|
||||
no-loopback-peers
|
||||
no-tcp-relay
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=169.254.0.0-169.254.255.255
|
||||
denied-peer-ip=127.0.0.0-127.255.255.255
|
||||
denied-peer-ip=172.16.0.0-172.31.255.255
|
||||
denied-peer-ip=192.0.0.0-192.0.0.255
|
||||
denied-peer-ip=192.0.2.0-192.0.2.255
|
||||
denied-peer-ip=192.88.99.0-192.88.99.255
|
||||
denied-peer-ip=192.168.0.0-192.168.255.255
|
||||
denied-peer-ip=198.18.0.0-198.19.255.255
|
||||
denied-peer-ip=198.51.100.0-198.51.100.255
|
||||
denied-peer-ip=203.0.113.0-203.0.113.255
|
||||
denied-peer-ip=240.0.0.0-255.255.255.255" >> $TURN_CONFIG
|
||||
|
||||
invoke-rc.d coturn restart || true
|
||||
fi
|
||||
|
||||
db_stop
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -17,6 +17,7 @@ no-tlsv1
|
||||
no-tlsv1_1
|
||||
# https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
|
||||
cipher-list=ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
|
||||
# jitsi-meet coturn relay disable config. Do not modify this line
|
||||
denied-peer-ip=0.0.0.0-0.255.255.255
|
||||
denied-peer-ip=10.0.0.0-10.255.255.255
|
||||
denied-peer-ip=100.64.0.0-100.127.255.255
|
||||
|
||||
10
index.html
10
index.html
@@ -8,7 +8,17 @@
|
||||
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!JitsiMeetJS.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({
|
||||
Component: JitsiMeetJS.app.entryPoints.APP
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<script>
|
||||
// IE11 and earlier can be identified via their user agent and be
|
||||
// redirected to a page that is known to have no newer js syntax.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -290,9 +290,9 @@
|
||||
"inviteLiveStream": "この会議のライブストリームを表示するには、このリンクをクリックしてください:{{url}}",
|
||||
"invitePhone": "",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURLFirstPartGeneral": "",
|
||||
"inviteURLFirstPartPersonal": "",
|
||||
"inviteURLSecondPart": "",
|
||||
"inviteURLFirstPartGeneral": "あなたはミーティングに招待されました。",
|
||||
"inviteURLFirstPartPersonal": "{{name}} があなたをミーティングに招待しました。\n",
|
||||
"inviteURLSecondPart": "\nミーティングにご参加ください:\n{{url}}\n",
|
||||
"liveStreamURL": "ライブストリーム:",
|
||||
"moreNumbers": "その他の番号",
|
||||
"noNumbers": "ダイヤルイン番号はありません。",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -639,7 +639,7 @@
|
||||
"raiseHand": "Podnieś / Opuść rękę",
|
||||
"raiseYourHand": "Podnieś rękę",
|
||||
"Settings": "Ustawienia",
|
||||
"sharedvideo": "Udostępnij wideo w Youtube",
|
||||
"sharedvideo": "Udostępnij wideo z Youtube",
|
||||
"shareRoom": "Zaproś kogoś",
|
||||
"shortcuts": "Wyświetl skróty",
|
||||
"speakerStats": "Statystyki mówców",
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
"localaddress_plural": "Local addresses:",
|
||||
"localport": "Local port:",
|
||||
"localport_plural": "Local ports:",
|
||||
"maxEnabledResolution": "send max",
|
||||
"more": "Show more",
|
||||
"packetloss": "Packet loss:",
|
||||
"quality": {
|
||||
@@ -202,6 +203,8 @@
|
||||
"enterDisplayName": "Please enter your name here",
|
||||
"error": "Error",
|
||||
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
||||
"grantModeratorDialog": "Are you sure you want to make this participant a moderator?",
|
||||
"grantModeratorTitle": "Grant moderator",
|
||||
"IamHost": "I am the host",
|
||||
"incorrectRoomLockPassword": "Incorrect password",
|
||||
"incorrectPassword": "Incorrect username or password",
|
||||
@@ -498,6 +501,7 @@
|
||||
"audioAndVideoError": "Audio and video error:",
|
||||
"audioOnlyError": "Audio error:",
|
||||
"audioTrackError": "Could not create audio track.",
|
||||
"calling": "Calling",
|
||||
"callMe": "Call me",
|
||||
"callMeAtNumber": "Call me at this number:",
|
||||
"configuringDevices": "Configuring devices...",
|
||||
@@ -521,7 +525,8 @@
|
||||
"linkCopied": "Link copied to clipboard",
|
||||
"lookGood": "It sounds like your microphone is working properly",
|
||||
"or": "or",
|
||||
"calling": "Calling",
|
||||
"premeeting": "Pre meeting",
|
||||
"showScreen": "Enable pre meeting screen",
|
||||
"startWithPhone": "Start with phone audio",
|
||||
"screenSharingError": "Screen sharing error:",
|
||||
"videoOnlyError": "Video error:",
|
||||
@@ -668,6 +673,7 @@
|
||||
"e2ee": "End-to-End Encryption",
|
||||
"feedback": "Leave feedback",
|
||||
"fullScreen": "Toggle full screen",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"hangup": "Leave the call",
|
||||
"help": "Help",
|
||||
"invite": "Invite people",
|
||||
@@ -816,6 +822,7 @@
|
||||
"domute": "Mute",
|
||||
"domuteOthers": "Mute everyone else",
|
||||
"flip": "Flip",
|
||||
"grantModerator": "Grant Moderator",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
@@ -864,7 +871,7 @@
|
||||
"header": "Help center"
|
||||
},
|
||||
"lobby": {
|
||||
"knockingParticipantList" : "Knocking participant list",
|
||||
"knockingParticipantList": "Knocking participant list",
|
||||
"allow": "Allow",
|
||||
"backToKnockModeButton": "No password, ask to join instead",
|
||||
"dialogTitle": "Lobby mode",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
JitsiParticipantConnectionStatus
|
||||
} from '../../../react/features/base/lib-jitsi-meet';
|
||||
import { VIDEO_TYPE } from '../../../react/features/base/media';
|
||||
import { CHAT_SIZE } from '../../../react/features/chat';
|
||||
import {
|
||||
updateKnownLargeVideoResolution
|
||||
} from '../../../react/features/large-video';
|
||||
@@ -323,7 +324,18 @@ export default class LargeVideoManager {
|
||||
* Update container size.
|
||||
*/
|
||||
updateContainerSize() {
|
||||
this.width = UIUtil.getAvailableVideoWidth();
|
||||
let widthToUse = UIUtil.getAvailableVideoWidth();
|
||||
const { isOpen } = APP.store.getState()['features/chat'];
|
||||
|
||||
if (isOpen) {
|
||||
/**
|
||||
* If chat state is open, we re-compute the container width
|
||||
* by subtracting the default width of the chat.
|
||||
*/
|
||||
widthToUse -= CHAT_SIZE;
|
||||
}
|
||||
|
||||
this.width = widthToUse;
|
||||
this.height = window.innerHeight;
|
||||
}
|
||||
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -10725,8 +10725,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#cd008d726f1f57562eb5d8e6a3cd91c7e69826a0",
|
||||
"from": "github:jitsi/lib-jitsi-meet#cd008d726f1f57562eb5d8e6a3cd91c7e69826a0",
|
||||
"version": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"from": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.0",
|
||||
"@jitsi/sdp-interop": "1.0.3",
|
||||
@@ -10793,9 +10793,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.13",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.13.tgz",
|
||||
"integrity": "sha512-vm3/XWXfWtRua0FkUyEHBZy8kCPjErNBT9fJx8Zvs+U6zjqPbTUOpkaoum3O5uiA8sm+yNMHXfYkTUHFoMxFNA=="
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
@@ -15003,6 +15003,13 @@
|
||||
"sdp": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"rtcstats": {
|
||||
"version": "github:jitsi/rtcstats#02a1a089d9a97d1414d216ff7d9c432253e50190",
|
||||
"from": "github:jitsi/rtcstats#v6.1.3",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.0"
|
||||
}
|
||||
},
|
||||
"run-async": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
|
||||
|
||||
@@ -56,9 +56,9 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#cd008d726f1f57562eb5d8e6a3cd91c7e69826a0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ff1813cbb2e7d36f1cc0bbc2b8baa0a16c6346a4",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.13",
|
||||
"lodash": "4.17.19",
|
||||
"moment": "2.19.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"pixelmatch": "5.1.0",
|
||||
@@ -90,6 +90,7 @@
|
||||
"redux": "4.0.4",
|
||||
"redux-thunk": "2.2.0",
|
||||
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
|
||||
"rtcstats": "github:jitsi/rtcstats#v6.1.3",
|
||||
"styled-components": "3.4.9",
|
||||
"util": "0.12.1",
|
||||
"uuid": "3.1.0",
|
||||
|
||||
@@ -538,6 +538,26 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rtcstats websocket onclose event. We send this to amplitude in order
|
||||
* to detect trace ws prematurely closing.
|
||||
*
|
||||
* @param {Object} closeEvent - The event with which the websocket closed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRTCStatsTraceCloseEvent(closeEvent) {
|
||||
const event = {
|
||||
action: 'trace.onclose',
|
||||
source: 'rtcstats'
|
||||
};
|
||||
|
||||
event.code = closeEvent.code;
|
||||
event.reason = closeEvent.reason;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to video blur
|
||||
* occurred (e.g. It was started or stopped).
|
||||
|
||||
@@ -30,6 +30,16 @@ export function sendAnalytics(event: Object) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return saved amplitude identity info such as session id, device id and user id. We assume these do not change for
|
||||
* the duration of the conference.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getAmplitudeIdentity() {
|
||||
return analytics.amplitudeIdentityProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the analytics adapter to its initial state - removes handlers, cache,
|
||||
* disabled state, etc.
|
||||
@@ -92,6 +102,8 @@ export function createHandlers({ getState }: { getState: Function }) {
|
||||
try {
|
||||
const amplitude = new AmplitudeHandler(handlerConstructorOptions);
|
||||
|
||||
analytics.amplitudeIdentityProps = amplitude.getIdentityProps();
|
||||
|
||||
handlers.push(amplitude);
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {}
|
||||
@@ -117,7 +129,9 @@ export function createHandlers({ getState }: { getState: Function }) {
|
||||
})
|
||||
.catch(e => {
|
||||
analytics.dispose();
|
||||
logger.error(e);
|
||||
if (handlers.length !== 0) {
|
||||
logger.error(e);
|
||||
}
|
||||
|
||||
return [];
|
||||
}));
|
||||
|
||||
@@ -65,4 +65,17 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
this._extractName(event),
|
||||
event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return amplitude identity information.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
return {
|
||||
sessionId: amplitude.getInstance(this._amplitudeOptions).getSessionId(),
|
||||
deviceId: amplitude.getInstance(this._amplitudeOptions).options.deviceId,
|
||||
userId: amplitude.getInstance(this._amplitudeOptions).options.userId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import '../recent-list/middleware';
|
||||
import '../recording/middleware';
|
||||
import '../rejoin/middleware';
|
||||
import '../room-lock/middleware';
|
||||
import '../rtcstats/middleware';
|
||||
import '../subtitles/middleware';
|
||||
import '../toolbox/middleware';
|
||||
import '../transcribing/middleware';
|
||||
|
||||
@@ -122,14 +122,14 @@ export default class BaseApp extends Component<*, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { route: { component }, store } = this.state;
|
||||
const { route: { component, props }, store } = this.state;
|
||||
|
||||
if (store) {
|
||||
return (
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<Provider store = { store }>
|
||||
<Fragment>
|
||||
{ this._createMainElement(component) }
|
||||
{ this._createMainElement(component, props) }
|
||||
<SoundCollection />
|
||||
{ this._createExtraElement() }
|
||||
{ this._renderDialogContainer() }
|
||||
|
||||
@@ -125,6 +125,7 @@ export default [
|
||||
'minParticipants',
|
||||
'nick',
|
||||
'openBridgeChannel',
|
||||
'opusMaxAvgBitrate',
|
||||
'p2p',
|
||||
'pcStatsInterval',
|
||||
'preferH264',
|
||||
|
||||
@@ -25,6 +25,12 @@ export const CALL_INTEGRATION_ENABLED = 'call-integration.enabled';
|
||||
*/
|
||||
export const CLOSE_CAPTIONS_ENABLED = 'close-captions.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if conference timer should be enabled.
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const CONFERENCE_TIMER_ENABLED = 'conference-timer.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if chat should be enabled.
|
||||
* Default: enabled (true).
|
||||
@@ -106,6 +112,12 @@ export const TILE_VIEW_ENABLED = 'tile-view.enabled';
|
||||
*/
|
||||
export const TOOLBOX_ALWAYS_VISIBLE = 'toolbox.alwaysVisible';
|
||||
|
||||
/**
|
||||
* Flag indicating if the video share button should be enabled
|
||||
* Default: enabled (true).
|
||||
*/
|
||||
export const VIDEO_SHARE_BUTTON_ENABLED = 'video-share.enabled';
|
||||
|
||||
/**
|
||||
* Flag indicating if the welcome page should be enabled.
|
||||
* Default: disabled (false).
|
||||
|
||||
3
react/features/base/icons/svg/crown.svg
Normal file
3
react/features/base/icons/svg/crown.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4C14 4.85739 13.4605 5.58876 12.7024 5.87317L14.2286 9.94296L14.9455 11.8546C15.0074 11.9292 15.0708 11.9292 15.1098 11.8902L16.5535 10.4465L18.5858 8.41421C18.2239 8.05228 18 7.55228 18 7C18 5.89543 18.8954 5 20 5C21.1046 5 22 5.89543 22 7C22 8.10457 21.1046 9 20 9C19.9441 9 19.8887 8.9977 19.8339 8.9932L19 19C19 20.1046 18.1046 21 17 21H7C5.89543 21 5 20.1046 5 19L4.1661 8.9932C4.11133 8.9977 4.05593 9 4 9C2.89543 9 2 8.10457 2 7C2 5.89543 2.89543 5 4 5C5.10457 5 6 5.89543 6 7C6 7.55228 5.77614 8.05228 5.41421 8.41421L7.44654 10.4465L8.89019 11.8902C8.9775 11.9325 9.03514 11.9063 9.05453 11.8546L9.77139 9.94296L11.2976 5.87317C10.5395 5.58876 10 4.85739 10 4C10 2.89543 10.8954 2 12 2C13.1046 2 14 2.89543 14 4ZM6.84027 17L6.44651 12.2749L7.47597 13.3044C7.68795 13.5164 7.94285 13.6805 8.22354 13.7858C9.30949 14.193 10.52 13.6428 10.9272 12.5568L12 9.696L13.0728 12.5568C13.1781 12.8375 13.3422 13.0924 13.5542 13.3044C14.3743 14.1245 15.7039 14.1245 16.524 13.3044L17.5535 12.2749L17.1597 17H6.84027Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -23,6 +23,7 @@ export { default as IconClosedCaption } from './closed_caption.svg';
|
||||
export { default as IconConnectionActive } from './gsm-bars.svg';
|
||||
export { default as IconConnectionInactive } from './ninja.svg';
|
||||
export { default as IconCopy } from './copy.svg';
|
||||
export { default as IconCrown } from './crown.svg';
|
||||
export { default as IconDeviceBluetooth } from './bluetooth.svg';
|
||||
export { default as IconDeviceEarpiece } from './phone-talk.svg';
|
||||
export { default as IconDeviceHeadphone } from './headset.svg';
|
||||
|
||||
@@ -12,6 +12,16 @@
|
||||
*/
|
||||
export const DOMINANT_SPEAKER_CHANGED = 'DOMINANT_SPEAKER_CHANGED';
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* {
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }
|
||||
*/
|
||||
export const GRANT_MODERATOR = 'GRANT_MODERATOR';
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
HIDDEN_PARTICIPANT_JOINED,
|
||||
HIDDEN_PARTICIPANT_LEFT,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
@@ -47,6 +48,22 @@ export function dominantSpeakerChanged(id, conference) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @returns {{
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }}
|
||||
*/
|
||||
export function grantModerator(id) {
|
||||
return {
|
||||
type: GRANT_MODERATOR,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
|
||||
@@ -259,6 +259,16 @@ export function getYoutubeParticipant(stateful: Object | Function) {
|
||||
return participants.filter(p => p.isFakeParticipant)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the participant is a moderator.
|
||||
*
|
||||
* @param {string} participant - Participant object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isParticipantModerator(participant: Object) {
|
||||
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all of the meeting participants are moderators.
|
||||
*
|
||||
@@ -269,13 +279,7 @@ export function getYoutubeParticipant(stateful: Object | Function) {
|
||||
export function isEveryoneModerator(stateful: Object | Function) {
|
||||
const participants = _getAllParticipants(stateful);
|
||||
|
||||
for (const participant of participants) {
|
||||
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return participants.every(isParticipantModerator);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@ import { playSound, registerSound, unregisterSound } from '../sounds';
|
||||
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_DISPLAY_NAME_CHANGED,
|
||||
@@ -86,6 +87,13 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case GRANT_MODERATOR: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
conference.grantOwner(action.id);
|
||||
break;
|
||||
}
|
||||
|
||||
case KICK_PARTICIPANT: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getCurrentConferenceUrl } from '../../../connection';
|
||||
import { translate } from '../../../i18n';
|
||||
import { Icon, IconCopy, IconCheck } from '../../../icons';
|
||||
import { connect } from '../../../redux';
|
||||
import { copyText } from '../../../util';
|
||||
import { copyText, getDecodedURI } from '../../../util';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -156,7 +156,7 @@ class CopyMeetingUrl extends Component<Props, State> {
|
||||
className = { `url ${showLinkCopied ? 'done' : ''}` }
|
||||
onClick = { _copyUrl } >
|
||||
<div className = 'copy-meeting-text'>
|
||||
{ !showCopyLink && !showLinkCopied && url }
|
||||
{ !showCopyLink && !showLinkCopied && getDecodedURI(url) }
|
||||
{ showCopyLink && t('prejoin.copyAndShare') }
|
||||
{ showLinkCopied && t('prejoin.linkCopied') }
|
||||
</div>
|
||||
|
||||
@@ -24,11 +24,26 @@ type Props = {
|
||||
*/
|
||||
name?: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Indicates whether the label and copy url action should be shown
|
||||
*/
|
||||
showConferenceInfo: boolean,
|
||||
|
||||
/**
|
||||
* Title of the screen.
|
||||
*/
|
||||
title: string,
|
||||
|
||||
/**
|
||||
* The 'Skip prejoin' button to be rendered (if any).
|
||||
*/
|
||||
skipPrejoinButton?: React$Node,
|
||||
|
||||
/**
|
||||
* True if the preview overlay should be muted, false otherwise.
|
||||
*/
|
||||
@@ -45,13 +60,23 @@ type Props = {
|
||||
* on the prejoin screen (pre-connection) or lobby (post-connection).
|
||||
*/
|
||||
export default class PreMeetingScreen extends PureComponent<Props> {
|
||||
/**
|
||||
* Default values for {@code Prejoin} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
showAvatar: true,
|
||||
showConferenceInfo: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements {@code PureComponent#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { name, title, videoMuted, videoTrack } = this.props;
|
||||
const { name, showAvatar, showConferenceInfo, title, videoMuted, videoTrack } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -59,18 +84,25 @@ export default class PreMeetingScreen extends PureComponent<Props> {
|
||||
id = 'lobby-screen'>
|
||||
<Preview
|
||||
name = { name }
|
||||
showAvatar = { showAvatar }
|
||||
videoMuted = { videoMuted }
|
||||
videoTrack = { videoTrack } />
|
||||
{!videoMuted && <div className = 'preview-overlay' />}
|
||||
<div className = 'content'>
|
||||
<div className = 'title'>
|
||||
{ title }
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
{showConferenceInfo && (
|
||||
<>
|
||||
<div className = 'title'>
|
||||
{ title }
|
||||
</div>
|
||||
<CopyMeetingUrl />
|
||||
</>
|
||||
)}
|
||||
{ this.props.children }
|
||||
<div className = 'media-btn-container'>
|
||||
<AudioSettingsButton visible = { true } />
|
||||
<VideoSettingsButton visible = { true } />
|
||||
</div>
|
||||
{ this.props.skipPrejoinButton }
|
||||
{ this.props.footer }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,11 @@ export type Props = {
|
||||
*/
|
||||
name: string,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
@@ -32,7 +37,7 @@ export type Props = {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function Preview(props: Props) {
|
||||
const { name, videoMuted, videoTrack } = props;
|
||||
const { name, showAvatar, videoMuted, videoTrack } = props;
|
||||
|
||||
if (!videoMuted && videoTrack) {
|
||||
return (
|
||||
@@ -44,19 +49,29 @@ function Preview(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'no-video'
|
||||
id = 'preview'>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
participantId = 'local'
|
||||
size = { 200 } />
|
||||
</div>
|
||||
);
|
||||
if (showAvatar) {
|
||||
return (
|
||||
<div
|
||||
className = 'no-video'
|
||||
id = 'preview'>
|
||||
<div className = 'preview-avatar-container'>
|
||||
<Avatar
|
||||
className = 'preview-avatar'
|
||||
displayName = { name }
|
||||
participantId = 'local'
|
||||
size = { 200 } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Preview.defaultProps = {
|
||||
showAvatar: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconCheck } from '../../../icons';
|
||||
|
||||
const mainClass = 'toggle-button';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Text of the button.
|
||||
*/
|
||||
children: React$Node,
|
||||
|
||||
/**
|
||||
* If the button is toggled or not.
|
||||
*/
|
||||
isToggled?: boolean,
|
||||
|
||||
/**
|
||||
* OnClick button handler.
|
||||
*/
|
||||
onClick: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Button used as a toggle.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function ToggleButton({ children, isToggled, onClick }: Props) {
|
||||
const className = isToggled ? `${mainClass} ${mainClass}--toggled` : mainClass;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
onClick = { onClick }>
|
||||
<div className = 'toggle-button-container'>
|
||||
<div className = 'toggle-button-icon-container'>
|
||||
<Icon
|
||||
className = 'toggle-button-icon'
|
||||
size = { 10 }
|
||||
src = { IconCheck } />
|
||||
</div>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ToggleButton;
|
||||
@@ -3,3 +3,4 @@
|
||||
export { default as ActionButton } from './ActionButton';
|
||||
export { default as InputField } from './InputField';
|
||||
export { default as PreMeetingScreen } from './PreMeetingScreen';
|
||||
export { default as ToggleButton } from './ToggleButton';
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { Store } from 'redux';
|
||||
|
||||
import { equals } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
@@ -37,6 +38,18 @@ type Listener
|
||||
*/
|
||||
type Selector = (state: Object, prevSelection: any) => any;
|
||||
|
||||
/**
|
||||
* Options that can be passed to the register method.
|
||||
*/
|
||||
type RegistrationOptions = {
|
||||
|
||||
/**
|
||||
* @property {boolean} [deepEquals=false] - whether or not a deep equals check should be performed on the selection
|
||||
* returned by {@link Selector}.
|
||||
*/
|
||||
deepEquals: ?boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A type of a {@link Selector}-{@link Listener} association in which the
|
||||
* {@code Listener} listens to changes in the values derived from a redux
|
||||
@@ -50,6 +63,11 @@ type SelectorListener = {
|
||||
*/
|
||||
listener: Listener,
|
||||
|
||||
/**
|
||||
* The {@link RegistrationOptions} passed during the registration to be applied on the listener.
|
||||
*/
|
||||
options: ?RegistrationOptions,
|
||||
|
||||
/**
|
||||
* The {@code Selector} which selects values whose changes are listened to
|
||||
* by {@link listener}.
|
||||
@@ -94,8 +112,10 @@ class StateListenerRegistry {
|
||||
= selectorListener.selector(
|
||||
store.getState(),
|
||||
prevSelection);
|
||||
const useDeepEquals = selectorListener?.options?.deepEquals;
|
||||
|
||||
if (prevSelection !== selection) {
|
||||
if ((useDeepEquals && !equals(prevSelection, selection))
|
||||
|| (!useDeepEquals && prevSelection !== selection)) {
|
||||
prevSelections.set(selectorListener, selection);
|
||||
selectorListener.listener(selection, store, prevSelection);
|
||||
}
|
||||
@@ -117,12 +137,14 @@ class StateListenerRegistry {
|
||||
* @param {Function} listener - The listener to register with this
|
||||
* {@code StateListenerRegistry} so that it gets invoked when the value
|
||||
* returned by the specified {@code selector} changes.
|
||||
* @param {RegistrationOptions} [options] - Any options to be applied to the registration.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(selector: Selector, listener: Listener) {
|
||||
register(selector: Selector, listener: Listener, options: ?RegistrationOptions) {
|
||||
this._selectorListeners.add({
|
||||
listener,
|
||||
selector
|
||||
selector,
|
||||
options
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,11 @@ type Props = {
|
||||
*/
|
||||
_localUserId: string,
|
||||
|
||||
/**
|
||||
* The local participant's role.
|
||||
*/
|
||||
_localUserRole: string,
|
||||
|
||||
/**
|
||||
* Indicates whether or not the test mode is currently on. Otherwise the
|
||||
* TestConnectionInfo component will not render.
|
||||
@@ -179,6 +184,12 @@ class TestConnectionInfo extends Component<Props, State> {
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.conference.joinedState'
|
||||
value = { this.props._conferenceJoinedState } />
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.conference.grantModeratorAvailable'
|
||||
value = { true } />
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.conference.localParticipantRole'
|
||||
value = { this.props._localUserRole } />
|
||||
<TestHint
|
||||
id = 'org.jitsi.meet.stats.rtp'
|
||||
value = { JSON.stringify(this.state.stats) } />
|
||||
@@ -193,12 +204,7 @@ class TestConnectionInfo extends Component<Props, State> {
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _conferenceConnectionState: string,
|
||||
* _conferenceJoinedState: string,
|
||||
* _localUserId: string,
|
||||
* _testMode: boolean
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const conferenceJoined
|
||||
@@ -208,7 +214,8 @@ function _mapStateToProps(state) {
|
||||
return {
|
||||
_conferenceConnectionState: state['features/testing'].connectionState,
|
||||
_conferenceJoinedState: conferenceJoined.toString(),
|
||||
_localUserId: localParticipant && localParticipant.id,
|
||||
_localUserId: localParticipant?.id,
|
||||
_localUserRole: localParticipant?.role,
|
||||
_testMode: isTestModeEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,13 +230,14 @@ export default class AbstractButton<P: Props, S: *> extends Component<P, S> {
|
||||
|
||||
/**
|
||||
* Helper function to be implemented by subclasses, which must return a
|
||||
* {@code boolean} value indicating if this button is toggled or not.
|
||||
* {@code boolean} value indicating if this button is toggled or not or
|
||||
* undefined if the button is not toggleable.
|
||||
*
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
* @returns {?boolean}
|
||||
*/
|
||||
_isToggled() {
|
||||
return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_onClick: (*) => void;
|
||||
|
||||
@@ -12,6 +12,41 @@ import type { Props } from './AbstractToolboxItem';
|
||||
* Web implementation of {@code AbstractToolboxItem}.
|
||||
*/
|
||||
export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
/**
|
||||
* Initializes a new {@code ToolboxItem} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
||||
* We should be handling Space onKeyUp but it conflicts with PTT.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles rendering of the actual item. If the label is being shown, which
|
||||
* is controlled with the `showLabel` prop, the item is rendered for its
|
||||
@@ -27,14 +62,21 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
|
||||
elementAfter,
|
||||
onClick,
|
||||
showLabel,
|
||||
tooltipPosition
|
||||
tooltipPosition,
|
||||
toggled
|
||||
} = this.props;
|
||||
const className = showLabel ? 'overflow-menu-item' : 'toolbox-button';
|
||||
const props = {
|
||||
'aria-pressed': toggled,
|
||||
'aria-disabled': disabled,
|
||||
'aria-label': this.accessibilityLabel,
|
||||
className: className + (disabled ? ' disabled' : ''),
|
||||
onClick: disabled ? undefined : onClick
|
||||
onClick: disabled ? undefined : onClick,
|
||||
onKeyDown: this._onKeyDown,
|
||||
tabIndex: 0,
|
||||
role: 'button'
|
||||
};
|
||||
|
||||
const elementType = showLabel ? 'li' : 'div';
|
||||
const useTooltip = this.tooltip && this.tooltip.length > 0;
|
||||
let children = (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global APP */
|
||||
|
||||
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media';
|
||||
import { MEDIA_TYPE, setAudioMuted } from '../media';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId
|
||||
@@ -125,6 +125,89 @@ export function createLocalTracksF(options = {}, firePermissionPromptIsShownEven
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing a promise which resolves with the created tracks &
|
||||
* the errors resulting from that process.
|
||||
*
|
||||
* @returns {Promise<JitsiLocalTrack>}
|
||||
*
|
||||
* @todo Refactor to not use APP
|
||||
*/
|
||||
export function createPrejoinTracks() {
|
||||
const errors = {};
|
||||
const initialDevices = [ 'audio' ];
|
||||
const requestedAudio = true;
|
||||
let requestedVideo = false;
|
||||
const { startAudioOnly, startWithAudioMuted, startWithVideoMuted } = APP.store.getState()['features/base/settings'];
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics even if the user joins the
|
||||
// conference muted. Previous implementation would only acquire the handle when the user first unmuted,
|
||||
// which would results in statistics ( such as "No audio input" or "Are you trying to speak?") being available
|
||||
// only after that point.
|
||||
if (startWithAudioMuted) {
|
||||
APP.store.dispatch(setAudioMuted(true));
|
||||
}
|
||||
|
||||
if (!startWithVideoMuted && !startAudioOnly) {
|
||||
initialDevices.push('video');
|
||||
requestedVideo = true;
|
||||
}
|
||||
|
||||
let tryCreateLocalTracks;
|
||||
|
||||
if (!requestedAudio && !requestedVideo) {
|
||||
// Resolve with no tracks
|
||||
tryCreateLocalTracks = Promise.resolve([]);
|
||||
} else {
|
||||
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
|
||||
.catch(err => {
|
||||
if (requestedAudio && requestedVideo) {
|
||||
|
||||
// Try audio only...
|
||||
errors.audioAndVideoError = err;
|
||||
|
||||
return (
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, true));
|
||||
} else if (requestedAudio && !requestedVideo) {
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
return [];
|
||||
} else if (requestedVideo && !requestedAudio) {
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
}
|
||||
logger.error('Should never happen');
|
||||
})
|
||||
.catch(err => {
|
||||
// Log this just in case...
|
||||
if (!requestedAudio) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
errors.audioOnlyError = err;
|
||||
|
||||
// Try video only...
|
||||
return requestedVideo
|
||||
? createLocalTracksF({ devices: [ 'video' ] }, true)
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
// Log this just in case...
|
||||
if (!requestedVideo) {
|
||||
logger.error('The impossible just happened', err);
|
||||
}
|
||||
errors.videoOnlyError = err;
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tryCreateLocalTracks,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local audio track.
|
||||
*
|
||||
|
||||
@@ -143,6 +143,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
if (typeof APP !== 'undefined') {
|
||||
const result = next(action);
|
||||
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { jitsiTrack } = action.track;
|
||||
const muted = jitsiTrack.isMuted();
|
||||
const participantID = jitsiTrack.getParticipantId();
|
||||
|
||||
@@ -591,3 +591,13 @@ export function addHashParamsToURL(url: URL, hashParamsToAdd: Object = {}) {
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decoded URI.
|
||||
*
|
||||
* @param {string} uri - The URI to decode.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getDecodedURI(uri: string) {
|
||||
return decodeURI(uri.replace(/^https?:\/\//i, ''));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
@@ -86,14 +88,14 @@ export function setPrivateMessageRecipient(participant: Object) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles display of the chat side panel.
|
||||
* Toggles display of the chat side panel while also taking window
|
||||
* resize into account.
|
||||
*
|
||||
* @returns {{
|
||||
* type: TOGGLE_CHAT
|
||||
* }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleChat() {
|
||||
return {
|
||||
type: TOGGLE_CHAT
|
||||
return function(dispatch: (Object) => Object) {
|
||||
dispatch({ type: TOGGLE_CHAT });
|
||||
VideoLayout.onResize();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import Transition from 'react-transition-group/Transition';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon, IconClose } from '../../../base/icons';
|
||||
@@ -84,11 +83,9 @@ class Chat extends AbstractChat<Props> {
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Transition
|
||||
in = { this.props._isOpen }
|
||||
timeout = { 500 }>
|
||||
{ this._renderPanelContent }
|
||||
</Transition>
|
||||
<>
|
||||
{ this._renderPanelContent() }
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,30 +142,25 @@ class Chat extends AbstractChat<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
_renderPanelContent: (string) => React$Node | null;
|
||||
_renderPanelContent: () => React$Node | null;
|
||||
|
||||
/**
|
||||
* Renders the contents of the chat panel, depending on the current
|
||||
* animation state provided by {@code Transition}.
|
||||
* Renders the contents of the chat panel.
|
||||
*
|
||||
* @param {string} state - The current display transition state of the
|
||||
* {@code Chat} component, as provided by {@code Transition}.
|
||||
* @private
|
||||
* @returns {ReactElement | null}
|
||||
*/
|
||||
_renderPanelContent(state) {
|
||||
this._isExited = state === 'exited';
|
||||
|
||||
_renderPanelContent() {
|
||||
const { _isOpen, _showNamePrompt } = this.props;
|
||||
const ComponentToRender = !_isOpen && state === 'exited'
|
||||
? null
|
||||
: (
|
||||
const ComponentToRender = _isOpen
|
||||
? (
|
||||
<>
|
||||
{ this._renderChatHeader() }
|
||||
{ _showNamePrompt
|
||||
? <DisplayNameForm /> : this._renderChat() }
|
||||
</>
|
||||
);
|
||||
)
|
||||
: null;
|
||||
let className = '';
|
||||
|
||||
if (_isOpen) {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
export const CHAT_VIEW_MODAL_ID = 'chatView';
|
||||
|
||||
/**
|
||||
* The size of the chat.
|
||||
*/
|
||||
export const CHAT_SIZE = 375;
|
||||
|
||||
/**
|
||||
* The audio ID of the audio element for which the {@link playAudio} action is
|
||||
* triggered when new chat message is received.
|
||||
|
||||
@@ -126,11 +126,12 @@ class LonelyMeetingExperience extends PureComponent<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state): $Shape<Props> {
|
||||
const { disableInviteFunctions } = state['features/base/config'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const flag = getFeatureFlag(state, INVITE_ENABLED, true);
|
||||
|
||||
return {
|
||||
_isInviteFunctionsDiabled: !flag || disableInviteFunctions,
|
||||
_isLonelyMeeting: getParticipantCount(state) === 1,
|
||||
_isLonelyMeeting: conference && getParticipantCount(state) === 1,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Conference')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { SafeAreaView, Text, View } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
|
||||
import { getConferenceName } from '../../../base/conference';
|
||||
import { getFeatureFlag, MEETING_NAME_ENABLED } from '../../../base/flags';
|
||||
import { getFeatureFlag, CONFERENCE_TIMER_ENABLED, MEETING_NAME_ENABLED } from '../../../base/flags';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
|
||||
import { isToolboxVisible } from '../../../toolbox';
|
||||
@@ -15,6 +15,11 @@ import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether displaying the current conference timer is enabled or not.
|
||||
*/
|
||||
_conferenceTimerEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Name of the meeting we're currently in.
|
||||
*/
|
||||
@@ -73,7 +78,9 @@ class NavigationBar extends Component<Props> {
|
||||
{ this.props._meetingName }
|
||||
</Text>
|
||||
}
|
||||
<ConferenceTimer />
|
||||
{
|
||||
this.props._conferenceTimerEnabled && <ConferenceTimer />
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
];
|
||||
@@ -89,6 +96,7 @@ class NavigationBar extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_conferenceTimerEnabled: getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true),
|
||||
_meetingName: getConferenceName(state),
|
||||
_meetingNameEnabled: getFeatureFlag(state, MEETING_NAME_ENABLED, true),
|
||||
_visible: isToolboxVisible(state)
|
||||
|
||||
@@ -28,10 +28,8 @@ import {
|
||||
} from '../AbstractConference';
|
||||
import type { AbstractProps } from '../AbstractConference';
|
||||
|
||||
import InviteMore from './InviteMore';
|
||||
import Labels from './Labels';
|
||||
import { default as Notice } from './Notice';
|
||||
import { default as Subject } from './Subject';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
@@ -201,8 +199,6 @@ class Conference extends AbstractConference<Props, *> {
|
||||
onMouseMove = { this._onShowToolbar }>
|
||||
|
||||
<Notice />
|
||||
<Subject />
|
||||
<InviteMore />
|
||||
<div id = 'videospace'>
|
||||
<LargeVideo />
|
||||
<KnockingParticipantList />
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
export { default as Conference } from './Conference';
|
||||
export { default as renderConferenceTimer } from './ConferenceTimerDisplay';
|
||||
export { default as InsecureRoomNameLabel } from './InsecureRoomNameLabel';
|
||||
export { default as InviteMore } from './InviteMore';
|
||||
export { default as Subject } from './Subject';
|
||||
|
||||
@@ -3,10 +3,8 @@ import { appNavigate } from '../app/actions';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
KICKED_OUT,
|
||||
VIDEO_QUALITY_LEVELS,
|
||||
conferenceLeft,
|
||||
getCurrentConference,
|
||||
setMaxReceiverVideoQuality
|
||||
getCurrentConference
|
||||
} from '../base/conference';
|
||||
import { hideDialog, isDialogOpen } from '../base/dialog';
|
||||
import { setActiveModalId } from '../base/modal';
|
||||
@@ -32,12 +30,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
dispatch(setToolboxEnabled(!reducedUI));
|
||||
dispatch(setFilmstripEnabled(!reducedUI));
|
||||
|
||||
dispatch(
|
||||
setMaxReceiverVideoQuality(
|
||||
reducedUI
|
||||
? VIDEO_QUALITY_LEVELS.LOW
|
||||
: VIDEO_QUALITY_LEVELS.HIGH));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -342,6 +342,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
bridgeCount,
|
||||
e2eRtt,
|
||||
framerate,
|
||||
maxEnabledResolution,
|
||||
packetLoss,
|
||||
region,
|
||||
resolution,
|
||||
@@ -358,6 +359,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
|
||||
e2eRtt = { e2eRtt }
|
||||
framerate = { framerate }
|
||||
isLocalVideo = { this.props.isLocalVideo }
|
||||
maxEnabledResolution = { maxEnabledResolution }
|
||||
onShowMore = { this._onToggleShowMore }
|
||||
packetLoss = { packetLoss }
|
||||
region = { region }
|
||||
|
||||
@@ -57,6 +57,12 @@ type Props = {
|
||||
*/
|
||||
isLocalVideo: boolean,
|
||||
|
||||
/**
|
||||
* The send-side max enabled resolution (aka the highest layer that is not
|
||||
* suspended on the send-side).
|
||||
*/
|
||||
maxEnabledResolution: number,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the show additional stats link is clicked.
|
||||
*/
|
||||
@@ -380,8 +386,8 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderResolution() {
|
||||
const { resolution, t } = this.props;
|
||||
const resolutionString = Object.keys(resolution || {})
|
||||
const { resolution, maxEnabledResolution, t } = this.props;
|
||||
let resolutionString = Object.keys(resolution || {})
|
||||
.map(ssrc => {
|
||||
const { width, height } = resolution[ssrc];
|
||||
|
||||
@@ -389,6 +395,12 @@ class ConnectionStatsTable extends Component<Props> {
|
||||
})
|
||||
.join(', ') || 'N/A';
|
||||
|
||||
if (maxEnabledResolution && maxEnabledResolution < 720) {
|
||||
const maxEnabledResolutionTitle = t('connectionindicator.maxEnabledResolution');
|
||||
|
||||
resolutionString += ` (${maxEnabledResolutionTitle} ${maxEnabledResolution}p)`;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
@@ -34,7 +34,19 @@ export const SET_FILMSTRIP_VISIBLE = 'SET_FILMSTRIP_VISIBLE';
|
||||
*
|
||||
* {
|
||||
* type: SET_TILE_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* dimensions: {
|
||||
* gridDimensions: {
|
||||
* columns: number,
|
||||
* height: number,
|
||||
* visibleRows: number,
|
||||
* width: number
|
||||
* },
|
||||
* thumbnailSize: {
|
||||
* height: number,
|
||||
* width: number
|
||||
* },
|
||||
* filmstripWidth: number
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const SET_TILE_VIEW_DIMENSIONS = 'SET_TILE_VIEW_DIMENSIONS';
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
import {
|
||||
SET_FILMSTRIP_ENABLED,
|
||||
SET_FILMSTRIP_HOVERED,
|
||||
SET_FILMSTRIP_VISIBLE
|
||||
SET_FILMSTRIP_VISIBLE,
|
||||
SET_TILE_VIEW_DIMENSIONS
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
@@ -53,3 +54,26 @@ export function setFilmstripVisible(visible: boolean) {
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions of the tile view grid. The action is only partially implemented on native as not all
|
||||
* of the values are currently used. Check the description of {@link SET_TILE_VIEW_DIMENSIONS} for the full set
|
||||
* of properties.
|
||||
*
|
||||
* @param {Object} dimensions - The tile view dimensions.
|
||||
* @param {Object} thumbnailSize - The size of an individual video thumbnail.
|
||||
* @param {number} thumbnailSize.height - The height of an individual video thumbnail.
|
||||
* @param {number} thumbnailSize.width - The width of an individual video thumbnail.
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }}
|
||||
*/
|
||||
export function setTileViewDimensions({ thumbnailSize }: Object) {
|
||||
return {
|
||||
type: SET_TILE_VIEW_DIMENSIONS,
|
||||
dimensions: {
|
||||
thumbnailSize
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { CHAT_SIZE } from '../chat/constants';
|
||||
|
||||
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
|
||||
|
||||
@@ -13,15 +15,25 @@ const TILE_VIEW_SIDE_MARGINS = 10 * 2;
|
||||
*
|
||||
* @param {Object} dimensions - Whether the filmstrip is visible.
|
||||
* @param {Object} windowSize - The size of the window.
|
||||
* @param {boolean} isChatOpen - Whether the chat panel is displayed, in
|
||||
* order to properly compute the tile view size.
|
||||
* @returns {{
|
||||
* type: SET_TILE_VIEW_DIMENSIONS,
|
||||
* dimensions: Object
|
||||
* }}
|
||||
*/
|
||||
export function setTileViewDimensions(dimensions: Object, windowSize: Object) {
|
||||
export function setTileViewDimensions(dimensions: Object, windowSize: Object, isChatOpen: boolean) {
|
||||
const { clientWidth, clientHeight } = windowSize;
|
||||
let widthToUse = clientWidth;
|
||||
|
||||
if (isChatOpen) {
|
||||
widthToUse -= CHAT_SIZE;
|
||||
}
|
||||
|
||||
const thumbnailSize = calculateThumbnailSizeForTileView({
|
||||
...dimensions,
|
||||
...windowSize
|
||||
clientWidth: widthToUse,
|
||||
clientHeight
|
||||
});
|
||||
const filmstripWidth = dimensions.columns * (TILE_VIEW_SIDE_MARGINS + thumbnailSize.width);
|
||||
|
||||
|
||||
@@ -8,12 +8,9 @@ import {
|
||||
} from 'react-native';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
getNearestReceiverVideoQualityLevel,
|
||||
setMaxReceiverVideoQuality
|
||||
} from '../../../base/conference';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setTileViewDimensions } from '../../actions.native';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
@@ -266,10 +263,14 @@ class TileView extends Component<Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateReceiverQuality() {
|
||||
const { height } = this._getTileDimensions();
|
||||
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
|
||||
const { height, width } = this._getTileDimensions();
|
||||
|
||||
this.props.dispatch(setMaxReceiverVideoQuality(qualityLevel));
|
||||
this.props.dispatch(setTileViewDimensions({
|
||||
thumbnailSize: {
|
||||
height,
|
||||
width
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -371,13 +371,15 @@ function _mapStateToProps(state) {
|
||||
const reduceHeight
|
||||
= !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
|
||||
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${reduceHeight ? 'reduce-height' : ''}`.trim();
|
||||
const { isOpen: shiftRight } = state['features/chat'];
|
||||
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
|
||||
reduceHeight ? 'reduce-height' : ''
|
||||
} ${shiftRight ? 'shift-right' : ''}`.trim();
|
||||
const videosClassName = `filmstrip__videos${
|
||||
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
|
||||
visible ? '' : ' hidden'}`;
|
||||
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
|
||||
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
_columns: gridDimensions.columns,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
|
||||
import { getNearestReceiverVideoQualityLevel, setMaxReceiverVideoQuality } from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { CLIENT_RESIZED } from '../base/responsive-ui';
|
||||
import {
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
} from '../video-layout';
|
||||
|
||||
import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||
|
||||
import './subscriber.web';
|
||||
|
||||
@@ -30,11 +29,18 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { gridDimensions } = state['features/filmstrip'].tileViewDimensions;
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { isOpen } = state['features/chat'];
|
||||
|
||||
store.dispatch(setTileViewDimensions(gridDimensions, {
|
||||
clientHeight,
|
||||
clientWidth
|
||||
}));
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
isOpen
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
@@ -48,9 +54,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
|
||||
const qualityLevel = getNearestReceiverVideoQualityLevel(height);
|
||||
|
||||
store.dispatch(setMaxReceiverVideoQuality(qualityLevel));
|
||||
|
||||
// Once the thumbnails are reactified this should be moved there too.
|
||||
Filmstrip.resizeThumbnailsForTileView(width, height, true);
|
||||
|
||||
@@ -5,7 +5,7 @@ import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
|
||||
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions';
|
||||
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
|
||||
|
||||
/**
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
@@ -19,12 +19,19 @@ StateListenerRegistry.register(
|
||||
const gridDimensions = getTileViewGridDimensions(state);
|
||||
const oldGridDimensions = state['features/filmstrip'].tileViewDimensions.gridDimensions;
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { isOpen } = state['features/chat'];
|
||||
|
||||
if (!equals(gridDimensions, oldGridDimensions)) {
|
||||
store.dispatch(setTileViewDimensions(gridDimensions, {
|
||||
clientHeight,
|
||||
clientWidth
|
||||
}));
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
isOpen
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -40,12 +47,18 @@ StateListenerRegistry.register(
|
||||
switch (layout) {
|
||||
case LAYOUTS.TILE_VIEW: {
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const { isOpen } = state['features/chat'];
|
||||
|
||||
store.dispatch(setTileViewDimensions(
|
||||
getTileViewGridDimensions(state), {
|
||||
clientHeight,
|
||||
clientWidth
|
||||
}));
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
getTileViewGridDimensions(state),
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
isOpen
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW:
|
||||
@@ -76,3 +89,36 @@ StateListenerRegistry.register(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Listens for changes in the chat state to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/chat'].isOpen,
|
||||
/* listener */ (isChatOpen, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (isChatOpen) {
|
||||
// $FlowFixMe
|
||||
document.body.classList.add('shift-right');
|
||||
} else {
|
||||
// $FlowFixMe
|
||||
document.body.classList.remove('shift-right');
|
||||
}
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
const gridDimensions = getTileViewGridDimensions(state);
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
store.dispatch(
|
||||
setTileViewDimensions(
|
||||
gridDimensions,
|
||||
{
|
||||
clientHeight,
|
||||
clientWidth
|
||||
},
|
||||
isChatOpen
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { Icon, IconCheck, IconCopy } from '../../../../base/icons';
|
||||
import { copyText } from '../../../../base/util';
|
||||
import { copyText, getDecodedURI } from '../../../../base/util';
|
||||
|
||||
|
||||
type Props = {
|
||||
@@ -82,7 +82,7 @@ function CopyMeetingLinkSection({ t, url }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const displayUrl = decodeURI(url.replace(/^https?:\/\//i, ''));
|
||||
const displayUrl = getDecodedURI(url);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { InviteMore, Subject } from '../../conference';
|
||||
import { fetchCustomBrandingData } from '../../dynamic-branding';
|
||||
import { Captions } from '../../subtitles/';
|
||||
|
||||
@@ -26,6 +27,11 @@ type Props = {
|
||||
*/
|
||||
_fetchCustomBrandingData: Function,
|
||||
|
||||
/**
|
||||
* Prop that indicates whether the chat is open.
|
||||
*/
|
||||
_isChatOpen: boolean,
|
||||
|
||||
/**
|
||||
* Used to determine the value of the autoplay attribute of the underlying
|
||||
* video element.
|
||||
@@ -57,12 +63,15 @@ class LargeVideo extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const style = this._getCustomSyles();
|
||||
const className = `videocontainer${this.props._isChatOpen ? ' shift-right' : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'videocontainer'
|
||||
className = { className }
|
||||
id = 'largeVideoContainer'
|
||||
style = { style }>
|
||||
<Subject />
|
||||
<InviteMore />
|
||||
<div id = 'sharedVideo'>
|
||||
<div id = 'sharedVideoIFrame' />
|
||||
</div>
|
||||
@@ -133,10 +142,12 @@ class LargeVideo extends Component<Props> {
|
||||
function _mapStateToProps(state) {
|
||||
const testingConfig = state['features/base/config'].testing;
|
||||
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
|
||||
return {
|
||||
_customBackgroundColor: backgroundColor,
|
||||
_customBackgroundImageUrl: backgroundImageUrl,
|
||||
_isChatOpen: isChatOpen,
|
||||
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getRoomName } from '../../base/conference';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { Icon, IconPhone, IconVolumeOff } from '../../base/icons';
|
||||
import { isVideoMutedByUser } from '../../base/media';
|
||||
import { ActionButton, InputField, PreMeetingScreen } from '../../base/premeeting';
|
||||
import { ActionButton, InputField, PreMeetingScreen, ToggleButton } from '../../base/premeeting';
|
||||
import { connect } from '../../base/redux';
|
||||
import { getDisplayName, updateSettings } from '../../base/settings';
|
||||
import { getLocalJitsiVideoTrack } from '../../base/tracks';
|
||||
@@ -21,7 +21,8 @@ import {
|
||||
isDeviceStatusVisible,
|
||||
isDisplayNameRequired,
|
||||
isJoinByPhoneButtonVisible,
|
||||
isJoinByPhoneDialogVisible
|
||||
isJoinByPhoneDialogVisible,
|
||||
isPrejoinSkipped
|
||||
} from '../functions';
|
||||
|
||||
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
|
||||
@@ -29,6 +30,11 @@ import DeviceStatus from './preview/DeviceStatus';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Flag signaling if the 'skip prejoin' button is toggled or not.
|
||||
*/
|
||||
buttonIsToggled: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling if the device status is visible or not.
|
||||
*/
|
||||
@@ -79,11 +85,21 @@ type Props = {
|
||||
*/
|
||||
setJoinByPhoneDialogVisiblity: Function,
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of camera preview.
|
||||
*/
|
||||
showCameraPreview: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of join label, input and buttons
|
||||
*/
|
||||
showJoinActions: boolean,
|
||||
|
||||
/**
|
||||
* If 'JoinByPhoneDialog' is visible or not.
|
||||
*/
|
||||
@@ -112,6 +128,15 @@ type State = {
|
||||
* This component is displayed before joining a meeting.
|
||||
*/
|
||||
class Prejoin extends Component<Props, State> {
|
||||
/**
|
||||
* Default values for {@code Prejoin} component's properties.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
showJoinActions: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code Prejoin} instance.
|
||||
*
|
||||
@@ -126,22 +151,22 @@ class Prejoin extends Component<Props, State> {
|
||||
|
||||
this._closeDialog = this._closeDialog.bind(this);
|
||||
this._showDialog = this._showDialog.bind(this);
|
||||
this._onCheckboxChange = this._onCheckboxChange.bind(this);
|
||||
this._onToggleButtonClick = this._onToggleButtonClick.bind(this);
|
||||
this._onDropdownClose = this._onDropdownClose.bind(this);
|
||||
this._onOptionsClick = this._onOptionsClick.bind(this);
|
||||
this._setName = this._setName.bind(this);
|
||||
}
|
||||
|
||||
_onCheckboxChange: () => void;
|
||||
_onToggleButtonClick: () => void;
|
||||
|
||||
/**
|
||||
* Handler for the checkbox.
|
||||
* Handler for the toggle button.
|
||||
*
|
||||
* @param {Object} e - The synthetic event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCheckboxChange(e) {
|
||||
this.props.setSkipPrejoin(e.target.checked);
|
||||
_onToggleButtonClick() {
|
||||
this.props.setSkipPrejoin(!this.props.buttonIsToggled);
|
||||
}
|
||||
|
||||
_onDropdownClose: () => void;
|
||||
@@ -223,74 +248,73 @@ class Prejoin extends Component<Props, State> {
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
name,
|
||||
showAvatar,
|
||||
showCameraPreview,
|
||||
showDialog,
|
||||
showJoinActions,
|
||||
t,
|
||||
videoTrack
|
||||
} = this.props;
|
||||
|
||||
const { _closeDialog, _onCheckboxChange, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { _closeDialog, _onDropdownClose, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const { showJoinByPhoneButtons } = this.state;
|
||||
|
||||
return (
|
||||
<PreMeetingScreen
|
||||
footer = { this._renderFooter() }
|
||||
name = { name }
|
||||
showAvatar = { showAvatar }
|
||||
showConferenceInfo = { showJoinActions }
|
||||
skipPrejoinButton = { this._renderSkipPrejoinButton() }
|
||||
title = { t('prejoin.joinMeeting') }
|
||||
videoMuted = { !showCameraPreview }
|
||||
videoTrack = { videoTrack }>
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<InputField
|
||||
onChange = { _setName }
|
||||
onSubmit = { joinConference }
|
||||
placeHolder = { t('dialog.enterDisplayName') }
|
||||
value = { name } />
|
||||
{showJoinActions && (
|
||||
<div className = 'prejoin-input-area-container'>
|
||||
<div className = 'prejoin-input-area'>
|
||||
<InputField
|
||||
onChange = { _setName }
|
||||
onSubmit = { joinConference }
|
||||
placeHolder = { t('dialog.enterDisplayName') }
|
||||
value = { name } />
|
||||
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { joinConferenceWithoutAudio }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconVolumeOff } />
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</div>
|
||||
{hasJoinByPhoneButton && <div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { _showDialog }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconPhone } />
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</div>}
|
||||
</div> }
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
<ActionButton
|
||||
disabled = { joinButtonDisabled }
|
||||
hasOptions = { true }
|
||||
onClick = { joinConference }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
type = 'primary'>
|
||||
{ t('prejoin.joinMeeting') }
|
||||
</ActionButton>
|
||||
</InlineDialog>
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { joinConferenceWithoutAudio }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconVolumeOff } />
|
||||
{ t('prejoin.joinWithoutAudio') }
|
||||
</div>
|
||||
{hasJoinByPhoneButton && <div
|
||||
className = 'prejoin-preview-dropdown-btn'
|
||||
onClick = { _showDialog }>
|
||||
<Icon
|
||||
className = 'prejoin-preview-dropdown-icon'
|
||||
size = { 24 }
|
||||
src = { IconPhone } />
|
||||
{ t('prejoin.joinAudioByPhone') }
|
||||
</div>}
|
||||
</div> }
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
<ActionButton
|
||||
disabled = { joinButtonDisabled }
|
||||
hasOptions = { true }
|
||||
onClick = { joinConference }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
type = 'primary'>
|
||||
{ t('prejoin.joinMeeting') }
|
||||
</ActionButton>
|
||||
</InlineDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className = 'prejoin-checkbox-container'>
|
||||
<input
|
||||
className = 'prejoin-checkbox'
|
||||
onChange = { _onCheckboxChange }
|
||||
type = 'checkbox' />
|
||||
<span>{t('prejoin.doNotShow')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{ showDialog && (
|
||||
<JoinByPhoneDialog
|
||||
joinConferenceWithoutAudio = { joinConferenceWithoutAudio }
|
||||
@@ -308,6 +332,25 @@ class Prejoin extends Component<Props, State> {
|
||||
_renderFooter() {
|
||||
return this.props.deviceStatusVisible && <DeviceStatus />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the 'skip prejoin' button.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderSkipPrejoinButton() {
|
||||
const { buttonIsToggled, t } = this.props;
|
||||
|
||||
return (
|
||||
<div className = 'prejoin-checkbox-container'>
|
||||
<ToggleButton
|
||||
isToggled = { buttonIsToggled }
|
||||
onClick = { this._onToggleButtonClick }>
|
||||
{t('prejoin.doNotShow')}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,6 +364,7 @@ function mapStateToProps(state): Object {
|
||||
const joinButtonDisabled = isDisplayNameRequired(state) && !name;
|
||||
|
||||
return {
|
||||
buttonIsToggled: isPrejoinSkipped(state),
|
||||
joinButtonDisabled,
|
||||
name,
|
||||
deviceStatusVisible: isDeviceStatusVisible(state),
|
||||
|
||||
93
react/features/prejoin/components/PrejoinApp.js
Normal file
93
react/features/prejoin/components/PrejoinApp.js
Normal file
@@ -0,0 +1,93 @@
|
||||
// @flow
|
||||
|
||||
import { AtlasKitThemeProvider } from '@atlaskit/theme';
|
||||
import React from 'react';
|
||||
|
||||
import { BaseApp } from '../../../features/base/app';
|
||||
import { setConfig } from '../../base/config';
|
||||
import { createPrejoinTracks } from '../../base/tracks';
|
||||
import { initPrejoin } from '../actions';
|
||||
|
||||
import Prejoin from './Prejoin';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Indicates whether the avatar should be shown when video is off
|
||||
*/
|
||||
showAvatar: boolean,
|
||||
|
||||
/**
|
||||
* Flag signaling the visibility of join label, input and buttons
|
||||
*/
|
||||
showJoinActions: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper application for prejoin.
|
||||
*
|
||||
* @extends BaseApp
|
||||
*/
|
||||
export default class PrejoinApp extends BaseApp<Props> {
|
||||
_init: Promise<*>;
|
||||
|
||||
/**
|
||||
* Navigates to {@link Prejoin} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
this._init.then(async () => {
|
||||
const { store } = this.state;
|
||||
const { dispatch } = store;
|
||||
const { showAvatar, showJoinActions } = this.props;
|
||||
|
||||
super._navigate({
|
||||
component: Prejoin,
|
||||
props: {
|
||||
showAvatar,
|
||||
showJoinActions
|
||||
}
|
||||
});
|
||||
|
||||
const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
|
||||
|
||||
dispatch(setConfig({
|
||||
prejoinPageEnabled: true,
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
}));
|
||||
|
||||
const { tryCreateLocalTracks, errors } = createPrejoinTracks();
|
||||
|
||||
const tracks = await tryCreateLocalTracks;
|
||||
|
||||
dispatch(initPrejoin(tracks, errors));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_createMainElement(component, props) {
|
||||
return (
|
||||
<AtlasKitThemeProvider mode = 'dark'>
|
||||
{ super._createMainElement(component, props) }
|
||||
</AtlasKitThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,16 @@ export function isDisplayNameRequired(state: Object): boolean {
|
||||
|| state['features/base/config'].requireDisplayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector for determining if the user has chosen to skip prejoin page.
|
||||
*
|
||||
* @param {Object} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isPrejoinSkipped(state: Object) {
|
||||
return state['features/prejoin'].userSelectedSkipPrejoin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the text for the prejoin status bar.
|
||||
*
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { getLocalVideoTrack, replaceLocalTrack } from '../base/tracks';
|
||||
|
||||
import { PREJOIN_START_CONFERENCE } from './actionTypes';
|
||||
import { setPrejoinPageVisibility } from './actions';
|
||||
import { isPrejoinPageVisible } from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -38,8 +40,26 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_AUDIO_MUTED: {
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(updateSettings({
|
||||
startWithAudioMuted: Boolean(action.muted)
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED: {
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(updateSettings({
|
||||
startWithVideoMuted: Boolean(action.muted)
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// @flow
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { IconCrown } from '../../base/icons';
|
||||
import {
|
||||
getParticipantById,
|
||||
isLocalParticipantModerator,
|
||||
isParticipantModerator
|
||||
} from '../../base/participants';
|
||||
import { AbstractButton } from '../../base/toolbox';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox';
|
||||
|
||||
import { GrantModeratorDialog } from '.';
|
||||
|
||||
export type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the participant for whom to grant moderator status.
|
||||
*/
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* The function to be used to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* An abstract remote video menu button which kicks the remote participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorButton extends AbstractButton<Props, *> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.grantModerator';
|
||||
icon = IconCrown;
|
||||
label = 'videothumbnail.grantModerator';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and kicks the participant.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
dispatch(openDialog(GrantModeratorDialog, { participantID }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that maps parts of Redux state tree into component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @param {Object} ownProps - Properties of component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* visible: boolean
|
||||
* }}
|
||||
*/
|
||||
export function _mapStateToProps(state: Object, ownProps: Props) {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
return {
|
||||
visible: isLocalParticipantModerator(state) && !isParticipantModerator(getParticipantById(state, participantID))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// @flow
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import {
|
||||
createRemoteVideoMenuButtonEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { grantModerator } from '../../base/participants';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* The ID of the remote participant to be granted moderator rights.
|
||||
*/
|
||||
participantID: string,
|
||||
|
||||
/**
|
||||
* Function to translate i18n labels.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract dialog to confirm granting moderator to a participant.
|
||||
*/
|
||||
export default class AbstractGrantModeratorDialog
|
||||
extends Component<Props> {
|
||||
/**
|
||||
* Initializes a new {@code AbstractGrantModeratorDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback for the confirm button.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} - True (to note that the modal should be closed).
|
||||
*/
|
||||
_onSubmit() {
|
||||
const { dispatch, participantID } = this.props;
|
||||
|
||||
sendAnalytics(createRemoteVideoMenuButtonEvent(
|
||||
'grant.moderator.button',
|
||||
{
|
||||
'participant_id': participantID
|
||||
}));
|
||||
|
||||
dispatch(grantModerator(participantID));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorButton, {
|
||||
_mapStateToProps
|
||||
} from '../AbstractGrantModeratorButton';
|
||||
|
||||
export default translate(connect(_mapStateToProps)(AbstractGrantModeratorButton));
|
||||
@@ -0,0 +1,32 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ConfirmDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorDialog
|
||||
from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Dialog to confirm a remote participant kick action.
|
||||
*/
|
||||
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ConfirmDialog
|
||||
contentKey = 'dialog.grantModeratorDialog'
|
||||
onSubmit = { this._onSubmit } />
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(GrantModeratorDialog));
|
||||
@@ -12,6 +12,7 @@ import { StyleType } from '../../../base/styles';
|
||||
import { PrivateMessageButton } from '../../../chat';
|
||||
import { hideRemoteVideoMenu } from '../../actions';
|
||||
|
||||
import GrantModeratorButton from './GrantModeratorButton';
|
||||
import KickButton from './KickButton';
|
||||
import MuteButton from './MuteButton';
|
||||
import PinButton from './PinButton';
|
||||
@@ -98,6 +99,8 @@ class RemoteVideoMenu extends Component<Props> {
|
||||
buttons.push(<MuteButton { ...buttonProps } />);
|
||||
}
|
||||
|
||||
buttons.push(<GrantModeratorButton { ...buttonProps } />);
|
||||
|
||||
if (!_disableKick) {
|
||||
buttons.push(<KickButton { ...buttonProps } />);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// @flow
|
||||
|
||||
export {
|
||||
default as GrantModeratorDialog
|
||||
} from './GrantModeratorDialog';
|
||||
export {
|
||||
default as KickRemoteParticipantDialog
|
||||
} from './KickRemoteParticipantDialog';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/* @flow */
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconCrown } from '../../../base/icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorButton, {
|
||||
_mapStateToProps,
|
||||
type Props
|
||||
} from '../AbstractGrantModeratorButton';
|
||||
|
||||
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays a button for granting
|
||||
* moderator to a participant.
|
||||
*/
|
||||
class GrantModeratorButton extends AbstractGrantModeratorButton {
|
||||
/**
|
||||
* Instantiates a new {@code GrantModeratorButton}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { participantID, t, visible } = this.props;
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<RemoteVideoMenuButton
|
||||
buttonText = { t('videothumbnail.grantModerator') }
|
||||
displayClass = 'grantmoderatorlink'
|
||||
icon = { IconCrown }
|
||||
id = { `grantmoderatorlink_${participantID}` }
|
||||
// eslint-disable-next-line react/jsx-handler-names
|
||||
onClick = { this._handleClick } />
|
||||
);
|
||||
}
|
||||
|
||||
_handleClick: () => void
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(GrantModeratorButton));
|
||||
@@ -0,0 +1,38 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Dialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
import AbstractGrantModeratorDialog
|
||||
from '../AbstractGrantModeratorDialog';
|
||||
|
||||
/**
|
||||
* Dialog to confirm a grant moderator action.
|
||||
*/
|
||||
class GrantModeratorDialog extends AbstractGrantModeratorDialog {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
okKey = 'dialog.Yes'
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.grantModeratorTitle'
|
||||
width = 'small'>
|
||||
<div>
|
||||
{ this.props.t('dialog.grantModeratorDialog') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
}
|
||||
|
||||
export default translate(connect()(GrantModeratorDialog));
|
||||
@@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import {
|
||||
GrantModeratorButton,
|
||||
MuteButton,
|
||||
MuteEveryoneElseButton,
|
||||
KickButton,
|
||||
@@ -195,6 +196,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<GrantModeratorButton
|
||||
key = 'grant-moderator'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
|
||||
if (!_disableKick) {
|
||||
buttons.push(
|
||||
<KickButton
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// @flow
|
||||
|
||||
export { default as GrantModeratorButton } from './GrantModeratorButton';
|
||||
export {
|
||||
default as GrantModeratorDialog
|
||||
} from './GrantModeratorDialog';
|
||||
export { default as KickButton } from './KickButton';
|
||||
export {
|
||||
default as KickRemoteParticipantDialog
|
||||
|
||||
111
react/features/rtcstats/RTCStats.js
Normal file
111
react/features/rtcstats/RTCStats.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import rtcstatsInit from 'rtcstats/rtcstats';
|
||||
import traceInit from 'rtcstats/trace-ws';
|
||||
|
||||
import {
|
||||
createRTCStatsTraceCloseEvent,
|
||||
sendAnalytics
|
||||
} from '../analytics';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Filter out RTCPeerConnection that are created by callstats.io.
|
||||
*
|
||||
* @param {*} config - Config object sent to the PC c'tor.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function connectionFilter(config) {
|
||||
if (config && config.iceServers[0] && config.iceServers[0].urls) {
|
||||
for (const iceUrl of config.iceServers[0].urls) {
|
||||
if (iceUrl.indexOf('taas.callstats.io') >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that controls the rtcstats flow, because it overwrites and proxies global function it should only be
|
||||
* initialized once.
|
||||
*/
|
||||
class RTCStats {
|
||||
/**
|
||||
* Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
|
||||
* that does the actual communication with the server. Secondly, the rtcstats component is initialized,
|
||||
* it overwrites GUM and PeerConnection global functions and adds a proxy over them used to capture stats.
|
||||
* Note, lib-jitsi-meet takes references to these methods before initializing so the init method needs to be
|
||||
* loaded before it does.
|
||||
*
|
||||
* @param {Object} options -.
|
||||
* @param {string} options.rtcstatsEndpoint - The Amplitude app key required.
|
||||
* @param {number} options.rtcstatsPollInterval - The getstats poll interval in ms.
|
||||
* @returns {void}
|
||||
*/
|
||||
init(options) {
|
||||
this.handleTraceWSClose = this.handleTraceWSClose.bind(this);
|
||||
this.trace = traceInit(options.rtcstatsEndpoint, this.handleTraceWSClose);
|
||||
rtcstatsInit(this.trace, options.rtcstatsPollInterval, [ '' ], connectionFilter);
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether or not the RTCStats is initialized.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send identity data to rtcstats server, this will be reflected in the identity section of the stats dump.
|
||||
* It can be generally used to send additional metadata that might be relevant such as amplitude user data
|
||||
* or deployment specific information.
|
||||
*
|
||||
* @param {Object} identityData - Metadata object to send as identity.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendIdentityData(identityData) {
|
||||
this.trace && this.trace('identity', null, identityData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
|
||||
* connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
|
||||
* connected and sent once it is established.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
connect() {
|
||||
this.trace && this.trace.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Self explanatory; closes the web socked connection.
|
||||
* Note, at the point of writing this documentation there was no method to reset the function overwrites,
|
||||
* thus even if the websocket is closed the global function proxies are still active but send no data,
|
||||
* this shouldn't influence the normal flow of the application.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
close() {
|
||||
this.trace && this.trace.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* The way rtcstats is currently designed the ws wouldn't normally be closed by the application logic but rather
|
||||
* by the page being closed/reloaded. Using this assumption any onclose event is most likely something abnormal
|
||||
* that happened on the ws. We then track this in order to determine how many rtcstats connection were closed
|
||||
* prematurely.
|
||||
*
|
||||
* @param {Object} closeEvent - Event sent by ws onclose.
|
||||
* @returns {void}
|
||||
*/
|
||||
handleTraceWSClose(closeEvent) {
|
||||
logger.info('RTCStats trace ws closed', closeEvent);
|
||||
|
||||
sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));
|
||||
}
|
||||
}
|
||||
|
||||
export default new RTCStats();
|
||||
1
react/features/rtcstats/index.js
Normal file
1
react/features/rtcstats/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware';
|
||||
5
react/features/rtcstats/logger.js
Normal file
5
react/features/rtcstats/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/rtcstats');
|
||||
79
react/features/rtcstats/middleware.js
Normal file
79
react/features/rtcstats/middleware.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// @flow
|
||||
|
||||
import { getAmplitudeIdentity } from '../analytics';
|
||||
import {
|
||||
CONFERENCE_JOINED
|
||||
} from '../base/conference';
|
||||
import { LIB_WILL_INIT } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant } from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import RTCStats from './RTCStats';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Middleware which intercepts lib-jitsi-meet initialization and conference join in order init the
|
||||
* rtcstats-client.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const config = state['features/base/config'];
|
||||
const { analytics } = config;
|
||||
|
||||
switch (action.type) {
|
||||
case LIB_WILL_INIT: {
|
||||
if (analytics.rtcstatsEndpoint) {
|
||||
// RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
|
||||
// window functions. Because lib-jitsi-meet uses references to those functions that are taken on
|
||||
// init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
|
||||
// original non proxy versions of these functions.
|
||||
try {
|
||||
// Default poll interval is 1000ms if not provided in the config.
|
||||
const pollInterval = analytics.rtcstatsPollInterval || 1000;
|
||||
|
||||
// Initialize but don't connect to the rtcstats server wss, as it will start sending data for all
|
||||
// media calls made even before the conference started.
|
||||
RTCStats.init({
|
||||
rtcstatsEndpoint: analytics.rtcstatsEndpoint,
|
||||
rtcstatsPollInterval: pollInterval
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize RTCStats: ', error);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_JOINED: {
|
||||
if (analytics.rtcstatsEndpoint && RTCStats.isInitialized()) {
|
||||
// Once the conference started connect to the rtcstats server and send data.
|
||||
try {
|
||||
RTCStats.connect();
|
||||
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
// The current implementation of rtcstats-server is configured to send data to amplitude, thus
|
||||
// we add identity specific information so we can corelate on the amplitude side. If amplitude is
|
||||
// not configured an empty object will be sent.
|
||||
// The current configuration of the conference is also sent as metadata to rtcstats server.
|
||||
// This is done in order to facilitate queries based on different conference configurations.
|
||||
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
|
||||
// conference with a specific version.
|
||||
RTCStats.sendIdentityData({
|
||||
...getAmplitudeIdentity(),
|
||||
...config,
|
||||
displayName: localParticipant?.name
|
||||
});
|
||||
} catch (error) {
|
||||
// If the connection failed do not impact jitsi-meet just silently fail.
|
||||
logger.error('RTCStats connect failed with: ', error);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -4,7 +4,7 @@
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { copyText } from '../../../invite';
|
||||
import { copyText } from '../../../base/util';
|
||||
|
||||
import PasswordForm from './PasswordForm';
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { setFollowMe, setStartMutedPolicy } from '../base/conference';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { i18next } from '../base/i18n';
|
||||
import { updateSettings } from '../base/settings';
|
||||
import { setPrejoinPageVisibility } from '../prejoin';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
@@ -64,6 +66,19 @@ export function submitMoreTab(newState: Object): Function {
|
||||
dispatch(setFollowMe(newState.followMeEnabled));
|
||||
}
|
||||
|
||||
const showPrejoinPage = newState.showPrejoinPage;
|
||||
|
||||
if (showPrejoinPage !== currentState.showPrejoinPage) {
|
||||
// The 'showPrejoin' flag starts as 'true' on every new session.
|
||||
// This prevents displaying the prejoin page when the user re-enables it.
|
||||
if (showPrejoinPage && getState()['features/prejoin']?.showPrejoin) {
|
||||
dispatch(setPrejoinPageVisibility(false));
|
||||
}
|
||||
dispatch(updateSettings({
|
||||
userSelectedSkipPrejoin: !showPrejoinPage
|
||||
}));
|
||||
}
|
||||
|
||||
if (newState.startAudioMuted !== currentState.startAudioMuted
|
||||
|| newState.startVideoMuted !== currentState.startVideoMuted) {
|
||||
dispatch(setStartMutedPolicy(
|
||||
|
||||
@@ -48,6 +48,16 @@ export type Props = {
|
||||
*/
|
||||
showModeratorSettings: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not to display the prejoin settings section.
|
||||
*/
|
||||
showPrejoinSettings: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not to show prejoin screen.
|
||||
*/
|
||||
showPrejoinPage: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the user has selected the Start Audio Muted feature to be
|
||||
* enabled.
|
||||
@@ -108,9 +118,13 @@ class MoreTab extends AbstractDialogTab<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { showModeratorSettings, showLanguageSettings } = this.props;
|
||||
const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = this.props;
|
||||
const content = [];
|
||||
|
||||
if (showPrejoinSettings) {
|
||||
content.push(this._renderPrejoinScreenSettings());
|
||||
}
|
||||
|
||||
if (showModeratorSettings) {
|
||||
content.push(this._renderModeratorSettings());
|
||||
}
|
||||
@@ -239,6 +253,35 @@ class MoreTab extends AbstractDialogTab<Props, State> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React Element for modifying prejoin screen settings.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderPrejoinScreenSettings() {
|
||||
const { t, showPrejoinPage } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane'
|
||||
key = 'prejoin-screen'>
|
||||
<div className = 'mock-atlaskit-label'>
|
||||
{ t('prejoin.premeeting') }
|
||||
</div>
|
||||
<Checkbox
|
||||
isChecked = { showPrejoinPage }
|
||||
label = { t('prejoin.showScreen') }
|
||||
name = 'show-prejoin-page'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onChange = {
|
||||
({ target: { checked } }) =>
|
||||
super._onChange({ showPrejoinPage: checked })
|
||||
} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(MoreTab);
|
||||
|
||||
@@ -131,7 +131,7 @@ function _mapStateToProps(state) {
|
||||
// The settings sections to display.
|
||||
const showDeviceSettings = configuredTabs.includes('devices');
|
||||
const moreTabProps = getMoreTabProps(state);
|
||||
const { showModeratorSettings, showLanguageSettings } = moreTabProps;
|
||||
const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = moreTabProps;
|
||||
const showProfileSettings
|
||||
= configuredTabs.includes('profile') && jwt.isGuest;
|
||||
const showCalendarSettings
|
||||
@@ -184,7 +184,7 @@ function _mapStateToProps(state) {
|
||||
});
|
||||
}
|
||||
|
||||
if (showModeratorSettings || showLanguageSettings) {
|
||||
if (showModeratorSettings || showLanguageSettings || showPrejoinSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MORE,
|
||||
component: MoreTab,
|
||||
@@ -197,6 +197,7 @@ function _mapStateToProps(state) {
|
||||
...newProps,
|
||||
currentLanguage: tabState.currentLanguage,
|
||||
followMeEnabled: tabState.followMeEnabled,
|
||||
showPrejoinPage: tabState.showPrejoinPage,
|
||||
startAudioMuted: tabState.startAudioMuted,
|
||||
startVideoMuted: tabState.startVideoMuted
|
||||
};
|
||||
|
||||
@@ -118,6 +118,8 @@ export function getMoreTabProps(stateful: Object | Function) {
|
||||
languages: LANGUAGES,
|
||||
showLanguageSettings: configuredTabs.includes('language'),
|
||||
showModeratorSettings,
|
||||
showPrejoinSettings: state['features/base/config'].prejoinPageEnabled,
|
||||
showPrejoinPage: !state['features/base/settings'].userSelectedSkipPrejoin,
|
||||
startAudioMuted: Boolean(conference && startAudioMutedPolicy),
|
||||
startVideoMuted: Boolean(conference && startVideoMutedPolicy)
|
||||
};
|
||||
|
||||
@@ -40,6 +40,41 @@ class ToolbarButton extends AbstractToolbarButton<Props> {
|
||||
tooltipPosition: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes a new {@code ToolbarButton} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._onKeyDown = this._onKeyDown.bind(this);
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key on the button to trigger onClick for accessibility.
|
||||
* We should be handling Space onKeyUp but it conflicts with PTT.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
// If the event coming to the dialog has been subject to preventDefault
|
||||
// we don't handle it here.
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button of this {@code ToolbarButton}.
|
||||
*
|
||||
@@ -52,8 +87,12 @@ class ToolbarButton extends AbstractToolbarButton<Props> {
|
||||
return (
|
||||
<div
|
||||
aria-label = { this.props.accessibilityLabel }
|
||||
aria-pressed = { this.props.toggled }
|
||||
className = 'toolbox-button'
|
||||
onClick = { this.props.onClick }>
|
||||
onClick = { this.props.onClick }
|
||||
onKeyDown = { this._onKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{ this.props.tooltip
|
||||
? <Tooltip
|
||||
content = { this.props.tooltip }
|
||||
|
||||
@@ -32,7 +32,7 @@ import { connect, equals } from '../../../base/redux';
|
||||
import { OverflowMenuItem } from '../../../base/toolbox';
|
||||
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
|
||||
import { VideoBlurButton } from '../../../blur';
|
||||
import { ChatCounter, toggleChat } from '../../../chat';
|
||||
import { CHAT_SIZE, ChatCounter, toggleChat } from '../../../chat';
|
||||
import { SharedDocumentButton } from '../../../etherpad';
|
||||
import { openFeedbackDialog } from '../../../feedback';
|
||||
import { beginAddPeople } from '../../../invite';
|
||||
@@ -322,6 +322,10 @@ class Toolbox extends Component<Props, State> {
|
||||
this._onSetOverflowVisible(false);
|
||||
this.props.dispatch(setToolbarHovered(false));
|
||||
}
|
||||
|
||||
if (this.props._chatOpen !== prevProps._chatOpen) {
|
||||
this._onResize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,9 +348,9 @@ class Toolbox extends Component<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _visible, _visibleButtons } = this.props;
|
||||
const { _chatOpen, _visible, _visibleButtons } = this.props;
|
||||
const rootClassNames = `new-toolbox ${_visible ? 'visible' : ''} ${
|
||||
_visibleButtons.size ? '' : 'no-buttons'}`;
|
||||
_visibleButtons.size ? '' : 'no-buttons'} ${_chatOpen ? 'shift-right' : ''}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -534,10 +538,15 @@ class Toolbox extends Component<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onResize() {
|
||||
const width = window.innerWidth;
|
||||
let widthToUse = window.innerWidth;
|
||||
|
||||
if (this.state.windowWidth !== width) {
|
||||
this.setState({ windowWidth: width });
|
||||
// Take chat size into account when resizing toolbox.
|
||||
if (this.props._chatOpen) {
|
||||
widthToUse -= CHAT_SIZE;
|
||||
}
|
||||
|
||||
if (this.state.windowWidth !== widthToUse) {
|
||||
this.setState({ windowWidth: widthToUse });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,6 +1183,9 @@ class Toolbox extends Component<Props, State> {
|
||||
/ 2 // divide by the number of groups(left and right group)
|
||||
);
|
||||
|
||||
if (this._shouldShowButton('chat')) {
|
||||
buttonsLeft.push('chat');
|
||||
}
|
||||
if (this._shouldShowButton('desktop')
|
||||
&& this._isDesktopSharingButtonVisible()) {
|
||||
buttonsLeft.push('desktop');
|
||||
@@ -1181,9 +1193,6 @@ class Toolbox extends Component<Props, State> {
|
||||
if (this._shouldShowButton('raisehand')) {
|
||||
buttonsLeft.push('raisehand');
|
||||
}
|
||||
if (this._shouldShowButton('chat')) {
|
||||
buttonsLeft.push('chat');
|
||||
}
|
||||
if (this._shouldShowButton('closedcaptions')) {
|
||||
buttonsLeft.push('closedcaptions');
|
||||
}
|
||||
@@ -1239,15 +1248,6 @@ class Toolbox extends Component<Props, State> {
|
||||
return (
|
||||
<div className = 'toolbox-content'>
|
||||
<div className = 'button-group-left'>
|
||||
{ buttonsLeft.indexOf('desktop') !== -1
|
||||
&& this._renderDesktopSharingButton() }
|
||||
{ buttonsLeft.indexOf('raisehand') !== -1
|
||||
&& <ToolbarButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = { IconRaisedHand }
|
||||
onClick = { this._onToolbarToggleRaiseHand }
|
||||
toggled = { _raisedHand }
|
||||
tooltip = { t('toolbar.raiseHand') } /> }
|
||||
{ buttonsLeft.indexOf('chat') !== -1
|
||||
&& <div className = 'toolbar-button-with-badge'>
|
||||
<ToolbarButton
|
||||
@@ -1258,6 +1258,15 @@ class Toolbox extends Component<Props, State> {
|
||||
tooltip = { t('toolbar.chat') } />
|
||||
<ChatCounter />
|
||||
</div> }
|
||||
{ buttonsLeft.indexOf('desktop') !== -1
|
||||
&& this._renderDesktopSharingButton() }
|
||||
{ buttonsLeft.indexOf('raisehand') !== -1
|
||||
&& <ToolbarButton
|
||||
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
|
||||
icon = { IconRaisedHand }
|
||||
onClick = { this._onToolbarToggleRaiseHand }
|
||||
toggled = { _raisedHand }
|
||||
tooltip = { t('toolbar.raiseHand') } /> }
|
||||
{
|
||||
buttonsLeft.indexOf('closedcaptions') !== -1
|
||||
&& <ClosedCaptionButton />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { hasAvailableDevices } from '../base/devices';
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
@@ -43,8 +44,10 @@ export function isToolboxVisible(state: Object) {
|
||||
visible
|
||||
} = state['features/toolbox'];
|
||||
const { audioSettingsVisible, videoSettingsVisible } = state['features/settings'];
|
||||
const { isOpen } = state['features/chat'];
|
||||
const isMobileChatOpen = isMobileBrowser() && isOpen;
|
||||
|
||||
return Boolean(!iAmSipGateway && (timeoutID || visible || alwaysVisible
|
||||
return Boolean(!isMobileChatOpen && !iAmSipGateway && (timeoutID || visible || alwaysVisible
|
||||
|| audioSettingsVisible || videoSettingsVisible));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
import {
|
||||
VIDEO_QUALITY_LEVELS,
|
||||
setMaxReceiverVideoQuality
|
||||
} from '../base/conference';
|
||||
import {
|
||||
getPinnedParticipant,
|
||||
pinParticipant
|
||||
@@ -32,8 +28,6 @@ StateListenerRegistry.register(
|
||||
dispatch(selectParticipant());
|
||||
|
||||
if (!displayTileView) {
|
||||
dispatch(setMaxReceiverVideoQuality(VIDEO_QUALITY_LEVELS.HIGH));
|
||||
|
||||
if (_getAutoPinSetting()) {
|
||||
_updateAutoPinnedParticipant(store);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { setPreferredVideoQuality } from '../base/conference/actions';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
VIDEO_QUALITY_LEVELS,
|
||||
getNearestReceiverVideoQualityLevel,
|
||||
setMaxReceiverVideoQuality,
|
||||
setPreferredVideoQuality
|
||||
} from '../base/conference';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||
import { shouldDisplayTileView } from '../video-layout';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
@@ -31,3 +37,35 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/**
|
||||
* Implements a state listener in order to calculate max receiver video quality.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
const _shouldDisplayTileView = shouldDisplayTileView(state);
|
||||
const thumbnailSize = state['features/filmstrip']?.tileViewDimensions?.thumbnailSize;
|
||||
|
||||
return {
|
||||
displayTileView: _shouldDisplayTileView,
|
||||
reducedUI,
|
||||
thumbnailHeight: thumbnailSize?.height
|
||||
};
|
||||
},
|
||||
/* listener */ ({ displayTileView, reducedUI, thumbnailHeight }, { dispatch, getState }) => {
|
||||
const { maxReceiverVideoQuality } = getState()['features/base/conference'];
|
||||
let newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.HIGH;
|
||||
|
||||
if (reducedUI) {
|
||||
newMaxRecvVideoQuality = VIDEO_QUALITY_LEVELS.LOW;
|
||||
} else if (displayTileView && !Number.isNaN(thumbnailHeight)) {
|
||||
newMaxRecvVideoQuality = getNearestReceiverVideoQualityLevel(thumbnailHeight);
|
||||
}
|
||||
|
||||
if (maxReceiverVideoQuality !== newMaxRecvVideoQuality) {
|
||||
dispatch(setMaxReceiverVideoQuality(newMaxRecvVideoQuality));
|
||||
}
|
||||
}, {
|
||||
deepEquals: true
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { getFeatureFlag, VIDEO_SHARE_BUTTON_ENABLED } from '../../base/flags';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { IconShareVideo } from '../../base/icons';
|
||||
import { getLocalParticipant } from '../../base/participants';
|
||||
@@ -90,21 +91,26 @@ class VideoShareButton extends AbstractButton<Props, *> {
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The properties explicitly passed to the component instance.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state): Object {
|
||||
function _mapStateToProps(state, ownProps): Object {
|
||||
const { ownerId, status: sharedVideoStatus } = state['features/youtube-player'];
|
||||
const localParticipantId = getLocalParticipant(state).id;
|
||||
const enabled = getFeatureFlag(state, VIDEO_SHARE_BUTTON_ENABLED, true);
|
||||
const { visible = enabled } = ownProps;
|
||||
|
||||
if (ownerId !== localParticipantId) {
|
||||
return {
|
||||
_isDisabled: isSharingStatus(sharedVideoStatus),
|
||||
_sharingVideo: false };
|
||||
_sharingVideo: false,
|
||||
visible };
|
||||
}
|
||||
|
||||
return {
|
||||
_sharingVideo: isSharingStatus(sharedVideoStatus)
|
||||
_sharingVideo: isSharingStatus(sharedVideoStatus),
|
||||
visible
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { getJitsiMeetTransport } from '../modules/transport';
|
||||
import { App } from './features/app/components';
|
||||
import { getLogger } from './features/base/logging/functions';
|
||||
import { Platform } from './features/base/react';
|
||||
import { getJitsiMeetGlobalNS } from './features/base/util';
|
||||
import PrejoinApp from './features/prejoin/components/PrejoinApp';
|
||||
|
||||
const logger = getLogger('index.web');
|
||||
const OS = Platform.OS;
|
||||
@@ -20,9 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
APP.connectionTimes['document.ready'] = now;
|
||||
logger.log('(TIME) document ready:\t', now);
|
||||
|
||||
// Render the main/root Component.
|
||||
ReactDOM.render(<App />, document.getElementById('react'));
|
||||
});
|
||||
|
||||
// Workaround for the issue when returning to a page with the back button and
|
||||
@@ -56,3 +55,21 @@ window.addEventListener('beforeunload', () => {
|
||||
APP.API.dispose();
|
||||
getJitsiMeetTransport().dispose();
|
||||
});
|
||||
|
||||
const globalNS = getJitsiMeetGlobalNS();
|
||||
|
||||
globalNS.entryPoints = {
|
||||
APP: App,
|
||||
PREJOIN: PrejoinApp
|
||||
};
|
||||
|
||||
globalNS.renderEntryPoint = ({
|
||||
Component,
|
||||
props = {},
|
||||
elementId = 'react'
|
||||
}) => {
|
||||
ReactDOM.render(
|
||||
<Component { ...props } />,
|
||||
document.getElementById(elementId)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -153,6 +153,20 @@ function filter_session(session)
|
||||
end
|
||||
end
|
||||
|
||||
function attach_lobby_room(room)
|
||||
local node = jid_split(room.jid);
|
||||
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
|
||||
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
|
||||
local new_room = lobby_muc_service.create_room(lobby_room_jid);
|
||||
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
|
||||
new_room.main_room = room;
|
||||
room._data.lobbyroom = new_room;
|
||||
room:save(true);
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
@@ -251,15 +265,14 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- hooks when lobby is enabled to create its room, only done here or by admin
|
||||
host_module:hook('muc-config-submitted', function(event)
|
||||
local actor, room = event.actor, event.room;
|
||||
local actor_node = jid_split(actor);
|
||||
if actor_node == 'focus' then
|
||||
return;
|
||||
end
|
||||
local members_only = event.fields['muc#roomconfig_membersonly'] and true or nil;
|
||||
if members_only then
|
||||
local node = jid_split(room.jid);
|
||||
|
||||
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
|
||||
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
|
||||
local new_room = lobby_muc_service.create_room(lobby_room_jid);
|
||||
new_room.main_room = room;
|
||||
room._data.lobbyroom = new_room;
|
||||
local lobby_created = attach_lobby_room(room);
|
||||
if lobby_created then
|
||||
event.status_codes['104'] = true;
|
||||
notify_lobby_enabled(room, actor, true);
|
||||
end
|
||||
@@ -373,6 +386,14 @@ function update_session(event)
|
||||
end
|
||||
end
|
||||
|
||||
function handle_create_lobby(event)
|
||||
local room = event.room;
|
||||
room:set_members_only(true);
|
||||
module:log("info","Set room jid = %s as members only",room.jid);
|
||||
attach_lobby_room(room)
|
||||
end
|
||||
|
||||
module:hook_global('bosh-session', update_session);
|
||||
module:hook_global('websocket-session', update_session);
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
module:hook_global('create-lobby-room', handle_create_lobby);
|
||||
16
resources/update-ljm.sh
Executable file
16
resources/update-ljm.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e -u
|
||||
|
||||
THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")")" && pwd)
|
||||
LATEST_LJM_COMMIT=$(git ls-remote https://github.com/jitsi/lib-jitsi-meet.git HEAD | awk '{ print $1 }')
|
||||
|
||||
pushd ${THIS_DIR}/..
|
||||
|
||||
npm install github:jitsi/lib-jitsi-meet#${LATEST_LJM_COMMIT}
|
||||
git add package.json package-lock.json
|
||||
git commit -m "deps: lib-jitsi-meet@latest"
|
||||
|
||||
popd
|
||||
|
||||
echo "Done! Now push your branch to GH and open a PR!"
|
||||
41
static/prejoin.html
Normal file
41
static/prejoin.html
Normal file
@@ -0,0 +1,41 @@
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--#include virtual="/base.html" -->
|
||||
<link rel="stylesheet" href="../css/all.css">
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!JitsiMeetJS.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(window.location.href);
|
||||
const params = new URLSearchParams(url.search);
|
||||
const showAvatar = params.get('showAvatar') === 'true';
|
||||
const showJoinActions = params.get('showJoinActions') === 'true';
|
||||
const css = params.get('style');
|
||||
const style = document.createElement('style');
|
||||
style.appendChild(document.createTextNode(css));
|
||||
document.head.appendChild(style);
|
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({
|
||||
Component: JitsiMeetJS.app.entryPoints.PREJOIN,
|
||||
props: {
|
||||
showAvatar,
|
||||
showJoinActions
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<!--#include virtual="/title.html" -->
|
||||
<script>var config = {}</script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script>var interfaceConfig = {}</script>
|
||||
<script src="../libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
<script src="../libs/app.bundle.min.js?v=139"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="react"></div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user