Compare commits

...

17 Commits

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

View File

@ -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.
//

67
css/_jiti.scss Normal file
View File

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

View File

@ -94,3 +94,9 @@ $flagsImagePath: "../images/";
@import 'notifications';
/* Modules END */
/* Jeet crew BEGIN */
@import 'jiti';
/* Jeet crew END */

View File

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

View File

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

View File

@ -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",

View File

@ -106,6 +106,8 @@ import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/fea
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture';
import { isScreenshotCaptureEnabled } from '../../react/features/screenshot-capture/functions';
import SettingsDialog from '../../react/features/settings/components/web/SettingsDialog';
import { SETTINGS_TABS } from '../../react/features/settings/constants';
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functions';
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
@ -113,7 +115,6 @@ import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/function
import { setTileView, toggleTileView } from '../../react/features/video-layout';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality';
import VirtualBackgroundDialog from '../../react/features/virtual-background/components/VirtualBackgroundDialog';
import { getJitsiMeetTransport } from '../transport';
import { API_ID, ENDPOINT_TEXT_MESSAGE_NAME } from './constants';
@ -798,7 +799,8 @@ function initCommands() {
APP.store.dispatch(overwriteConfig(whitelistedConfig));
},
'toggle-virtual-background': () => {
APP.store.dispatch(toggleDialog(VirtualBackgroundDialog));
APP.store.dispatch(toggleDialog(SettingsDialog, {
defaultTab: SETTINGS_TABS.VIRTUAL_BACKGROUND }));
},
'end-conference': () => {
APP.store.dispatch(endConference());

View File

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

View File

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

10
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

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

After

Width:  |  Height:  |  Size: 814 B

View File

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

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -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';

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

@ -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.
*

View File

@ -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.
*

View File

@ -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
};
}

View File

@ -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;
}
/**

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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)));

View File

@ -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,

View File

@ -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!`);
}
}
};
}

View File

@ -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));

View File

@ -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,

View File

@ -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));

View File

@ -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

View File

@ -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'
};
/**

View File

@ -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
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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 }

View File

@ -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));
}
/**

View File

@ -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>);
}
}

View File

@ -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));

View File

@ -1,2 +1 @@
export { default as VideoBackgroundButton } from './VideoBackgroundButton';
export { default as VirtualBackgroundDialog } from './VirtualBackgroundDialog';

View File

@ -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

View File

@ -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"/>