Compare commits

..

59 Commits

Author SHA1 Message Date
xenia 4bd73cc368 jiti modifications >:3 2023-03-08 22:19:23 -08:00
Boris Grozev e12999d44f chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1588.0.0+04e906cc...v1589.0.0+d43c349d
2023-03-08 11:16:05 -05:00
Robert Pintilii 8982f17ce1
feat(virtual-background) Move dialog to SettingsDialog tab (#13005)
Implement redesign
2023-03-08 13:15:07 +02:00
Robert Pintilii c8f1690057
ref(feedback-dialog) Update design (#12926)
Convert file to TS
Move styles from SCSS to JSS
2023-03-08 12:46:10 +02:00
Robert Pintilii aa57309057
ref(more-tab) Update design on SettingsDialog More tab (#13006) 2023-03-08 10:40:40 +02:00
damencho fb81619fc5 fix: Fixes muc rate limit to fire occupant-pre-join.
If any handler returns a value (that isn't nil) then processing will halt and that value will be returned.
2023-03-07 18:54:06 -06:00
Hristo Terezov 5a5656020b fix(e2ee): enabled/supported flags calculation. 2023-03-07 15:36:47 -06:00
Hristo Terezov 0ff44a2f22 fix(participants-reducer):old particpant selection 2023-03-07 15:36:47 -06:00
Hristo Terezov 4d04ea325e fix(everyoneIsModerator): Optimize. 2023-03-07 15:36:47 -06:00
Hristo Terezov 42ce6dcc58 fix(e2ee): Optimize. 2023-03-07 15:36:47 -06:00
Hristo Terezov b033d0268a fix(speaker-stats): dispatch action only on change 2023-03-07 15:36:47 -06:00
Hristo Terezov 4aea40d34f fix: Batch actions. 2023-03-07 15:36:47 -06:00
Hristo Terezov e5a170fb28 fix(Filmstrip): Use id for localScreenShare. 2023-03-07 15:36:47 -06:00
Hristo Terezov d1cf5578fc fix(avatar): Remove unnecessary code. 2023-03-07 15:36:47 -06:00
Hristo Terezov 4b29af6b5f fix(lastN): Update only if neccessary. 2023-03-07 15:36:47 -06:00
bgrozev f3481576ff
doc: Document new bridgeChannel options. (#13010) 2023-03-07 14:21:41 -06:00
bgrozev 455a91a5c6
chore(deps) lib-jitsi-meet@latest (#13009)
https://github.com/jitsi/lib-jitsi-meet/compare/v1586.0.0+df2c3096...v1588.0.0+04e906cc
2023-03-07 13:44:42 -06:00
Gabriel Borlea 297ab194a8
fix(dialog-portal): set z-index to high value (#13004) 2023-03-07 16:39:27 +02:00
Christoph Settgast 077a88a803
lang: update German translation (#13000) 2023-03-07 08:17:41 +01:00
Gabriel Borlea 02c232440e
fix(av-moderation): buttons for participants pane (#12977)
* fix(av): buttons for participants pane

* fix tests

* fix lint

* rename cliked from participant pane
2023-03-06 11:05:26 -06:00
Emmanuel Pelletier f727b9295f
Use tabs ARIA design pattern when using tabbed UI (#12994)
feat(a11y): use tabs ARIA design pattern when using tabbed UI
2023-03-06 17:13:29 +02:00
Robert Pintilii 0d0bec3aad
feat(device-selection) Separate Devices into Audio and Video in Settings (#12987)
Create separate tabs for Audio and Video in the Settings Dialog
Move some settings from the More tab to Audio/ Video tab
Implement redesign
Convert some files to TS
Move some styles from SCSS to JSS
Enable device selection on welcome page
2023-03-06 15:14:52 +02:00
Emmanuel Pelletier cfb8589bef
Use focus-visible for some focus styles to fix a11y issues (#12968)
feat(a11y): fix-focus-styles
2023-03-06 12:39:46 +02:00
japm48 65730e256e fix(lang) update Spanish translation 2023-03-04 15:41:39 +01:00
Robert Pintilii 7b8b911fee
feat(shortcuts) Update dialog (#12993)
Create Shortcuts tab in Settings Dialog
Move keyboard shortcut option from More to this tab
Move shortcuts info from KeyboardShortcuts dialog to this tab
Remove KeyboardShortcuts dialog
2023-03-03 13:48:17 +02:00
Robert Pintilii 036286a1d6
feat(notification-settings) Update Sounds tab in Settings Dialog (#12990)
Rename from Sounds to Notifications
Move Notifications settings from More tab to this tab
2023-03-03 12:53:39 +02:00
Robert Pintilii d550254f31
ref(moderator-settings) Update Moderator tab in Settings Dialog (#12991)
Update design
2023-03-03 11:48:00 +02:00
Robert Pintilii b1a71d55d7
feat(profile-tab) Update Profile tab in Settings Dialog (#12992)
Implement redesign
Move some options from More to this tab
2023-03-03 10:42:59 +02:00
George Politis 17ed45799c feat: Sends the statisticsDisplayName to rtcstats. 2023-03-02 19:34:25 +01:00
Jaya Allamsetty e5681382b0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1585.0.0+362d1b2c...v1586.0.0+df2c3096
2023-03-01 20:58:55 -05:00
Robert Pintilii c27cb25afe
chore(deps) Upgrade eslint (#12981) 2023-03-01 13:30:43 +02:00
Avram Tudor baf5aa14e8
feat(devices) scroll overflow devices texts on hover (#12974) 2023-03-01 09:38:26 +02:00
Horatiu Muresan 29b6ce7721 fix(numbers-list) Re-add sip svg 2023-02-28 11:53:26 -06:00
Mihaela Dumitru 4f95c45e50
fix(ui) change streaming icon background + add tooltip (#12973) 2023-02-28 16:52:33 +02:00
Emmanuel Pelletier 72dd609247
feat(a11y) add headings across the app for easier screen reader nav (#12427)
feat(a11y): added headings across the app for easier screen reader nav
2023-02-28 16:52:06 +02:00
Horatiu Muresan fed74afffe
fix(sound-settings) Disable checkbox for disabled sounds (#12976) 2023-02-28 16:47:00 +02:00
Mihaela Dumitru 204f34cccb
fix(pinning) Fix whiteboard pinning behavior when stage filmstrip is enabled (#12966) 2023-02-28 14:44:37 +02:00
Emmanuel Pelletier c81777a475
Make (most) UI elements reachable via keyboard (#12657)
feat(a11y): make (most) UI elements reachable via keyboard
2023-02-28 12:21:15 +02:00
Horatiu Muresan 778bca3031
fix(always-on-top) Show participant`s avatar (#12967) 2023-02-27 16:20:40 +02:00
Avram Tudor 336fa304ce
feat(notifications) trigger iframe api event when a notification occurs (#12952)
* feat(notifications) trigger iframe api event when a notification occurs

* remove useless comment

* fix typo
2023-02-27 15:31:54 +02:00
damencho f14b69166c fix: Fixes visitors count when the data is missing. 2023-02-24 11:44:31 -06:00
Calin-Teodor e405595a11 feat(notifications): moved notifications to the bottom of the screen 2023-02-24 17:22:55 +02:00
zobadaniel cabe48d66a
lang: Add lower sorbian translation (#12947)
* add lower sorbian translation

* adjustments after using update-translation.js

* adjustments after running lint

* add translation into language list
2023-02-24 09:04:06 -06:00
Duduman Bogdan Vlad 8d7f46024b
feat(webhid) - add integration webhid telephony device (#12904) 2023-02-24 16:37:30 +02:00
damencho d7f6c2bbf0 feat: Introduces iAmVisitor mode. 2023-02-24 07:51:47 -06:00
damencho 3c69645169 feat(mobile): Adds visitors count. 2023-02-24 07:51:47 -06:00
damencho abe2fa4dd4 feat: Adds visitors count in conference info. 2023-02-24 07:51:47 -06:00
damencho 863fd12488 feat: Hide self-view and buttons in visitors mode. 2023-02-24 07:51:47 -06:00
damencho 50c4748d40 feat: Adds visitors count in participants pane. 2023-02-24 07:51:47 -06:00
damencho f83840a3bc feat: Adds initial impl of visitors feature. 2023-02-24 07:51:47 -06:00
damencho 1466d7d149 fix: Always normalizes bosh config. 2023-02-24 07:51:47 -06:00
Calinteodor d0fe034db5
feat(base/dialog): visibility control for PageReloadDialog (#12961)
* feat(base/dialog): visibility control for  PageReloadDialog
2023-02-24 13:51:27 +02:00
Дамян Минков 8225f5e363
feat: Drops external connect optimization. (#12958)
* feat: Drops external connect optimization.

A bosh only optimization which is no longer used and does not bring any significant value when measured.

* squash: Updates ljm.
2023-02-24 08:59:00 +01:00
Saúl Ibarra Corretgé c641835d0f fix(virtual-background) don't disable uploads
Even when images are provided via dyamic branding.

Fixes: https://github.com/jitsi/jitsi-meet/issues/12941
2023-02-23 16:41:27 +01:00
Horatiu Muresan 35ee92869f
feat(deeplinking) Refactor deeplinking (#12950)
- redesign deeplinking mobile page, desktop page and dial in number page
- now dial in number page is an entry point in app.bundle.
2023-02-23 17:01:55 +02:00
Stefan Plücken 9b7a5ffdd1 fix(reservations): fixes errors not being displayed when reservation system returns error. removed translations applied twice. (#11144) 2023-02-23 08:52:54 -06:00
Calin-Teodor 581c2e621c feat(notifications): fix width for notifications on all devices 2023-02-23 16:39:19 +02:00
Mihaela Dumitru f3117f3037
fix(video-layout) get pinned participant from payload to determine pinning behavior (#12951) 2023-02-23 14:41:29 +02:00
Saúl Ibarra Corretgé 877ef58dfb deps(rn) react-native-webrtc@106.0.6
Fixes to SDP negotiation and transceiver lifetime.
2023-02-23 13:08:49 +01:00
203 changed files with 7489 additions and 3455 deletions

View File

@ -44,8 +44,6 @@ deploy-appbundle:
cp \
$(BUILD_DIR)/app.bundle.min.js \
$(BUILD_DIR)/app.bundle.min.js.map \
$(BUILD_DIR)/do_external_connect.min.js \
$(BUILD_DIR)/do_external_connect.min.js.map \
$(BUILD_DIR)/external_api.min.js \
$(BUILD_DIR)/external_api.min.js.map \
$(BUILD_DIR)/alwaysontop.min.js \
@ -68,7 +66,6 @@ deploy-lib-jitsi-meet:
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.js \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.min.map \
$(LIBJITSIMEET_DIR)/dist/umd/lib-jitsi-meet.e2ee-worker.js \
$(LIBJITSIMEET_DIR)/connection_optimization/external_connect.js \
$(LIBJITSIMEET_DIR)/modules/browser/capabilities.json \
$(DEPLOY_DIR)
@ -129,7 +126,7 @@ dev: deploy-init deploy-css deploy-rnnoise-binary deploy-tflite deploy-meet-mode
source-package:
mkdir -p source_package/jitsi-meet/css && \
cp -r *.js *.html resources/*.txt connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
cp -r *.js *.html resources/*.txt favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
cp css/all.css source_package/jitsi-meet/css && \
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
rm -rf source_package

11
app.js
View File

@ -33,17 +33,6 @@ window.APP = {
API,
conference,
// Used by do_external_connect.js if we receive the attach data after
// connect was already executed. status property can be 'initialized',
// 'ready', or 'connecting'. We are interested in 'ready' status only which
// means that connect was executed but we have to wait for the attach data.
// In status 'ready' handler property will be set to a function that will
// finish the connect process when the attach data or error is received.
connect: {
handler: null,
status: 'initialized'
},
// Used for automated performance tests.
connectionTimes: {
'index.loaded': window.indexLoadedTime

View File

@ -153,6 +153,7 @@ import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise'
import { endpointMessageReceived } from './react/features/subtitles';
import { handleToggleVideoMuted } from './react/features/toolbox/actions.any';
import { muteLocal } from './react/features/video-menu/actions.any';
import { setIAmVisitor } from './react/features/visitors/actions';
import UIEvents from './service/UI/UIEvents';
const logger = Logger.getLogger(__filename);
@ -342,6 +343,8 @@ class ConferenceConnector {
case JitsiConferenceErrors.REDIRECTED: {
generateVisitorConfig(APP.store.getState(), params);
APP.store.dispatch(setIAmVisitor(true));
connection.disconnect().then(() => {
connect(this._conference.roomName).then(con => {
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);

View File

@ -46,9 +46,9 @@ var config = {
},
// BOSH URL. FIXME: use XEP-0156 to discover it.
bosh: '//jitsi-meet.example.com/' + subdir + 'http-bind',
bosh: 'https://jitsi-meet.example.com/' + subdir + 'http-bind',
// Websocket URL
// Websocket URL (XMPP)
// websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
// The real JID of focus participant - can be overridden here
@ -56,6 +56,19 @@ var config = {
// https://github.com/jitsi/jitsi-meet/issues/7376
// focusUserJid: 'focus@auth.jitsi-meet.example.com',
// Options related to the bridge (colibri) data channel
bridgeChannel: {
// If the backend advertises multiple colibri websockets, this options allows
// to filter some of them out based on the domain name. We use the first URL
// which does not match ignoreDomain, falling back to the first one that matches
// ignoreDomain. Has no effect if undefined.
// ignoreDomain: 'example.com',
// Prefer SCTP (WebRTC data channels over the media path) over a colibri websocket.
// If SCTP is available in the backend it will be used instead of a WS. Defaults to
// false (SCTP is used only if available and no WS are available).
// preferSctp: false
},
// Testing / experimental features.
//
@ -1372,7 +1385,6 @@ var config = {
dialOutRegionUrl
disableRemoteControl
displayJids
externalConnectUrl
e2eeLabels
firefox_fake_device
googleApiApplicationClientID

View File

@ -32,54 +32,6 @@ const logger = Logger.getLogger(__filename);
*/
export const DISCO_JIBRI_FEATURE = 'http://jitsi.org/protocol/jibri';
/**
* Checks if we have data to use attach instead of connect. If we have the data
* executes attach otherwise check if we have to wait for the data. If we have
* to wait for the attach data we are setting handler to APP.connect.handler
* which is going to be called when the attach data is received otherwise
* executes connect.
*
* @param {string} [id] user id
* @param {string} [password] password
* @param {string} [roomName] the name of the conference.
*/
function checkForAttachParametersAndConnect(id, password, connection) {
if (window.XMPPAttachInfo) {
APP.connect.status = 'connecting';
// When connection optimization is not deployed or enabled the default
// value will be window.XMPPAttachInfo.status = "error"
// If the connection optimization is deployed and enabled and there is
// a failure the value will be window.XMPPAttachInfo.status = "error"
if (window.XMPPAttachInfo.status === 'error') {
connection.connect({
id,
password
});
return;
}
const attachOptions = window.XMPPAttachInfo.data;
if (attachOptions) {
connection.attach(attachOptions);
delete window.XMPPAttachInfo.data;
} else {
connection.connect({
id,
password
});
}
} else {
APP.connect.status = 'ready';
APP.connect.handler
= checkForAttachParametersAndConnect.bind(
null,
id, password, connection);
}
}
/**
* Try to open connection using provided credentials.
* @param {string} [id]
@ -182,7 +134,10 @@ export async function connect(id, password) {
APP.store.dispatch(setPrejoinDisplayNameRequired());
}
checkForAttachParametersAndConnect(id, password, connection);
connection.connect({
id,
password
});
});
}

View File

@ -1,3 +0,0 @@
module.exports = {
'extends': '../react/.eslintrc.js'
};

View File

@ -1,86 +0,0 @@
/* global config, createConnectionExternally */
import getRoomName from '../react/features/base/config/getRoomName';
import { parseURLParams } from '../react/features/base/util/parseURLParams';
/**
* Implements external connect using createConnectionExternally function defined
* in external_connect.js for Jitsi Meet. Parses the room name and JSON Web
* Token (JWT) from the URL and executes createConnectionExternally.
*
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet, you should use this
* file as reference only because the implementation is Jitsi Meet-specific.
*
* NOTE: For optimal results this file should be included right after
* external_connect.js.
*/
if (typeof createConnectionExternally === 'function') {
// URL params have higher priority than config params.
// Do not use external connect if websocket is enabled.
let url
= parseURLParams(window.location, true, 'hash')[
'config.externalConnectUrl']
|| config.websocket ? undefined : config.externalConnectUrl;
const isRecorder
= parseURLParams(window.location, true, 'hash')['config.iAmRecorder'];
let roomName;
if (url && (roomName = getRoomName()) && !isRecorder) {
url += `?room=${roomName}`;
const token = parseURLParams(window.location, true, 'search').jwt;
if (token) {
url += `&token=${token}`;
}
createConnectionExternally(
url,
connectionInfo => {
// Sets that global variable to be used later by connect method
// in connection.js.
window.XMPPAttachInfo = {
status: 'success',
data: connectionInfo
};
checkForConnectHandlerAndConnect();
},
errorCallback);
} else {
errorCallback();
}
} else {
errorCallback();
}
/**
* Check if connect from connection.js was executed and executes the handler
* that is going to finish the connect work.
*
* @returns {void}
*/
function checkForConnectHandlerAndConnect() {
window.APP
&& window.APP.connect.status === 'ready'
&& window.APP.connect.handler();
}
/**
* Implements a callback to be invoked if anything goes wrong.
*
* @param {Error} error - The specifics of what went wrong.
* @returns {void}
*/
function errorCallback(error) {
// The value of error is undefined if external connect is disabled.
error && console.warn(error);
// Sets that global variable to be used later by connect method in
// connection.js.
window.XMPPAttachInfo = {
status: 'error'
};
checkForConnectHandlerAndConnect();
}

View File

@ -22,11 +22,7 @@
}
&-entry-text {
display: inline-block;
text-overflow: ellipsis;
max-width: 213px;
overflow: hidden;
white-space: nowrap;
&.left-margin {
margin-left: 36px;

67
css/_jiti.scss Normal file
View File

@ -0,0 +1,67 @@
@keyframes rotateAroundY {
from { transform: rotateY(0deg); }
to { transform: rotateY(360deg); }
}
@keyframes rainbowRoad {
to {
background-position: 400% 0 !important;
}
}
body {
font-family: "Comic Sans MS", "Comic Sans", sans-serif !important;
}
a {
background: linear-gradient(90deg, rgba(255,0,0,1) 0%, rgba(255,154,0,1) 10%, rgba(208,222,33,1) 20%, rgba(79,220,74,1) 30%, rgba(63,218,216,1) 40%, rgba(47,201,226,1) 50%, rgba(28,127,238,1) 60%, rgba(95,21,242,1) 70%, rgba(186,12,248,1) 80%, rgba(251,7,217,1) 90%, rgba(255,0,0,1) 100%) !important;
-webkit-background-clip: text !important;
-webkit-text-fill-color: transparent !important;
-moz-background-clip: text !important;
-moz-text-fill-color: transparent !important;
animation: rainbowRoad 8s linear infinite !important;
}
a:hover {
}
.dominant-speaker {
box-shadow: inset 0px 0px 0px 4px rgba(255,0,255,0.33) !important;
}
.display-avatar-only {
background-image: url("");
}
.videocontainer:nth-child(odd) {
transform: rotate(1.5deg);
}
.videocontainer:nth-child(even) {
transform: rotate(-1.3deg);
}
#largeVideoContainer.videocontainer {
transform: none;
}
.sideToolbarContainer {
transform: rotate(-1.1deg);
}
.displayname:before {
content: "♡︎ ";
}
.displayname:after {
content: " ♡︎";
}
.avatar, .userAvatar {
transform: rotateY(0deg);
}
.avatar:hover, .userAvatar:hover {
animation: rotateAroundY 3.6s linear infinite;
}

View File

@ -82,6 +82,7 @@
}
.left-column {
order: -1;
display: flex;
flex-direction: column;
flex-grow: 0;
@ -92,6 +93,7 @@
.right-column {
display: flex;
flex-direction: column;
align-items: flex-start;
flex-grow: 1;
padding-left: 16px;
padding-top: 13px;
@ -99,11 +101,11 @@
}
.title {
font-size: 12px;
font-weight: 600;
line-height: 16px;
padding-bottom: 4px;
}
font-size: 12px;
font-weight: 600;
line-height: 16px;
margin-bottom: 4px;
}
.subtitle {
color: #5E6D7A;
@ -125,8 +127,7 @@
cursor: pointer;
}
&.with-click-handler:hover,
&.with-click-handler:focus {
&.with-click-handler:hover {
background-color: #c7ddff;
}

View File

@ -1,3 +1,3 @@
#polls-panel {
.polls-panel {
height: calc(100% - 119px);
}

View File

@ -90,7 +90,7 @@ body.welcome-page {
font-size: 14px;
padding-left: 10px;
&:focus {
&.focus-visible {
outline: auto 2px #005fcc;
}
}
@ -167,7 +167,7 @@ body.welcome-page {
margin: 4px;
display: $welcomePageTabButtonsDisplay;
.tab {
[role="tab"] {
background-color: #c7ddff;
border-radius: 7px;
cursor: pointer;
@ -176,8 +176,10 @@ body.welcome-page {
margin: 2px;
padding: 7px 0;
text-align: center;
color: inherit;
border: 0;
&.selected {
&[aria-selected="true"] {
background-color: #FFF;
}
}

View File

@ -33,7 +33,6 @@ $flagsImagePath: "../images/";
@import 'reload_overlay/reload_overlay';
@import 'mini_toolbox';
@import 'modals/desktop-picker/desktop-picker';
@import 'modals/device-selection/device-selection';
@import 'modals/dialog';
@import 'modals/embed-meeting/embed-meeting';
@import 'modals/feedback/feedback';
@ -95,3 +94,9 @@ $flagsImagePath: "../images/";
@import 'notifications';
/* Modules END */
/* Jeet crew BEGIN */
@import 'jiti';
/* Jeet crew END */

View File

@ -1,148 +0,0 @@
.device-selection {
.device-selectors {
font-size: 14px;
> div {
display: block;
margin-bottom: 4px;
}
.device-selector-icon {
align-self: center;
color: inherit;
font-size: 20px;
margin-left: 3px;
}
.device-selector-label {
margin-bottom: 1px;
}
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
.device-selector-trigger {
background-color: #0E1624;
border: 1px solid #455166;
border-radius: 5px;
display: flex;
height: 2.3em;
justify-content: space-between;
line-height: 2.3em;
overflow: hidden;
padding: 0 8px;
}
.device-selector-trigger-disabled {
.device-selector-trigger {
color: #a5adba;
cursor: default;
}
}
.device-selector-trigger-text {
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.device-selection-column {
box-sizing: border-box;
display: inline-block;
vertical-align: top;
&.column-selectors {
margin-left: 15px;
width: 45%;
}
&.column-video {
width: 50%;
}
}
.device-selection-video-container {
border-radius: 3px;
margin-bottom: 5px;
.video-input-preview {
margin-top: 2px;
position: relative;
> video {
border-radius: 3px;
}
.video-input-preview-error {
color: $participantNameColor;
display: none;
left: 0;
position: absolute;
right: 0;
text-align: center;
top: 50%;
}
&.video-preview-has-error {
background: black;
.video-input-preview-error {
display: block;
}
}
.video-input-preview-display {
height: auto;
overflow: hidden;
width: 100%;
}
}
}
.audio-output-preview {
font-size: 14px;
a {
color: #6FB1EA;
cursor: pointer;
text-decoration: none;
}
a:hover {
color: #B3D4FF;
}
}
.audio-input-preview {
background: #1B2638;
border-radius: 5px;
height: 8px;
.audio-input-preview-level {
background: #75B1FF;
border-radius: 5px;
height: 100%;
-webkit-transition: width .1s ease-in-out;
-moz-transition: width .1s ease-in-out;
-o-transition: width .1s ease-in-out;
transition: width .1s ease-in-out;
}
}
}
.device-selection.video-hidden {
display: flex;
flex-direction: column;
width: 100%;
.column-selectors {
width: 100%;
margin-left: 0;
}
.column-video {
order: 1;
width: 100%;
margin-top: 8px;
}
}

View File

@ -44,61 +44,3 @@
-webkit-animation-timing-function: ease-in-out;
animation-timing-function: ease-in-out
}
.feedback-dialog {
margin-bottom: 5px;
.details {
textarea {
min-height: 100px;
}
}
.input-control {
background-color: $feedbackInputBg;
color: $feedbackInputTextColor;
&::-webkit-input-placeholder {
color: $feedbackInputPlaceholderColor;
}
&::-moz-placeholder { /* Firefox 19+ */
color: $feedbackInputPlaceholderColor;
}
&:-ms-input-placeholder {
color: $feedbackInputPlaceholderColor;
}
}
.rating {
line-height: 1.2;
margin-top: 10px;
text-align: center;
.star-label {
font-size: 14px;
height: 16px;
}
.star-btn {
color: inherit;
cursor: pointer;
display: inline-block;
font-size: 34px;
outline: none;
position: relative;
text-decoration: none;
@include transition(all .2s ease);
&.active,
&:hover,
&.starHover {
color: #36B37E;
};
}
.star-btn:focus,
.star-btn:active {
outline: 1px solid #B8C7E0;
}
}
}

View File

@ -8,7 +8,6 @@ sounds /usr/share/jitsi-meet/
fonts /usr/share/jitsi-meet/
images /usr/share/jitsi-meet/
lang /usr/share/jitsi-meet/
connection_optimization /usr/share/jitsi-meet/
resources/robots.txt /usr/share/jitsi-meet/
resources/*.sh /usr/share/jitsi-meet/scripts/
pwa-worker.js /usr/share/jitsi-meet/

View File

@ -93,7 +93,7 @@ server {
}
# ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
location ~ ^/(libs|css|static|images|fonts|lang|sounds|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;

View File

@ -182,8 +182,6 @@
'error', loadErrHandler, true /* capture phase type of listener */);
</script>
<script><!--#include virtual="/config.js" --></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<!--#include virtual="connection_optimization/connection_optimization.html" -->
<script src="libs/do_external_connect.min.js?v=1"></script>
<script><!--#include virtual="/interface_config.js" --></script>
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
<script src="libs/app.bundle.min.js?v=139"></script>

View File

@ -9,7 +9,7 @@
*/
var interfaceConfig = {
APP_NAME: 'Jitsi Meet',
APP_NAME: 'JitSea 🏴‍☠️',
AUDIO_LEVEL_PRIMARY_COLOR: 'rgba(255,255,255,0.4)',
AUDIO_LEVEL_SECONDARY_COLOR: 'rgba(255,255,255,0.2)',

View File

@ -389,7 +389,7 @@ PODS:
- react-native-video/Video (6.0.0-alpha.1):
- PromisesSwift
- React-Core
- react-native-webrtc (106.0.5):
- react-native-webrtc (106.0.6):
- JitsiWebRTC (~> 106.0.0)
- React-Core
- react-native-webview (11.15.1):
@ -754,7 +754,7 @@ SPEC CHECKSUMS:
react-native-slider: 6e9b86e76cce4b9e35b3403193a6432ed07e0c81
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: bb6f12a7198db53b261fefb5d609dc77417acc8b
react-native-webrtc: ef315d8adb68e78298b22100377d12ef168efdb5
react-native-webrtc: 22ac6c64a1e38552bb173dde81ffea6979a58ef3
react-native-webview: ea4899a1056c782afa96dd082179a66cbebf5504
React-perflogger: 0458a87ea9a7342079e7a31b0d32b3734fb8415f
React-RCTActionSheet: 22538001ea2926dea001111dd2846c13a0730bc9

View File

@ -7,6 +7,7 @@
"cs": "Čeština",
"da": "Dansk",
"de": "Deutsch",
"dsb": "Dolnoserbšćina",
"el": "Ελληνικά",
"en": "English",
"enGB": "English (United Kingdom)",

View File

@ -184,13 +184,21 @@
"deepLinking": {
"appNotInstalled": "Sie benötigen die „{{app}}“-App, um der Konferenz auf dem Smartphone beizutreten.",
"description": "Nichts passiert? Wir haben versucht, die Konferenz in {{app}} zu öffnen. Versuchen Sie es erneut oder treten Sie der Konferenz in {{app}} im Web bei.",
"descriptionNew": "Nichts passiert? Wir haben versucht, die Konferenz in {{app}} zu öffnen. <br /><br /> Versuchen Sie es erneut oder treten Sie der Konferenz im Web bei.",
"descriptionWithoutWeb": "Ist nichts passiert? Wir haben versucht, Ihre Besprechung in der „{{app}}“-Desktop-App zu starten.",
"downloadApp": "App herunterladen",
"downloadMobileApp": "Aus dem App Store herunterladen",
"ifDoNotHaveApp": "Wenn Sie die App noch nicht haben:",
"ifHaveApp": "Wenn Sie die App bereits haben:",
"joinInApp": "Mit der App am Meeting teilnehmen",
"joinInAppNew": "Mit der App",
"joinInBrowser": "Im Browser",
"launchMeetingLabel": "Wie möchten Sie an der Konferenz teilnehmen?",
"launchWebButton": "Im Web öffnen",
"noMobileApp": "Sie haben die App noch nicht installiert?",
"termsAndConditions": "Indem Sie fortfahren, stimmen Sie underen<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>Nutzungsbedingungen</a> zu.",
"title": "Die Konferenz wird in {{app}} geöffnet …",
"titleNew": "Konferenz starten ...",
"tryAgainButton": "Erneut mit der nativen Applikation versuchen",
"unsupportedBrowser": "Sie verwenden einen Browser, der noch nicht unterstützt wird."
},
@ -203,6 +211,12 @@
"microphonePermission": "Fehler beim Bezug der Mikrofon-Zugriffsberechtigungen"
},
"deviceSelection": {
"hid": {
"callControl": "Anrufsteuerung",
"connectedDevices": "Verbundene Geräte:",
"deleteDevice": "Gerät löschen",
"pairDevice": "Gerät verbinden"
},
"noPermission": "Berechtigungen nicht erteilt",
"previewUnavailable": "Keine Vorschau verfügbar",
"selectADevice": "Ein Gerät wählen",
@ -226,7 +240,9 @@
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja",
"accessibilityLabel": {
"liveStreaming": "Livestream"
"close": "Popup schließen",
"liveStreaming": "Livestream",
"sharingTabs": "Optionen zum Teilen"
},
"add": "Hinzufügen",
"addMeetingNote": "Notiz zu dieser Konferenz hinzufügen",
@ -438,6 +454,11 @@
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Videominiaturen"
}
},
"giphy": {
"noResults": "Keine Ergebnisse :(",
"search": "GIPHY durchsuchen"
@ -751,6 +772,7 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Anwesende ({{count}})",
"visitors": "Gäste ({{count}})",
"waitingLobby": "In der Lobby ({{count}})"
},
"search": "Suche Anwesende",
@ -758,6 +780,7 @@
},
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
"passwordSetRemotely": "von einer anderen Person gesetzt",
"pinParticipant": "{{participantName}} - anheften",
"pinnedParticipant": "Die Person ist angeheftet",
"polls": {
"answer": {
@ -950,6 +973,7 @@
"title": "Sicherheitsoptionen"
},
"settings": {
"audio": "Audio",
"buttonLabel": "Einstellungen",
"calendar": {
"about": "Die Kalenderintegration von {{appName}} wird verwendet, um ein sicheres Zugreifen auf Ihren Kalender und Auslesen der bevorstehenden Termine zu ermöglichen.",
@ -970,9 +994,11 @@
"maxStageParticipants": "Maximale Anzahl an Personen, die zur Hauptansicht angeheftet werden können",
"microphones": "Mikrofon",
"moderator": "Moderation",
"moderatorOptions": "Moderationseinstellungen",
"more": "Mehr",
"name": "Name",
"noDevice": "Kein",
"notifications": "Benachrichtigungen",
"participantJoined": "Neue Person nimmt teil",
"participantKnocking": "Person hat Lobby betreten",
"participantLeft": "Person verlässt die Konferenz",
@ -983,13 +1009,14 @@
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"selfView": "Eigene Ansicht",
"sounds": "Hinweistöne",
"shortcuts": "Tastaturkürzel",
"speakers": "Lautsprecher",
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
"startReactionsMuted": "Interaktionstöne für alle deaktivieren",
"startVideoMuted": "Alle Personen treten ohne Video bei",
"talkWhileMuted": "Wenn bei Stummschaltung gesprochen wird",
"title": "Einstellungen"
"title": "Einstellungen",
"video": "Kamera"
},
"settingsView": {
"advanced": "Erweitert",
@ -1081,6 +1108,7 @@
"giphy": "GIPHY ein-/ausschalten",
"grantModerator": "Moderationsrechte vergeben",
"hangup": "Konferenz verlassen",
"heading": "Toolbar",
"help": "Hilfe",
"invite": "Person einladen",
"kick": "Person entfernen",
@ -1147,6 +1175,7 @@
"download": "Unsere Apps herunterladen",
"e2ee": "Ende-zu-Ende-Verschlüsselung",
"embedMeeting": "Konferenz einbetten",
"enableNoiseSuppression": "Rauschunterdrückung einschalten",
"endConference": "Konferenz für alle beenden",
"enterFullScreen": "Vollbildmodus",
"enterTileView": "Kachelansicht einschalten",
@ -1234,6 +1263,7 @@
"subtitlesOff": "Ausschalten",
"tr": "TR"
},
"unpinParticipant": "{{participantName}} - Nicht mehr anheften",
"userMedia": {
"androidGrantPermissions": "Wählen Sie <b><i>Zulassen</i></b>, wenn der Browser um Berechtigungen bittet.",
"chromeGrantPermissions": "Wählen Sie <b><i>Zulassen</i></b>, wenn der Browser um Berechtigungen bittet.",
@ -1272,9 +1302,11 @@
"ldTooltip": "Video wird in niedriger Auflösung angezeigt",
"lowDefinition": "Niedrige Auflösung",
"performanceSettings": "Qualitätseinstellungen",
"recording": "Aufnahme läuft",
"sd": "SD",
"sdTooltip": "Video wird in Standardauflösung angezeigt",
"standardDefinition": "Standardauflösung"
"standardDefinition": "Standardauflösung",
"streaming": "Streaming läuft"
},
"videothumbnail": {
"connectionInfo": "Verbindungsinformationen",
@ -1324,6 +1356,7 @@
"webAssemblyWarning": "WebAssembly wird nicht unterstützt",
"webAssemblyWarningDescription": "WebAssembly ist deaktiviert oder wird in diesem Browser nicht unterstützt"
},
"visitorsLabel": "Anzahl Gäste: {{count}}",
"volumeSlider": "Lautstärkeregler",
"welcomepage": {
"accessibilityLabel": {
@ -1356,6 +1389,7 @@
"microsoftLogo": "Microsoft Logo",
"policyLogo": "Richtlinienlogo"
},
"meetingsAccessibilityLabel": "Konferenzen",
"mobileDownLoadLinkAndroid": "Android App Download",
"mobileDownLoadLinkFDroid": "F-Droid App Download",
"mobileDownLoadLinkIos": "iOS App Download",
@ -1375,5 +1409,10 @@
"terms": "AGB",
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen",
"upcomingMeetings": "Ihre zukünftigen Konferenzen"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "Whiteboard"
}
}
}

1379
lang/main-dsb.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -838,7 +838,7 @@
"selectCamera": "Cámara",
"selectMic": "Micrófono",
"sounds": "Sonidos",
"speakers": "Parlantes",
"speakers": "Altavoces",
"startAudioMuted": "Todos inician silenciados",
"startVideoMuted": "Todos inician con cámara desactivada",
"talkWhileMuted": "Hablar en silencio",

View File

@ -211,10 +211,16 @@
"microphonePermission": "Error obtaining microphone permission"
},
"deviceSelection": {
"hid": {
"callControl": "Call control",
"connectedDevices": "Connected devices:",
"deleteDevice": "Delete device",
"pairDevice": "Pair device"
},
"noPermission": "Permission not granted",
"previewUnavailable": "Preview unavailable",
"selectADevice": "Select a device",
"testAudio": "Play a test sound"
"testAudio": "Test"
},
"dialIn": {
"screenTitle": "Dial-in summary"
@ -234,7 +240,9 @@
"WaitingForHostTitle": "Waiting for the host ...",
"Yes": "Yes",
"accessibilityLabel": {
"liveStreaming": "Live Stream"
"close": "Close dialog",
"liveStreaming": "Live Stream",
"sharingTabs": "Sharing options"
},
"add": "Add",
"addMeetingNote": "Add a note about this meeting",
@ -446,6 +454,11 @@
"veryBad": "Very Bad",
"veryGood": "Very Good"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Video thumbnails"
}
},
"giphy": {
"noResults": "No results found :(",
"search": "Search GIPHY"
@ -759,6 +772,7 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})",
"visitors": "Visitors ({{count}})",
"waitingLobby": "Waiting in lobby ({{count}})"
},
"search": "Search participants",
@ -766,6 +780,7 @@
},
"passwordDigitsOnly": "Up to {{number}} digits",
"passwordSetRemotely": "Set by another participant",
"pinParticipant": "{{participantName}} - Pin",
"pinnedParticipant": "The participant is pinned",
"polls": {
"answer": {
@ -873,9 +888,9 @@
},
"profile": {
"avatar": "avatar",
"setDisplayNameLabel": "Set your display name",
"setDisplayNameLabel": "Name",
"setEmailInput": "Enter email",
"setEmailLabel": "Set your gravatar email",
"setEmailLabel": "Gravatar email",
"title": "Profile"
},
"raisedHand": "Would like to speak",
@ -958,6 +973,7 @@
"title": "Security Options"
},
"settings": {
"audio": "Audio",
"buttonLabel": "Settings",
"calendar": {
"about": "The {{appName}} calendar integration is used to securely access your calendar so it can read upcoming events.",
@ -978,9 +994,11 @@
"maxStageParticipants": "Maximum number of participants who can be pinned to the main stage (EXPERIMENTAL)",
"microphones": "Microphones",
"moderator": "Moderator",
"more": "More",
"moderatorOptions": "Moderator options",
"more": "General",
"name": "Name",
"noDevice": "None",
"notifications": "Notifications",
"participantJoined": "Participant Joined",
"participantKnocking": "Participant entered lobby",
"participantLeft": "Participant Left",
@ -991,13 +1009,14 @@
"selectCamera": "Camera",
"selectMic": "Microphone",
"selfView": "Self view",
"sounds": "Sounds",
"shortcuts": "Shortcuts",
"speakers": "Speakers",
"startAudioMuted": "Everyone starts muted",
"startReactionsMuted": "Mute reaction sounds for everyone",
"startVideoMuted": "Everyone starts hidden",
"talkWhileMuted": "Talk while muted",
"title": "Settings"
"title": "Settings",
"video": "Video"
},
"settingsView": {
"advanced": "Advanced",
@ -1089,6 +1108,7 @@
"giphy": "Toggle GIPHY menu",
"grantModerator": "Grant Moderator Rights",
"hangup": "Leave the meeting",
"heading": "Toolbar",
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
@ -1155,6 +1175,7 @@
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"embedMeeting": "Embed meeting",
"enableNoiseSuppression": "Enable noise suppression",
"endConference": "End meeting for all",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
@ -1242,6 +1263,7 @@
"subtitlesOff": "Off",
"tr": "TR"
},
"unpinParticipant": "{{participantName}} - Unpin",
"userMedia": {
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
@ -1280,9 +1302,11 @@
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"performanceSettings": "Performance settings",
"recording": "Recording in progress",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition"
"standardDefinition": "Standard definition",
"streaming": "Streaming in progress"
},
"videothumbnail": {
"connectionInfo": "Connection Info",
@ -1326,12 +1350,13 @@
"none": "None",
"pleaseWait": "Please wait...",
"removeBackground": "Remove background",
"slightBlur": "Slight Blur",
"slightBlur": "Half Blur",
"title": "Virtual backgrounds",
"uploadedImage": "Uploaded image {{index}}",
"webAssemblyWarning": "WebAssembly not supported",
"webAssemblyWarningDescription": "WebAssembly disabled or not supported by this browser"
},
"visitorsLabel": "Number of visitors: {{count}}",
"volumeSlider": "Volume slider",
"welcomepage": {
"accessibilityLabel": {
@ -1364,6 +1389,7 @@
"microsoftLogo": "Microsoft logo",
"policyLogo": "Policy logo"
},
"meetingsAccessibilityLabel": "Meetings",
"mobileDownLoadLinkAndroid": "Download mobile app for Android",
"mobileDownLoadLinkFDroid": "Download mobile app for F-Droid",
"mobileDownLoadLinkIos": "Download mobile app for iOS",
@ -1383,5 +1409,10 @@
"terms": "Terms",
"title": "Secure, fully featured, and completely free video conferencing",
"upcomingMeetings": "Your upcoming meetings"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "Whiteboard"
}
}
}

View File

@ -106,6 +106,8 @@ import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/fea
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture';
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
@ -113,7 +115,6 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
import { setTileView, toggleTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality';
import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog';
import { getJitsiMeetTransport } from '../transport';
import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
@ -798,7 +799,8 @@ function initCommands() {
APP.store.dispatch(overwriteConfig(whitelistedConfig));
},
'toggle-virtual-background': () => {
APP.store.dispatch(toggleDialog(VirtualBackgroundDialog));
APP.store.dispatch(toggleDialog(SettingsDialog, {
defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND }));
},
'end-conference': () => {
APP.store.dispatch(endConference());
@ -1229,6 +1231,22 @@ class API {
});
}
/**
* Notify the external app that a notification has been triggered.
*
* @param {string} title - The notification title.
* @param {string} description - The notification description.
*
* @returns {void}
*/
notifyNotificationTriggered(title: string, description: string) {
this._sendEvent({
description,
name: 'notification-triggered',
title
});
}
/**
* Notify external application that the video quality setting has changed.
*

View File

@ -127,6 +127,7 @@ const events = {
'mouse-enter': 'mouseEnter',
'mouse-leave': 'mouseLeave',
'mouse-move': 'mouseMove',
'notification-triggered': 'notificationTriggered',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
'participant-kicked-out': 'participantKickedOut',

View File

@ -292,17 +292,6 @@ UI.showToolbar = timeout => APP.store.dispatch(showToolbox(timeout));
// Used by torture.
UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
/**
* Updates the displayed avatar for participant.
*
* @param {string} id - User id whose avatar should be updated.
* @param {string} avatarURL - The URL to avatar image to display.
* @returns {void}
*/
UI.refreshAvatarDisplay = function(id) {
VideoLayout.changeUserAvatar(id);
};
/**
* Notify user that connection failed.
* @param {string} stropheErrorMsg raw Strophe error message

View File

@ -139,12 +139,6 @@ const VideoLayout = {
}
},
changeUserAvatar(id, avatarUrl) {
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
}
},
isLargeVideoVisible() {
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
},

View File

@ -8,10 +8,9 @@ import {
createShortcutEvent,
sendAnalytics
} from '../../react/features/analytics';
import { toggleDialog } from '../../react/features/base/dialog';
import { clickOnVideo } from '../../react/features/filmstrip/actions';
import { KeyboardShortcutsDialog }
from '../../react/features/keyboard-shortcuts';
import { openSettingsDialog } from '../../react/features/settings/actions';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
const logger = Logger.getLogger(__filename);
@ -120,15 +119,17 @@ const KeyboardShortcut = {
return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
},
getShortcutsDescriptions() {
return _shortcutsHelp;
},
/**
* Opens the {@KeyboardShortcutsDialog} dialog.
* Opens the {@SettingsDialog} dialog on the Shortcuts page.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false));
},
/**

172
package-lock.json generated
View File

@ -50,6 +50,7 @@
"@types/amplitude-js": "8.16.2",
"@types/audioworklet": "0.0.29",
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.7.9",
@ -71,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@ -110,7 +111,7 @@
"react-native-url-polyfill": "1.3.0",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-webrtc": "106.0.5",
"react-native-webrtc": "106.0.6",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
@ -157,7 +158,7 @@
"circular-dependency-plugin": "5.2.0",
"clean-css-cli": "4.3.0",
"css-loader": "3.6.0",
"eslint": "8.25.0",
"eslint": "8.35.0",
"eslint-plugin-flowtype": "8.0.3",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jsdoc": "37.0.3",
@ -3249,15 +3250,15 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz",
"integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@ -3278,9 +3279,9 @@
"dev": true
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@ -3332,6 +3333,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz",
"integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@giphy/js-analytics": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz",
@ -3469,14 +3479,14 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
@ -6506,6 +6516,11 @@
"@types/webrtc": "*"
}
},
"node_modules/@types/w3c-web-hid": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/w3c-web-hid/-/w3c-web-hid-1.0.3.tgz",
"integrity": "sha512-eTQRkPd2JukZfS9+kRtrBAaTCCb6waGh5X8BJHmH1MiVQPLMYwm4+EvhwFfOo9SDna15o9dFAwmWwN6r/YM53A=="
},
"node_modules/@types/webgl-ext": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
@ -9879,14 +9894,16 @@
}
},
"node_modules/eslint": {
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
"integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz",
"integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.3",
"@humanwhocodes/config-array": "^0.10.5",
"@eslint/eslintrc": "^2.0.0",
"@eslint/js": "8.35.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -9897,19 +9914,19 @@
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
@ -10318,9 +10335,9 @@
}
},
"node_modules/eslint/node_modules/globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@ -10448,9 +10465,9 @@
}
},
"node_modules/espree": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
"dev": true,
"dependencies": {
"acorn": "^8.8.0",
@ -10486,9 +10503,9 @@
}
},
"node_modules/esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
"integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
"dev": true,
"dependencies": {
"estraverse": "^5.1.0"
@ -13399,8 +13416,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"integrity": "sha512-lzgRkJtdlZ7bfq2seBuONRM6UND8NJVjMOZPlVoq7uP4UuxffBztsoHGc0g5Y5zEqi1AnfYLwVZZvXkpd82iew==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@ -16391,9 +16408,9 @@
}
},
"node_modules/react-native-webrtc": {
"version": "106.0.5",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.5.tgz",
"integrity": "sha512-EINzYpTZh6zXb2lcGH13Ieli1ur3M1FaT8R8WMqfUZEW8/y0WV6yBeQQVz55OA4LtWnBUX0RZyaYQ4aZN4e1Sw==",
"version": "106.0.6",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.6.tgz",
"integrity": "sha512-mxRqR/sNZfVnbTM8cd90Y+A23H53jvZ/j0W7MSSFAbsQrj9Jdew1+7tJVTcBieF1S4ytTLI/R95scOQ4+qeE2Q==",
"hasInstallScript": true,
"dependencies": {
"adm-zip": "0.5.9",
@ -22625,15 +22642,15 @@
}
},
"@eslint/eslintrc": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz",
"integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.0.tgz",
"integrity": "sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@ -22648,9 +22665,9 @@
"dev": true
},
"globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
@ -22683,6 +22700,12 @@
}
}
},
"@eslint/js": {
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.35.0.tgz",
"integrity": "sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==",
"dev": true
},
"@giphy/js-analytics": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@giphy/js-analytics/-/js-analytics-4.0.7.tgz",
@ -22804,14 +22827,14 @@
}
},
"@humanwhocodes/config-array": {
"version": "0.10.7",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz",
"integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==",
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
"integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.5"
}
},
"@humanwhocodes/module-importer": {
@ -25057,6 +25080,11 @@
"@types/webrtc": "*"
}
},
"@types/w3c-web-hid": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@types/w3c-web-hid/-/w3c-web-hid-1.0.3.tgz",
"integrity": "sha512-eTQRkPd2JukZfS9+kRtrBAaTCCb6waGh5X8BJHmH1MiVQPLMYwm4+EvhwFfOo9SDna15o9dFAwmWwN6r/YM53A=="
},
"@types/webgl-ext": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
@ -27593,14 +27621,16 @@
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
},
"eslint": {
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz",
"integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==",
"version": "8.35.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.35.0.tgz",
"integrity": "sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.3.3",
"@humanwhocodes/config-array": "^0.10.5",
"@eslint/eslintrc": "^2.0.0",
"@eslint/js": "8.35.0",
"@humanwhocodes/config-array": "^0.11.8",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -27611,19 +27641,19 @@
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"js-sdsl": "^4.1.4",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
@ -27705,9 +27735,9 @@
}
},
"globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
@ -28019,9 +28049,9 @@
"dev": true
},
"espree": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz",
"integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==",
"dev": true,
"requires": {
"acorn": "^8.8.0",
@ -28043,9 +28073,9 @@
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
},
"esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz",
"integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==",
"dev": true,
"requires": {
"estraverse": "^5.1.0"
@ -30278,8 +30308,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"integrity": "sha512-lzgRkJtdlZ7bfq2seBuONRM6UND8NJVjMOZPlVoq7uP4UuxffBztsoHGc0g5Y5zEqi1AnfYLwVZZvXkpd82iew==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"integrity": "sha512-6QuR109o4sq24c9EU73NGLWAdJO+piiEylsqtmOL/B+I2GMTFeIras0tMOl6eQpncpZS5nD9gqiJmTNDnZqWbw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
@ -32562,9 +32592,9 @@
}
},
"react-native-webrtc": {
"version": "106.0.5",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.5.tgz",
"integrity": "sha512-EINzYpTZh6zXb2lcGH13Ieli1ur3M1FaT8R8WMqfUZEW8/y0WV6yBeQQVz55OA4LtWnBUX0RZyaYQ4aZN4e1Sw==",
"version": "106.0.6",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-106.0.6.tgz",
"integrity": "sha512-mxRqR/sNZfVnbTM8cd90Y+A23H53jvZ/j0W7MSSFAbsQrj9Jdew1+7tJVTcBieF1S4ytTLI/R95scOQ4+qeE2Q==",
"requires": {
"adm-zip": "0.5.9",
"base64-js": "1.5.1",

View File

@ -55,6 +55,7 @@
"@types/amplitude-js": "8.16.2",
"@types/audioworklet": "0.0.29",
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@vladmandic/human": "2.6.5",
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.7.9",
@ -76,7 +77,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1583.0.0+931ca368/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1589.0.0+d43c349d/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@ -115,7 +116,7 @@
"react-native-url-polyfill": "1.3.0",
"react-native-video": "https://git@github.com/react-native-video/react-native-video#7c48ae7c8544b2b537fb60194e9620b9fcceae52",
"react-native-watch-connectivity": "1.0.11",
"react-native-webrtc": "106.0.5",
"react-native-webrtc": "106.0.6",
"react-native-webview": "11.15.1",
"react-native-youtube-iframe": "2.2.1",
"react-redux": "7.1.0",
@ -162,7 +163,7 @@
"circular-dependency-plugin": "5.2.0",
"clean-css-cli": "4.3.0",
"css-loader": "3.6.0",
"eslint": "8.25.0",
"eslint": "8.35.0",
"eslint-plugin-flowtype": "8.0.3",
"eslint-plugin-import": "2.25.2",
"eslint-plugin-jsdoc": "37.0.3",

View File

@ -199,7 +199,7 @@ export default class AlwaysOnTop extends Component<*, State> {
color = { getAvatarColor(displayName, customAvatarBackgrounds) }
id = 'avatar'
initials = { getInitials(displayName) }
url = { displayName ? null : avatarURL } />)
url = { avatarURL } />)
</div>
<div
className = 'displayname'

View File

@ -48,5 +48,6 @@ import '../transcribing/middleware';
import '../video-layout/middleware';
import '../video-quality/middleware';
import '../videosipgw/middleware';
import '../visitors/middleware';
import './middleware';

View File

@ -5,7 +5,6 @@ import '../base/media/middleware';
import '../dynamic-branding/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';
import '../keyboard-shortcuts/middleware';
import '../no-audio-signal/middleware';
import '../notifications/middleware';
import '../noise-detection/middleware';
@ -15,6 +14,7 @@ import '../prejoin/middleware';
import '../remote-control/middleware';
import '../screen-share/middleware';
import '../shared-video/middleware';
import '../web-hid/middleware';
import '../settings/middleware';
import '../talk-while-muted/middleware';
import '../toolbox/middleware';

View File

@ -56,3 +56,4 @@ import '../transcribing/reducer';
import '../video-layout/reducer';
import '../video-quality/reducer';
import '../videosipgw/reducer';
import '../visitors/reducer';

View File

@ -16,5 +16,6 @@ import '../screenshot-capture/reducer';
import '../talk-while-muted/reducer';
import '../virtual-background/reducer';
import '../whiteboard/reducer';
import '../web-hid/reducer';
import './reducers.any';

View File

@ -76,6 +76,8 @@ import { IVideoLayoutState } from '../video-layout/reducer';
import { IVideoQualityPersistedState, IVideoQualityState } from '../video-quality/reducer';
import { IVideoSipGW } from '../videosipgw/reducer';
import { IVirtualBackground } from '../virtual-background/reducer';
import { IVisitorsState } from '../visitors/reducer';
import { IWebHid } from '../web-hid/reducer';
import { IWhiteboardState } from '../whiteboard/reducer';
export interface IStore {
@ -163,6 +165,8 @@ export interface IReduxState {
'features/video-quality-persistent-storage': IVideoQualityPersistedState;
'features/videosipgw': IVideoSipGW;
'features/virtual-background': IVirtualBackground;
'features/visitors': IVisitorsState;
'features/web-hid': IWebHid;
'features/whiteboard': IWhiteboardState;
}

View File

@ -98,7 +98,7 @@ function _upgradeRoleFinished(
name: authenticationError || connectionError,
...other
};
progress = authenticationError ? 0.5 : 0;
progress = 0;
}
return {

View File

@ -207,7 +207,7 @@ class LoginDialog extends Component<IProps, IState> {
let messageKey;
if (progress && progress < 1) {
messageKey = t('connection.FETCH_SESSION_ID');
messageKey = 'connection.FETCH_SESSION_ID';
} else if (error) {
const { name } = error;
@ -218,14 +218,14 @@ class LoginDialog extends Component<IProps, IState> {
&& credentials.jid === toJid(username, configHosts ?? { authdomain: '',
domain: '' })
&& credentials.password === password) {
messageKey = t('dialog.incorrectPassword');
messageKey = 'dialog.incorrectPassword';
}
} else if (name) {
messageKey = t('dialog.connectErrorWithMsg');
messageKey = 'dialog.connectErrorWithMsg';
messageOptions.msg = `${name} ${error.message}`;
}
} else if (connecting) {
messageKey = t('connection.CONNECTING');
messageKey = 'connection.CONNECTING';
}
if (messageKey) {

View File

@ -88,7 +88,7 @@ const useStyles = makeStyles()(theme => {
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)',
minHeight: '40px',
'&:hover': {
'&:hover, &:focus-within': {
backgroundColor: theme.palette.ui02,
'& .indicators': {
@ -97,6 +97,8 @@ const useStyles = makeStyles()(theme => {
'& .actions': {
display: 'flex',
position: 'relative',
top: 'auto',
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
backgroundColor: theme.palette.ui02
}
@ -154,7 +156,8 @@ const useStyles = makeStyles()(theme => {
},
actionsContainer: {
display: 'none',
position: 'absolute',
top: '-1000px',
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
backgroundColor: theme.palette.ui02
},

View File

@ -98,7 +98,7 @@ export function overwriteConfig(config: Object) {
* base/config.
* @returns {Function}
*/
export function setConfig(config: Object = {}) {
export function setConfig(config: IConfig = {}) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { locationURL } = getState()['features/base/connection'];
@ -119,6 +119,26 @@ export function setConfig(config: Object = {}) {
window.interfaceConfig,
locationURL);
let { bosh } = config;
if (bosh) {
// Normalize the BOSH URL.
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL?.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL?.href);
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
config.bosh = bosh;
}
dispatch({
type: SET_CONFIG,
config

View File

@ -61,6 +61,11 @@ export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background',
*/
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];
/**
* The toolbar buttons to show when in visitors mode.
*/
export const VISITORS_MODE_BUTTONS = [ 'hangup', 'tileview' ];
/**
* The set of feature flags.
*

View File

@ -3,8 +3,7 @@ import _ from 'lodash';
import { IReduxState } from '../../app/types';
import {
appendURLParam,
getBackendSafeRoomName,
parseURIString
getBackendSafeRoomName
} from '../util/uri';
import {
@ -145,7 +144,8 @@ export function constructOptions(state: IReduxState) {
// redux store.
const options = _.cloneDeep(state['features/base/config']);
let { bosh, websocket } = options;
const { bosh } = options;
let { websocket } = options;
// TESTING: Only enable WebSocket for some percentage of users.
if (websocket && navigator.product === 'ReactNative') {
@ -154,25 +154,6 @@ export function constructOptions(state: IReduxState) {
}
}
// Normalize the BOSH URL.
if (bosh && !websocket) {
const { locationURL } = state['features/base/connection'];
if (bosh.startsWith('//')) {
// By default our config.js doesn't include the protocol.
bosh = `${locationURL?.protocol}${bosh}`;
} else if (bosh.startsWith('/')) {
// Handle relative URLs, which won't work on mobile.
const {
protocol,
host,
contextRoot
} = parseURIString(locationURL?.href);
bosh = `${protocol}//${host}${contextRoot || '/'}${bosh.substr(1)}`;
}
}
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;

View File

@ -48,7 +48,7 @@ type Props = {
/**
* Dialog title.
*/
title?: string,
title?: string
};
/**

View File

@ -11,6 +11,7 @@ import { IReduxState } from '../../../../app/types';
import { translate } from '../../../i18n/functions';
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native';
import { connect } from '../../../redux/functions';
import { hideDialog } from '../../actions';
// @ts-ignore
import logger from '../../logger';
@ -33,10 +34,7 @@ interface IPageReloadDialogProps extends WithTranslation {
* {@link PageReloadDialog}.
*/
interface IPageReloadDialogState {
message: string;
timeLeft: number;
timeoutSeconds: number;
title: string;
}
/**
@ -48,6 +46,7 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
// @ts-ignore
_interval: IntervalID;
_timeoutSeconds: number;
/**
* Initializes a new PageReloadOverlay instance.
@ -59,27 +58,15 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
constructor(props: IPageReloadDialogProps) {
super(props);
const timeoutSeconds = 10 + randomInt(0, 20);
let message, title;
if (this.props.isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
}
this._timeoutSeconds = 10 + randomInt(0, 20);
this.state = {
message,
timeLeft: timeoutSeconds,
timeoutSeconds,
title
timeLeft: this._timeoutSeconds
};
this._onCancel = this._onCancel.bind(this);
this._onReloadNow = this._onReloadNow.bind(this);
this._onReconnecting = this._onReconnecting.bind(this);
}
/**
@ -89,32 +76,14 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
* @returns {void}
*/
componentDidMount() {
const { dispatch } = this.props;
const { timeLeft } = this.state;
logger.info(
`The conference will be reloaded after ${
this.state.timeoutSeconds} seconds.`);
`The conference will be reloaded after ${timeLeft} seconds.`
);
this._interval
= setInterval(
() => {
if (timeLeft === 0) {
if (this._interval) {
clearInterval(this._interval);
this._interval = undefined;
}
dispatch(reloadNow());
} else {
this.setState(prevState => {
return {
timeLeft: prevState.timeLeft - 1
};
});
}
},
1000);
this._interval = setInterval(() =>
this._onReconnecting(), 1000);
}
/**
@ -138,12 +107,37 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
* @returns {boolean}
*/
_onCancel() {
const { dispatch } = this.props;
clearInterval(this._interval);
this.props.dispatch(appNavigate(undefined));
dispatch(appNavigate(undefined));
return true;
}
/**
* Handles automatic reconnection.
*
* @private
* @returns {void}
*/
_onReconnecting() {
const { dispatch } = this.props;
const { timeLeft } = this.state;
if (timeLeft === 0) {
if (this._interval) {
dispatch(hideDialog());
this._onReloadNow();
this._interval = undefined;
}
}
this.setState({
timeLeft: timeLeft - 1
});
}
/**
* Handle clicking on the "Reload Now" button. It will navigate to the same
* conference URL as before immediately, without waiting for the timer to
@ -153,8 +147,10 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
* @returns {boolean}
*/
_onReloadNow() {
const { dispatch } = this.props;
clearInterval(this._interval);
this.props.dispatch(reloadNow());
dispatch(reloadNow());
return true;
}
@ -166,8 +162,18 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const { message, timeLeft, title } = this.state;
const { isNetworkFailure, t } = this.props;
const { timeLeft } = this.state;
let message, title;
if (isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
}
return (
<ConfirmDialog
@ -187,9 +193,8 @@ class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDial
* @param {Object} state - The redux state.
* @protected
* @returns {{
* message: string,
* reason: string,
* title: string
* isNetworkFailure: boolean,
* reason: string
* }}
*/
function mapStateToProps(state: IReduxState) {

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" />
</svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 5.77465L10.9742 9.05416C10.6778 10.0019 9.79505 10.6433 8.80206 10.6323L5.36607 10.5945L8.16808 12.5835C8.97785 13.1583 9.31502 14.1961 8.99778 15.1371L7.90002 18.3932L10.6576 16.343C11.4545 15.7505 12.5456 15.7505 13.3425 16.343L16.1001 18.3932L15.0023 15.1371C14.6851 14.1961 15.0222 13.1583 15.832 12.5835L18.634 10.5945L15.198 10.6323C14.205 10.6433 13.3223 10.0019 13.0258 9.05416L12 5.77465ZM12.7158 3.03843C12.4964 2.33696 11.5037 2.33696 11.2842 3.03843L9.54263 8.60636C9.44381 8.92229 9.14957 9.13606 8.81858 9.13242L2.98497 9.0682C2.25003 9.06011 1.94325 10.0043 2.54258 10.4297L7.29982 13.8067C7.56974 13.9983 7.68213 14.3442 7.57638 14.6579L5.71262 20.1861C5.47782 20.8826 6.28099 21.4661 6.87081 21.0276L11.5525 17.5467C11.8182 17.3492 12.1819 17.3492 12.4475 17.5467L17.1293 21.0276C17.7191 21.4661 18.5223 20.8826 18.2875 20.1861L16.4237 14.6579C16.3179 14.3442 16.4303 13.9983 16.7003 13.8067L21.4575 10.4297C22.0568 10.0043 21.75 9.06011 21.0151 9.0682L15.1815 9.13242C14.8505 9.13606 14.5563 8.92228 14.4574 8.60636L12.7158 3.03843Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.1602 8.24439C13.3281 8.16225 14.25 7.18879 14.25 6C14.25 4.75736 13.2426 3.75 12 3.75C10.7574 3.75 9.75 4.75736 9.75 6C9.75 7.18151 10.6607 8.15032 11.8184 8.24278C11.5197 8.21896 11.2375 8.13687 10.9831 8.00775C9.85816 9.83693 8.19346 10.3318 6.74461 10.3424C6.66364 9.17333 5.68961 8.25 4.5 8.25C3.25736 8.25 2.25 9.25736 2.25 10.5C2.25 11.7426 3.25736 12.75 4.5 12.75C5.32135 12.75 6.03995 12.31 6.43279 11.6528C6.39557 11.715 6.35542 11.7754 6.31253 11.8336C6.67901 11.8506 7.06503 11.8447 7.4618 11.805C8.64456 11.6868 9.95784 11.2623 11.0918 10.2228C11.4736 9.87283 11.8205 9.46641 12.1289 9.0006C12.4727 9.47803 12.8468 9.89069 13.2474 10.2432C14.3929 11.2513 15.6592 11.6829 16.8118 11.8042C17.1152 11.8362 17.4101 11.8467 17.6932 11.8412C17.6739 11.8152 17.6551 11.7887 17.6369 11.7619C18.0415 12.3582 18.725 12.75 19.5 12.75C20.7426 12.75 21.75 11.7426 21.75 10.5C21.75 9.25736 20.7426 8.25 19.5 8.25C18.3129 8.25 17.3405 9.16938 17.256 10.335C15.9245 10.2658 14.3912 9.7053 13.1957 7.90649C12.8918 8.09752 12.5389 8.21778 12.1602 8.24439ZM12 6.75C12.4142 6.75 12.75 6.41421 12.75 6C12.75 5.58579 12.4142 5.25 12 5.25C11.5858 5.25 11.25 5.58579 11.25 6C11.25 6.41421 11.5858 6.75 12 6.75ZM20.25 10.5C20.25 10.9142 19.9142 11.25 19.5 11.25C19.0858 11.25 18.75 10.9142 18.75 10.5C18.75 10.0858 19.0858 9.75 19.5 9.75C19.9142 9.75 20.25 10.0858 20.25 10.5ZM4.5 11.25C4.91421 11.25 5.25 10.9142 5.25 10.5C5.25 10.0858 4.91421 9.75 4.5 9.75C4.08579 9.75 3.75 10.0858 3.75 10.5C3.75 10.9142 4.08579 11.25 4.5 11.25Z" fill="white"/>
<path d="M17.9485 12.1296C18.3135 12.4773 18.7952 12.7036 19.3289 12.7437L19.2591 12.9706C19.1623 13.2853 18.8715 13.5 18.5423 13.5C18.0377 13.5 17.677 13.0117 17.8254 12.5294L17.9485 12.1296Z" fill="white"/>
<path d="M12.75 18.0001C13.1642 18.0001 13.5 18.3359 13.5 18.7501C13.5 19.1643 13.1642 19.5001 12.75 19.5001H7.85792C7.19941 19.5001 6.61791 19.0706 6.42425 18.4412L4.6712 12.7437C5.20487 12.7036 5.6866 12.4773 6.05164 12.1296L7.85792 18.0001H12.75Z" fill="white"/>
<path d="M11.25 18.0002C10.8358 18.0002 10.5 18.336 10.5 18.7502C10.5 19.1644 10.8358 19.5002 11.25 19.5002H16.1421C16.8006 19.5002 17.3821 19.0707 17.5757 18.4413L19.3289 12.7437C18.7952 12.7036 18.3135 12.4773 17.9485 12.1296L16.1421 18.0002H11.25Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -39,10 +39,13 @@ export { default as IconExclamationSolid } from './exclamation-solid.svg';
export { default as IconExclamationTriangle } from './exclamation-triangle.svg';
export { default as IconExitFullscreen } from './exit-fullscreen.svg';
export { default as IconFaceSmile } from './face-smile.svg';
export { default as IconFavorite } from './favorite.svg';
export { default as IconFavoriteSolid } from './favorite-solid.svg';
export { default as IconFeedback } from './feedback.svg';
export { default as IconGear } from './gear.svg';
export { default as IconGoogle } from './google.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHost } from './host.svg';
export { default as IconHelp } from './help.svg';
export { default as IconHighlight } from './highlight.svg';
export { default as IconImage } from './image.svg';
@ -81,6 +84,7 @@ export { default as IconSend } from './send.svg';
export { default as IconShare } from './share.svg';
export { default as IconShareDoc } from './share-doc.svg';
export { default as IconShortcuts } from './shortcuts.svg';
export { default as IconSip } from './sip.svg';
export { default as IconSites } from './sites.svg';
export { default as IconStop } from './stop.svg';
export { default as IconStopScreenshare } from './stop-screenshare.svg';

View File

@ -0,0 +1,3 @@
<svg width="20" height="14" viewBox="0 0 20 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.41201 1.90767C3.50689 3.37865 5.24438 7.52348 7.36096 9.64007C7.36096 9.64004 7.36175 9.63923 7.36329 9.63765C9.48 11.7541 13.6215 13.4932 15.0923 12.5882C16.1022 11.9668 16.0078 9.51337 15.2427 8.76783C14.7369 8.2749 13.1882 8.01994 12.5497 8.14762C12.3496 8.18763 11.7907 8.76515 11.4793 9.08696C11.4184 9.14994 11.3669 9.20313 11.3295 9.24058C11.1007 9.46937 9.63912 8.22168 9.20588 7.78845L7.60102 9.39838C8.10053 8.89635 9.2057 7.78701 9.2057 7.78701C8.77247 7.35377 7.53081 5.89935 7.7596 5.67056C7.79705 5.63311 7.85024 5.58164 7.91322 5.5207C8.23503 5.20928 8.81255 4.65041 8.85256 4.45033C8.98024 3.81178 8.72528 2.26311 8.23236 1.75727C7.48681 0.992193 5.03342 0.897765 4.41201 1.90767Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 868 B

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
@ -92,13 +92,27 @@ const Label = ({
}: IProps) => {
const { classes, cx } = useStyles();
const onKeyPress = useCallback(event => {
if (!onClick) {
return;
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick();
}
}, [ onClick ]);
return (
<div
className = { cx(classes.label, onClick && classes.clickable,
color && classes[color], className
) }
id = { id }
onClick = { onClick }>
onClick = { onClick }
onKeyPress = { onKeyPress }
role = { onClick ? 'button' : undefined }
tabIndex = { onClick ? 0 : undefined }>
{icon && <Icon
color = { iconColor }
size = '16'

View File

@ -88,7 +88,11 @@ const _updateLastN = debounce(({ dispatch, getState }: IStore) => {
lastNSelected = 1;
}
dispatch(setLastN(lastNSelected));
const { lastN } = state['features/base/lastn'];
if (lastN !== lastNSelected) {
dispatch(setLastN(lastNSelected));
}
}, 1000); /* Don't send this more often than once a second. */

View File

@ -15,11 +15,15 @@ export type MediaType = 'audio' | 'video' | 'screenshare';
*
* @enum {string}
*/
export const MEDIA_TYPE: { [key: string]: MediaType; } = {
AUDIO: 'audio',
SCREENSHARE: 'screenshare',
VIDEO: 'video'
};
export const MEDIA_TYPE: {
AUDIO: MediaType;
SCREENSHARE: MediaType;
VIDEO: MediaType;
} = {
AUDIO: 'audio',
SCREENSHARE: 'screenshare',
VIDEO: 'video'
};
/* eslint-disable no-bitwise */

View File

@ -601,7 +601,7 @@ export function getDominantSpeakerParticipant(stateful: IStateful) {
export function isEveryoneModerator(stateful: IStateful) {
const state = toState(stateful)['features/base/participants'];
return state.everyoneIsModerator === true;
return state.numberOfNonModeratorParticipants === 0;
}
/**

View File

@ -429,11 +429,12 @@ StateListenerRegistry.register(
'e2ee.enabled': (participant: IJitsiParticipant, value: string) =>
_e2eeUpdated(store, conference, participant.getId(), value),
'features_e2ee': (participant: IJitsiParticipant, value: boolean) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: value
})),
getParticipantById(store.getState(), participant.getId())?.e2eeSupported !== value
&& store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: value
})),
'features_jigasi': (participant: IJitsiParticipant, value: boolean) =>
store.dispatch(participantUpdated({
conference,
@ -506,7 +507,12 @@ StateListenerRegistry.register(
function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConference,
participantId: string, newValue: string | boolean) {
const e2eeEnabled = newValue === 'true';
const { e2ee = {} } = getState()['features/base/config'];
const state = getState();
const { e2ee = {} } = state['features/base/config'];
if (e2eeEnabled === getParticipantById(state, participantId)?.e2eeEnabled) {
return;
}
dispatch(participantUpdated({
conference,
@ -641,7 +647,6 @@ function _participantJoinedOrUpdated(store: IStore, next: Function, action: any)
// Send an external update of the local participant's raised hand state
// if a new raised hand state is defined in the action.
if (typeof raisedHandTimestamp !== 'undefined') {
if (local) {
const { conference } = getState()['features/base/conference'];
const rHand = parseInt(raisedHandTimestamp, 10);
@ -691,14 +696,6 @@ function _participantJoinedOrUpdated(store: IStore, next: Function, action: any)
}
}
// Notify external listeners of potential avatarURL changes.
if (typeof APP === 'object') {
const currentKnownId = local ? APP.conference.getMyUserId() : id;
// Force update of local video getting a new id.
APP.UI.refreshAvatarDisplay(currentKnownId);
}
return result;
}

View File

@ -63,10 +63,12 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
const DEFAULT_STATE = {
dominantSpeaker: undefined,
everyoneIsModerator: false,
fakeParticipants: new Map(),
local: undefined,
localScreenShare: undefined,
numberOfNonModeratorParticipants: 0,
numberOfParticipantsDisabledE2EE: 0,
numberOfParticipantsNotSupportingE2EE: 0,
overwrittenNameList: {},
pinnedParticipant: undefined,
raisedHandsQueue: [],
@ -79,10 +81,12 @@ const DEFAULT_STATE = {
export interface IParticipantsState {
dominantSpeaker?: string;
everyoneIsModerator: boolean;
fakeParticipants: Map<string, IParticipant>;
local?: ILocalParticipant;
localScreenShare?: IParticipant;
numberOfNonModeratorParticipants: number;
numberOfParticipantsDisabledE2EE: number;
numberOfParticipantsNotSupportingE2EE: number;
overwrittenNameList: { [id: string]: string; };
pinnedParticipant?: string;
raisedHandsQueue: Array<{ id: string; raisedHandTimestamp: number; }>;
@ -200,23 +204,30 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
}
let newParticipant: IParticipant | null = null;
const oldParticipant = local || state.local?.id === id ? state.local : state.remote.get(id);
if (state.remote.has(id)) {
newParticipant = _participant(state.remote.get(id), action);
newParticipant = _participant(oldParticipant, action);
state.remote.set(id, newParticipant);
} else if (id === state.local?.id) {
newParticipant = state.local = _participant(state.local, action);
}
if (newParticipant) {
// everyoneIsModerator calculation:
if (oldParticipant && newParticipant && !newParticipant.fakeParticipant) {
const isModerator = isParticipantModerator(newParticipant);
if (state.everyoneIsModerator && !isModerator) {
state.everyoneIsModerator = false;
} else if (!state.everyoneIsModerator && isModerator) {
state.everyoneIsModerator = _isEveryoneModerator(state);
if (isParticipantModerator(oldParticipant) !== isModerator) {
state.numberOfNonModeratorParticipants += isModerator ? -1 : 1;
}
const e2eeEnabled = Boolean(newParticipant.e2eeEnabled);
const e2eeSupported = Boolean(newParticipant.e2eeSupported);
if (Boolean(oldParticipant.e2eeEnabled) !== e2eeEnabled) {
state.numberOfParticipantsDisabledE2EE += e2eeEnabled ? -1 : 1;
}
if (!local && Boolean(oldParticipant.e2eeSupported) !== e2eeSupported) {
state.numberOfParticipantsNotSupportingE2EE += e2eeSupported ? -1 : 1;
}
}
@ -267,13 +278,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
state.dominantSpeaker = id;
}
const isModerator = isParticipantModerator(participant);
const { local, remote } = state;
if (!fakeParticipant) {
const isModerator = isParticipantModerator(participant);
if (state.everyoneIsModerator && !isModerator) {
state.everyoneIsModerator = false;
} else if (!local && remote.size === 0 && isModerator) {
state.everyoneIsModerator = true;
if (!isModerator) {
state.numberOfNonModeratorParticipants += 1;
}
const { e2eeEnabled, e2eeSupported } = participant as IParticipant;
if (!e2eeEnabled) {
state.numberOfParticipantsDisabledE2EE += 1;
}
if (!participant.local && !e2eeSupported) {
state.numberOfParticipantsNotSupportingE2EE += 1;
}
}
if (participant.local) {
@ -349,6 +369,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
pinnedParticipant
} = state;
let oldParticipant = remote.get(id);
let isLocalScreenShare = false;
if (oldParticipant?.sources?.size) {
const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
@ -373,6 +394,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
oldParticipant = state.local;
delete state.local;
} else if (localScreenShare?.id === id) {
isLocalScreenShare = true;
oldParticipant = state.local;
delete state.localScreenShare;
} else {
@ -383,10 +405,6 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
state.sortedRemoteParticipants.delete(id);
state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
state.everyoneIsModerator = _isEveryoneModerator(state);
}
if (dominantSpeaker === id) {
state.dominantSpeaker = undefined;
}
@ -407,6 +425,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
}
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
const { e2eeEnabled, e2eeSupported } = oldParticipant;
if (!isParticipantModerator(oldParticipant)) {
state.numberOfNonModeratorParticipants -= 1;
}
if (!e2eeEnabled) {
state.numberOfParticipantsDisabledE2EE -= 1;
}
if (!oldParticipant.local && !e2eeSupported) {
state.numberOfParticipantsNotSupportingE2EE -= 1;
}
}
return { ...state };
}
case PARTICIPANT_SOURCES_UPDATED: {
@ -465,27 +499,6 @@ function _getDisplayName(state: Object, name?: string): string {
return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
}
/**
* Loops through the participants in the state in order to check if all participants are moderators.
*
* @param {Object} state - The local participant redux state.
* @returns {boolean}
*/
function _isEveryoneModerator(state: IParticipantsState) {
if (isParticipantModerator(state.local)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [ k, p ] of state.remote) {
if (!isParticipantModerator(p)) {
return false;
}
}
return true;
}
return false;
}
/**
* Reducer function for a single participant.
*

View File

@ -1,4 +1,5 @@
import React, { Component, ReactNode } from 'react';
import ReactFocusLock from 'react-focus-lock';
import { IReduxState } from '../../../app/types';
import DialogPortal from '../../../toolbox/components/web/DialogPortal';
@ -34,6 +35,18 @@ interface IProps {
*/
disablePopover?: boolean;
/**
* The id of the dom element acting as the Popover label (matches aria-labelledby).
*/
headingId?: string;
/**
* String acting as the Popover label (matches aria-label).
*
* If headingId is set, this will not be used.
*/
headingLabel?: string;
/**
* An id attribute to apply to the root of the {@code Popover}
* component.
@ -186,7 +199,16 @@ class Popover extends Component<IProps, IState> {
* @returns {ReactElement}
*/
render() {
const { children, className, content, id, overflowDrawer, visible, trigger } = this.props;
const { children,
className,
content,
headingId,
headingLabel,
id,
overflowDrawer,
visible,
trigger
} = this.props;
if (overflowDrawer) {
return (
@ -197,6 +219,7 @@ class Popover extends Component<IProps, IState> {
{ children }
<JitsiPortal>
<Drawer
headingId = { headingId }
isOpen = { visible }
onClose = { this._onHideDialog }>
{ content }
@ -214,7 +237,8 @@ class Popover extends Component<IProps, IState> {
onKeyPress = { this._onKeyPress }
{ ...(trigger === 'hover' ? {
onMouseEnter: this._onShowDialog,
onMouseLeave: this._onHideDialog
onMouseLeave: this._onHideDialog,
tabIndex: 0
} : {}) }
ref = { this._containerRef }>
{ visible && (
@ -222,7 +246,16 @@ class Popover extends Component<IProps, IState> {
getRef = { this._setContextMenuRef }
setSize = { this._setContextMenuStyle }
style = { this.state.contextMenuStyle }>
{this._renderContent()}
<ReactFocusLock
lockProps = {{
role: 'dialog',
'aria-modal': true,
'aria-labelledby': headingId,
'aria-label': !headingId && headingLabel ? headingLabel : undefined
}}
returnFocus = { true }>
{this._renderContent()}
</ReactFocusLock>
</DialogPortal>
)}
{ children }

View File

@ -105,18 +105,14 @@ class MeetingsList extends Component<Props> {
* @returns {React.ReactNode}
*/
render() {
const { listEmptyComponent, meetings, t } = this.props;
const { listEmptyComponent, meetings } = this.props;
/**
* If there are no recent meetings we don't want to display anything.
*/
if (meetings) {
return (
<Container
aria-label = { t('welcomepage.recentList') }
className = 'meetings-list'
role = 'menu'
tabIndex = '-1'>
<Container className = 'meetings-list'>
{
meetings.length === 0
? listEmptyComponent
@ -238,23 +234,16 @@ class MeetingsList extends Component<Props> {
return (
<Container
aria-label = { title }
className = { rootClassName }
key = { index }
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'menuitem'
tabIndex = { 0 }>
<Container className = 'left-column'>
<Text className = 'title'>
{ _toDateString(date) }
</Text>
<Text className = 'subtitle'>
{ _toTimeString(time) }
</Text>
</Container>
onClick = { onPress }>
<Container className = 'right-column'>
<Text className = 'title'>
<Text
className = 'title'
onClick = { onPress }
onKeyPress = { onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ title }
</Text>
{
@ -270,6 +259,14 @@ class MeetingsList extends Component<Props> {
</Text>) : null
}
</Container>
<Container className = 'left-column'>
<Text className = 'title'>
{ _toDateString(date) }
</Text>
<Text className = 'subtitle'>
{ _toTimeString(time) }
</Text>
</Container>
<Container className = 'actions'>
{ elementAfter || null }

View File

@ -1,4 +1,5 @@
import { IReduxState } from '../../app/types';
import { iAmVisitor } from '../../visitors/functions';
import { IStateful } from '../app/types';
import CONFIG_WHITELIST from '../config/configWhitelist';
import { IConfigState } from '../config/reducer';
@ -114,11 +115,12 @@ export function shouldHideShareAudioHelper(state: IReduxState): boolean | undefi
}
/**
* Gets the disable self view setting.
* Gets the disabled self view setting.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function getHideSelfView(state: IReduxState) {
return state['features/base/config'].disableSelfView || state['features/base/settings'].disableSelfView;
return state['features/base/config'].disableSelfView || state['features/base/settings'].disableSelfView
|| iAmVisitor(state);
}

View File

@ -84,7 +84,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
onKeyDown: disabled ? undefined : onKeyDown,
onKeyPress: this._onKeyPress,
tabIndex: 0,
role: showLabel ? 'menuitem' : 'button'
role: 'button'
};
const elementType = showLabel ? 'li' : 'div';

View File

@ -123,7 +123,7 @@ class OverflowMenuItem extends Component<Props> {
className = { className }
onClick = { disabled ? null : onClick }
onKeyPress = { this._onKeyPress }
role = 'menuitem'
role = 'button'
tabIndex = { 0 }>
<span className = 'overflow-menu-item-icon'>
<Icon

View File

@ -121,6 +121,7 @@ export default function ToolboxButtonWithIconPopup(props: Props) {
<div className = 'settings-button-small-icon-container'>
<Popover
content = { popoverContent }
headingLabel = { ariaLabel }
onPopoverClose = { onPopoverClose }
onPopoverOpen = { onPopoverOpen }
position = 'top'

View File

@ -65,7 +65,7 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
onClick: disabled ? undefined : onClick,
onKeyPress: this._onKeyPress,
tabIndex: 0,
role: showLabel ? 'menuitem' : 'button'
role: 'button'
};
const elementType = showLabel ? 'li' : 'div';

View File

@ -182,7 +182,9 @@ const BaseDialog = ({
<div
className = { classes.backdrop }
onClick = { onBackdropClick } />
<FocusLock className = { classes.focusLock }>
<FocusLock
className = { classes.focusLock }
returnFocus = { true }>
<div
aria-describedby = { description }
aria-labelledby = { title ?? t(titleKey ?? '') }

View File

@ -69,7 +69,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.action01Active
},
'&:focus': {
'&.focus-visible': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
},

View File

@ -53,6 +53,10 @@ const useStyles = makeStyles()(theme => {
}
},
disabled: {
cursor: 'not-allowed'
},
activeArea: {
display: 'grid',
placeContent: 'center',
@ -73,7 +77,6 @@ const useStyles = makeStyles()(theme => {
height: '18px',
border: `2px solid ${theme.palette.icon03}`,
borderRadius: '3px',
cursor: 'pointer',
display: 'grid',
placeContent: 'center',
@ -154,7 +157,7 @@ const Checkbox = ({
return (
<div className = { cx(styles.formControl, isMobile && 'is-mobile', className) }>
<label className = { cx(styles.activeArea, isMobile && 'is-mobile') }>
<label className = { cx(styles.activeArea, isMobile && 'is-mobile', disabled && styles.disabled) }>
<input
checked = { checked }
disabled = { disabled }

View File

@ -25,7 +25,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui02
},
'&:focus': {
'&.focus-visible': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`
},

View File

@ -262,7 +262,7 @@ const ContextMenu = ({
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role ?? 'menu' }
role = { role }
tabIndex = { tabIndex }>
{children}
</div>;

View File

@ -5,6 +5,9 @@ import { makeStyles } from 'tss-react/mui';
import { showOverflowDrawer } from '../../../../toolbox/functions.web';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { TEXT_OVERFLOW_TYPES } from '../../constants.any';
import TextWithOverflow from './TextWithOverflow';
export interface IProps {
@ -23,6 +26,13 @@ export interface IProps {
*/
className?: string;
/**
* Id of dom element controlled by this item. Matches aria-controls.
* Useful if you need this item as a tab element.
*
*/
controls?: string;
/**
* Custom icon. If used, the icon prop is ignored.
* Used to allow custom children instead of just the default icons.
@ -52,13 +62,23 @@ export interface IProps {
/**
* Keydown handler.
*/
onKeyDown?: (e?: React.KeyboardEvent) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
/**
* Keypress handler.
*/
onKeyPress?: (e?: React.KeyboardEvent) => void;
/**
* Overflow type for item text.
*/
overflowType?: TEXT_OVERFLOW_TYPES;
/**
* You can use this item as a tab. Defaults to button if not set.
*/
role?: 'tab' | 'button';
/**
* Whether the item is marked as selected.
*/
@ -102,7 +122,7 @@ const useStyles = makeStyles()(theme => {
backgroundColor: theme.palette.ui03
},
'&:focus': {
'&.focus-visible': {
boxShadow: `inset 0 0 0 2px ${theme.palette.action01Hover}`
}
},
@ -142,6 +162,7 @@ const ContextMenuItem = ({
accessibilityLabel,
children,
className,
controls,
customIcon,
disabled,
id,
@ -149,6 +170,8 @@ const ContextMenuItem = ({
onClick,
onKeyDown,
onKeyPress,
overflowType,
role = 'button',
selected,
testId,
text,
@ -158,8 +181,10 @@ const ContextMenuItem = ({
return (
<div
aria-controls = { controls }
aria-disabled = { disabled }
aria-label = { accessibilityLabel }
aria-selected = { role === 'tab' ? selected : undefined }
className = { cx(styles.contextMenuItem,
_overflowDrawer && styles.contextMenuItemDrawer,
disabled && styles.contextMenuItemDisabled,
@ -172,19 +197,24 @@ const ContextMenuItem = ({
onClick = { disabled ? undefined : onClick }
onKeyDown = { disabled ? undefined : onKeyDown }
onKeyPress = { disabled ? undefined : onKeyPress }
role = 'menuitem'>
role = { role }
tabIndex = { role === 'tab'
? selected ? 0 : -1
: disabled ? undefined : 0
}>
{customIcon ? customIcon
: icon && <Icon
className = { styles.contextMenuItemIcon }
size = { 20 }
src = { icon } />}
{text && (
<span
<TextWithOverflow
className = { cx(styles.text,
_overflowDrawer && styles.drawerText,
textClassName) }>
_overflowDrawer && styles.drawerText,
textClassName) }
overflowType = { overflowType } >
{text}
</span>
</TextWithOverflow>
)}
{children}
</div>

View File

@ -23,12 +23,6 @@ const useStyles = makeStyles()(theme => {
justifyContent: 'space-between'
},
closeIcon: {
'&:focus': {
boxShadow: 'none'
}
},
title: {
color: theme.palette.text01,
...withPixelLineHeight(theme.typography.heading5),
@ -137,8 +131,7 @@ const Dialog = ({
</p>
{!hideCloseButton && (
<ClickableIcon
accessibilityLabel = { t('dialog.close') }
className = { classes.closeIcon }
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
icon = { IconCloseLarge }
id = 'modal-header-close-button'
onClick = { onClose } />

View File

@ -1,4 +1,5 @@
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
import { MoveFocusInside } from 'react-focus-lock';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@ -105,6 +106,7 @@ const useStyles = makeStyles()(theme => {
backContainer: {
display: 'flex',
flexDirection: 'row-reverse',
alignItems: 'center',
'& > button': {
@ -112,12 +114,6 @@ const useStyles = makeStyles()(theme => {
}
},
closeIcon: {
'&:focus': {
boxShadow: 'none'
}
},
content: {
flexGrow: 1,
overflowY: 'auto',
@ -129,8 +125,14 @@ const useStyles = makeStyles()(theme => {
}
},
header: {
order: -1,
paddingBottom: theme.spacing(4)
},
footer: {
justifyContent: 'flex-end',
paddingTop: theme.spacing(4),
'& button:last-child': {
marginLeft: '16px'
@ -143,20 +145,21 @@ interface IObject {
[key: string]: string | string[] | boolean | number | number[] | {} | undefined;
}
export interface IDialogTab {
export interface IDialogTab<P> {
cancel?: Function;
className?: string;
component: ComponentType<any>;
icon: Function;
labelKey: string;
name: string;
props?: IObject;
propsUpdateFunction?: (tabState: IObject, newProps: IObject) => IObject;
propsUpdateFunction?: (tabState: IObject, newProps: P) => P;
submit?: Function;
}
interface IProps extends IBaseProps {
defaultTab?: string;
tabs: IDialogTab[];
tabs: IDialogTab<any>[];
}
const DialogWithTabs = ({
@ -169,6 +172,7 @@ const DialogWithTabs = ({
const dispatch = useDispatch();
const { t } = useTranslation();
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
const [ userSelected, setUserSelected ] = useState(false);
const [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const [ isMobile, setIsMobile ] = useState(false);
@ -189,18 +193,63 @@ const DialogWithTabs = ({
}
}, [ isMobile ]);
const back = useCallback(() => {
setSelectedTab(undefined);
const onUserSelection = useCallback((tabName?: string) => {
setUserSelected(true);
setSelectedTab(tabName);
}, []);
const onClose = useCallback(() => {
const back = useCallback(() => {
onUserSelection(undefined);
}, []);
// the userSelected state is used to prevent setting focus when the user
// didn't actually interact (for the first rendering for example)
useEffect(() => {
if (userSelected) {
document.querySelector<HTMLElement>(isMobile
? `.${classes.title}`
: `#${`dialogtab-button-${selectedTab}`}`
)?.focus();
setUserSelected(false);
}
}, [ isMobile, userSelected, selectedTab ]);
const onClose = useCallback((isCancel = true) => {
if (isCancel) {
tabs.forEach(({ cancel }) => {
cancel && dispatch(cancel());
});
}
dispatch(hideDialog());
}, []);
const onClick = useCallback((tabName: string) => () => {
setSelectedTab(tabName);
onUserSelection(tabName);
}, []);
const onTabKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLDivElement>) => {
let newTab: IDialogTab<any> | null = null;
if (event.key === 'ArrowUp') {
newTab = index === 0 ? tabs[tabs.length - 1] : tabs[index - 1];
}
if (event.key === 'ArrowDown') {
newTab = index === tabs.length - 1 ? tabs[0] : tabs[index + 1];
}
if (newTab !== null) {
onUserSelection(newTab.name);
}
}, [ tabs.length ]);
const onMobileKeyDown = useCallback((tabName: string) => (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === ' ' || event.key === 'Enter') {
onUserSelection(tabName);
}
}, [ classes.contentContainer ]);
const getTabProps = (tabId: number) => {
const tabConfiguration = tabs[tabId];
const currentTabState = tabStates[tabId];
@ -225,7 +274,7 @@ const DialogWithTabs = ({
tabs.forEach(({ submit }, idx) => {
submit?.(tabStates[idx]);
});
onClose();
onClose(false);
}, [ tabs, tabStates ]);
const selectedTabIndex = useMemo(() => {
@ -257,8 +306,7 @@ const DialogWithTabs = ({
const closeIcon = useMemo(() => (
<ClickableIcon
accessibilityLabel = { t('dialog.close') }
className = { classes.closeIcon }
accessibilityLabel = { t('dialog.accessibilityLabel.close') }
icon = { IconCloseLarge }
id = 'modal-header-close-button'
onClick = { onClose } />
@ -270,21 +318,39 @@ const DialogWithTabs = ({
onClose = { onClose }
size = 'large'>
{(!isMobile || !selectedTab) && (
<div className = { classes.sidebar }>
<div
aria-orientation = 'vertical'
className = { classes.sidebar }
role = { isMobile ? undefined : 'tablist' }>
<div className = { classes.titleContainer }>
<h2 className = { classes.title }>{t(titleKey ?? '')}</h2>
<MoveFocusInside>
<h2
className = { classes.title }
tabIndex = { -1 }>
{t(titleKey ?? '')}
</h2>
</MoveFocusInside>
{isMobile && closeIcon}
</div>
{tabs.map(tab => {
{tabs.map((tab, index) => {
const label = t(tab.labelKey);
/**
* When not on mobile, the items behave as tabs,
* that's why we set `controls`, `role` and `selected` attributes
* only when not on mobile, they are useful only for the tab behavior.
*/
return (
<ContextMenuItem
accessibilityLabel = { label }
className = { cx(isMobile && classes.menuItemMobile) }
controls = { isMobile ? undefined : `dialogtab-content-${tab.name}` }
icon = { tab.icon }
id = { `dialogtab-button-${tab.name}` }
key = { tab.name }
onClick = { onClick(tab.name) }
onKeyDown = { isMobile ? onMobileKeyDown(tab.name) : onTabKeyDown(index) }
role = { isMobile ? undefined : 'tab' }
selected = { tab.name === selectedTab }
text = { label } />
);
@ -292,26 +358,45 @@ const DialogWithTabs = ({
</div>
)}
{(!isMobile || selectedTab) && (
<div className = { classes.contentContainer }>
<div className = { classes.buttonContainer }>
{isMobile && (
<div
className = { classes.contentContainer }
tabIndex = { isMobile ? -1 : undefined }>
{/* DOM order is important for keyboard users: show whole heading first when on mobile… */}
{isMobile && (
<div className = { cx(classes.buttonContainer, classes.header) }>
<span className = { classes.backContainer }>
<h2
className = { classes.title }
tabIndex = { -1 }>
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
</h2>
<ClickableIcon
accessibilityLabel = { t('dialog.Back') }
className = { classes.closeIcon }
icon = { IconArrowBack }
id = 'modal-header-back-button'
onClick = { back } />
<h2 className = { classes.title }>
{(selectedTabIndex !== null) && t(tabs[selectedTabIndex].labelKey)}
</h2>
</span>
)}
{closeIcon}
</div>
<div className = { classes.content }>
{selectedTabComponent}
</div>
{closeIcon}
</div>
)}
{tabs.map(tab => (
<div
aria-labelledby = { isMobile ? undefined : `${tab.name}-button` }
className = { cx(classes.content, tab.name !== selectedTab && 'hide') }
id = { `dialogtab-content-${tab.name}` }
key = { tab.name }
role = { isMobile ? undefined : 'tabpanel' }
tabIndex = { isMobile ? -1 : 0 }>
{ tab.name === selectedTab && selectedTabComponent }
</div>
))}
{/* But show the close button *after* tab panels when not on mobile (using tabs).
This is so that we can tab back and forth tab buttons and tab panels easily. */}
{!isMobile && (
<div className = { cx(classes.buttonContainer, classes.header) }>
{closeIcon}
</div>
)}
<div
className = { cx(classes.buttonContainer, classes.footer) }>
<Button

View File

@ -79,7 +79,7 @@ const useStyles = makeStyles()(theme => {
width: '100%',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
padding: '8px 16px',
padding: '10px 16px',
paddingRight: '42px',
border: 0,
appearance: 'none',

View File

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
@ -11,6 +11,7 @@ interface ITabProps {
selected: string;
tabs: Array<{
accessibilityLabel: string;
controlsId: string;
countBadge?: number;
disabled?: boolean;
id: string;
@ -44,7 +45,7 @@ const useStyles = makeStyles()(theme => {
borderColor: theme.palette.ui10
},
'&:focus': {
'&.focus-visible': {
outline: 0,
boxShadow: `0px 0px 0px 2px ${theme.palette.focus01}`,
border: 0,
@ -87,26 +88,52 @@ const Tabs = ({
}: ITabProps) => {
const { classes, cx } = useStyles();
const isMobile = isMobileBrowser();
const handleChange = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
onChange(e.currentTarget.id);
const onClick = useCallback(id => () => {
onChange(id);
}, []);
const onKeyDown = useCallback((index: number) => (event: React.KeyboardEvent<HTMLButtonElement>) => {
let newIndex: number | null = null;
if (event.key === 'ArrowLeft') {
event.preventDefault();
newIndex = index === 0 ? tabs.length - 1 : index - 1;
}
if (event.key === 'ArrowRight') {
event.preventDefault();
newIndex = index === tabs.length - 1 ? 0 : index + 1;
}
if (newIndex !== null) {
onChange(tabs[newIndex].id);
}
}, [ tabs ]);
useEffect(() => {
// this test is needed to make sure the effect is triggered because of user actually changing tab
if (document.activeElement?.getAttribute('role') === 'tab') {
document.querySelector<HTMLButtonElement>(`#${selected}`)?.focus();
}
}, [ selected ]);
return (
<div
aria-label = { accessibilityLabel }
className = { cx(classes.container, className) }
role = 'tablist'>
{tabs.map(tab => (
{tabs.map((tab, index) => (
<button
aria-controls = { tab.controlsId }
aria-label = { tab.accessibilityLabel }
aria-selected = { selected === tab.id }
className = { cx(classes.tab, selected === tab.id && 'selected', isMobile && 'is-mobile') }
disabled = { tab.disabled }
id = { tab.id }
key = { tab.id }
onClick = { handleChange }
role = 'tab'>
onClick = { onClick(tab.id) }
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { selected === tab.id ? undefined : -1 }>
{tab.label}
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
</button>

View File

@ -0,0 +1,74 @@
import React, { ReactNode, useRef } from 'react';
import { keyframes } from 'styled-components';
import { makeStyles } from 'tss-react/mui';
import { TEXT_OVERFLOW_TYPES } from '../../constants.web';
interface ITextWithOverflowProps {
children: ReactNode;
className?: string;
overflowType?: TEXT_OVERFLOW_TYPES;
}
const useStyles = makeStyles<{ translateDiff: number; }>()((_, { translateDiff }) => {
return {
animation: {
'&:hover': {
animation: `${keyframes`
0%, 20% {
transform: translateX(0%);
left: 0%;
}
80%, 100% {
transform: translateX(-${translateDiff}px);
left: 100%;
}
`} ${Math.max(translateDiff * 50, 2000)}ms infinite alternate linear;`
}
},
textContainer: {
overflow: 'hidden'
},
[TEXT_OVERFLOW_TYPES.ELLIPSIS]: {
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
},
[TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER]: {
display: 'inline-block',
overflow: 'visible',
whiteSpace: 'nowrap'
}
};
});
const TextWithOverflow = ({
className,
overflowType = TEXT_OVERFLOW_TYPES.ELLIPSIS,
children
}: ITextWithOverflowProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLSpanElement>(null);
const shouldAnimateOnHover = overflowType === TEXT_OVERFLOW_TYPES.SCROLL_ON_HOVER
&& containerRef.current
&& contentRef.current
&& containerRef.current.clientWidth < contentRef.current.clientWidth;
const translateDiff = shouldAnimateOnHover ? contentRef.current.clientWidth - containerRef.current.clientWidth : 0;
const { classes: styles, cx } = useStyles({ translateDiff });
return (
<div
className = { cx(className, styles.textContainer) }
ref = { containerRef }>
<span
className = { cx(styles[overflowType], shouldAnimateOnHover && styles.animation) }
ref = { contentRef }>
{children}
</span>
</div>
);
};
export default TextWithOverflow;

View File

@ -8,6 +8,14 @@ export enum BUTTON_TYPES {
TERTIARY = 'tertiary'
}
/**
* Behaviour types for showing overflow text content.
*/
export enum TEXT_OVERFLOW_TYPES {
ELLIPSIS = 'ellipsis',
SCROLL_ON_HOVER = 'scroll-on-hover'
}
/**
* The modes of the buttons.
*/

View File

@ -202,7 +202,8 @@ class CalendarList extends AbstractPage<Props> {
className = 'meetings-list-empty-button'
onClick = { this._onOpenSettings }
onKeyPress = { this._onKeyPressOpenSettings }
role = 'button'>
role = 'button'
tabIndex = { 0 }>
<Icon
className = 'meetings-list-empty-icon'
src = { IconCalendar } />

View File

@ -134,35 +134,38 @@ class Chat extends AbstractChat<Props> {
_renderChat() {
const { _isPollsEnabled, _isPollsTabFocused } = this.props;
if (_isPollsTabFocused) {
return (
<>
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = { CHAT_TABS.POLLS }
id = 'polls-panel'
role = 'tabpanel'>
<PollsPane />
</div>
<KeyboardAvoider />
</>
);
}
return (
<>
{ _isPollsEnabled && this._renderTabs() }
<div
aria-labelledby = { CHAT_TABS.CHAT }
className = { clsx('chat-panel', !_isPollsEnabled && 'chat-panel-no-tabs') }
id = 'chat-panel'
role = 'tabpanel'>
className = { clsx(
'chat-panel',
!_isPollsEnabled && 'chat-panel-no-tabs',
_isPollsTabFocused && 'hide'
) }
id = { `${CHAT_TABS.CHAT}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<MessageContainer
messages = { this.props._messages } />
<MessageRecipient />
<ChatInput
onSend = { this._onSendMessage } />
</div>
{ _isPollsEnabled && (
<>
<div
aria-labelledby = { CHAT_TABS.POLLS }
className = { clsx('polls-panel', !_isPollsTabFocused && 'hide') }
id = { `${CHAT_TABS.POLLS}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<PollsPane />
</div>
<KeyboardAvoider />
</>
)}
</>
);
}
@ -185,11 +188,13 @@ class Chat extends AbstractChat<Props> {
accessibilityLabel: t('chat.tabs.chat'),
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: CHAT_TABS.CHAT,
controlsId: `${CHAT_TABS.CHAT}-panel`,
label: t('chat.tabs.chat')
}, {
accessibilityLabel: t('chat.tabs.polls'),
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: CHAT_TABS.POLLS,
controlsId: `${CHAT_TABS.POLLS}-panel`,
label: t('chat.tabs.polls')
}
] } />

View File

@ -46,9 +46,12 @@ function Header({ onCancel, className, isPollsEnabled, t }: Props) {
return (
<div
className = { className || 'chat-dialog-header' }
role = 'heading'>
{ t(isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
className = { className || 'chat-dialog-header' }>
<span
aria-level = { 1 }
role = 'heading'>
{ t(isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
</span>
<Icon
ariaLabel = { t('toolbar.closeChat') }
onClick = { onCancel }

View File

@ -115,7 +115,6 @@ class ChatInput extends Component<IProps, IState> {
</div>
)}
<Input
autoFocus = { true }
className = 'chat-input'
icon = { this.props._areSmileysDisabled ? undefined : IconFaceSmile }
iconClick = { this._toggleSmileysPanel }

View File

@ -8,6 +8,7 @@ export const CONFERENCE_INFO = {
'e2ee',
'transcribing',
'video-quality',
'visitors-count',
'insecure-room',
'top-panel-toggle'
]

View File

@ -8,12 +8,14 @@ import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { RecordingLabel } from '../../../recording';
import { openHighlightDialog } from '../../../recording/actions.native';
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
import VisitorsCountLabel from '../../../visitors/components/native/VisitorsCountLabel';
import RaisedHandsCountLabel from './RaisedHandsCountLabel';
import {
LABEL_ID_RAISED_HANDS_COUNT,
LABEL_ID_RECORDING,
LABEL_ID_STREAMING,
LABEL_ID_VISITORS_COUNT,
LabelHitSlop
} from './constants';
@ -51,6 +53,11 @@ const AlwaysOnLabels = ({ createOnPress }: Props) => {
onPress = { createOnPress(LABEL_ID_RAISED_HANDS_COUNT) } >
<RaisedHandsCountLabel />
</TouchableOpacity>
<TouchableOpacity
hitSlop = { LabelHitSlop }
onPress = { createOnPress(LABEL_ID_VISITORS_COUNT) } >
<VisitorsCountLabel />
</TouchableOpacity>
</>);
};

View File

@ -436,6 +436,7 @@ class Conference extends AbstractConference<Props, State> {
_shouldDisplayTileView
|| <>
<Filmstrip />
{ this._renderNotificationsContainer() }
<Toolbox />
</>
}
@ -467,7 +468,6 @@ class Conference extends AbstractConference<Props, State> {
{/* eslint-disable-next-line react/jsx-no-bind */}
<AlwaysOnLabels createOnPress = { this._createOnPress } />
</View>
{ this._renderNotificationsContainer() }
</SafeAreaView>
<TestConnectionInfo />
@ -530,7 +530,8 @@ class Conference extends AbstractConference<Props, State> {
return super.renderNotificationsContainer(
{
style: notificationsStyle
style: notificationsStyle,
toolboxVisible: this.props._toolboxVisible
}
);
}

View File

@ -26,6 +26,7 @@ export const LABEL_ID_STREAMING = 'streaming';
export const LABEL_ID_TRANSCRIBING = 'transcribing';
export const LABEL_ID_INSECURE_ROOM_NAME = 'insecure-room-name';
export const LABEL_ID_RAISED_HANDS_COUNT = 'raised-hands-count';
export const LABEL_ID_VISITORS_COUNT = 'visitors-count';
/**
* The {@code ExpandedLabel} components to be rendered for the individual

View File

@ -210,7 +210,8 @@ class Conference extends AbstractConference<Props, *> {
_notificationsVisible,
_overflowDrawer,
_showLobby,
_showPrejoin
_showPrejoin,
t
} = this.props;
return (
@ -240,7 +241,17 @@ class Conference extends AbstractConference<Props, *> {
}
</div>
{ _showPrejoin || _showLobby || <Toolbox /> }
{ _showPrejoin || _showLobby || (
<>
<span
aria-level = { 1 }
className = 'sr-only'
role = 'heading'>
{ t('toolbar.accessibilityLabel.heading') }
</span>
<Toolbox />
</>
)}
{_notificationsVisible && !_isAnyOverlayVisible && (_overflowDrawer
? <JitsiPortal className = 'notification-portal'>

View File

@ -9,9 +9,11 @@ import { connect } from '../../../base/redux';
import E2EELabel from '../../../e2ee/components/E2EELabel';
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
import RecordingLabel from '../../../recording/components/web/RecordingLabel';
import { showToolbox } from '../../../toolbox/actions';
import { isToolboxVisible } from '../../../toolbox/functions.web';
import TranscribingLabel from '../../../transcribing/components/TranscribingLabel.web';
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
import VisitorsCountLabel from '../../../visitors/components/web/VisitorsCountLabel';
import ConferenceTimer from '../ConferenceTimer';
import { getConferenceInfo } from '../functions';
@ -32,6 +34,11 @@ type Props = {
*/
_conferenceInfo: Object,
/**
* Invoked to active other features of the app.
*/
dispatch: Function;
/**
* Indicates whether the component should be visible or not.
*/
@ -80,6 +87,10 @@ const COMPONENTS = [
Component: VideoQualityLabel,
id: 'video-quality'
},
{
Component: VisitorsCountLabel,
id: 'visitors-count'
},
{
Component: InsecureRoomNameLabel,
id: 'insecure-room'
@ -108,6 +119,21 @@ class ConferenceInfo extends Component<Props> {
this._renderAutoHide = this._renderAutoHide.bind(this);
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
this._onTabIn = this._onTabIn.bind(this);
}
_onTabIn: () => void;
/**
* Callback invoked when the component is focused to show the conference
* info if necessary.
*
* @returns {void}
*/
_onTabIn() {
if (this.props._conferenceInfo.autoHide?.length && !this.props._visible) {
this.props.dispatch(showToolbox());
}
}
_renderAutoHide: () => void;
@ -176,7 +202,9 @@ class ConferenceInfo extends Component<Props> {
*/
render() {
return (
<div className = 'details-container' >
<div
className = 'details-container'
onFocus = { this._onTabIn }>
{ this._renderAlwaysVisible() }
{ this._renderAutoHide() }
</div>

View File

@ -216,7 +216,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
*/
render() {
// @ts-ignore
const { enableStatsDisplay, participantId, statsPopoverPosition, classes } = this.props;
const { enableStatsDisplay, participantId, statsPopoverPosition, classes, t } = this.props;
const visibilityClass = this._getVisibilityClass();
// @ts-ignore
@ -233,6 +233,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, IState> {
inheritedStats = { this.state.stats }
participantId = { participantId } /> }
disablePopover = { !enableStatsDisplay }
headingLabel = { t('videothumbnail.connectionInfo') }
id = 'participant-connection-indicator'
onPopoverClose = { this._onHidePopover }
onPopoverOpen = { this._onShowPopover }

View File

@ -191,7 +191,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
* @inheritdoc
*/
render() {
const { selectedTab, selectedSource, sources } = this.state;
const { selectedTab, selectedSource, sources, types } = this.state;
return (
<Dialog
@ -204,14 +204,27 @@ class DesktopPicker extends PureComponent<IProps, IState> {
size = 'large'
titleKey = 'dialog.shareYourScreen'>
{ this._renderTabs() }
<DesktopPickerPane
key = { selectedTab }
onClick = { this._onPreviewClick }
onDoubleClick = { this._onSubmit }
onShareAudioChecked = { this._onShareAudioChecked }
selectedSourceId = { selectedSource.id }
sources = { sources[selectedTab as keyof typeof sources] }
type = { selectedTab } />
{types.map(type => (
<div
aria-labelledby = { `${type}-button` }
className = { selectedTab === type ? undefined : 'hide' }
id = { `${type}-panel` }
key = { type }
role = 'tabpanel'
tabIndex = { 0 }>
{selectedTab === type && (
<DesktopPickerPane
key = { selectedTab }
onClick = { this._onPreviewClick }
onDoubleClick = { this._onSubmit }
onShareAudioChecked = { this._onShareAudioChecked }
selectedSourceId = { selectedSource.id }
sources = { sources[selectedTab as keyof typeof sources] }
type = { selectedTab } />
)}
</div>
))}
</Dialog>
);
}
@ -348,17 +361,18 @@ class DesktopPicker extends PureComponent<IProps, IState> {
type => {
return {
accessibilityLabel: t(TAB_LABELS[type as keyof typeof TAB_LABELS]),
id: type,
id: `${type}-tab`,
controlsId: `${type}-panel`,
label: t(TAB_LABELS[type as keyof typeof TAB_LABELS])
};
});
return (
<Tabs
accessibilityLabel = ''
accessibilityLabel = { t('dialog.sharingTabs') }
className = 'desktop-picker-tabs-container'
onChange = { this._onTabSelected }
selected = { this.state.selectedTab }
selected = { `${this.state.selectedTab}-tab` }
tabs = { tabs } />);
}

View File

@ -7,31 +7,23 @@ import {
} from '../base/devices/actions';
import { getDeviceLabelById, setAudioOutputDeviceId } from '../base/devices/functions';
import { updateSettings } from '../base/settings/actions';
import { toggleNoiseSuppression } from '../noise-suppression/actions';
import { setScreenshareFramerate } from '../screen-share/actions';
import { getDeviceSelectionDialogProps } from './functions';
import { getAudioDeviceSelectionDialogProps, getVideoDeviceSelectionDialogProps } from './functions';
import logger from './logger';
/**
* Submits the settings related to device selection.
* Submits the settings related to audio device selection.
*
* @param {Object} newState - The new settings.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Function}
*/
export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
export function submitAudioDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const currentState = getDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
}
const currentState = getAudioDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
if (newState.selectedAudioInputId && newState.selectedAudioInputId !== currentState.selectedAudioInputId) {
dispatch(updateSettings({
@ -44,8 +36,8 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
}
if (newState.selectedAudioOutputId
&& newState.selectedAudioOutputId
!== currentState.selectedAudioOutputId) {
&& newState.selectedAudioOutputId
!== currentState.selectedAudioOutputId) {
sendAnalytics(createDeviceChangedEvent('audio', 'output'));
setAudioOutputDeviceId(
@ -62,5 +54,45 @@ export function submitDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage
err);
});
}
if (newState.noiseSuppressionEnabled !== currentState.noiseSuppressionEnabled) {
dispatch(toggleNoiseSuppression());
}
};
}
/**
* Submits the settings related to device selection.
*
* @param {Object} newState - The new settings.
* @param {boolean} isDisplayedOnWelcomePage - Indicates whether the device selection dialog is displayed on the
* welcome page or not.
* @returns {Function}
*/
export function submitVideoDeviceSelectionTab(newState: any, isDisplayedOnWelcomePage: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const currentState = getVideoDeviceSelectionDialogProps(getState(), isDisplayedOnWelcomePage);
if (newState.selectedVideoInputId && (newState.selectedVideoInputId !== currentState.selectedVideoInputId)) {
dispatch(updateSettings({
userSelectedCameraDeviceId: newState.selectedVideoInputId,
userSelectedCameraDeviceLabel:
getDeviceLabelById(getState(), newState.selectedVideoInputId, 'videoInput')
}));
dispatch(setVideoInputDevice(newState.selectedVideoInputId));
}
if (newState.localFlipX !== currentState.localFlipX) {
dispatch(updateSettings({
localFlipX: newState.localFlipX
}));
}
if (newState.currentFramerate !== currentState.currentFramerate) {
const frameRate = parseInt(newState.currentFramerate, 10);
dispatch(setScreenshareFramerate(frameRate));
}
};
}

View File

@ -0,0 +1,387 @@
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../app/types';
import { getAvailableDevices } from '../../base/devices/actions.web';
import AbstractDialogTab, {
type IProps as AbstractDialogTabProps
} from '../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../base/i18n/functions';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
import Checkbox from '../../base/ui/components/web/Checkbox';
import logger from '../logger';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceHidContainer from './DeviceHidContainer.web';
import DeviceSelector from './DeviceSelector.web';
/**
* The type of the React {@code Component} props of {@link AudioDevicesSelection}.
*/
interface IProps extends AbstractDialogTabProps, WithTranslation {
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
availableDevices: {
audioInput?: MediaDeviceInfo[];
audioOutput?: MediaDeviceInfo[];
};
/**
* CSS classes object.
*/
classes: any;
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: boolean;
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: boolean;
/**
* Redux dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Whether or not the audio permission was granted.
*/
hasAudioPermission: boolean;
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: boolean;
/**
* If true, the button to play a test sound on the selected speaker will not be displayed.
* This needs to be hidden on browsers that do not support selecting an audio output device.
*/
hideAudioOutputPreview: boolean;
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered.
*/
hideAudioOutputSelect: boolean;
/**
* Whether or not the hid device container should display.
*/
hideDeviceHIDContainer: boolean;
/**
* Whether to hide noise suppression checkbox or not.
*/
hideNoiseSuppression: boolean;
/**
* Wether noise suppression is on or not.
*/
noiseSuppressionEnabled: boolean;
/**
* The id of the audio input device to preview.
*/
selectedAudioInputId: string;
/**
* The id of the audio output device to preview.
*/
selectedAudioOutputId: string;
}
/**
* The type of the React {@code Component} state of {@link AudioDevicesSelection}.
*/
type State = {
/**
* The JitsiTrack to use for previewing audio input.
*/
previewAudioTrack?: any | null;
};
const styles = (theme: Theme) => {
return {
container: {
display: 'flex',
flexDirection: 'column' as const,
padding: '0 2px',
width: '100%'
},
inputContainer: {
marginBottom: theme.spacing(3)
},
outputContainer: {
margin: `${theme.spacing(5)} 0`,
display: 'flex',
alignItems: 'flex-end'
},
outputButton: {
marginLeft: theme.spacing(3)
},
noiseSuppressionContainer: {
marginBottom: theme.spacing(5)
}
};
};
/**
* React {@code Component} for previewing audio and video input/output devices.
*
* @augments Component
*/
class AudioDevicesSelection extends AbstractDialogTab<IProps, State> {
/**
* Whether current component is mounted or not.
*
* In component did mount we start a Promise to create tracks and
* set the tracks in the state, if we unmount the component in the meanwhile
* tracks will be created and will never been disposed (dispose tracks is
* in componentWillUnmount). When tracks are created and component is
* unmounted we dispose the tracks.
*/
_unMounted: boolean;
/**
* Initializes a new DeviceSelection instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
this.state = {
previewAudioTrack: null
};
this._unMounted = true;
}
/**
* Generate the initial previews for audio input and video input.
*
* @inheritdoc
*/
componentDidMount() {
this._unMounted = false;
Promise.all([
this._createAudioInputTrack(this.props.selectedAudioInputId)
])
.catch(err => logger.warn('Failed to initialize preview tracks', err))
.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
/**
* Checks if audio / video permissions were granted. Updates audio input and
* video input previews.
*
* @param {Object} prevProps - Previous props this component received.
* @returns {void}
*/
componentDidUpdate(prevProps: IProps) {
if (prevProps.selectedAudioInputId
!== this.props.selectedAudioInputId) {
this._createAudioInputTrack(this.props.selectedAudioInputId);
}
}
/**
* Ensure preview tracks are destroyed to prevent continued use.
*
* @inheritdoc
*/
componentWillUnmount() {
this._unMounted = true;
this._disposeAudioInputPreview();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
classes,
hasAudioPermission,
hideAudioInputPreview,
hideAudioOutputPreview,
hideDeviceHIDContainer,
hideNoiseSuppression,
noiseSuppressionEnabled,
selectedAudioOutputId,
t
} = this.props;
const { audioInput, audioOutput } = this._getSelectors();
return (
<div className = { classes.container }>
<div
aria-live = 'polite'
className = { classes.inputContainer }>
{this._renderSelector(audioInput)}
</div>
{!hideAudioInputPreview && hasAudioPermission
&& <AudioInputPreview
track = { this.state.previewAudioTrack } />}
<div
aria-live = 'polite'
className = { classes.outputContainer }>
{this._renderSelector(audioOutput)}
{!hideAudioOutputPreview && hasAudioPermission
&& <AudioOutputPreview
className = { classes.outputButton }
deviceId = { selectedAudioOutputId } />}
</div>
{!hideNoiseSuppression && (
<div className = { classes.noiseSuppressionContainer }>
<Checkbox
checked = { noiseSuppressionEnabled }
label = { t('toolbar.enableNoiseSuppression') }
// eslint-disable-next-line react/jsx-no-bind
onChange = { () => super._onChange({
noiseSuppressionEnabled: !noiseSuppressionEnabled
}) } />
</div>
)}
{!hideDeviceHIDContainer
&& <DeviceHidContainer />}
</div>
);
}
/**
* Creates the JitsiTrack for the audio input preview.
*
* @param {string} deviceId - The id of audio input device to preview.
* @private
* @returns {void}
*/
_createAudioInputTrack(deviceId: string) {
const { hideAudioInputPreview } = this.props;
if (hideAudioInputPreview) {
return;
}
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => {
if (this._unMounted) {
jitsiLocalTrack.dispose();
return;
}
this.setState({
previewAudioTrack: jitsiLocalTrack
});
})
.catch(() => {
this.setState({
previewAudioTrack: null
});
});
}
/**
* Utility function for disposing the current audio input preview.
*
* @private
* @returns {Promise}
*/
_disposeAudioInputPreview(): Promise<any> {
return this.state.previewAudioTrack
? this.state.previewAudioTrack.dispose() : Promise.resolve();
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
* @returns {ReactElement}
*/
_renderSelector(deviceSelectorProps: any) {
return deviceSelectorProps ? (
<DeviceSelector
{ ...deviceSelectorProps }
key = { deviceSelectorProps.id } />
) : null;
}
/**
* Returns object configurations for audio input and output.
*
* @private
* @returns {Object} Configurations.
*/
_getSelectors() {
const { availableDevices, hasAudioPermission } = this.props;
const audioInput = {
devices: availableDevices.audioInput,
hasPermission: hasAudioPermission,
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
key: 'audioInput',
id: 'audioInput',
label: 'settings.selectMic',
onSelect: (selectedAudioInputId: string) => super._onChange({ selectedAudioInputId }),
selectedDeviceId: this.state.previewAudioTrack
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
};
let audioOutput;
if (!this.props.hideAudioOutputSelect) {
audioOutput = {
devices: availableDevices.audioOutput,
hasPermission: hasAudioPermission,
icon: 'icon-speaker',
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
id: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: (selectedAudioOutputId: string) => super._onChange({ selectedAudioOutputId }),
selectedDeviceId: this.props.selectedAudioOutputId
};
}
return { audioInput,
audioOutput };
}
}
const mapStateToProps = (state: IReduxState) => {
return {
availableDevices: state['features/base/devices'].availableDevices ?? {}
};
};
export default connect(mapStateToProps)(withStyles(styles)(translate(AudioDevicesSelection)));

View File

@ -1,150 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import JitsiMeetJS from '../../base/lib-jitsi-meet/_';
const JitsiTrackEvents = JitsiMeetJS.events.track;
/**
* The type of the React {@code Component} props of {@link AudioInputPreview}.
*/
type Props = {
/**
* The JitsiLocalTrack to show an audio level meter for.
*/
track: Object
};
/**
* The type of the React {@code Component} props of {@link AudioInputPreview}.
*/
type State = {
/**
* The current audio input level being received, from 0 to 1.
*/
audioLevel: number
};
/**
* React component for displaying a audio level meter for a JitsiLocalTrack.
*/
class AudioInputPreview extends Component<Props, State> {
/**
* Initializes a new AudioInputPreview instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
audioLevel: 0
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
}
/**
* Starts listening for audio level updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates(this.props.track);
}
/**
* Stops listening for audio level updates on the old track and starts
* listening instead on the new track.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
if (prevProps.track !== this.props.track) {
this._listenForAudioUpdates(this.props.track);
this._updateAudioLevel(0);
}
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const audioMeterFill = {
width: `${Math.floor(this.state.audioLevel * 100)}%`
};
return (
<div className = 'audio-input-preview' >
<div
className = 'audio-input-preview-level'
style = { audioMeterFill } />
</div>
);
}
/**
* Starts listening for audio level updates from the library.
*
* @param {JitstiLocalTrack} track - The track to listen to for audio level
* updates.
* @private
* @returns {void}
*/
_listenForAudioUpdates(track) {
this._stopListeningForAudioUpdates();
track && track.on(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateAudioLevel);
}
/**
* Stops listening to further updates from the current track.
*
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates() {
this.props.track && this.props.track.off(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
this._updateAudioLevel);
}
_updateAudioLevel: (number) => void;
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
}
export default AudioInputPreview;

View File

@ -0,0 +1,103 @@
import React, { useEffect, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
// @ts-ignore
import JitsiMeetJS from '../../base/lib-jitsi-meet/_.web';
const JitsiTrackEvents = JitsiMeetJS.events.track;
/**
* The type of the React {@code Component} props of {@link AudioInputPreview}.
*/
interface IProps {
/**
* The JitsiLocalTrack to show an audio level meter for.
*/
track: any;
}
const useStyles = makeStyles()(theme => {
return {
container: {
display: 'flex'
},
section: {
flex: 1,
height: '4px',
borderRadius: '1px',
backgroundColor: theme.palette.ui04,
marginRight: theme.spacing(1),
'&:last-of-type': {
marginRight: 0
}
},
activeSection: {
backgroundColor: theme.palette.success01
}
};
});
const NO_OF_PREVIEW_SECTIONS = 11;
const AudioInputPreview = (props: IProps) => {
const [ audioLevel, setAudioLevel ] = useState(0);
const { classes, cx } = useStyles();
/**
* Starts listening for audio level updates from the library.
*
* @param {JitsiLocalTrack} track - The track to listen to for audio level
* updates.
* @private
* @returns {void}
*/
function _listenForAudioUpdates(track: any) {
_stopListeningForAudioUpdates();
track?.on(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
setAudioLevel);
}
/**
* Stops listening to further updates from the current track.
*
* @private
* @returns {void}
*/
function _stopListeningForAudioUpdates() {
props.track?.off(
JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED,
setAudioLevel);
}
useEffect(() => {
_listenForAudioUpdates(props.track);
return _stopListeningForAudioUpdates;
}, []);
useEffect(() => {
_listenForAudioUpdates(props.track);
setAudioLevel(0);
}, [ props.track ]);
const audioMeterFill = Math.ceil(Math.floor(audioLevel * 100) / (100 / NO_OF_PREVIEW_SECTIONS));
return (
<div className = { classes.container } >
{new Array(NO_OF_PREVIEW_SECTIONS).fill(0)
.map((_, idx) =>
(<div
className = { cx(classes.section, idx < audioMeterFill && classes.activeSection) }
key = { idx } />)
)}
</div>
);
};
export default AudioInputPreview;

View File

@ -1,35 +1,38 @@
/* @flow */
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { translate } from '../../base/i18n/functions';
import Audio from '../../base/media/components/Audio';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import Audio from '../../base/media/components/Audio.web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants.any';
const TEST_SOUND_PATH = 'sounds/ring.mp3';
/**
* The type of the React {@code Component} props of {@link AudioOutputPreview}.
*/
type Props = {
interface IProps extends WithTranslation {
/**
* Button className.
*/
className?: string;
/**
* The device id of the audio output device to use.
*/
deviceId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
deviceId: string;
}
/**
* React component for playing a test sound through a specified audio device.
*
* @augments Component
*/
class AudioOutputPreview extends Component<Props> {
_audioElement: ?Object;
class AudioOutputPreview extends Component<IProps> {
_audioElement: HTMLAudioElement | null;
/**
* Initializes a new AudioOutputPreview instance.
@ -37,7 +40,7 @@ class AudioOutputPreview extends Component<Props> {
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
constructor(props: IProps) {
super(props);
this._audioElement = null;
@ -66,24 +69,21 @@ class AudioOutputPreview extends Component<Props> {
*/
render() {
return (
<div className = 'audio-output-preview'>
<a
aria-label = { this.props.t('deviceSelection.testAudio') }
<>
<Button
accessibilityLabel = { this.props.t('deviceSelection.testAudio') }
className = { this.props.className }
labelKey = 'deviceSelection.testAudio'
onClick = { this._onClick }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>
{ this.props.t('deviceSelection.testAudio') }
</a>
type = { BUTTON_TYPES.SECONDARY } />
<Audio
setRef = { this._audioElementReady }
src = { TEST_SOUND_PATH } />
</div>
</>
);
}
_audioElementReady: (Object) => void;
/**
* Sets the instance variable for the component's audio element so it can be
* accessed directly.
@ -92,14 +92,12 @@ class AudioOutputPreview extends Component<Props> {
* @private
* @returns {void}
*/
_audioElementReady(element: Object) {
_audioElementReady(element: HTMLAudioElement) {
this._audioElement = element;
this._setAudioSink();
}
_onClick: () => void;
/**
* Plays a test sound.
*
@ -107,12 +105,9 @@ class AudioOutputPreview extends Component<Props> {
* @returns {void}
*/
_onClick() {
this._audioElement
&& this._audioElement.play();
this._audioElement?.play();
}
_onKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
@ -120,7 +115,7 @@ class AudioOutputPreview extends Component<Props> {
*
* @returns {void}
*/
_onKeyPress(e) {
_onKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onClick();
@ -135,7 +130,7 @@ class AudioOutputPreview extends Component<Props> {
*/
_setAudioSink() {
this._audioElement
&& this.props.deviceId
&& this.props.deviceId // @ts-ignore
&& this._audioElement.setSinkId(this.props.deviceId);
}
}

View File

@ -0,0 +1,108 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../base/icons/components/Icon';
import { IconTrash } from '../../base/icons/svg';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import Button from '../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../base/ui/constants.any';
import { closeHidDevice, requestHidDevice } from '../../web-hid/actions';
import { getDeviceInfo, shouldRequestHIDDevice } from '../../web-hid/functions';
const useStyles = makeStyles()(theme => {
return {
callControlContainer: {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
},
label: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
marginBottom: theme.spacing(2)
},
deviceRow: {
display: 'flex',
justifyContent: 'space-between'
},
deleteDevice: {
cursor: 'pointer',
textAlign: 'center'
},
headerConnectedDevice: {
fontWeight: 600
},
hidContainer: {
'> span': {
marginLeft: '16px'
}
}
};
});
/**
* Device hid container.
*
* @param {IProps} props - The props of the component.
* @returns {ReactElement}
*/
function DeviceHidContainer() {
const { t } = useTranslation();
const deviceInfo = useSelector(getDeviceInfo);
const showRequestDeviceInfo = shouldRequestHIDDevice(deviceInfo);
const { classes } = useStyles();
const dispatch = useDispatch();
const onRequestControl = useCallback(() => {
dispatch(requestHidDevice());
}, [ dispatch ]);
const onDeleteHid = useCallback(() => {
dispatch(closeHidDevice());
}, [ dispatch ]);
return (
<div
className = { classes.callControlContainer }
key = 'callControl'>
<label
className = { classes.label }
htmlFor = 'callControl'>
{t('deviceSelection.hid.callControl')}
</label>
{showRequestDeviceInfo && (
<Button
accessibilityLabel = { t('deviceSelection.hid.pairDevice') }
id = 'request-control-btn'
key = 'request-control-btn'
label = { t('deviceSelection.hid.pairDevice') }
onClick = { onRequestControl }
type = { BUTTON_TYPES.SECONDARY } />
)}
{!showRequestDeviceInfo && (
<div className = { classes.hidContainer }>
<p className = { classes.headerConnectedDevice }>{t('deviceSelection.hid.connectedDevices')}</p>
<div className = { classes.deviceRow }>
<span>{deviceInfo.device?.productName}</span>
<Icon
ariaLabel = { t('deviceSelection.hid.deleteDevice') }
className = { classes.deleteDevice }
onClick = { onDeleteHid }
role = 'button'
src = { IconTrash }
tabIndex = { 0 } />
</div>
</div>
)}
</div>
);
}
export default DeviceHidContainer;

View File

@ -1,420 +0,0 @@
// @flow
import React from 'react';
import { getAvailableDevices } from '../../base/devices/actions.web';
import AbstractDialogTab, {
type Props as AbstractDialogTabProps
} from '../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../base/i18n/functions';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
import logger from '../logger';
import AudioInputPreview from './AudioInputPreview';
import AudioOutputPreview from './AudioOutputPreview';
import DeviceSelector from './DeviceSelector';
import VideoInputPreview from './VideoInputPreview';
/**
* The type of the React {@code Component} props of {@link DeviceSelection}.
*/
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
availableDevices: Object,
/**
* Whether or not the audio selector can be interacted with. If true,
* the audio input selector will be rendered as disabled. This is
* specifically used to prevent audio device changing in Firefox, which
* currently does not work due to a browser-side regression.
*/
disableAudioInputChange: boolean,
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: boolean,
/**
* Whether video input dropdown should be enabled or not.
*/
disableVideoInputSelect: boolean,
/**
* Whether or not the audio permission was granted.
*/
hasAudioPermission: boolean,
/**
* Whether or not the audio permission was granted.
*/
hasVideoPermission: boolean,
/**
* If true, the audio meter will not display. Necessary for browsers or
* configurations that do not support local stats to prevent a
* non-responsive mic preview from displaying.
*/
hideAudioInputPreview: boolean,
/**
* If true, the button to play a test sound on the selected speaker will not be displayed.
* This needs to be hidden on browsers that do not support selecting an audio output device.
*/
hideAudioOutputPreview: boolean,
/**
* Whether or not the audio output source selector should display. If
* true, the audio output selector and test audio link will not be
* rendered.
*/
hideAudioOutputSelect: boolean,
/**
* Whether video input preview should be displayed or not.
* (In the case of iOS Safari).
*/
hideVideoInputPreview: boolean,
/**
* The id of the audio input device to preview.
*/
selectedAudioInputId: string,
/**
* The id of the audio output device to preview.
*/
selectedAudioOutputId: string,
/**
* The id of the video input device to preview.
*/
selectedVideoInputId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* The type of the React {@code Component} state of {@link DeviceSelection}.
*/
type State = {
/**
* The JitsiTrack to use for previewing audio input.
*/
previewAudioTrack: ?Object,
/**
* The JitsiTrack to use for previewing video input.
*/
previewVideoTrack: ?Object,
/**
* The error message from trying to use a video input device.
*/
previewVideoTrackError: ?string
};
/**
* React {@code Component} for previewing audio and video input/output devices.
*
* @augments Component
*/
class DeviceSelection extends AbstractDialogTab<Props, State> {
/**
* Whether current component is mounted or not.
*
* In component did mount we start a Promise to create tracks and
* set the tracks in the state, if we unmount the component in the meanwhile
* tracks will be created and will never been disposed (dispose tracks is
* in componentWillUnmount). When tracks are created and component is
* unmounted we dispose the tracks.
*/
_unMounted: boolean;
/**
* Initializes a new DeviceSelection instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
previewAudioTrack: null,
previewVideoTrack: null,
previewVideoTrackError: null
};
this._unMounted = true;
}
/**
* Generate the initial previews for audio input and video input.
*
* @inheritdoc
*/
componentDidMount() {
this._unMounted = false;
Promise.all([
this._createAudioInputTrack(this.props.selectedAudioInputId),
this._createVideoInputTrack(this.props.selectedVideoInputId)
])
.catch(err => logger.warn('Failed to initialize preview tracks', err))
.then(() => getAvailableDevices());
}
/**
* Checks if audio / video permissions were granted. Updates audio input and
* video input previews.
*
* @param {Object} prevProps - Previous props this component received.
* @returns {void}
*/
componentDidUpdate(prevProps) {
if (prevProps.selectedAudioInputId
!== this.props.selectedAudioInputId) {
this._createAudioInputTrack(this.props.selectedAudioInputId);
}
if (prevProps.selectedVideoInputId
!== this.props.selectedVideoInputId) {
this._createVideoInputTrack(this.props.selectedVideoInputId);
}
}
/**
* Ensure preview tracks are destroyed to prevent continued use.
*
* @inheritdoc
*/
componentWillUnmount() {
this._unMounted = true;
this._disposeAudioInputPreview();
this._disposeVideoInputPreview();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
hideAudioInputPreview,
hideAudioOutputPreview,
hideVideoInputPreview,
selectedAudioOutputId
} = this.props;
return (
<div className = { `device-selection${hideVideoInputPreview ? ' video-hidden' : ''}` }>
<div className = 'device-selection-column column-video'>
{ !hideVideoInputPreview
&& <div className = 'device-selection-video-container'>
<VideoInputPreview
error = { this.state.previewVideoTrackError }
track = { this.state.previewVideoTrack } />
</div>
}
{ !hideAudioInputPreview
&& <AudioInputPreview
track = { this.state.previewAudioTrack } /> }
</div>
<div className = 'device-selection-column column-selectors'>
<div
aria-live = 'polite all'
className = 'device-selectors'>
{ this._renderSelectors() }
</div>
{ !hideAudioOutputPreview
&& <AudioOutputPreview
deviceId = { selectedAudioOutputId } /> }
</div>
</div>
);
}
/**
* Creates the JitiTrack for the audio input preview.
*
* @param {string} deviceId - The id of audio input device to preview.
* @private
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
const { hideAudioInputPreview } = this.props;
if (hideAudioInputPreview) {
return;
}
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => {
if (this._unMounted) {
jitsiLocalTrack.dispose();
return;
}
this.setState({
previewAudioTrack: jitsiLocalTrack
});
})
.catch(() => {
this.setState({
previewAudioTrack: null
});
});
}
/**
* Creates the JitiTrack for the video input preview.
*
* @param {string} deviceId - The id of video device to preview.
* @private
* @returns {void}
*/
_createVideoInputTrack(deviceId) {
const { hideVideoInputPreview } = this.props;
if (hideVideoInputPreview) {
return;
}
return this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId, 5000))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {
return Promise.reject();
}
if (this._unMounted) {
jitsiLocalTrack.dispose();
return;
}
this.setState({
previewVideoTrack: jitsiLocalTrack,
previewVideoTrackError: null
});
})
.catch(() => {
this.setState({
previewVideoTrack: null,
previewVideoTrackError:
this.props.t('deviceSelection.previewUnavailable')
});
});
}
/**
* Utility function for disposing the current audio input preview.
*
* @private
* @returns {Promise}
*/
_disposeAudioInputPreview(): Promise<*> {
return this.state.previewAudioTrack
? this.state.previewAudioTrack.dispose() : Promise.resolve();
}
/**
* Utility function for disposing the current video input preview.
*
* @private
* @returns {Promise}
*/
_disposeVideoInputPreview(): Promise<*> {
return this.state.previewVideoTrack
? this.state.previewVideoTrack.dispose() : Promise.resolve();
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @param {Object} deviceSelectorProps - The props for the DeviceSelector.
* @returns {ReactElement}
*/
_renderSelector(deviceSelectorProps) {
return (
<div key = { deviceSelectorProps.label }>
<label
className = 'device-selector-label'
htmlFor = { deviceSelectorProps.id }>
{ this.props.t(deviceSelectorProps.label) }
</label>
<DeviceSelector { ...deviceSelectorProps } />
</div>
);
}
/**
* Creates DeviceSelector instances for video output, audio input, and audio
* output.
*
* @private
* @returns {Array<ReactElement>} DeviceSelector instances.
*/
_renderSelectors() {
const { availableDevices, hasAudioPermission, hasVideoPermission } = this.props;
const configurations = [
{
devices: availableDevices.audioInput,
hasPermission: hasAudioPermission,
icon: 'icon-microphone',
isDisabled: this.props.disableAudioInputChange || this.props.disableDeviceChange,
key: 'audioInput',
id: 'audioInput',
label: 'settings.selectMic',
onSelect: selectedAudioInputId => super._onChange({ selectedAudioInputId }),
selectedDeviceId: this.state.previewAudioTrack
? this.state.previewAudioTrack.getDeviceId() : this.props.selectedAudioInputId
},
{
devices: availableDevices.videoInput,
hasPermission: hasVideoPermission,
icon: 'icon-camera',
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
key: 'videoInput',
id: 'videoInput',
label: 'settings.selectCamera',
onSelect: selectedVideoInputId => super._onChange({ selectedVideoInputId }),
selectedDeviceId: this.state.previewVideoTrack
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
}
];
if (!this.props.hideAudioOutputSelect) {
configurations.push({
devices: availableDevices.audioOutput,
hasPermission: hasAudioPermission || hasVideoPermission,
icon: 'icon-speaker',
isDisabled: this.props.disableDeviceChange,
key: 'audioOutput',
id: 'audioOutput',
label: 'settings.selectAudioOutput',
onSelect: selectedAudioOutputId => super._onChange({ selectedAudioOutputId }),
selectedDeviceId: this.props.selectedAudioOutputId
});
}
return configurations.map(config => this._renderSelector(config));
}
}
export default translate(DeviceSelection);

View File

@ -1,58 +1,76 @@
/* @flow */
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { translate } from '../../base/i18n/functions';
import { withPixelLineHeight } from '../../base/styles/functions.web';
import Select from '../../base/ui/components/web/Select';
/**
* The type of the React {@code Component} props of {@link DeviceSelector}.
*/
type Props = {
interface IProps extends WithTranslation {
/**
* CSS classes object.
*/
classes: any;
/**
* MediaDeviceInfos used for display in the select element.
*/
devices: Array<Object>,
devices: Array<MediaDeviceInfo> | undefined;
/**
* If false, will return a selector with no selection options.
*/
hasPermission: boolean,
hasPermission: boolean;
/**
* CSS class for the icon to the left of the dropdown trigger.
*/
icon: string,
/**
* If true, will render the selector disabled with a default selection.
*/
isDisabled: boolean,
/**
* The translation key to display as a menu label.
*/
label: string,
/**
* The callback to invoke when a selection is made.
*/
onSelect: Function,
/**
* The default device to display as selected.
*/
selectedDeviceId: string,
/**
* Invoked to obtain translated strings.
*/
t: Function,
icon: string;
/**
* The id of the dropdown element.
*/
id: string
id: string;
/**
* If true, will render the selector disabled with a default selection.
*/
isDisabled: boolean;
/**
* The translation key to display as a menu label.
*/
label: string;
/**
* The callback to invoke when a selection is made.
*/
onSelect: Function;
/**
* The default device to display as selected.
*/
selectedDeviceId: string;
}
const styles = (theme: Theme) => {
return {
textSelector: {
width: '100%',
boxSizing: 'border-box',
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.uiBackground,
padding: '10px 16px',
textAlign: 'center',
...withPixelLineHeight(theme.typography.bodyShortRegular),
border: `1px solid ${theme.palette.ui03}`
}
};
};
/**
@ -61,17 +79,18 @@ type Props = {
*
* @augments Component
*/
class DeviceSelector extends Component<Props> {
class DeviceSelector extends Component<IProps> {
/**
* Initializes a new DeviceSelector instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props) {
constructor(props: IProps) {
super(props);
this._onSelect = this._onSelect.bind(this);
this._createDropdown = this._createDropdown.bind(this);
}
/**
@ -111,25 +130,6 @@ class DeviceSelector extends Component<Props> {
});
}
/**
* Creates a React Element for displaying the passed in text surrounded by
* two icons. The left icon is the icon class passed in through props and
* the right icon is AtlasKit ExpandIcon.
*
* @param {string} triggerText - The text to display within the element.
* @private
* @returns {ReactElement}
*/
_createDropdownTrigger(triggerText) {
return (
<div className = 'device-selector-trigger'>
<span className = 'device-selector-trigger-text'>
{ triggerText }
</span>
</div>
);
}
/**
* Creates a AKDropdownMenu Component using passed in props and options. If
* the dropdown needs to be disabled, then only the AKDropdownMenu trigger
@ -146,32 +146,30 @@ class DeviceSelector extends Component<Props> {
* @private
* @returns {ReactElement}
*/
_createDropdown(options) {
_createDropdown(options: { defaultSelected?: MediaDeviceInfo; isDisabled: boolean;
items?: Array<{ label: string; value: string; }>; placeholder: string; }) {
const triggerText
= (options.defaultSelected && (options.defaultSelected.label || options.defaultSelected.deviceId))
|| options.placeholder;
const trigger = this._createDropdownTrigger(triggerText);
const { classes } = this.props;
if (options.isDisabled || !options.items.length) {
if (options.isDisabled || !options.items?.length) {
return (
<div className = 'device-selector-trigger-disabled'>
{ trigger }
<div className = { classes.textSelector }>
{triggerText}
</div>
);
}
return (
<div className = 'dropdown-menu'>
<Select
onChange = { this._onSelect }
options = { options.items }
value = { this.props.selectedDeviceId } />
</div>
<Select
label = { this.props.t(this.props.label) }
onChange = { this._onSelect }
options = { options.items }
value = { this.props.selectedDeviceId } />
);
}
_onSelect: (Object) => void;
/**
* Invokes the passed in callback to notify of selection changes.
*
@ -180,7 +178,7 @@ class DeviceSelector extends Component<Props> {
* @private
* @returns {void}
*/
_onSelect(e) {
_onSelect(e: React.ChangeEvent<HTMLSelectElement>) {
const deviceId = e.target.value;
if (this.props.selectedDeviceId !== deviceId) {
@ -217,4 +215,4 @@ class DeviceSelector extends Component<Props> {
}
}
export default translate(DeviceSelector);
export default withStyles(styles)(translate(DeviceSelector));

View File

@ -0,0 +1,368 @@
import { Theme } from '@mui/material';
import { withStyles } from '@mui/styles';
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../app/types';
import { getAvailableDevices } from '../../base/devices/actions.web';
import AbstractDialogTab, {
type IProps as AbstractDialogTabProps
} from '../../base/dialog/components/web/AbstractDialogTab';
import { translate } from '../../base/i18n/functions';
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions.web';
import Checkbox from '../../base/ui/components/web/Checkbox';
import Select from '../../base/ui/components/web/Select';
import { SS_DEFAULT_FRAME_RATE } from '../../settings/constants';
import logger from '../logger';
import DeviceSelector from './DeviceSelector.web';
import VideoInputPreview from './VideoInputPreview';
/**
* The type of the React {@code Component} props of {@link VideoDeviceSelection}.
*/
export interface IProps extends AbstractDialogTabProps, WithTranslation {
/**
* All known audio and video devices split by type. This prop comes from
* the app state.
*/
availableDevices: { videoInput?: MediaDeviceInfo[]; };
/**
* CSS classes object.
*/
classes: any;
/**
* The currently selected desktop share frame rate in the frame rate select dropdown.
*/
currentFramerate: string;
/**
* All available desktop capture frame rates.
*/
desktopShareFramerates: Array<number>;
/**
* True if device changing is configured to be disallowed. Selectors
* will display as disabled.
*/
disableDeviceChange: boolean;
/**
* Whether video input dropdown should be enabled or not.
*/
disableVideoInputSelect: boolean;
/**
* Redux dispatch.
*/
dispatch: IStore['dispatch'];
/**
* Whether or not the audio permission was granted.
*/
hasVideoPermission: boolean;
/**
* Whether to hide the additional settings or not.
*/
hideAdditionalSettings: boolean;
/**
* Whether video input preview should be displayed or not.
* (In the case of iOS Safari).
*/
hideVideoInputPreview: boolean;
/**
* Whether or not the local video is flipped.
*/
localFlipX: boolean;
/**
* The id of the video input device to preview.
*/
selectedVideoInputId: string;
}
/**
* The type of the React {@code Component} state of {@link VideoDeviceSelection}.
*/
type State = {
/**
* The JitsiTrack to use for previewing video input.
*/
previewVideoTrack: any | null;
/**
* The error message from trying to use a video input device.
*/
previewVideoTrackError: string | null;
};
const styles = (theme: Theme) => {
return {
container: {
display: 'flex',
flexDirection: 'column' as const,
padding: '0 2px',
width: '100%'
},
checkboxContainer: {
margin: `${theme.spacing(4)} 0`
}
};
};
/**
* React {@code Component} for previewing audio and video input/output devices.
*
* @augments Component
*/
class VideoDeviceSelection extends AbstractDialogTab<IProps, State> {
/**
* Whether current component is mounted or not.
*
* In component did mount we start a Promise to create tracks and
* set the tracks in the state, if we unmount the component in the meanwhile
* tracks will be created and will never been disposed (dispose tracks is
* in componentWillUnmount). When tracks are created and component is
* unmounted we dispose the tracks.
*/
_unMounted: boolean;
/**
* Initializes a new DeviceSelection instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: IProps) {
super(props);
this.state = {
previewVideoTrack: null,
previewVideoTrackError: null
};
this._unMounted = true;
this._onFramerateItemSelect = this._onFramerateItemSelect.bind(this);
}
/**
* Generate the initial previews for audio input and video input.
*
* @inheritdoc
*/
componentDidMount() {
this._unMounted = false;
Promise.all([
this._createVideoInputTrack(this.props.selectedVideoInputId)
])
.catch(err => logger.warn('Failed to initialize preview tracks', err))
.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
/**
* Checks if audio / video permissions were granted. Updates audio input and
* video input previews.
*
* @param {Object} prevProps - Previous props this component received.
* @returns {void}
*/
componentDidUpdate(prevProps: IProps) {
if (prevProps.selectedVideoInputId
!== this.props.selectedVideoInputId) {
this._createVideoInputTrack(this.props.selectedVideoInputId);
}
}
/**
* Ensure preview tracks are destroyed to prevent continued use.
*
* @inheritdoc
*/
componentWillUnmount() {
this._unMounted = true;
this._disposeVideoInputPreview();
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const {
classes,
hideAdditionalSettings,
hideVideoInputPreview,
localFlipX,
t
} = this.props;
return (
<div className = { classes.container }>
{ !hideVideoInputPreview
&& <VideoInputPreview
error = { this.state.previewVideoTrackError }
localFlipX = { localFlipX }
track = { this.state.previewVideoTrack } />
}
<div
aria-live = 'polite'>
{this._renderVideoSelector()}
</div>
{!hideAdditionalSettings && (
<>
<div className = { classes.checkboxContainer }>
<Checkbox
checked = { localFlipX }
label = { t('videothumbnail.mirrorVideo') }
// eslint-disable-next-line react/jsx-no-bind
onChange = { () => super._onChange({ localFlipX: !localFlipX }) } />
</div>
{this._renderFramerateSelect()}
</>
)}
</div>
);
}
/**
* Creates the JitsiTrack for the video input preview.
*
* @param {string} deviceId - The id of video device to preview.
* @private
* @returns {void}
*/
_createVideoInputTrack(deviceId: string) {
const { hideVideoInputPreview } = this.props;
if (hideVideoInputPreview) {
return;
}
return this._disposeVideoInputPreview()
.then(() => createLocalTrack('video', deviceId, 5000))
.then(jitsiLocalTrack => {
if (!jitsiLocalTrack) {
return Promise.reject();
}
if (this._unMounted) {
jitsiLocalTrack.dispose();
return;
}
this.setState({
previewVideoTrack: jitsiLocalTrack,
previewVideoTrackError: null
});
})
.catch(() => {
this.setState({
previewVideoTrack: null,
previewVideoTrackError:
this.props.t('deviceSelection.previewUnavailable')
});
});
}
/**
* Utility function for disposing the current video input preview.
*
* @private
* @returns {Promise}
*/
_disposeVideoInputPreview(): Promise<any> {
return this.state.previewVideoTrack
? this.state.previewVideoTrack.dispose() : Promise.resolve();
}
/**
* Creates a DeviceSelector instance based on the passed in configuration.
*
* @private
* @returns {ReactElement}
*/
_renderVideoSelector() {
const { availableDevices, hasVideoPermission } = this.props;
const videoConfig = {
devices: availableDevices.videoInput,
hasPermission: hasVideoPermission,
icon: 'icon-camera',
isDisabled: this.props.disableVideoInputSelect || this.props.disableDeviceChange,
key: 'videoInput',
id: 'videoInput',
label: 'settings.selectCamera',
onSelect: (selectedVideoInputId: string) => super._onChange({ selectedVideoInputId }),
selectedDeviceId: this.state.previewVideoTrack
? this.state.previewVideoTrack.getDeviceId() : this.props.selectedVideoInputId
};
return (
<DeviceSelector
{ ...videoConfig }
key = { videoConfig.id } />
);
}
/**
* Callback invoked to select a frame rate from the select dropdown.
*
* @param {Object} e - The key event to handle.
* @private
* @returns {void}
*/
_onFramerateItemSelect(e: React.ChangeEvent<HTMLSelectElement>) {
const frameRate = e.target.value;
super._onChange({ currentFramerate: frameRate });
}
/**
* Returns the React Element for the desktop share frame rate dropdown.
*
* @returns {JSX}
*/
_renderFramerateSelect() {
const { currentFramerate, desktopShareFramerates, t } = this.props;
const frameRateItems = desktopShareFramerates.map((frameRate: number) => {
return {
value: frameRate,
label: `${frameRate} ${t('settings.framesPerSecond')}`
};
});
return (
<Select
bottomLabel = { parseInt(currentFramerate, 10) > SS_DEFAULT_FRAME_RATE
? t('settings.desktopShareHighFpsWarning')
: t('settings.desktopShareWarning') }
label = { t('settings.desktopShareFramerate') }
onChange = { this._onFramerateItemSelect }
options = { frameRateItems }
value = { currentFramerate } />
);
}
}
const mapStateToProps = (state: IReduxState) => {
return {
availableDevices: state['features/base/devices'].availableDevices ?? {}
};
};
export default connect(mapStateToProps)(withStyles(styles)(translate(VideoDeviceSelection)));

View File

@ -1,58 +0,0 @@
/* @flow */
import React, { Component } from 'react';
import Video from '../../base/media/components/Video';
const VIDEO_ERROR_CLASS = 'video-preview-has-error';
/**
* The type of the React {@code Component} props of {@link VideoInputPreview}.
*/
type Props = {
/**
* An error message to display instead of a preview. Displaying an error
* will take priority over displaying a video preview.
*/
error: ?string,
/**
* The JitsiLocalTrack to display.
*/
track: Object
};
/**
* React component for displaying video. This component defers to lib-jitsi-meet
* logic for rendering the video.
*
* @augments Component
*/
class VideoInputPreview extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { error } = this.props;
const errorClass = error ? VIDEO_ERROR_CLASS : '';
const className = `video-input-preview ${errorClass}`;
return (
<div className = { className }>
<Video
className = 'video-input-preview-display flipVideoX'
playsinline = { true }
videoTrack = {{ jitsiTrack: this.props.track }} />
<div className = 'video-input-preview-error'>
{ error || '' }
</div>
</div>
);
}
}
export default VideoInputPreview;

View File

@ -0,0 +1,73 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import Video from '../../base/media/components/Video.web';
/**
* The type of the React {@code Component} props of {@link VideoInputPreview}.
*/
interface IProps {
/**
* An error message to display instead of a preview. Displaying an error
* will take priority over displaying a video preview.
*/
error: string | null;
/**
* Whether or not the local video is flipped.
*/
localFlipX: boolean;
/**
* The JitsiLocalTrack to display.
*/
track: Object;
}
const useStyles = makeStyles()(theme => {
return {
container: {
position: 'relative',
borderRadius: '3px',
overflow: 'hidden',
marginBottom: theme.spacing(4),
backgroundColor: theme.palette.uiBackground
},
video: {
height: 'auto',
width: '100%',
overflow: 'hidden'
},
errorText: {
color: theme.palette.text01,
left: 0,
position: 'absolute',
right: 0,
textAlign: 'center',
top: '50%'
}
};
});
const VideoInputPreview = ({ error, localFlipX, track }: IProps) => {
const { classes, cx } = useStyles();
return (
<div className = { classes.container }>
<Video
className = { cx(classes.video, localFlipX && 'flipVideoX') }
playsinline = { true }
videoTrack = {{ jitsiTrack: track }} />
{error && (
<div className = { classes.errorText }>
{error}
</div>
)}
</div>
);
};
export default VideoInputPreview;

Some files were not shown because too many files have changed in this diff Show More