Compare commits
17 Commits
update-con
...
jitihouse/
Author | SHA1 | Date |
---|---|---|
xenia | 4bd73cc368 | |
Boris Grozev | e12999d44f | |
Robert Pintilii | 8982f17ce1 | |
Robert Pintilii | c8f1690057 | |
Robert Pintilii | aa57309057 | |
damencho | fb81619fc5 | |
Hristo Terezov | 5a5656020b | |
Hristo Terezov | 0ff44a2f22 | |
Hristo Terezov | 4d04ea325e | |
Hristo Terezov | 42ce6dcc58 | |
Hristo Terezov | b033d0268a | |
Hristo Terezov | 4aea40d34f | |
Hristo Terezov | e5a170fb28 | |
Hristo Terezov | d1cf5578fc | |
Hristo Terezov | 4b29af6b5f | |
bgrozev | f3481576ff | |
bgrozev | 455a91a5c6 |
15
config.js
15
config.js
|
@ -48,7 +48,7 @@ var config = {
|
|||
// BOSH URL. FIXME: use XEP-0156 to discover it.
|
||||
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.
|
||||
//
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -94,3 +94,9 @@ $flagsImagePath: "../images/";
|
|||
@import 'notifications';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
/* Jeet crew BEGIN */
|
||||
|
||||
@import 'jiti';
|
||||
|
||||
/* Jeet crew END */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)',
|
||||
|
||||
|
|
|
@ -995,7 +995,7 @@
|
|||
"microphones": "Microphones",
|
||||
"moderator": "Moderator",
|
||||
"moderatorOptions": "Moderator options",
|
||||
"more": "More",
|
||||
"more": "General",
|
||||
"name": "Name",
|
||||
"noDevice": "None",
|
||||
"notifications": "Notifications",
|
||||
|
@ -1350,7 +1350,7 @@
|
|||
"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",
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -139,12 +139,6 @@ const VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
changeUserAvatar(id, avatarUrl) {
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(avatarUrl);
|
||||
}
|
||||
},
|
||||
|
||||
isLargeVideoVisible() {
|
||||
return this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE);
|
||||
},
|
||||
|
|
|
@ -72,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/v1586.0.0+df2c3096/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",
|
||||
|
@ -13416,8 +13416,8 @@
|
|||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1586.0.0+df2c3096/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-0VJRjO2RWgBDEt+HFvOBz25UMwyFivBnQruR0UmWcCmDNO4GziqhSwSTrDVYbk54rgbz5MJCQe8snbsxbPiZ/Q==",
|
||||
"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",
|
||||
|
@ -30308,8 +30308,8 @@
|
|||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1586.0.0+df2c3096/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-0VJRjO2RWgBDEt+HFvOBz25UMwyFivBnQruR0UmWcCmDNO4GziqhSwSTrDVYbk54rgbz5MJCQe8snbsxbPiZ/Q==",
|
||||
"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",
|
||||
|
|
|
@ -77,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/v1586.0.0+df2c3096/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",
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -39,6 +39,8 @@ 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';
|
||||
|
|
|
@ -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. */
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -146,6 +146,7 @@ interface IObject {
|
|||
}
|
||||
|
||||
export interface IDialogTab<P> {
|
||||
cancel?: Function;
|
||||
className?: string;
|
||||
component: ComponentType<any>;
|
||||
icon: Function;
|
||||
|
@ -214,7 +215,12 @@ const DialogWithTabs = ({
|
|||
}
|
||||
}, [ isMobile, userSelected, selectedTab ]);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
const onClose = useCallback((isCancel = true) => {
|
||||
if (isCancel) {
|
||||
tabs.forEach(({ cancel }) => {
|
||||
cancel && dispatch(cancel());
|
||||
});
|
||||
}
|
||||
dispatch(hideDialog());
|
||||
}, []);
|
||||
|
||||
|
@ -268,7 +274,7 @@ const DialogWithTabs = ({
|
|||
tabs.forEach(({ submit }, idx) => {
|
||||
submit?.(tabStates[idx]);
|
||||
});
|
||||
onClose();
|
||||
onClose(false);
|
||||
}, [ tabs, tabStates ]);
|
||||
|
||||
const selectedTabIndex = useMemo(() => {
|
||||
|
|
|
@ -7,25 +7,6 @@
|
|||
*/
|
||||
export const TOGGLE_E2EE = 'TOGGLE_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value whether everyone has E2EE enabled.
|
||||
*
|
||||
* {
|
||||
* type: SET_EVERYONE_ENABLED_E2EE,
|
||||
* everyoneEnabledE2EE: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_EVERYONE_ENABLED_E2EE = 'SET_EVERYONE_ENABLED_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value whether everyone supports E2EE.
|
||||
*
|
||||
* {
|
||||
* type: SET_EVERYONE_SUPPORT_E2EE
|
||||
* }
|
||||
*/
|
||||
export const SET_EVERYONE_SUPPORT_E2EE = 'SET_EVERYONE_SUPPORT_E2EE';
|
||||
|
||||
/**
|
||||
* The type of the action which signals to set new value E2EE maxMode.
|
||||
*
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import {
|
||||
PARTICIPANT_VERIFIED,
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
SET_MEDIA_ENCRYPTION_KEY,
|
||||
START_VERIFICATION,
|
||||
|
@ -20,38 +18,6 @@ export function toggleE2EE(enabled: boolean) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new value whether everyone has E2EE enabled.
|
||||
*
|
||||
* @param {boolean} everyoneEnabledE2EE - The new value.
|
||||
* @returns {{
|
||||
* type: SET_EVERYONE_ENABLED_E2EE,
|
||||
* everyoneEnabledE2EE: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setEveryoneEnabledE2EE(everyoneEnabledE2EE: boolean) {
|
||||
return {
|
||||
type: SET_EVERYONE_ENABLED_E2EE,
|
||||
everyoneEnabledE2EE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new value whether everyone support E2EE.
|
||||
*
|
||||
* @param {boolean} everyoneSupportE2EE - The new value.
|
||||
* @returns {{
|
||||
* type: SET_EVERYONE_SUPPORT_E2EE,
|
||||
* everyoneSupportE2EE: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setEveryoneSupportE2EE(everyoneSupportE2EE: boolean) {
|
||||
return {
|
||||
type: SET_EVERYONE_SUPPORT_E2EE,
|
||||
everyoneSupportE2EE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to set E2EE maxMode.
|
||||
*
|
||||
|
|
|
@ -27,6 +27,6 @@ export function _mapStateToProps(state: IReduxState) {
|
|||
|
||||
return {
|
||||
_e2eeLabels: e2ee.labels,
|
||||
_showLabel: state['features/e2ee'].everyoneEnabledE2EE
|
||||
_showLabel: state['features/base/participants'].numberOfParticipantsDisabledE2EE === 0
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getParticipantById, getParticipantCount } from '../base/participants/functions';
|
||||
import { getParticipantById, getParticipantCount, getParticipantCountWithFake } from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
|
||||
|
@ -19,17 +19,17 @@ import { MAX_MODE_LIMIT, MAX_MODE_THRESHOLD } from './constants';
|
|||
*/
|
||||
export function doesEveryoneSupportE2EE(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { everyoneSupportE2EE } = state['features/e2ee'];
|
||||
const { numberOfParticipantsNotSupportingE2EE } = state['features/base/participants'];
|
||||
const { e2eeSupported } = state['features/base/conference'];
|
||||
const participantCount = getParticipantCount(state);
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
|
||||
if (typeof everyoneSupportE2EE === 'undefined' && participantCount === 1) {
|
||||
if (participantCount === 1) {
|
||||
// This will happen if we are alone.
|
||||
|
||||
return e2eeSupported;
|
||||
}
|
||||
|
||||
return everyoneSupportE2EE;
|
||||
return numberOfParticipantsNotSupportingE2EE === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app/actionTypes';
|
||||
|
@ -6,13 +5,11 @@ import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
|||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { openDialog } from '../base/dialog/actions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT, PARTICIPANT_UPDATED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../base/participants/actionTypes';
|
||||
import { participantUpdated } from '../base/participants/actions';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getRemoteParticipants,
|
||||
isScreenShareParticipant
|
||||
} from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
@ -20,7 +17,7 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
|||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
|
||||
import { PARTICIPANT_VERIFIED, SET_MEDIA_ENCRYPTION_KEY, START_VERIFICATION, TOGGLE_E2EE } from './actionTypes';
|
||||
import { setE2EEMaxMode, setEveryoneEnabledE2EE, setEveryoneSupportE2EE, toggleE2EE } from './actions';
|
||||
import { setE2EEMaxMode, toggleE2EE } from './actions';
|
||||
import ParticipantVerificationDialog from './components/ParticipantVerificationDialog';
|
||||
import { E2EE_OFF_SOUND_ID, E2EE_ON_SOUND_ID, MAX_MODE } from './constants';
|
||||
import { isMaxModeReached, isMaxModeThresholdReached } from './functions';
|
||||
|
@ -58,137 +55,24 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
|||
|
||||
break;
|
||||
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { id, e2eeEnabled, e2eeSupported } = action.participant;
|
||||
const oldParticipant = getParticipantById(getState(), id);
|
||||
const result = next(action);
|
||||
|
||||
if (e2eeEnabled !== oldParticipant?.e2eeEnabled
|
||||
|| e2eeSupported !== oldParticipant?.e2eeSupported) {
|
||||
const state = getState();
|
||||
let newEveryoneSupportE2EE = true;
|
||||
let newEveryoneEnabledE2EE = true;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ key, p ] of getRemoteParticipants(state)) {
|
||||
if (!p.e2eeEnabled) {
|
||||
newEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
if (!p.e2eeSupported) {
|
||||
newEveryoneSupportE2EE = false;
|
||||
}
|
||||
|
||||
if (!newEveryoneEnabledE2EE && !newEveryoneSupportE2EE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getLocalParticipant(state)?.e2eeEnabled) {
|
||||
newEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
dispatch(setEveryoneEnabledE2EE(newEveryoneEnabledE2EE));
|
||||
dispatch(setEveryoneSupportE2EE(newEveryoneSupportE2EE));
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_JOINED: {
|
||||
const result = next(action);
|
||||
const { e2eeEnabled, e2eeSupported, local } = action.participant;
|
||||
const { everyoneEnabledE2EE } = getState()['features/e2ee'];
|
||||
const participantCount = getParticipantCount(getState);
|
||||
|
||||
if (isScreenShareParticipant(action.participant)) {
|
||||
return result;
|
||||
if (!isScreenShareParticipant(action.participant) && !action.participant.local) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
// the initial values
|
||||
if (participantCount === 1) {
|
||||
batch(() => {
|
||||
dispatch(setEveryoneEnabledE2EE(e2eeEnabled));
|
||||
dispatch(setEveryoneSupportE2EE(e2eeSupported));
|
||||
});
|
||||
}
|
||||
|
||||
// if all had it enabled and this one disabled it, change value in store
|
||||
// otherwise there is no change in the value we store
|
||||
if (everyoneEnabledE2EE && !e2eeEnabled) {
|
||||
dispatch(setEveryoneEnabledE2EE(false));
|
||||
}
|
||||
|
||||
if (local) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const { everyoneSupportE2EE } = getState()['features/e2ee'];
|
||||
|
||||
// if all supported it and this one does not, change value in store
|
||||
// otherwise there is no change in the value we store
|
||||
if (everyoneSupportE2EE && !e2eeSupported) {
|
||||
dispatch(setEveryoneSupportE2EE(false));
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
const previosState = getState();
|
||||
const participant = getParticipantById(previosState, action.participant?.id);
|
||||
const participant = getParticipantById(getState(), action.participant?.id);
|
||||
const result = next(action);
|
||||
const newState = getState();
|
||||
const { e2eeEnabled = false, e2eeSupported = false } = participant ?? {};
|
||||
|
||||
if (isScreenShareParticipant(participant)) {
|
||||
return result;
|
||||
if (!isScreenShareParticipant(participant)) {
|
||||
_updateMaxMode(dispatch, getState);
|
||||
}
|
||||
|
||||
const { everyoneEnabledE2EE, everyoneSupportE2EE } = newState['features/e2ee'];
|
||||
|
||||
|
||||
// if it was not enabled by everyone, and the participant leaving had it disabled, or if it was not supported
|
||||
// by everyone, and the participant leaving had it not supported let's check is it enabled for all that stay
|
||||
if ((!everyoneEnabledE2EE && !e2eeEnabled) || (!everyoneSupportE2EE && !e2eeSupported)) {
|
||||
let latestEveryoneEnabledE2EE = true;
|
||||
let latestEveryoneSupportE2EE = true;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [ key, p ] of getRemoteParticipants(newState)) {
|
||||
if (!p.e2eeEnabled) {
|
||||
latestEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
if (!p.e2eeSupported) {
|
||||
latestEveryoneSupportE2EE = false;
|
||||
}
|
||||
|
||||
if (!latestEveryoneEnabledE2EE && !latestEveryoneSupportE2EE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!getLocalParticipant(newState)?.e2eeEnabled) {
|
||||
latestEveryoneEnabledE2EE = false;
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
if (!everyoneEnabledE2EE && latestEveryoneEnabledE2EE) {
|
||||
dispatch(setEveryoneEnabledE2EE(true));
|
||||
}
|
||||
|
||||
if (!everyoneSupportE2EE && latestEveryoneSupportE2EE) {
|
||||
dispatch(setEveryoneSupportE2EE(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateMaxMode(dispatch, getState);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -314,12 +198,23 @@ function _updateMaxMode(dispatch: IStore['dispatch'], getState: IStore['getState
|
|||
return;
|
||||
}
|
||||
|
||||
if (isMaxModeThresholdReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.THRESHOLD_EXCEEDED));
|
||||
dispatch(toggleE2EE(false));
|
||||
const { maxMode, enabled } = state['features/e2ee'];
|
||||
const isMaxModeThresholdReachedValue = isMaxModeThresholdReached(state);
|
||||
let newMaxMode: string;
|
||||
|
||||
if (isMaxModeThresholdReachedValue) {
|
||||
newMaxMode = MAX_MODE.THRESHOLD_EXCEEDED;
|
||||
} else if (isMaxModeReached(state)) {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.ENABLED));
|
||||
newMaxMode = MAX_MODE.ENABLED;
|
||||
} else {
|
||||
dispatch(setE2EEMaxMode(MAX_MODE.DISABLED));
|
||||
newMaxMode = MAX_MODE.DISABLED;
|
||||
}
|
||||
|
||||
if (maxMode !== newMaxMode) {
|
||||
dispatch(setE2EEMaxMode(newMaxMode));
|
||||
}
|
||||
|
||||
if (isMaxModeThresholdReachedValue && !enabled) {
|
||||
dispatch(toggleE2EE(false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
SET_EVERYONE_ENABLED_E2EE,
|
||||
SET_EVERYONE_SUPPORT_E2EE,
|
||||
SET_MAX_MODE,
|
||||
TOGGLE_E2EE
|
||||
} from './actionTypes';
|
||||
|
@ -15,8 +13,6 @@ const DEFAULT_STATE = {
|
|||
|
||||
export interface IE2EEState {
|
||||
enabled: boolean;
|
||||
everyoneEnabledE2EE?: boolean;
|
||||
everyoneSupportE2EE?: boolean;
|
||||
maxMode: string;
|
||||
}
|
||||
|
||||
|
@ -34,16 +30,6 @@ ReducerRegistry.register<IE2EEState>('features/e2ee', (state = DEFAULT_STATE, ac
|
|||
...state,
|
||||
enabled: action.enabled
|
||||
};
|
||||
case SET_EVERYONE_ENABLED_E2EE:
|
||||
return {
|
||||
...state,
|
||||
everyoneEnabledE2EE: action.everyoneEnabledE2EE
|
||||
};
|
||||
case SET_EVERYONE_SUPPORT_E2EE:
|
||||
return {
|
||||
...state,
|
||||
everyoneSupportE2EE: action.everyoneSupportE2EE
|
||||
};
|
||||
|
||||
case SET_MAX_MODE: {
|
||||
return {
|
||||
|
|
|
@ -1,23 +1,74 @@
|
|||
// @flow
|
||||
|
||||
import StarIcon from '@atlaskit/icon/glyph/star';
|
||||
import StarFilledIcon from '@atlaskit/icon/glyph/star-filled';
|
||||
import { Theme } from '@mui/material';
|
||||
import { ClassNameMap, withStyles } from '@mui/styles';
|
||||
import React, { Component } from 'react';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
createFeedbackOpenEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { createFeedbackOpenEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { IJitsiConference } from '../../base/conference/reducer';
|
||||
import { isMobileBrowser } from '../../base/environment/utils';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconFavorite, IconFavoriteSolid } from '../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import Input from '../../base/ui/components/web/Input';
|
||||
import { cancelFeedback, submitFeedback } from '../actions';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
dialog: {
|
||||
marginBottom: theme.spacing(1)
|
||||
},
|
||||
|
||||
rating: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: theme.spacing(4),
|
||||
marginBottom: theme.spacing(3)
|
||||
},
|
||||
|
||||
ratingLabel: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text01,
|
||||
marginBottom: theme.spacing(2),
|
||||
height: '20px'
|
||||
},
|
||||
|
||||
stars: {
|
||||
display: 'flex'
|
||||
},
|
||||
|
||||
starBtn: {
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'&:last-of-type': {
|
||||
marginRight: 0
|
||||
},
|
||||
|
||||
'&.active svg': {
|
||||
fill: theme.palette.success01
|
||||
},
|
||||
|
||||
'&:focus': {
|
||||
outline: `1px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
|
||||
details: {
|
||||
'& textarea': {
|
||||
minHeight: '122px'
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const scoreAnimationClass
|
||||
= interfaceConfig.ENABLE_FEEDBACK_ANIMATION ? 'shake-rotate' : '';
|
||||
|
@ -34,49 +85,51 @@ const SCORES = [
|
|||
'feedback.veryGood'
|
||||
];
|
||||
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
type Scrollable = {
|
||||
scroll: Function
|
||||
}
|
||||
scroll: Function;
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link FeedbackDialog}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* The cached feedback message, if any, that was set when closing a previous
|
||||
* instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_message: string,
|
||||
_message: string;
|
||||
|
||||
/**
|
||||
* The cached feedback score, if any, that was set when closing a previous
|
||||
* instance of {@code FeedbackDialog}.
|
||||
*/
|
||||
_score: number,
|
||||
_score: number;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes: ClassNameMap<string>;
|
||||
|
||||
/**
|
||||
* The JitsiConference that is being rated. The conference is passed in
|
||||
* because feedback can occur after a conference has been left, so
|
||||
* references to it may no longer exist in redux.
|
||||
*/
|
||||
conference: Object,
|
||||
conference: IJitsiConference;
|
||||
|
||||
/**
|
||||
* Invoked to signal feedback submission or canceling.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Callback invoked when {@code FeedbackDialog} is unmounted.
|
||||
*/
|
||||
onClose: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
onClose: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link FeedbackDialog}.
|
||||
|
@ -86,20 +139,20 @@ type State = {
|
|||
/**
|
||||
* The currently entered feedback message.
|
||||
*/
|
||||
message: string,
|
||||
message: string;
|
||||
|
||||
/**
|
||||
* The score selection index which is currently being hovered. The value -1
|
||||
* is used as a sentinel value to match store behavior of using -1 for no
|
||||
* score having been selected.
|
||||
*/
|
||||
mousedOverScore: number,
|
||||
mousedOverScore: number;
|
||||
|
||||
/**
|
||||
* The currently selected score selection index. The score will not be 0
|
||||
* indexed so subtract one to map with SCORES.
|
||||
*/
|
||||
score: number
|
||||
score: number;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -109,13 +162,19 @@ type State = {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class FeedbackDialog extends Component<Props, State> {
|
||||
class FeedbackDialog extends Component<IProps, State> {
|
||||
/**
|
||||
* An array of objects with click handlers for each of the scores listed in
|
||||
* the constant SCORES. This pattern is used for binding event handlers only
|
||||
* once for each score selection icon.
|
||||
*/
|
||||
_scoreClickConfigurations: Array<Object>;
|
||||
_scoreClickConfigurations: Array<{
|
||||
_onClick: (e: React.MouseEvent) => void;
|
||||
_onKeyDown: (e: React.KeyboardEvent) => void;
|
||||
_onMouseOver: (e: React.MouseEvent) => void;
|
||||
}>;
|
||||
|
||||
_onScrollTop: (node: Scrollable | null) => void;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code FeedbackDialog} instance.
|
||||
|
@ -123,7 +182,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @param {Object} props - The read-only React {@code Component} props with
|
||||
* which the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const { _message, _score } = this.props;
|
||||
|
@ -157,8 +216,9 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
this._scoreClickConfigurations = SCORES.map((textKey, index) => {
|
||||
return {
|
||||
_onClick: () => this._onScoreSelect(index),
|
||||
_onKeyPres: e => {
|
||||
_onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this._onScoreSelect(index);
|
||||
}
|
||||
|
@ -176,8 +236,8 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
|
||||
// On some mobile browsers opening Feedback dialog scrolls down the whole content because of the keyboard.
|
||||
// By scrolling to the top we prevent hiding the feedback stars so the user knows those exist.
|
||||
this._onScrollTop = (node: ?Scrollable) => {
|
||||
node && node.scroll && node.scroll(0, 0);
|
||||
this._onScrollTop = (node: Scrollable | null) => {
|
||||
node?.scroll?.(0, 0);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -215,14 +275,14 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
const scoreToDisplayAsSelected
|
||||
= mousedOverScore > -1 ? mousedOverScore : score;
|
||||
|
||||
const { t } = this.props;
|
||||
const { classes, t } = this.props;
|
||||
|
||||
const scoreIcons = this._scoreClickConfigurations.map(
|
||||
(config, index) => {
|
||||
const isFilled = index <= scoreToDisplayAsSelected;
|
||||
const activeClass = isFilled ? 'active' : '';
|
||||
const className
|
||||
= `star-btn ${scoreAnimationClass} ${activeClass}`;
|
||||
= `${classes.starBtn} ${scoreAnimationClass} ${activeClass}`;
|
||||
|
||||
return (
|
||||
<span
|
||||
|
@ -230,19 +290,19 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
className = { className }
|
||||
key = { index }
|
||||
onClick = { config._onClick }
|
||||
onKeyPress = { config._onKeyPres }
|
||||
onKeyDown = { config._onKeyDown }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }
|
||||
{ ...(isMobileBrowser() ? {} : {
|
||||
onMouseOver: config._onMouseOver
|
||||
}) }>
|
||||
{ isFilled
|
||||
? <StarFilledIcon
|
||||
label = 'star-filled'
|
||||
size = 'xlarge' />
|
||||
: <StarIcon
|
||||
label = 'star'
|
||||
size = 'xlarge' /> }
|
||||
? <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavoriteSolid } />
|
||||
: <Icon
|
||||
size = { ICON_SIZE }
|
||||
src = { IconFavorite } /> }
|
||||
</span>
|
||||
);
|
||||
});
|
||||
|
@ -255,23 +315,24 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
}}
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
size = 'large'
|
||||
titleKey = 'feedback.rateExperience'>
|
||||
<div className = 'feedback-dialog'>
|
||||
<div className = 'rating'>
|
||||
<div className = { classes.dialog }>
|
||||
<div className = { classes.rating }>
|
||||
<div
|
||||
aria-label = { this.props.t('feedback.star') }
|
||||
className = 'star-label' >
|
||||
className = { classes.ratingLabel } >
|
||||
<p id = 'starLabel'>
|
||||
{ t(SCORES[scoreToDisplayAsSelected]) }
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className = 'stars'
|
||||
className = { classes.stars }
|
||||
onMouseLeave = { this._onScoreContainerMouseLeave }>
|
||||
{ scoreIcons }
|
||||
</div>
|
||||
</div>
|
||||
<div className = 'details'>
|
||||
<div className = { classes.details }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'feedbackTextArea'
|
||||
|
@ -285,8 +346,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
_onCancel: () => boolean;
|
||||
|
||||
/**
|
||||
* Dispatches an action notifying feedback was not submitted. The submitted
|
||||
* score will have one added as the rest of the app does not expect 0
|
||||
|
@ -304,8 +363,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
return true;
|
||||
}
|
||||
|
||||
_onMessageChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the known entered feedback message.
|
||||
*
|
||||
|
@ -314,7 +371,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessageChange(newValue) {
|
||||
_onMessageChange(newValue: string) {
|
||||
this.setState({ message: newValue });
|
||||
}
|
||||
|
||||
|
@ -325,12 +382,10 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreSelect(score) {
|
||||
_onScoreSelect(score: number) {
|
||||
this.setState({ score });
|
||||
}
|
||||
|
||||
_onScoreContainerMouseLeave: () => void;
|
||||
|
||||
/**
|
||||
* Sets the currently hovered score to null to indicate no hover is
|
||||
* occurring.
|
||||
|
@ -350,12 +405,10 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onScoreMouseOver(mousedOverScore) {
|
||||
_onScoreMouseOver(mousedOverScore: number) {
|
||||
this.setState({ mousedOverScore });
|
||||
}
|
||||
|
||||
_onSubmit: () => void;
|
||||
|
||||
/**
|
||||
* Dispatches the entered feedback for submission. The submitted score will
|
||||
* have one added as the rest of the app does not expect 0 indexing.
|
||||
|
@ -373,8 +426,6 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
|
||||
return true;
|
||||
}
|
||||
|
||||
_onScrollTop: (node: ?Scrollable) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -386,7 +437,7 @@ class FeedbackDialog extends Component<Props, State> {
|
|||
* @returns {{
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { message, score } = state['features/feedback'];
|
||||
|
||||
return {
|
||||
|
@ -407,4 +458,4 @@ function _mapStateToProps(state) {
|
|||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(FeedbackDialog));
|
||||
export default withStyles(styles)(translate(connect(_mapStateToProps)(FeedbackDialog)));
|
|
@ -13,7 +13,6 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
|||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { connect } from '../../../base/redux/functions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
|
@ -113,7 +112,7 @@ interface IProps extends WithTranslation {
|
|||
/**
|
||||
* The local screen share participant. This prop is behind the sourceNameSignaling feature flag.
|
||||
*/
|
||||
_localScreenShare: IParticipant;
|
||||
_localScreenShareId: string | undefined;
|
||||
|
||||
/**
|
||||
* Whether or not the filmstrip videos should currently be displayed.
|
||||
|
@ -333,7 +332,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
const {
|
||||
_currentLayout,
|
||||
_disableSelfView,
|
||||
_localScreenShare,
|
||||
_localScreenShareId,
|
||||
_mainFilmstripVisible,
|
||||
_resizableFilmstrip,
|
||||
_topPanelFilmstrip,
|
||||
|
@ -408,7 +407,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
}
|
||||
</div>
|
||||
)}
|
||||
{_localScreenShare && !_disableSelfView && !_verticalViewGrid && (
|
||||
{_localScreenShareId && !_disableSelfView && !_verticalViewGrid && (
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalScreenShare'>
|
||||
|
@ -416,7 +415,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
|||
{
|
||||
!tileViewActive && filmstripType === FILMSTRIP_TYPE.MAIN && <Thumbnail
|
||||
key = 'localScreenShare'
|
||||
participantID = { _localScreenShare.id } />
|
||||
participantID = { _localScreenShareId } />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -919,7 +918,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
|||
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
|
||||
_isToolboxVisible: isToolboxVisible(state),
|
||||
_isVerticalFilmstrip,
|
||||
_localScreenShare: localScreenShare,
|
||||
_localScreenShareId: localScreenShare?.id,
|
||||
_mainFilmstripVisible: visible,
|
||||
_maxFilmstripWidth: clientWidth - MIN_STAGE_VIEW_WIDTH,
|
||||
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
import { openDialog } from '../base/dialog/actions';
|
||||
import i18next from '../base/i18n/i18next';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { toggleBackgroundEffect } from '../virtual-background/actions';
|
||||
import virtualBackgroundLogger from '../virtual-background/logger';
|
||||
|
||||
import {
|
||||
SET_AUDIO_SETTINGS_VISIBILITY,
|
||||
|
@ -24,7 +26,8 @@ import {
|
|||
getMoreTabProps,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getShortcutsTabProps
|
||||
getShortcutsTabProps,
|
||||
getVirtualBackgroundTabProps
|
||||
} from './functions';
|
||||
|
||||
/**
|
||||
|
@ -249,3 +252,31 @@ export function submitShortcutsTab(newState: any) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the settings from the "Virtual Background" tab of the settings dialog.
|
||||
*
|
||||
* @param {Object} newState - The new settings.
|
||||
* @param {boolean} isCancel - Whether the change represents a cancel.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function submitVirtualBackgroundTab(newState: any, isCancel = false) {
|
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const currentState = getVirtualBackgroundTabProps(getState());
|
||||
|
||||
if (newState.options?.selectedThumbnail) {
|
||||
await dispatch(toggleBackgroundEffect(newState.options, currentState._jitsiTrack));
|
||||
|
||||
if (!isCancel) {
|
||||
// Set x scale to default value.
|
||||
dispatch(updateSettings({
|
||||
localFlipX: true
|
||||
}));
|
||||
|
||||
virtualBackgroundLogger.info(`Virtual background type: '${
|
||||
typeof newState.options.backgroundType === 'undefined'
|
||||
? 'none' : newState.options.backgroundType}' applied!`);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
|
@ -12,7 +15,12 @@ import { MAX_ACTIVE_PARTICIPANTS } from '../../../filmstrip/constants';
|
|||
/**
|
||||
* The type of the React {@code Component} props of {@link MoreTab}.
|
||||
*/
|
||||
export type Props = AbstractDialogTabProps & WithTranslation & {
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Whether or not follow me is currently active (enabled by some other participant).
|
||||
|
@ -43,11 +51,23 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
* Wether or not the stage filmstrip is enabled.
|
||||
*/
|
||||
stageFilmstripEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
|
||||
divider: {
|
||||
margin: `${theme.spacing(4)} 0`,
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
border: 0,
|
||||
backgroundColor: theme.palette.ui03
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -55,14 +75,14 @@ export type Props = AbstractDialogTabProps & WithTranslation & {
|
|||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class MoreTab extends AbstractDialogTab<Props, any> {
|
||||
class MoreTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code MoreTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
|
@ -78,16 +98,17 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const content = [];
|
||||
|
||||
content.push(this._renderSettingsLeft());
|
||||
content.push(this._renderSettingsRight());
|
||||
const { showPrejoinSettings, classes } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'more-tab box'
|
||||
className = { clsx('more-tab', classes.container) }
|
||||
key = 'more'>
|
||||
{ content }
|
||||
{showPrejoinSettings && <>
|
||||
{this._renderPrejoinScreenSettings()}
|
||||
<hr className = { classes.divider } />
|
||||
</>}
|
||||
{this._renderMaxStageParticipantsSelect()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -127,18 +148,11 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
const { t, showPrejoinPage } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'prejoin-screen'>
|
||||
<span className = 'checkbox-label'>
|
||||
{ t('prejoin.premeeting') }
|
||||
</span>
|
||||
<Checkbox
|
||||
checked = { showPrejoinPage }
|
||||
label = { t('prejoin.showScreen') }
|
||||
name = 'show-prejoin-page'
|
||||
onChange = { this._onShowPrejoinPageChanged } />
|
||||
</div>
|
||||
<Checkbox
|
||||
checked = { showPrejoinPage }
|
||||
label = { t('prejoin.showScreen') }
|
||||
name = 'show-prejoin-page'
|
||||
onChange = { this._onShowPrejoinPageChanged } />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -162,52 +176,13 @@ class MoreTab extends AbstractDialogTab<Props, any> {
|
|||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane-element'
|
||||
key = 'maxStageParticipants'>
|
||||
<div className = 'dropdown-menu'>
|
||||
<Select
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
value = { maxStageParticipants } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React element that needs to be displayed on the right half of the more tabs.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSettingsRight() {
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane right'
|
||||
key = 'settings-sub-pane-right'>
|
||||
{ this._renderMaxStageParticipantsSelect() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the React element that needs to be displayed on the left half of the more tabs.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderSettingsLeft() {
|
||||
const { showPrejoinSettings } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'settings-sub-pane left'
|
||||
key = 'settings-sub-pane-left'>
|
||||
{ showPrejoinSettings && this._renderPrejoinScreenSettings() }
|
||||
</div>
|
||||
<Select
|
||||
label = { t('settings.maxStageParticipants') }
|
||||
onChange = { this._onMaxStageParticipantsSelect }
|
||||
options = { maxParticipantsItems }
|
||||
value = { maxStageParticipants } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(MoreTab);
|
||||
export default withStyles(styles)(translate(MoreTab));
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
IconCalendar,
|
||||
IconGear,
|
||||
IconHost,
|
||||
IconImage,
|
||||
IconShortcuts,
|
||||
IconUser,
|
||||
IconVideo,
|
||||
|
@ -24,12 +25,14 @@ import {
|
|||
getAudioDeviceSelectionDialogProps,
|
||||
getVideoDeviceSelectionDialogProps
|
||||
} from '../../../device-selection/functions.web';
|
||||
import { checkBlurSupport } from '../../../virtual-background/functions';
|
||||
import {
|
||||
submitModeratorTab,
|
||||
submitMoreTab,
|
||||
submitNotificationsTab,
|
||||
submitProfileTab,
|
||||
submitShortcutsTab
|
||||
submitShortcutsTab,
|
||||
submitVirtualBackgroundTab
|
||||
} from '../../actions';
|
||||
import { SETTINGS_TABS } from '../../constants';
|
||||
import {
|
||||
|
@ -38,7 +41,8 @@ import {
|
|||
getNotificationsMap,
|
||||
getNotificationsTabProps,
|
||||
getProfileTabProps,
|
||||
getShortcutsTabProps
|
||||
getShortcutsTabProps,
|
||||
getVirtualBackgroundTabProps
|
||||
} from '../../functions';
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -48,6 +52,7 @@ import MoreTab from './MoreTab';
|
|||
import NotificationsTab from './NotificationsTab';
|
||||
import ProfileTab from './ProfileTab';
|
||||
import ShortcutsTab from './ShortcutsTab';
|
||||
import VirtualBackgroundTab from './VirtualBackgroundTab';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of
|
||||
|
@ -254,6 +259,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
const showSoundsSettings = configuredTabs.includes('sounds');
|
||||
const enabledNotifications = getNotificationsMap(state);
|
||||
const showNotificationsSettings = Object.keys(enabledNotifications).length > 0;
|
||||
const virtualBackgroundSupported = checkBlurSupport();
|
||||
const tabs: IDialogTab<any>[] = [];
|
||||
|
||||
if (showDeviceSettings) {
|
||||
|
@ -305,12 +311,37 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
});
|
||||
}
|
||||
|
||||
if (virtualBackgroundSupported) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.VIRTUAL_BACKGROUND,
|
||||
component: VirtualBackgroundTab,
|
||||
labelKey: 'virtualBackground.title',
|
||||
props: getVirtualBackgroundTabProps(state),
|
||||
className: `settings-pane ${classes.settingsDialog}`,
|
||||
submit: (newState: any) => submitVirtualBackgroundTab(newState),
|
||||
cancel: () => {
|
||||
const { _virtualBackground } = getVirtualBackgroundTabProps(state);
|
||||
|
||||
return submitVirtualBackgroundTab({
|
||||
options: {
|
||||
backgroundType: _virtualBackground.backgroundType,
|
||||
enabled: _virtualBackground.backgroundEffectEnabled,
|
||||
url: _virtualBackground.virtualSource,
|
||||
selectedThumbnail: _virtualBackground.selectedThumbnail,
|
||||
blurValue: _virtualBackground.blurValue
|
||||
}
|
||||
}, true);
|
||||
},
|
||||
icon: IconImage
|
||||
});
|
||||
}
|
||||
|
||||
if (showSoundsSettings || showNotificationsSettings) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.NOTIFICATIONS,
|
||||
component: NotificationsTab,
|
||||
labelKey: 'settings.notifications',
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getNotificationsTabProps>) => {
|
||||
return {
|
||||
...newProps,
|
||||
enabledNotifications: tabState?.enabledNotifications || {}
|
||||
|
@ -373,7 +404,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
component: ShortcutsTab,
|
||||
labelKey: 'settings.shortcuts',
|
||||
props: getShortcutsTabProps(state, isDisplayedOnWelcomePage),
|
||||
propsUpdateFunction: (tabState: any, newProps: any) => {
|
||||
propsUpdateFunction: (tabState: any, newProps: ReturnType<typeof getShortcutsTabProps>) => {
|
||||
// Updates tab props, keeping users selection
|
||||
|
||||
return {
|
||||
|
@ -389,8 +420,6 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
|||
if (showMoreTab) {
|
||||
tabs.push({
|
||||
name: SETTINGS_TABS.MORE,
|
||||
|
||||
// @ts-ignore
|
||||
component: MoreTab,
|
||||
labelKey: 'settings.more',
|
||||
props: moreTabProps,
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import AbstractDialogTab, {
|
||||
IProps as AbstractDialogTabProps
|
||||
} from '../../../base/dialog/components/web/AbstractDialogTab';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import VirtualBackgrounds from '../../../virtual-background/components/VirtualBackgrounds';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VirtualBackgroundTab}.
|
||||
*/
|
||||
export interface IProps extends AbstractDialogTabProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Returns the jitsi track that will have background effect applied.
|
||||
*/
|
||||
_jitsiTrack: Object;
|
||||
|
||||
/**
|
||||
* CSS classes object.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* Virtual background options.
|
||||
*/
|
||||
options: any;
|
||||
|
||||
/**
|
||||
* The selected thumbnail identifier.
|
||||
*/
|
||||
selectedThumbnail: string;
|
||||
}
|
||||
|
||||
const styles = () => {
|
||||
return {
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for modifying language and moderator settings.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class VirtualBackgroundTab extends AbstractDialogTab<IProps, any> {
|
||||
/**
|
||||
* Initializes a new {@code ModeratorTab} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onOptionsChanged = this._onOptionsChanged.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked to select if follow-me mode
|
||||
* should be activated.
|
||||
*
|
||||
* @param {Object} options - The new background options.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOptionsChanged(options: any) {
|
||||
super._onChange({ options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
classes,
|
||||
options,
|
||||
selectedThumbnail,
|
||||
_jitsiTrack
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'virtual-background-dialog'
|
||||
key = 'virtual-background'>
|
||||
<VirtualBackgrounds
|
||||
_jitsiTrack = { _jitsiTrack }
|
||||
onOptionsChange = { this._onOptionsChanged }
|
||||
options = { options }
|
||||
selectedThumbnail = { selectedThumbnail } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(translate(VirtualBackgroundTab));
|
|
@ -3,7 +3,6 @@ import { WithTranslation } from 'react-i18next';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import { openDialog } from '../../../../base/dialog/actions';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { IconImage } from '../../../../base/icons/svg';
|
||||
import Video from '../../../../base/media/components/Video.web';
|
||||
|
@ -13,7 +12,8 @@ import Checkbox from '../../../../base/ui/components/web/Checkbox';
|
|||
import ContextMenu from '../../../../base/ui/components/web/ContextMenu';
|
||||
import ContextMenuItem from '../../../../base/ui/components/web/ContextMenuItem';
|
||||
import ContextMenuItemGroup from '../../../../base/ui/components/web/ContextMenuItemGroup';
|
||||
import VirtualBackgroundDialog from '../../../../virtual-background/components/VirtualBackgroundDialog';
|
||||
import { openSettingsDialog } from '../../../actions';
|
||||
import { SETTINGS_TABS } from '../../../constants';
|
||||
import { createLocalVideoTracks } from '../../../functions.web';
|
||||
|
||||
const videoClassName = 'video-preview-video flipVideoX';
|
||||
|
@ -297,7 +297,7 @@ const mapStateToProps = (state: IReduxState) => {
|
|||
|
||||
const mapDispatchToProps = (dispatch: IStore['dispatch']) => {
|
||||
return {
|
||||
selectBackground: () => dispatch(openDialog(VirtualBackgroundDialog)),
|
||||
selectBackground: () => dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND)),
|
||||
changeFlip: (flip: boolean) => {
|
||||
dispatch(updateSettings({
|
||||
localFlipX: flip
|
||||
|
|
|
@ -6,7 +6,8 @@ export const SETTINGS_TABS = {
|
|||
NOTIFICATIONS: 'notifications_tab',
|
||||
PROFILE: 'profile_tab',
|
||||
SHORTCUTS: 'shortcuts_tab',
|
||||
VIDEO: 'video_tab'
|
||||
VIDEO: 'video_tab',
|
||||
VIRTUAL_BACKGROUND: 'virtual-background_tab'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { getHideSelfView } from '../base/settings/functions';
|
||||
import { getLocalVideoTrack } from '../base/tracks/functions.any';
|
||||
import { parseStandardURIString } from '../base/util/uri';
|
||||
import { isStageFilmstripEnabled } from '../filmstrip/functions';
|
||||
import { isFollowMeActive } from '../follow-me/functions';
|
||||
|
@ -293,3 +294,22 @@ export function getShortcutsTabProps(stateful: IStateful, isDisplayedOnWelcomePa
|
|||
keyboardShortcutsEnabled: keyboardShortcut.getEnabled()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the properties for the "Virtual Background" tab from settings dialog from Redux
|
||||
* state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful -The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @returns {Object} - The properties for the "Shortcuts" tab from settings
|
||||
* dialog.
|
||||
*/
|
||||
export function getVirtualBackgroundTabProps(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
return {
|
||||
_virtualBackground: state['features/virtual-background'],
|
||||
selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
|
||||
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { batch } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
|
@ -49,11 +50,14 @@ MiddlewareRegistry.register(({ dispatch, getState }: IStore) => (next: Function)
|
|||
const stats = filterBySearchCriteria(state, speakerStats);
|
||||
const pendingReorder = getPendingReorder(state);
|
||||
|
||||
if (pendingReorder) {
|
||||
dispatch(updateSortedSpeakerStatsIds(getSortedSpeakerStatsIds(state, stats) ?? []));
|
||||
}
|
||||
batch(() => {
|
||||
if (pendingReorder) {
|
||||
dispatch(updateSortedSpeakerStatsIds(getSortedSpeakerStatsIds(state, stats) ?? []));
|
||||
}
|
||||
|
||||
dispatch(updateStats(stats));
|
||||
});
|
||||
|
||||
dispatch(updateStats(stats));
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -69,8 +73,11 @@ MiddlewareRegistry.register(({ dispatch, getState }: IStore) => (next: Function)
|
|||
case PARTICIPANT_LEFT:
|
||||
case PARTICIPANT_KICKED:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
dispatch(initReorderStats());
|
||||
const { pendingReorder } = getState()['features/speaker-stats'];
|
||||
|
||||
if (!pendingReorder) {
|
||||
dispatch(initReorderStats());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { batch } from 'react-redux';
|
||||
|
||||
import {
|
||||
HIDDEN_PARTICIPANT_JOINED,
|
||||
HIDDEN_PARTICIPANT_LEFT,
|
||||
|
@ -64,8 +66,10 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
if (potentialTranscriberJIDs.includes(participant.id)
|
||||
&& participant.name === TRANSCRIBER_DISPLAY_NAME) {
|
||||
store.dispatch(transcriberJoined(participant.id));
|
||||
store.dispatch(hidePendingTranscribingNotification());
|
||||
batch(() => {
|
||||
store.dispatch(transcriberJoined(participant.id));
|
||||
store.dispatch(hidePendingTranscribingNotification());
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconPlus } from '../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
import { type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
|
||||
import { resizeImage } from '../functions';
|
||||
import logger from '../logger';
|
||||
|
@ -40,24 +41,25 @@ interface IProps extends WithTranslation {
|
|||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
label: {
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.link01,
|
||||
marginBottom: theme.spacing(3),
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
||||
addBackground: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginRight: theme.spacing(3),
|
||||
|
||||
'& svg': {
|
||||
fill: '#669aec !important'
|
||||
fill: `${theme.palette.link01} !important`
|
||||
}
|
||||
},
|
||||
button: {
|
||||
|
||||
input: {
|
||||
display: 'none'
|
||||
},
|
||||
label: {
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
lineHeight: '20px',
|
||||
marginTop: theme.spacing(3),
|
||||
marginBottom: theme.spacing(2),
|
||||
color: '#669aec',
|
||||
display: 'inline-flex',
|
||||
cursor: 'pointer'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -127,14 +129,14 @@ function UploadImageButton({
|
|||
tabIndex = { 0 } >
|
||||
<Icon
|
||||
className = { classes.addBackground }
|
||||
size = { 20 }
|
||||
size = { 24 }
|
||||
src = { IconPlus } />
|
||||
{t('virtualBackground.addBackground')}
|
||||
</label>}
|
||||
|
||||
<input
|
||||
accept = 'image/*'
|
||||
className = { classes.button }
|
||||
className = { classes.input }
|
||||
id = 'file-upload'
|
||||
onChange = { uploadImage }
|
||||
ref = { uploadImageButton }
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
// @flow
|
||||
|
||||
import { openDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import { IconImage } from '../../base/icons';
|
||||
import { connect } from '../../base/redux';
|
||||
import { AbstractButton } from '../../base/toolbox/components';
|
||||
import type { AbstractButtonProps } from '../../base/toolbox/components';
|
||||
import { openSettingsDialog } from '../../settings/actions';
|
||||
import { SETTINGS_TABS } from '../../settings/constants';
|
||||
import { checkBlurSupport } from '../functions';
|
||||
|
||||
import { VirtualBackgroundDialog } from './index';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link VideoBackgroundButton}.
|
||||
*/
|
||||
|
@ -45,7 +44,7 @@ class VideoBackgroundButton extends AbstractButton<Props, *> {
|
|||
_handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openDialog(VirtualBackgroundDialog));
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.VIRTUAL_BACKGROUND));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,8 +8,6 @@ import { connect } from 'react-redux';
|
|||
import { IReduxState } from '../../app/types';
|
||||
import { hideDialog } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import Video from '../../base/media/components/Video';
|
||||
import { equals } from '../../base/redux/functions';
|
||||
import { getCurrentCameraDeviceId } from '../../base/settings/functions.web';
|
||||
|
@ -83,39 +81,28 @@ interface IState {
|
|||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
virtualBackgroundPreview: {
|
||||
'& .video-preview': {
|
||||
height: '250px'
|
||||
},
|
||||
|
||||
'& .video-background-preview-entry': {
|
||||
height: '250px',
|
||||
width: '570px',
|
||||
marginBottom: theme.spacing(2),
|
||||
zIndex: 2,
|
||||
|
||||
'@media (max-width: 632px)': {
|
||||
maxWidth: '336px'
|
||||
}
|
||||
},
|
||||
height: 'auto',
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
marginBottom: theme.spacing(3),
|
||||
zIndex: 2,
|
||||
borderRadius: '3px',
|
||||
backgroundColor: theme.palette.uiBackground,
|
||||
position: 'relative' as const,
|
||||
|
||||
'& .video-preview-loader': {
|
||||
borderRadius: '6px',
|
||||
backgroundColor: 'transparent',
|
||||
height: '250px',
|
||||
marginBottom: theme.spacing(2),
|
||||
width: '572px',
|
||||
position: 'fixed',
|
||||
zIndex: 2,
|
||||
height: '220px',
|
||||
|
||||
'& svg': {
|
||||
position: 'absolute',
|
||||
position: 'absolute' as const,
|
||||
top: '40%',
|
||||
left: '45%'
|
||||
},
|
||||
|
||||
'@media (min-width: 432px) and (max-width: 632px)': {
|
||||
width: '340px'
|
||||
}
|
||||
},
|
||||
|
||||
'& .video-preview-error': {
|
||||
height: '220px',
|
||||
position: 'relative'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -238,31 +225,21 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
|
|||
*/
|
||||
_renderPreviewEntry(data: Object) {
|
||||
const { t } = this.props;
|
||||
const className = 'video-background-preview-entry';
|
||||
|
||||
if (this.state.loading) {
|
||||
return this._loadVideoPreview();
|
||||
}
|
||||
if (!data) {
|
||||
return (
|
||||
<div
|
||||
className = { className }
|
||||
video-preview-container = { true }>
|
||||
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
|
||||
</div>
|
||||
<div className = 'video-preview-error'>{t('deviceSelection.previewUnavailable')}</div>
|
||||
);
|
||||
}
|
||||
const props: Object = {
|
||||
className
|
||||
};
|
||||
|
||||
return (
|
||||
<div { ...props }>
|
||||
<Video
|
||||
className = { videoClassName }
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: data }} />
|
||||
</div>
|
||||
<Video
|
||||
className = { videoClassName }
|
||||
playsinline = { true }
|
||||
videoTrack = {{ jitsiTrack: data }} />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -310,8 +287,8 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
|
|||
|
||||
return (<div className = { classes.virtualBackgroundPreview }>
|
||||
{jitsiTrack
|
||||
? <div className = 'video-preview'>{this._renderPreviewEntry(jitsiTrack)}</div>
|
||||
: <div className = 'video-preview-loader'>{this._loadVideoPreview()}</div>
|
||||
? this._renderPreviewEntry(jitsiTrack)
|
||||
: this._loadVideoPreview()
|
||||
}</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,22 @@ import Bourne from '@hapi/bourne';
|
|||
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { getMultipleVideoSendingSupportFeatureFlag } from '../../base/config/functions.any';
|
||||
import { hideDialog } from '../../base/dialog/actions';
|
||||
import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../base/icons/svg';
|
||||
import { connect } from '../../base/redux/functions';
|
||||
import { updateSettings } from '../../base/settings/actions';
|
||||
import { withPixelLineHeight } from '../../base/styles/functions.web';
|
||||
// @ts-ignore
|
||||
import { Tooltip } from '../../base/tooltip';
|
||||
import { getLocalVideoTrack } from '../../base/tracks/functions';
|
||||
import Dialog from '../../base/ui/components/web/Dialog';
|
||||
import { toggleBackgroundEffect } from '../actions';
|
||||
import { BACKGROUNDS_LIMIT, IMAGES, type Image, VIRTUAL_BACKGROUND_TYPE } from '../constants';
|
||||
import { toDataURL } from '../functions';
|
||||
import logger from '../logger';
|
||||
|
||||
import UploadImageButton from './UploadImageButton';
|
||||
// @ts-ignore
|
||||
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
|
||||
/* eslint-enable lines-around-comment */
|
||||
|
||||
|
@ -38,7 +33,7 @@ interface IProps extends WithTranslation {
|
|||
_images: Array<Image>;
|
||||
|
||||
/**
|
||||
* Returns the jitsi track that will have backgraund effect applied.
|
||||
* Returns the jitsi track that will have background effect applied.
|
||||
*/
|
||||
_jitsiTrack: Object;
|
||||
|
||||
|
@ -52,11 +47,6 @@ interface IProps extends WithTranslation {
|
|||
*/
|
||||
_multiStreamModeEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Returns the selected thumbnail identifier.
|
||||
*/
|
||||
_selectedThumbnail: string;
|
||||
|
||||
/**
|
||||
* If the upload button should be displayed or not.
|
||||
*/
|
||||
|
@ -78,192 +68,131 @@ interface IProps extends WithTranslation {
|
|||
* NOTE: currently used only for electron in order to open the dialog in the correct state after desktop sharing
|
||||
* selection.
|
||||
*/
|
||||
initialOptions: Object;
|
||||
initialOptions?: Object;
|
||||
|
||||
/**
|
||||
* Options change handler.
|
||||
*/
|
||||
onOptionsChange: Function;
|
||||
|
||||
/**
|
||||
* Virtual background options.
|
||||
*/
|
||||
options: any;
|
||||
|
||||
/**
|
||||
* Returns the selected thumbnail identifier.
|
||||
*/
|
||||
selectedThumbnail: string;
|
||||
}
|
||||
|
||||
const onError = (event: any) => {
|
||||
event.target.style.display = 'none';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code VirtualBackground} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{Props}}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState): Object {
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
|
||||
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
|
||||
|
||||
return {
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
|
||||
_virtualBackground: state['features/virtual-background'],
|
||||
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
|
||||
_showUploadButton: state['features/base/config'].disableAddingBackgroundImages,
|
||||
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack,
|
||||
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state)
|
||||
};
|
||||
}
|
||||
|
||||
const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackground));
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialogContainer: {
|
||||
width: 'auto'
|
||||
virtualBackgroundLoading: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '50px'
|
||||
},
|
||||
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
dialog: {
|
||||
alignSelf: 'flex-start',
|
||||
position: 'relative',
|
||||
maxHeight: '300px',
|
||||
color: 'white',
|
||||
|
||||
thumbnailContainer: {
|
||||
width: '100%',
|
||||
display: 'inline-grid',
|
||||
gridTemplateColumns: 'auto auto auto auto auto',
|
||||
columnGap: '9px',
|
||||
cursor: 'pointer',
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
|
||||
gap: theme.spacing(1),
|
||||
|
||||
// @ts-ignore
|
||||
[[ '& .desktop-share:hover',
|
||||
'& .thumbnail:hover',
|
||||
'& .blur:hover',
|
||||
'& .slight-blur:hover',
|
||||
'& .virtual-background-none:hover' ]]: {
|
||||
opacity: 0.5,
|
||||
border: '2px solid #99bbf3'
|
||||
'@media (min-width: 608px) and (max-width: 712px)': {
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr'
|
||||
},
|
||||
'& .background-option': {
|
||||
marginTop: theme.spacing(2),
|
||||
borderRadius: `${theme.shape.borderRadius}px`,
|
||||
height: '60px',
|
||||
width: '107px',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center',
|
||||
fontWeight: 'bold',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
'& thumbnail-container': {
|
||||
position: 'relative',
|
||||
'&:focus-within .thumbnail ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
'& .thumbnail': {
|
||||
objectFit: 'cover'
|
||||
},
|
||||
'& .thumbnail:hover ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .thumbnail-selected': {
|
||||
objectFit: 'cover',
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .blur': {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#7e8287',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .blur-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .slight-blur': {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#a4a4a4',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .slight-blur-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .virtual-background-none': {
|
||||
background: '#525252',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& .none-selected': {
|
||||
border: '2px solid #246fe5'
|
||||
},
|
||||
'& .desktop-share': {
|
||||
background: '#525252'
|
||||
},
|
||||
'& .desktop-share-selected': {
|
||||
border: '2px solid #246fe5',
|
||||
padding: '0 10px'
|
||||
},
|
||||
'& delete-image-icon': {
|
||||
background: '#3d3d3d',
|
||||
position: 'absolute',
|
||||
display: 'none',
|
||||
left: '96px',
|
||||
bottom: '51px',
|
||||
'&:hover': {
|
||||
display: 'block'
|
||||
},
|
||||
'@media (max-width: 632px)': {
|
||||
left: '51px'
|
||||
}
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
gridTemplateColumns: 'auto auto auto auto'
|
||||
},
|
||||
'@media (max-width: 632px)': {
|
||||
gridTemplateColumns: 'auto auto auto auto auto',
|
||||
fontSize: '1.5vw',
|
||||
|
||||
// @ts-ignore
|
||||
[[ '& .desktop-share:hover',
|
||||
'& .thumbnail:hover',
|
||||
'& .blur:hover',
|
||||
'& .slight-blur:hover',
|
||||
'& .virtual-background-none:hover' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
[[ '& .desktop-share',
|
||||
'& .virtual-background-none,',
|
||||
'& .thumbnail,',
|
||||
'& .blur,',
|
||||
'& .slight-blur' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
},
|
||||
|
||||
// @ts-ignore
|
||||
[[ '& .desktop-share-selected',
|
||||
'& .thumbnail-selected',
|
||||
'& .none-selected',
|
||||
'& .blur-selected',
|
||||
'& .slight-blur-selected' ]]: {
|
||||
height: '60px',
|
||||
width: '60px'
|
||||
}
|
||||
},
|
||||
'@media (max-width: 360px)': {
|
||||
gridTemplateColumns: 'auto auto auto auto'
|
||||
},
|
||||
'@media (max-width: 319px)': {
|
||||
gridTemplateColumns: 'auto auto'
|
||||
'@media (max-width: 607px)': {
|
||||
gridTemplateColumns: '1fr 1fr 1fr',
|
||||
gap: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
dialogMarginTop: {
|
||||
marginTop: '8px'
|
||||
|
||||
thumbnail: {
|
||||
height: '54px',
|
||||
width: '100%',
|
||||
borderRadius: '4px',
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
...withPixelLineHeight(theme.typography.labelBold),
|
||||
color: theme.palette.text01,
|
||||
objectFit: 'cover',
|
||||
|
||||
[[ '&:hover', '&:focus' ] as any]: {
|
||||
opacity: 0.5,
|
||||
cursor: 'pointer',
|
||||
|
||||
'& ~ .delete-image-icon': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
height: '70px'
|
||||
}
|
||||
},
|
||||
virtualBackgroundLoading: {
|
||||
overflow: 'hidden',
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
marginTop: '10px',
|
||||
transform: 'translateX(-50%)'
|
||||
|
||||
selectedThumbnail: {
|
||||
border: `2px solid ${theme.palette.action01Hover}`
|
||||
},
|
||||
|
||||
noneThumbnail: {
|
||||
backgroundColor: theme.palette.ui04
|
||||
},
|
||||
|
||||
slightBlur: {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#a4a4a4'
|
||||
},
|
||||
|
||||
blur: {
|
||||
boxShadow: 'inset 0 0 12px #000000',
|
||||
background: '#7e8287'
|
||||
},
|
||||
|
||||
storedImageContainer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'&:focus-within .delete-image-container': {
|
||||
display: 'block'
|
||||
}
|
||||
},
|
||||
|
||||
deleteImageIcon: {
|
||||
position: 'absolute',
|
||||
top: '3px',
|
||||
right: '3px',
|
||||
background: theme.palette.ui03,
|
||||
borderRadius: '3px',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
|
||||
'@media (max-width: 607px)': {
|
||||
display: 'block',
|
||||
padding: '3px'
|
||||
},
|
||||
|
||||
[[ '&:hover', '&:focus' ] as any]: {
|
||||
display: 'block'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -273,24 +202,28 @@ const useStyles = makeStyles()(theme => {
|
|||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function VirtualBackground({
|
||||
function VirtualBackgrounds({
|
||||
_images,
|
||||
_jitsiTrack,
|
||||
_localFlipX,
|
||||
_selectedThumbnail,
|
||||
selectedThumbnail,
|
||||
_showUploadButton,
|
||||
_virtualBackground,
|
||||
dispatch,
|
||||
onOptionsChange,
|
||||
options,
|
||||
initialOptions,
|
||||
t
|
||||
}: IProps) {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ previewIsLoaded, setPreviewIsLoaded ] = useState(false);
|
||||
const [ options, setOptions ] = useState<any>({ ...initialOptions });
|
||||
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
|
||||
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ initialVirtualBackground ] = useState(_virtualBackground);
|
||||
|
||||
useEffect(() => {
|
||||
onOptionsChange({ ...initialOptions });
|
||||
}, []);
|
||||
|
||||
const deleteStoredImage = useCallback(e => {
|
||||
const imageId = e.currentTarget.getAttribute('data-imageid');
|
||||
|
||||
|
@ -320,7 +253,7 @@ function VirtualBackground({
|
|||
}, [ storedImages ]);
|
||||
|
||||
const enableBlur = useCallback(async () => {
|
||||
setOptions({
|
||||
onOptionsChange({
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
|
||||
enabled: true,
|
||||
blurValue: 25,
|
||||
|
@ -338,7 +271,7 @@ function VirtualBackground({
|
|||
}, [ enableBlur ]);
|
||||
|
||||
const enableSlideBlur = useCallback(async () => {
|
||||
setOptions({
|
||||
onOptionsChange({
|
||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.BLUR,
|
||||
enabled: true,
|
||||
blurValue: 8,
|
||||
|
@ -356,7 +289,7 @@ function VirtualBackground({
|
|||
}, [ enableSlideBlur ]);
|
||||
|
||||
const removeBackground = useCallback(async () => {
|
||||
setOptions({
|
||||
onOptionsChange({
|
||||
enabled: false,
|
||||
selectedThumbnail: 'none'
|
||||
});
|
||||
|
@ -376,7 +309,7 @@ function VirtualBackground({
|
|||
const image = storedImages.find(img => img.id === imageId);
|
||||
|
||||
if (image) {
|
||||
setOptions({
|
||||
onOptionsChange({
|
||||
backgroundType: 'image',
|
||||
enabled: true,
|
||||
url: image.src,
|
||||
|
@ -394,7 +327,7 @@ function VirtualBackground({
|
|||
try {
|
||||
const url = await toDataURL(image.src);
|
||||
|
||||
setOptions({
|
||||
onOptionsChange({
|
||||
backgroundType: 'image',
|
||||
enabled: true,
|
||||
url,
|
||||
|
@ -423,48 +356,12 @@ function VirtualBackground({
|
|||
}
|
||||
}, [ setUploadedImageBackground ]);
|
||||
|
||||
const applyVirtualBackground = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await dispatch(toggleBackgroundEffect(options, _jitsiTrack));
|
||||
await setLoading(false);
|
||||
|
||||
// Set x scale to default value.
|
||||
dispatch(updateSettings({
|
||||
localFlipX: true
|
||||
}));
|
||||
|
||||
dispatch(hideDialog());
|
||||
logger.info(`Virtual background type: '${typeof options.backgroundType === 'undefined'
|
||||
? 'none' : options.backgroundType}' applied!`);
|
||||
}, [ dispatch, options, _localFlipX ]);
|
||||
|
||||
// Prevent the selection of a new virtual background if it has not been applied by default
|
||||
const cancelVirtualBackground = useCallback(async () => {
|
||||
await setOptions({
|
||||
backgroundType: initialVirtualBackground.backgroundType,
|
||||
enabled: initialVirtualBackground.backgroundEffectEnabled,
|
||||
url: initialVirtualBackground.virtualSource,
|
||||
selectedThumbnail: initialVirtualBackground.selectedThumbnail,
|
||||
blurValue: initialVirtualBackground.blurValue
|
||||
});
|
||||
dispatch(hideDialog());
|
||||
}, []);
|
||||
|
||||
const loadedPreviewState = useCallback(async loaded => {
|
||||
await setPreviewIsLoaded(loaded);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className = { classes.dialogContainer }
|
||||
ok = {{
|
||||
disabled: !options || loading || !previewIsLoaded,
|
||||
translationKey: 'virtualBackground.apply'
|
||||
}}
|
||||
onCancel = { cancelVirtualBackground }
|
||||
onSubmit = { applyVirtualBackground }
|
||||
size = 'large'
|
||||
titleKey = 'virtualBackground.title' >
|
||||
<>
|
||||
<VirtualBackgroundPreview
|
||||
loadedPreview = { loadedPreviewState }
|
||||
options = { options } />
|
||||
|
@ -481,23 +378,22 @@ function VirtualBackground({
|
|||
{_showUploadButton
|
||||
&& <UploadImageButton
|
||||
setLoading = { setLoading }
|
||||
setOptions = { setOptions }
|
||||
setOptions = { onOptionsChange }
|
||||
setStoredImages = { setStoredImages }
|
||||
showLabel = { previewIsLoaded }
|
||||
storedImages = { storedImages } />}
|
||||
<div
|
||||
className = { cx(classes.dialog, { [classes.dialogMarginTop]: previewIsLoaded }) }
|
||||
className = { classes.thumbnailContainer }
|
||||
role = 'radiogroup'
|
||||
tabIndex = { -1 }>
|
||||
<Tooltip
|
||||
content = { t('virtualBackground.removeBackground') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { _selectedThumbnail === 'none' }
|
||||
aria-checked = { selectedThumbnail === 'none' }
|
||||
aria-label = { t('virtualBackground.removeBackground') }
|
||||
className = { cx('background-option', 'virtual-background-none', {
|
||||
'none-selected': _selectedThumbnail === 'none'
|
||||
}) }
|
||||
className = { cx(classes.thumbnail, classes.noneThumbnail,
|
||||
selectedThumbnail === 'none' && classes.selectedThumbnail) }
|
||||
onClick = { removeBackground }
|
||||
onKeyPress = { removeBackgroundKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -509,11 +405,10 @@ function VirtualBackground({
|
|||
content = { t('virtualBackground.slightBlur') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { _selectedThumbnail === 'slight-blur' }
|
||||
aria-checked = { selectedThumbnail === 'slight-blur' }
|
||||
aria-label = { t('virtualBackground.slightBlur') }
|
||||
className = { cx('background-option', 'slight-blur', {
|
||||
'slight-blur-selected': _selectedThumbnail === 'slight-blur'
|
||||
}) }
|
||||
className = { cx(classes.thumbnail, classes.slightBlur,
|
||||
selectedThumbnail === 'slight-blur' && classes.selectedThumbnail) }
|
||||
onClick = { enableSlideBlur }
|
||||
onKeyPress = { enableSlideBlurKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -525,11 +420,10 @@ function VirtualBackground({
|
|||
content = { t('virtualBackground.blur') }
|
||||
position = { 'top' }>
|
||||
<div
|
||||
aria-checked = { _selectedThumbnail === 'blur' }
|
||||
aria-checked = { selectedThumbnail === 'blur' }
|
||||
aria-label = { t('virtualBackground.blur') }
|
||||
className = { cx('background-option', 'blur', {
|
||||
'blur-selected': _selectedThumbnail === 'blur'
|
||||
}) }
|
||||
className = { cx(classes.thumbnail, classes.blur,
|
||||
selectedThumbnail === 'blur' && classes.selectedThumbnail) }
|
||||
onClick = { enableBlur }
|
||||
onKeyPress = { enableBlurKeyPress }
|
||||
role = 'radio'
|
||||
|
@ -544,11 +438,11 @@ function VirtualBackground({
|
|||
position = { 'top' }>
|
||||
<img
|
||||
alt = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
|
||||
aria-checked = { options.selectedThumbnail === image.id
|
||||
|| _selectedThumbnail === image.id }
|
||||
className = {
|
||||
options.selectedThumbnail === image.id || _selectedThumbnail === image.id
|
||||
? 'background-option thumbnail-selected' : 'background-option thumbnail' }
|
||||
aria-checked = { options?.selectedThumbnail === image.id
|
||||
|| selectedThumbnail === image.id }
|
||||
className = { cx(classes.thumbnail,
|
||||
(options?.selectedThumbnail === image.id
|
||||
|| selectedThumbnail === image.id) && classes.selectedThumbnail) }
|
||||
data-imageid = { image.id }
|
||||
onClick = { setImageBackground }
|
||||
onError = { onError }
|
||||
|
@ -560,15 +454,13 @@ function VirtualBackground({
|
|||
))}
|
||||
{storedImages.map((image, index) => (
|
||||
<div
|
||||
className = { 'thumbnail-container' }
|
||||
className = { classes.storedImageContainer }
|
||||
key = { image.id }>
|
||||
<img
|
||||
alt = { t('virtualBackground.uploadedImage', { index: index + 1 }) }
|
||||
aria-checked = { _selectedThumbnail === image.id }
|
||||
className = { cx('background-option', {
|
||||
'thumbnail-selected': _selectedThumbnail === image.id,
|
||||
'thumbnail': _selectedThumbnail !== image.id
|
||||
}) }
|
||||
aria-checked = { selectedThumbnail === image.id }
|
||||
className = { cx(classes.thumbnail,
|
||||
selectedThumbnail === image.id && classes.selectedThumbnail) }
|
||||
data-imageid = { image.id }
|
||||
onClick = { setUploadedImageBackground }
|
||||
onError = { onError }
|
||||
|
@ -579,12 +471,12 @@ function VirtualBackground({
|
|||
|
||||
<Icon
|
||||
ariaLabel = { t('virtualBackground.deleteImage') }
|
||||
className = { 'delete-image-icon' }
|
||||
className = { cx(classes.deleteImageIcon, 'delete-image-icon') }
|
||||
data-imageid = { image.id }
|
||||
onClick = { deleteStoredImage }
|
||||
onKeyPress = { deleteStoredImageKeyPress }
|
||||
role = 'button'
|
||||
size = { 15 }
|
||||
size = { 16 }
|
||||
src = { IconCloseLarge }
|
||||
tabIndex = { 0 } />
|
||||
</div>
|
||||
|
@ -592,8 +484,30 @@ function VirtualBackground({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default VirtualBackgroundDialog;
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for the
|
||||
* {@code VirtualBackground} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{Props}}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { localFlipX } = state['features/base/settings'];
|
||||
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
|
||||
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
|
||||
|
||||
return {
|
||||
_localFlipX: Boolean(localFlipX),
|
||||
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
|
||||
_virtualBackground: state['features/virtual-background'],
|
||||
_showUploadButton: !state['features/base/config'].disableAddingBackgroundImages,
|
||||
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(translate(VirtualBackgrounds));
|
|
@ -1,2 +1 @@
|
|||
export { default as VideoBackgroundButton } from './VideoBackgroundButton';
|
||||
export { default as VirtualBackgroundDialog } from './VirtualBackgroundDialog';
|
||||
|
|
|
@ -83,7 +83,7 @@ module:hook("muc-occupant-pre-join", function (event)
|
|||
-- skipping events we had produced and clear our flag
|
||||
if stanza.delayed_join_skip == true then
|
||||
event.stanza.delayed_join_skip = nil;
|
||||
return false;
|
||||
return nil;
|
||||
end
|
||||
|
||||
local throttle = room.join_rate_throttle;
|
||||
|
@ -101,7 +101,7 @@ module:hook("muc-occupant-pre-join", function (event)
|
|||
|
||||
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from) then
|
||||
-- let's not stop processing the event
|
||||
return false;
|
||||
return nil;
|
||||
end
|
||||
|
||||
if not room.join_rate_queue_timer then
|
||||
|
|
13
title.html
13
title.html
|
@ -1,9 +1,10 @@
|
|||
<title>Jitsi Meet</title>
|
||||
<meta property="og:title" content="Jitsi Meet"/>
|
||||
<title>JitSea 🏴☠️</title>
|
||||
<meta property="og:title" content="JitSea 🏴☠️"/>
|
||||
<meta property="og:image" content="images/jitsilogo.png?v=1"/>
|
||||
<meta property="og:description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||
<meta description="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||
<meta itemprop="name" content="Jitsi Meet"/>
|
||||
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||
<meta property="og:description" content="meow meow meowmeow meow"/>
|
||||
<meta description="meow meow meowmeow meow"/>
|
||||
<meta itemprop="name" content="JitSea 🏴☠️"/>
|
||||
<meta itemprop="description" content="meow meow meowmeow meow"/>
|
||||
<meta itemprop="image" content="images/jitsilogo.png?v=1"/>
|
||||
<link rel="icon" type="image/png" href="images/favicon.ico?v=1"/>
|
||||
|
||||
|
|
Loading…
Reference in New Issue