diff --git a/conference.js b/conference.js index 68737e6c08b57..32a89eee7c72f 100644 --- a/conference.js +++ b/conference.js @@ -1503,14 +1503,14 @@ export default { * * @param {boolean} didHaveVideo indicates if there was a camera video being * used, before switching to screen sharing. - * @param {boolean} wasVideoMuted indicates if the video was muted, before - * switching to screen sharing. + * @param {boolean} ignoreDidHaveVideo indicates if the camera video should be + * ignored when switching screen sharing off. * @return {Promise} resolved after the screen sharing is turned off, or * rejected with some error (no idea what kind of error, possible GUM error) * in case it fails. * @private */ - async _turnScreenSharingOff(didHaveVideo) { + async _turnScreenSharingOff(didHaveVideo, ignoreDidHaveVideo) { this._untoggleScreenSharing = null; this.videoSwitchInProgress = true; @@ -1554,7 +1554,7 @@ export default { APP.store.dispatch(setScreenAudioShareState(false)); - if (didHaveVideo) { + if (didHaveVideo && !ignoreDidHaveVideo) { promise = promise.then(() => createLocalTracksF({ devices: [ 'video' ] })) .then(([ stream ]) => { logger.debug(`_turnScreenSharingOff using ${stream} for useVideoStream`); @@ -1605,9 +1605,10 @@ export default { * @param {Array} [options.desktopSharingSources] - Array with the * sources that have to be displayed in the desktop picker window ('screen', * 'window', etc.). + * @param {boolean} ignoreDidHaveVideo - if true ignore if video was on when sharing started. * @return {Promise.} */ - async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}) { + async toggleScreenSharing(toggle = !this._untoggleScreenSharing, options = {}, ignoreDidHaveVideo) { logger.debug(`toggleScreenSharing: ${toggle}`); if (this.videoSwitchInProgress) { return Promise.reject('Switch in progress.'); @@ -1633,7 +1634,7 @@ export default { } return this._untoggleScreenSharing - ? this._untoggleScreenSharing() + ? this._untoggleScreenSharing(ignoreDidHaveVideo) : Promise.resolve(); }, @@ -2476,8 +2477,8 @@ export default { }); APP.UI.addListener( - UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly }) => { - this.toggleScreenSharing(enabled, { audioOnly }); + UIEvents.TOGGLE_SCREENSHARING, ({ enabled, audioOnly, ignoreDidHaveVideo }) => { + this.toggleScreenSharing(enabled, { audioOnly }, ignoreDidHaveVideo); } ); }, diff --git a/config.js b/config.js index 1695557e7224b..910f1f9667958 100644 --- a/config.js +++ b/config.js @@ -545,6 +545,19 @@ var config = { // '__end' // ], + // Holds values related to toolbar visibility control. + // toolbarConfig: { + // // Moved from interfaceConfig.INITIAL_TOOLBAR_TIMEOUT + // // The initial numer of miliseconds for the toolbar buttons to be visible on screen. + // initialTimeout: 20000, + // // Moved from interfaceConfig.TOOLBAR_TIMEOUT + // // Number of miliseconds for the toolbar buttons to be visible on screen. + // timeout: 4000, + // // Moved from interfaceConfig.TOOLBAR_ALWAYS_VISIBLE + // // Whether toolbar should be always visible or should hide after x miliseconds. + // alwaysVisible: false + // }, + // Toolbar buttons which have their click event exposed through the API on // `toolbarButtonClicked` event instead of executing the normal click routine. // buttonsWithNotifyClick: [ @@ -896,6 +909,10 @@ var config = { */ // dynamicBrandingUrl: '', + // When true the user cannot add more images to be used as virtual background. + // Only the default ones from will be available. + // disableAddingBackgroundImages: false, + // Sets the background transparency level. '0' is fully transparent, '1' is opaque. // backgroundAlpha: 1, diff --git a/css/_chat.scss b/css/_chat.scss index b3a2cdf4bd3bb..20c5486613c5a 100644 --- a/css/_chat.scss +++ b/css/_chat.scss @@ -213,6 +213,7 @@ } #usermsg { + -ms-overflow-style: none; border: 0px none; border-radius:0; box-shadow: none; @@ -221,8 +222,13 @@ padding: 10px; overflow-y: auto; resize: none; + scrollbar-width: none; width: 100%; word-break: break-word; + + &::-webkit-scrollbar { + display: none; + } } #usermsg:hover { diff --git a/css/_subject.scss b/css/_subject.scss index 61ceaea6a4394..f63d45b50137b 100644 --- a/css/_subject.scss +++ b/css/_subject.scss @@ -75,3 +75,7 @@ top: 0; height: 48px; } + +.shift-right .details-container { + margin-left: calc(#{$sidebarWidth} / 2); +} diff --git a/interface_config.js b/interface_config.js index 2ccbd8148af99..ddcd693c649e6 100644 --- a/interface_config.js +++ b/interface_config.js @@ -26,11 +26,6 @@ var interfaceConfig = { CLOSE_PAGE_GUEST_HINT: false, // A html text to be shown to guests on the close page, false disables it - // Connection indicators ( - // CONNECTION_INDICATOR_AUTO_HIDE_ENABLED, - // CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT, - // CONNECTION_INDICATOR_DISABLED) got moved to config.js. - DEFAULT_BACKGROUND: '#474747', DEFAULT_LOCAL_DISPLAY_NAME: 'me', DEFAULT_LOGO_URL: 'images/watermark.svg', @@ -39,9 +34,6 @@ var interfaceConfig = { DISABLE_DOMINANT_SPEAKER_INDICATOR: false, - // Deprecated. Please use disableModeratorIndicator from config.js - // DISABLE_FOCUS_INDICATOR: false, - /** * If true, notifications regarding joining/leaving are no longer displayed. */ @@ -97,7 +89,6 @@ var interfaceConfig = { */ HIDE_INVITE_MORE_HEADER: false, - INITIAL_TOOLBAR_TIMEOUT: 20000, JITSI_WATERMARK_LINK: 'https://jitsi.org', LANG_DETECTION: true, // Allow i18n to detect the system language @@ -183,16 +174,6 @@ var interfaceConfig = { */ SUPPORT_URL: 'https://community.jitsi.org/', - TOOLBAR_ALWAYS_VISIBLE: false, - - /** - * DEPRECATED! - * This config was moved to config.js as `toolbarButtons`. - */ - // TOOLBAR_BUTTONS: [], - - TOOLBAR_TIMEOUT: 4000, - // Browsers, in addition to those which do not fully support WebRTC, that // are not supported and should show the unsupported browser page. UNSUPPORTED_BROWSERS: [], @@ -257,6 +238,28 @@ var interfaceConfig = { PHONE_NUMBER_REGEX */ + // -----------------DEPRECATED CONFIGS BELOW THIS LINE----------------------------- + + // Connection indicators ( + // CONNECTION_INDICATOR_AUTO_HIDE_ENABLED, + // CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT, + // CONNECTION_INDICATOR_DISABLED) got moved to config.js. + + // Please use disableModeratorIndicator from config.js + // DISABLE_FOCUS_INDICATOR: false, + + // Moved to config.js as `toolbarConfig.initialTimeout`. + // INITIAL_TOOLBAR_TIMEOUT: 20000, + + // Moved to config.js as `toolbarConfig.alwaysVisible`. + // TOOLBAR_ALWAYS_VISIBLE: false, + + // This config was moved to config.js as `toolbarButtons`. + // TOOLBAR_BUTTONS: [], + + // Moved to config.js as `toolbarConfig.timeout`. + // TOOLBAR_TIMEOUT: 4000, + // Allow all above example options to include a trailing comma and // prevent fear when commenting out the last value. // eslint-disable-next-line sort-keys diff --git a/lang/main-fr.json b/lang/main-fr.json index 2fa644b77fa5e..8f9beba5e7c58 100644 --- a/lang/main-fr.json +++ b/lang/main-fr.json @@ -248,6 +248,8 @@ "micPermissionDeniedError": "Vous n'avez pas autorisé l'utilisation de votre microphone. Vous pouvez toujours participer à la conférence, mais les autres ne vont pas vous entendre. Utilisez le bouton du microphone dans la barre d'adresse pour résoudre ce problème.", "micTimeoutError": "Impossible de démarrer la source audio. Délai dépassé!", "micUnknownError": "Vous ne pouvez pas utiliser le microphone pour une raison inconnue.", + "moderationAudioLabel": "Autoriser les participants à réactiver leur micro", + "moderationVideoLabel": "Autoriser les participants à démarrer leur vidéo", "muteEveryoneElseDialog": "Une fois leur micro coupé, vous ne pourrez plus le réactiver, mais ils pourront l'activer par eux-mêmes à tout moment.", "muteEveryoneElseTitle": "Couper le micro de tout le monde sauf de {{whom}} ?", "muteEveryoneDialog": "Êtes-vous sûr de vouloir couper les micros de tout le monde ? Vous ne pourrez plus réactiver leur micro, mais ils pourront l'activer par eux-mêmes à tout moment.", @@ -563,12 +565,21 @@ "close": "Fermer", "headings": { "lobby": "Salle d'attente ({{count}})", - "participantsList": "Participants de la réunion ({{count}})" + "participantsList": "Participants de la réunion ({{count}})", + "waitingLobby": "Dans la salle d'attente ({{count}})" }, "actions": { + "allow": "Autoriser les participant à:", + "blockEveryoneMicCamera": "Bloquer tous les micros et caméras", "invite": "Inviter quelqu'un", - "muteAll": "couper le micro de tout le monde", - "stopVideo": "couper la vidéo" + "askUnmute": "Demander de réactiver le micro", + "mute": "Couper le micro", + "muteAll": "Couper le micro de tout le monde", + "muteEveryoneElse": "Couper le micro de tous les autres", + "startModeration": "Réactiver son micro ou démarrer sa vidéo", + "stopEveryonesVideo": "Couper toutes les caméras", + "stopVideo": "Couper la vidéo", + "unblockEveryoneMicCamera": "Débloquer tous les micros et caméras" } }, "passwordSetRemotely": "défini par un autre participant", diff --git a/lang/main-oc.json b/lang/main-oc.json index c206768755175..c54a4038458b9 100644 --- a/lang/main-oc.json +++ b/lang/main-oc.json @@ -17,21 +17,17 @@ "inviteMoreMailSubject": "Rejónher la conferéncia {{appName}}", "inviteMorePrompt": "Convidar mai de monde", "linkCopied": "Ligam copiat al quichapapièrs", - "loading": "Recèrca de monde e de numèro de telefòn", - "loadingNumber": "Validacion del numèro de telefòn", - "loadingPeople": "Recèrca de monde de convidar", "noResults": "Pas cap de resultat trobat", - "noValidNumbers": "Picatz lo numèro de telefòn", "outlookEmail": "Outlook Email", - "searchNumbers": "Apondre de numèros de telefòn", - "searchPeople": "Cercar de monde", - "searchPeopleAndNumbers": "Cercar de monde o apondre lor numèros de telefòn", "shareInvite": "Partejar invitacion conferéncia", "shareLink": "Partejar lo ligam de la conferéncia per convidar de monde", "shareStream": "Partejar la ligam de la difusion en dirècte", "telephone": "Telefòn : {{number}}", "title": "Convidatz de monde a vòstra conferéncia", - "yahooEmail": "Yahoo Email" + "yahooEmail": "Yahoo Email", + "phoneNumbers": "numèros de telefòn", + "searching": "Recèrca...", + "sipAddresses": "adreças sip" }, "audioDevices": { "bluetooth": "Bluetooth", @@ -79,12 +75,18 @@ "smileysPanel": "Panèl d’Emoji", "title": "Messatjariá", "titleWithPolls": "Messatjariá", - "you": "vos" + "you": "vos", + "enter": "Dintrar dins la sala", + "tabs": { + "chat": "Messatjariá", + "polls": "Sondatges" + } }, "chromeExtensionBanner": { "buttonText": "Installar l’extension Chrome", "dontShowAgain": "Me mostrar pas mai aquò", - "installExtensionText": "Installar l’extension per l’integracion de Google Calendar e Office 365" + "installExtensionText": "Installar l’extension per l’integracion de Google Calendar e Office 365", + "close": "Tampar" }, "connectingOverlay": { "joiningRoom": "Connexion a vòstra reünion…" @@ -102,8 +104,7 @@ "FETCH_SESSION_ID": "Obtencion de session-id...", "GET_SESSION_ID_ERROR": "Obténer l’error session-id : {{code}}", "GOT_SESSION_ID": "Obtencion de session-id... Fach", - "LOW_BANDWIDTH": "La vidèo per {{displayName}} es estada copada per estalviar la banda passanta", - "RECONNECTING": "Un problèma ret s'es produita. Reconnexion en cors..." + "LOW_BANDWIDTH": "La vidèo per {{displayName}} es estada copada per estalviar la banda passanta" }, "connectionindicator": { "address": "Adreça :", @@ -156,7 +157,6 @@ "ifHaveApp": "S’avètz ja l’aplicacion :", "joinInApp": "Rejónher la conferéncia en utilizant l’aplicacion", "launchWebButton": "Lançar del navigador", - "openApp": "Telecargar l’aplicacion", "title": "Aviada de vòstra conferéncia dins {{app}}…", "tryAgainButton": "Tornar ensajar del burèu" }, @@ -214,17 +214,12 @@ "e2eeWarning": "AVERTIMENT : pas totes los participants d'aquesta conferéncia semblan poder suportar lo chiframent del cap a la fin. Se l'activatz poiràn pas vos veire nimai vos entendre.", "enterDisplayName": "Volgatz picar vòstre nom aquí", "error": "Error", - "externalInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.", - "externalInstallationTitle": "Extension requesida", - "goToStore": "Anar al webstore", "gracefulShutdown": "Lo servici es actualament en mantenença. Ensajatz tornamai pus tard.", "grantModeratorDialog": "Volètz vertadièrament far venir aqueste participant moderator ?", "grantModeratorTitle": "Passar moderator", "IamHost": "Soi l’òste", "incorrectPassword": "Nom de compte o senhal incorrècte", "incorrectRoomLockPassword": "Senhal incorrècte", - "inlineInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.", - "inlineInstallExtension": "Installar ara", "internalError": "Òu ! Quicòm a pas foncionat. L'error seguenta s'es producha : {{error}}", "internalErrorTitle": "Error intèrna", "kickMessage": "Podètz contactat {{participantDisplayName}} per mai de detalhs.", @@ -234,7 +229,6 @@ "kickTitle": "Ai ! {{participantDisplayName}} vos a forabandit de la conferéncia", "liveStreaming": "La difusion en dirècte es estada arrestada", "liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible pendent un enregistrament actiu", - "liveStreamingDisabledForGuestTooltip": "Los convidats pòdon pas aviar una difusion en dirècte.", "liveStreamingDisabledTooltip": "Difusion en dirècte desactivada.", "lockMessage": "Impossible de verrolhar la conferéncia.", "lockRoom": "Ajustar un $t(lockRoomPasswordUppercase) a la conferéncia", @@ -269,7 +263,6 @@ "readMore": "mai", "recording": "Enregistrament", "recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible pendent una difusion activa", - "recordingDisabledForGuestTooltip": "Los convits pòdon pas lançar d’enregistraments.", "recordingDisabledTooltip": "L’enregistrament es desactivat.", "rejoinNow": "Participar ara", "remoteControlAllowedMessage": "{{user}} a acceptat vòstra demanda de contraròtle alonhat !", @@ -289,10 +282,6 @@ "screenSharingAudio": "Partejar l’àudio", "screenSharingFailed": "Ops ! Quicòm a trucat, avèm pas pogut començar lo partiment d'ecran!", "screenSharingFailedTitle": "Fracàs del partiment d'ecran !", - "screenSharingFailedToInstall": "Òu ! Fracàs de l'installacion de partatge d'ecran.", - "screenSharingFailedToInstallTitle": "Fracàs de l'installacion de partatge d'ecran", - "screenSharingFirefoxPermissionDeniedError": "Quicòm a fach mèuca quand èrem a ensajar de partejar vòstre ecran. Mercés de verificar qu’avètz donat l’autorizacion de lo partejar.", - "screenSharingFirefoxPermissionDeniedTitle": "Ops ! Avèm pas pogut aviar lo partatge d’ecran.", "screenSharingPermissionDeniedError": "Òps ! Quicòm s'es pas ben passat amb l'autorizacion de vòstra autorizacion de partatge d'ecran. Mercés de recargar e tornar ensajar.", "sendPrivateMessage": "Avètz recentament recebut un messatge privat. Avètz ensajat d’i respondre en privat, o volètz enviar lo messatge al grop ?", "sendPrivateMessageCancel": "Enviar al grop", @@ -305,7 +294,6 @@ "shareVideoTitle": "Partejar una vidèo", "shareYourScreen": "Partejar vòstre ecran", "shareYourScreenDisabled": "Lo partiment d’ecran es desactivat.", - "shareYourScreenDisabledForGuest": "Los convits pòdon pas partejar l’ecran.", "startLiveStreaming": "Aviar una difusion en dirècte", "startRecording": "Arrestar l'enregistrament", "startRemoteControlErrorMessage": "Una error s'es produsida en ensajar de començar la session de contraròtle a distància !", @@ -325,9 +313,47 @@ "userPassword": "senhal utilizaire", "WaitForHostMsg": "La conferéncia {{room}} a pas encara començat. Se sètz l’òst volgatz ben vos identificar. Autrament esperatz qu’arribe l’òste.", "WaitForHostMsgWOk": "La conferéncia {{room}} a pas encara començat. Se sètz l’òst volgatz ben clicar Ok per vos identificar. Autrament esperatz qu’arribe l’òste.", - "WaitingForHost": "En espèra de l’òste...", "Yes": "Òc", - "yourEntireScreen": "Vòstre ecran complet" + "yourEntireScreen": "Vòstre ecran complet", + "authenticationRequired": "Autentificacion requerida", + "login": "Connexion", + "muteEveryoneElsesVideoDialog": "Un còp la camèra desactivada, poiretz pas la reactivar, mas pòdon la reactivar quand vòlgan.", + "WaitingForHostTitle": "En espèra de l’òste...", + "embedMeeting": "Integrar la conferéncia", + "hideShareAudioHelper": "Afichar pas mai aquesta fenèstra", + "micTimeoutError": "Aviada impossibla de la font àudio. Relambi despassat !", + "moderationAudioLabel": "Permetre als convidats de se copar lo son", + "moderationVideoLabel": "Permetre als convidats d’aviar lor vidèo", + "muteEveryoneDialogModerationOn": "Los participants pòdon enviar una requèsta per parlar quand vòlgan.", + "muteEveryoneElsesVideoTitle": "Arrestar la vidèo de tot lo monde levat {{whom}} ?", + "muteEveryonesVideoTitle": "Arrestar la vidèo de tot lo monde ?", + "muteParticipantsVideoButton": "Arrestar la camèra", + "muteParticipantsVideoTitle": "Desactivar la camèra d’aqueste participant ?", + "noDropboxToken": "Cap de geton Dropbox pas valid", + "password": "Senhal", + "sessionRestarted": "Sonada reaviada pel pont", + "shareAudio": "Contunhar", + "shareAudioTitle": "Cossí partejar l’àudio", + "shareAudioWarningTitle": "Devètz arrestar lo partiment d’ecran abans lo partiment d’àudio", + "shareAudioWarningH1": "Se volètz partejar pas que l’àudio :", + "shareAudioWarningD1": "devètz arrestar lo partiment d’ecran abans lo partiment d’àudio.", + "shareMediaWarningGenericH2": "Se volètz partejar vòstre ecran e l’àudio", + "shareScreenWarningTitle": "Devètz arrestar lo partiment àudio abans lo partiment de l’ecran", + "shareScreenWarningH1": "Se volètz partejar pas que l’ecran :", + "shareScreenWarningD1": "devètz arrestar lo partiment àudio abans lo partiment de l’ecran.", + "sharedVideoLinkPlaceholder": "Ligam YouTube o ligam vidèo dirèct", + "userIdentifier": "Identificador utilizaire", + "cameraTimeoutError": "Aviada impossibla de la font vidèo. Relambi despassat !", + "muteEveryonesVideoDialog": "Los participants pòdon activar lor vidèo quand vòlgan.", + "muteEveryonesVideoDialogModerationOn": "Los participants pòdon enviar una requèsta per activar lor vidèo quand vòlgan.", + "muteEveryonesVideoDialogOk": "Desactivar", + "permissionErrorTitle": "Autorizacion requerida", + "videoLink": "ligam vidèo", + "viewUpgradeOptions": "Veire las opcions de mesa a nivèl", + "viewUpgradeOptionsTitle": "Avètz descobèrt una foncionalitat premium !", + "e2eeDisabledDueToMaxModeDescription": "Impossible d'activar lo chiframent del cap a la fin a causa d'un nombre tròp bèl de participants dins la conferéncia.", + "remoteUserControls": "Contraròtle a distància de l'utilizaire {{username}}", + "e2eeWillDisableDueToMaxModeDescription": "AVERTIMENT : lo chiframent del cap a la fin serà automaticament desactivat se mai de participants rejonhent la conferéncia." }, "dialOut": { "statusMessage": "ara es {{status}}" @@ -348,7 +374,8 @@ "good": "Bona", "rateExperience": "Mercés de donar una nòta a vòstra experiéncia", "veryBad": "Fòrça marrida", - "veryGood": "Fòrça bona" + "veryGood": "Fòrça bona", + "star": "Estela" }, "helpView": { "header": "Centre d’ajuda" @@ -388,7 +415,10 @@ "numbers": "Sonar de numèros", "password": "$t(lockRoomPasswordUppercase) :", "title": "Partejar", - "tooltip": "Partejar lo ligam e las informacions d’aquesta conferéncia" + "tooltip": "Partejar lo ligam e las informacions d’aquesta conferéncia", + "copyNumber": "Copiar lo numèro", + "inviteTextiOSPersonal": "{{name}} vos convida a la conferéncia.", + "sip": "adreça SIP" }, "inlineDialogFailure": { "msg": "Avèm un pauc patit a manténer la connexion.", @@ -419,7 +449,7 @@ "toggleScreensharing": "Caplevar entre camèra e partatge d'ecran", "toggleShortcuts": "Mostrar o amagar los acorchis clavièr", "videoMute": "Aviar o arrestar vòstra camèra", - "videoQuality": "Gerir la qualitat de las sonadas" + "toggleParticipantsPane": "Afichar o amagar lo panèl dels participants" }, "liveStreaming": { "busy": "Sèm a ensajar de liurar de ressorças flux. Mercés de tornar ensajar dins una estona.", @@ -484,7 +514,11 @@ "passwordField": "Picatz lo senhal de la conferéncia", "passwordJoinButton": "Rejónher", "reject": "Regetar", - "toggleLabel": "Activar la sala d'espèra" + "toggleLabel": "Activar la sala d'espèra", + "admit": "Acceptar", + "admitAll": "Tot acceptar", + "rejectAll": "Tot regetar", + "errorMissingPassword": "Mercés de picar lo senhal de la conferéncia" }, "localRecording": { "clientState": { @@ -529,7 +563,6 @@ "disconnected": "desconnectat", "focus": "Focus de conferéncia", "focusFail": "{{component}} es pas disponible - ensajatz tornamai dins {{ms}} sec", - "grantedTo": "Dreches moderator acordats a {{to}} !", "invitedOneMember": "{{name}} es estat convidat", "invitedThreePlusMembers": "{{name}} e {{count}} autres son estats convidats", "invitedTwoMembers": "{{first}} e {{second}} son estats convidats", @@ -555,7 +588,22 @@ "startSilentTitle": "Avètz jonch sens cap de sortida àudio !", "suboptimalBrowserWarning": "Planhèm que vòstra experiéncia de la conferéncia siá pas de las bonas. Sèm a cercar de solucions per melhorar aquò, d’aquel temps, ensajatz un dels navegators compatibles.", "suboptimalExperienceTitle": "Avertiment del navegador", - "unmute": "Restablir lo son" + "unmute": "Restablir lo son", + "allowAction": "Autorizar", + "videoMutedRemotelyTitle": "{{moderator}} a copat vòstra camèra", + "moderationInEffectTitle": "Lo moderator a amudit vòstre microfòn", + "moderationInEffectVideoTitle": "Lo moderator a blocat vòstra camèra", + "videoMutedRemotelyDescription": "La podètz totjorn tornar activar.", + "moderationInEffectCSTitle": "Lo moderator a blocat lo partiment d’ecran", + "moderationRequestFromModerator": "L’òste volriá que restabliguèssetz vòstre son", + "moderationRequestFromParticipant": "Vòl parlar", + "moderationStartedTitle": "Moderacion començada", + "moderationStoppedTitle": "Moderacion arrestada", + "moderationToggleDescription": "per {{participantDisplayName}}", + "raiseHandAction": "Levar la man", + "reactionSounds": "Desactivar los sons", + "groupTitle": "Notificacions", + "hostAskedUnmute": "Lo moderator vos que parletz" }, "passwordDigitsOnly": "Fins a {{number}} chifras", "passwordSetRemotely": "causit per qualqu'un mai", @@ -749,7 +797,6 @@ "chat": "Passar a la fenèstra chat", "document": "Tampar los documents partejats", "download": "Telecargar nòstra aplicacion", - "e2ee": "Chiframent del cap a la fin", "embedMeeting": "Conferéncia integrada", "feedback": "Daissar un comentari", "fullScreen": "Passar al ecran complèt", @@ -818,8 +865,8 @@ "muteEveryone": "Rendre mut tot lo monde", "noAudioSignalDesc": "S’avètz pas volontàriament copat lo son a partir dels paramètres sistèma o material, pensatz de cambiar d’aparelh.", "noAudioSignalDescSuggestion": "S’avètz pas volontàriament copat lo son a partir dels paramètres sistèma o material, pensatz d’utilizar un autre aparelh suggerit.", - "noAudioSignalDialInDesc": "", - "noAudioSignalDialInLinkDesc": "", + "noAudioSignalDialInDesc": "Podètz tanben sonar en utilizant :", + "noAudioSignalDialInLinkDesc": "Numèros de sonada", "noAudioSignalTitle": "I a pas cap de son en entrada del microfòn !", "noisyAudioInputDesc": "Sembla que vòstre microfòn mene bruch, pensatz de lo copar o de lo cambiar.", "noisyAudioInputTitle": "Vòstre microfòn sembla brusent !", @@ -837,15 +884,25 @@ "speakerStats": "Estatisticas parladors", "startScreenSharing": "Aviar lo partatge d’ecran", "startSubtitles": "Aviar los sostítols", - "startvideoblur": "Trebolar mon rèire-plan", "stopScreenSharing": "Arrestar lo partatge d’ecran", "stopSharedVideo": "Arrestar la vidèo Youtube", "stopSubtitles": "Arrestar los sostítols", - "stopvideoblur": "Desactivar lo borrolatge del rèire-plan", "talkWhileMutedPopup": "Ensajatz de parlar ? Vòstre microfòn es copat.", "tileViewToggle": "Activar/Desactivar la vista en mosaïc", "toggleCamera": "Passar a la camèra", - "videomute": "Aviar / Arrestar la camèra" + "videomute": "Aviar / Arrestar la camèra", + "openReactionsMenu": "Dobrir lo menú de reaccions", + "participants": "Participants", + "shareaudio": "Partejar l’àudio", + "silence": "Amudir", + "surprised": "Suspresa", + "selectBackground": "Seleccionar un rèireplan", + "audioSettings": "Paramètres àudio", + "videoSettings": "Paramètres vidèo", + "laugh": "Rire", + "muteEveryonesVideo": "Desactivar la camèra de tot lo monde", + "stopAudioSharing": "Arrestar lo partiment àudio", + "closeReactionsMenu": "Tampar lo menú de reaccions" }, "transcribing": { "ccButtonTooltip": "Aviar / Arrestat los sostítols", @@ -893,8 +950,6 @@ "ld": "Bassa definicion", "ldTooltip": "Difusion vidèo en bassa definicion", "lowDefinition": "Bassa definicion", - "onlyAudioAvailable": "Pas que l’àudio es disponible", - "onlyAudioSupported": "Sèm compatibles solament amb l’àudio dins aqueste navigator.", "sd": "SD", "sdTooltip": "Difusion vidèo en definicion estandard", "standardDefinition": "Definicion estandard" @@ -911,7 +966,10 @@ "muted": "Mut", "remoteControl": "Contraròtle alonhat", "show": "Mostrar davant", - "videomute": "Lo participant a arrestat la camèra" + "videomute": "Lo participant a arrestat la camèra", + "domuteVideo": "Desactivar la camèra", + "domuteVideoOfOthers": "Desactivar la camèra dels demai", + "videoMuted": "Camèra desactivada" }, "welcomepage": { "accessibilityLabel": { @@ -932,7 +990,6 @@ "goSmall": "Crear", "headerTitle": "Jitsi Meet", "info": "Infor", - "jitsiMeet": "Jitsi Meet", "jitsiOnMobile": "Jitsi sus mobil –telecargatz nòstra aplicacion e començatz de conferéncias de pertot", "join": "CREAR / REJÓNHER", "moderatedMessage": "O reservatz una URL de conferéncia a l'avança ont sètz l'unic moderator.", @@ -944,10 +1001,95 @@ "roomname": "Sasissètz un nom de sala", "roomNameAllowedChars": "Lo nom de la conferéncia deu pas conténer aqueles caractèrs : ?, &, :, ', \", %, #.", "roomnameHint": "Picatz lo nom o l’URL de la sala que volètz jónher. Podètz inventar un nom, cal pas que lo monde que volètz convidar lo sàpian.", - "secureMeetings": "Conferéncias seguras e de nauta qualitat", "sendFeedback": "Mandar vòstra opinion", "startMeeting": "Començar la reünion", "terms": "Tèrmes", - "title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas" + "title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas", + "addMeetingName": "Nomenar la conferéncia", + "mobileDownLoadLinkIos": "Telecargar l'aplicacion per iOS", + "mobileDownLoadLinkAndroid": "Telecargar l'aplicacion per Android", + "mobileDownLoadLinkFDroid": "Telecargar l'aplicacion per F-Droid", + "headerSubtitle": "Conferéncias securizadas e de Nauta qualitat", + "logo": { + "policyLogo": "Logotipe de la politica", + "calendar": "Logotipe Calendar", + "microsoftLogo": "Logotipe Microsoft", + "logoDeepLinking": "Logotipe Jitsi meet", + "desktopPreviewThumbnail": "Vinheta d'apercebut del burèu", + "googleLogo": "Logotipe Google" + } + }, + "virtualBackground": { + "addBackground": "Apondre un rèireplan", + "image7": "Solelh levant", + "backgroundEffectError": "Aplicacion fracassada de l’efeich al rèireplan.", + "apply": "Aplicar", + "title": "Rèireplans virtuals", + "blur": "Fosc", + "slightBlur": "Fosc leugièr", + "removeBackground": "Suprimir lo rèireplan", + "pleaseWait": "Esperatz...", + "none": "Cap", + "uploadedImage": "Imatge enviat {{index}}", + "deleteImage": "Suprimir l’imatge", + "image1": "Plaja", + "image2": "Paret blanca neutra", + "image3": "Sala voida blanca", + "image4": "Lampadari negre", + "image5": "Montanha", + "image6": "Forèst ", + "desktopShareError": "Creacion impossibla d’un partiment de burèu", + "desktopShare": "Partiment de burèu", + "webAssemblyWarning": "WebAssembly pas pres en carga" + }, + "participantsPane": { + "headings": { + "lobby": "Sala d’espèra ({{count}})", + "participantsList": "Participants de la conferéncia ({{count}})", + "waitingLobby": "Dins la sala d'espèra ({{count}})" + }, + "close": "Tancar", + "header": "Participants", + "actions": { + "allow": "Permetre als convidats de :", + "audioModeration": "Se tornar lo son", + "blockEveryoneMicCamera": "Blocar lo microfòn e la camèra del monde", + "invite": "Convidar qualqu'un", + "askUnmute": "Demandar a restablir lo son", + "mute": "Amudir", + "muteAll": "Amudir tot lo monde", + "muteEveryoneElse": "Amudir tot los demai", + "stopEveryonesVideo": "Arrestar la vidèo de tot lo monde", + "stopVideo": "Arrestar la vidèo", + "unblockEveryoneMicCamera": "Desblocar lo microfòn e la camèra de tot lo monde", + "videoModeration": "Aviar lor vidèo" + } + }, + "jitsiHome": "{{logo}} Logotipe, mena a la pagina d'acuèlh", + "polls": { + "create": { + "addOption": "Apondre una opcion", + "answerPlaceholder": "Opcion {{index}}", + "create": "Crear un sondatge", + "cancel": "Anullar", + "pollOption": "Opcion sondarge {{index}}", + "pollQuestion": "Question del sondatge", + "questionPlaceholder": "Pausar una question", + "removeOption": "Suprimir l'opcion", + "send": "Enviar" + }, + "answer": { + "skip": "Sautar", + "submit": "Enviar" + }, + "results": { + "vote": "Votar", + "changeVote": "Cambiar lo vote", + "hideDetailedResults": "Rescondre los detalhs", + "showDetailedResults": "Mostrar los detalhs" + }, + "notification": { + "title": "Un sondatge novèl es estat apondut a la conferéncia" + } } } diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index 74702332ae912..19cbeec0541c1 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -2,6 +2,7 @@ import '../authentication/middleware'; import '../base/devices/middleware'; +import '../dynamic-branding/middleware'; import '../e2ee/middleware'; import '../external-api/middleware'; import '../keyboard-shortcuts/middleware'; diff --git a/react/features/base/config/configWhitelist.js b/react/features/base/config/configWhitelist.js index fa65bed93856f..23f834896938e 100644 --- a/react/features/base/config/configWhitelist.js +++ b/react/features/base/config/configWhitelist.js @@ -84,6 +84,7 @@ export default [ 'disableAEC', 'disableAGC', 'disableAP', + 'disableAddingBackgroundImages', 'disableAudioLevels', 'disableChatSmileys', 'disableDeepLinking', @@ -188,6 +189,7 @@ export default [ 'subject', 'testing', 'toolbarButtons', + 'toolbarConfig', 'useHostPageLocalStorage', 'useTurnUdp', 'videoQuality.persist', diff --git a/react/features/base/config/reducer.js b/react/features/base/config/reducer.js index a22edffa28719..0a7ac85f088a3 100644 --- a/react/features/base/config/reducer.js +++ b/react/features/base/config/reducer.js @@ -227,6 +227,28 @@ function _translateLegacyConfig(oldValue: Object) { newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS; } + if (!oldValue.toolbarConfig) { + oldValue.toolbarConfig = {}; + } + + if (typeof oldValue.toolbarConfig.alwaysVisible !== 'boolean' + && typeof interfaceConfig === 'object' + && typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE === 'boolean') { + newValue.toolbarConfig.alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE; + } + + if (typeof oldValue.toolbarConfig.initialTimeout !== 'number' + && typeof interfaceConfig === 'object' + && typeof interfaceConfig.INITIAL_TOOLBAR_TIMEOUT === 'number') { + newValue.toolbarConfig.initialTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT; + } + + if (typeof oldValue.toolbarConfig.timeout !== 'number' + && typeof interfaceConfig === 'object' + && typeof interfaceConfig.TOOLBAR_TIMEOUT === 'number') { + newValue.toolbarConfig.timeout = interfaceConfig.TOOLBAR_TIMEOUT; + } + const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]); if (filteredConferenceInfo.length) { diff --git a/react/features/base/tracks/actions.js b/react/features/base/tracks/actions.js index 4ee1743fc87b9..2933bc938bd44 100644 --- a/react/features/base/tracks/actions.js +++ b/react/features/base/tracks/actions.js @@ -261,17 +261,20 @@ export function showNoDataFromSourceVideoError(jitsiTrack) { * * @param {boolean} enabled - The state to toggle screen sharing to. * @param {boolean} audioOnly - Only share system audio. + * @param {boolean} ignoreDidHaveVideo - Wether or not to ignore if video was on when sharing started. * @returns {{ * type: TOGGLE_SCREENSHARING, * on: boolean, - * audioOnly: boolean + * audioOnly: boolean, + * ignoreDidHaveVideo: boolean * }} */ -export function toggleScreensharing(enabled, audioOnly = false) { +export function toggleScreensharing(enabled, audioOnly = false, ignoreDidHaveVideo = false) { return { type: TOGGLE_SCREENSHARING, enabled, - audioOnly + audioOnly, + ignoreDidHaveVideo }; } diff --git a/react/features/base/tracks/middleware.js b/react/features/base/tracks/middleware.js index 7f6bf3f7a39c6..65b0fee2103cf 100644 --- a/react/features/base/tracks/middleware.js +++ b/react/features/base/tracks/middleware.js @@ -149,10 +149,11 @@ MiddlewareRegistry.register(store => next => action => { return; } - const { enabled, audioOnly } = action; + const { enabled, audioOnly, ignoreDidHaveVideo } = action; APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, { enabled, - audioOnly }); + audioOnly, + ignoreDidHaveVideo }); } break; @@ -178,7 +179,7 @@ MiddlewareRegistry.register(store => next => action => { } else if (jitsiTrack.isLocal() && !(jitsiTrack.videoType === VIDEO_TYPE.DESKTOP)) { APP.conference.setVideoMuteStatus(); } else if (jitsiTrack.isLocal() && muted && jitsiTrack.videoType === VIDEO_TYPE.DESKTOP) { - store.dispatch(toggleScreensharing(false)); + store.dispatch(toggleScreensharing(false, false, true)); } else { APP.UI.setVideoMuted(participantID); } diff --git a/react/features/dynamic-branding/functions.js b/react/features/dynamic-branding/functions.js index 8363f87dea1d3..dd5be7142fa2c 100644 --- a/react/features/dynamic-branding/functions.js +++ b/react/features/dynamic-branding/functions.js @@ -7,8 +7,8 @@ * @param {string} path - The URL path. * @returns {string} */ -export function extractFqnFromPath(path: string) { - const parts = path.split('/'); +export function extractFqnFromPath() { + const parts = window.location.pathname.split('/'); const len = parts.length; return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : ''; @@ -28,7 +28,7 @@ export function getDynamicBrandingUrl(state: Object) { } const baseUrl = state['features/base/config'].brandingDataUrl; - const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname); + const fqn = extractFqnFromPath(); if (baseUrl && fqn) { return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`; diff --git a/react/features/dynamic-branding/middleware.js b/react/features/dynamic-branding/middleware.js new file mode 100644 index 0000000000000..184103d0a21d0 --- /dev/null +++ b/react/features/dynamic-branding/middleware.js @@ -0,0 +1,18 @@ +// @flow + +import { APP_WILL_MOUNT } from '../base/app'; +import { MiddlewareRegistry } from '../base/redux'; + +import { fetchCustomBrandingData } from './actions'; + +MiddlewareRegistry.register(store => next => action => { + switch (action.type) { + case APP_WILL_MOUNT: { + + store.dispatch(fetchCustomBrandingData()); + break; + } + } + + return next(action); +}); diff --git a/react/features/dynamic-branding/reducer.js b/react/features/dynamic-branding/reducer.js index 6d13327ee0c18..ee0a9c5bcf743 100644 --- a/react/features/dynamic-branding/reducer.js +++ b/react/features/dynamic-branding/reducer.js @@ -1,6 +1,7 @@ // @flow import { ReducerRegistry } from '../base/redux'; +import { type Image } from '../virtual-background/constants'; import { SET_DYNAMIC_BRANDING_DATA, @@ -113,7 +114,15 @@ const DEFAULT_STATE = { * @public * @type {boolean} */ - useDynamicBrandingData: false + useDynamicBrandingData: false, + + /** + * An array of images to be used as virtual backgrounds instead of the default ones. + * + * @public + * @type {Array} + */ + virtualBackgrounds: [] }; /** @@ -131,7 +140,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { inviteDomain, logoClickUrl, logoImageUrl, - premeetingBackground + premeetingBackground, + virtualBackgrounds } = action.value; return { @@ -146,7 +156,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { premeetingBackground, customizationFailed: false, customizationReady: true, - useDynamicBrandingData: true + useDynamicBrandingData: true, + virtualBackgrounds: formatImages(virtualBackgrounds || []) }; } case SET_DYNAMIC_BRANDING_FAILED: { @@ -166,3 +177,30 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { return state; }); + +/** + * Transforms the branding images into an array of Images objects ready + * to be used as virtual backgrounds. + * + * @param {Array} images - + * @private + * @returns {{Props}} + */ +function formatImages(images: Array | Array): Array { + return images.map((img, i) => { + let src; + let tooltip; + + if (typeof img === 'object') { + ({ src, tooltip } = img); + } else { + src = img; + } + + return { + id: `branding-${i}`, + src, + tooltip + }; + }); +} diff --git a/react/features/feedback/actions.js b/react/features/feedback/actions.js index 3d96ddd064f31..a7edbe5b50d52 100644 --- a/react/features/feedback/actions.js +++ b/react/features/feedback/actions.js @@ -131,7 +131,7 @@ export function sendJaasFeedbackMetadata(conference: Object, feedback: Object) { return Promise.resolve(); } - const meetingFqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname); + const meetingFqn = extractFqnFromPath(); const feedbackData = { ...feedback, sessionId: conference.sessionId, diff --git a/react/features/large-video/components/LargeVideo.web.js b/react/features/large-video/components/LargeVideo.web.js index 1ebf763be5cb5..66f91e384ae1b 100644 --- a/react/features/large-video/components/LargeVideo.web.js +++ b/react/features/large-video/components/LargeVideo.web.js @@ -5,7 +5,6 @@ import React, { Component } from 'react'; import { Watermarks } from '../../base/react'; import { connect } from '../../base/redux'; import { setColorAlpha } from '../../base/util'; -import { fetchCustomBrandingData } from '../../dynamic-branding'; import { SharedVideo } from '../../shared-video/components/web'; import { Captions } from '../../subtitles/'; @@ -28,11 +27,6 @@ type Props = { */ _customBackgroundImageUrl: string, - /** - * Fetches the branding data. - */ - _fetchCustomBrandingData: Function, - /** * Prop that indicates whether the chat is open. */ @@ -52,14 +46,6 @@ type Props = { * @extends Component */ class LargeVideo extends Component { - /** - * Implements React's {@link Component#componentDidMount}. - * - * @inheritdoc - */ - componentDidMount() { - this.props._fetchCustomBrandingData(); - } /** * Implements React's {@link Component#render()}. @@ -167,8 +153,4 @@ function _mapStateToProps(state) { }; } -const _mapDispatchToProps = { - _fetchCustomBrandingData: fetchCustomBrandingData -}; - -export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo); +export default connect(_mapStateToProps)(LargeVideo); diff --git a/react/features/lobby/actions.any.js b/react/features/lobby/actions.any.js index 1c5b6ff659a2b..e7ae64ccffb69 100644 --- a/react/features/lobby/actions.any.js +++ b/react/features/lobby/actions.any.js @@ -3,10 +3,202 @@ import { type Dispatch } from 'redux'; import { - getCurrentConference + conferenceWillJoin, + getCurrentConference, + sendLocalParticipant, + setPassword } from '../base/conference'; +import { getLocalParticipant } from '../base/participants'; -import { SET_LOBBY_VISIBILITY } from './actionTypes'; +import { + KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, + KNOCKING_PARTICIPANT_LEFT, + SET_KNOCKING_STATE, + SET_LOBBY_MODE_ENABLED, + SET_PASSWORD_JOIN_FAILED, + SET_LOBBY_VISIBILITY +} from './actionTypes'; + +/** + * Tries to join with a preset password. + * + * @param {string} password - The password to join with. + * @returns {Function} + */ +export function joinWithPassword(password: string) { + return async (dispatch: Dispatch, getState: Function) => { + const conference = getCurrentConference(getState); + + dispatch(setPassword(conference, conference.join, password)); + }; +} + +/** + * Action to be dispatched when a knocking poarticipant leaves before any response. + * + * @param {string} id - The ID of the participant. + * @returns {{ + * id: string, + * type: KNOCKING_PARTICIPANT_LEFT + * }} + */ +export function knockingParticipantLeft(id: string) { + return { + id, + type: KNOCKING_PARTICIPANT_LEFT + }; +} + +/** + * Action to be executed when a participant starts knocking or an already knocking participant gets updated. + * + * @param {Object} participant - The knocking participant. + * @returns {{ + * participant: Object, + * type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED + * }} + */ +export function participantIsKnockingOrUpdated(participant: Object) { + return { + participant, + type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED + }; +} + +/** + * Approves (lets in) or rejects a knocking participant. + * + * @param {string} id - The id of the knocking participant. + * @param {boolean} approved - True if the participant is approved, false otherwise. + * @returns {Function} + */ +export function setKnockingParticipantApproval(id: string, approved: boolean) { + return async (dispatch: Dispatch, getState: Function) => { + const conference = getCurrentConference(getState); + + if (conference) { + if (approved) { + conference.lobbyApproveAccess(id); + } else { + conference.lobbyDenyAccess(id); + } + } + }; +} + +/** + * Action used to admit multiple participants in the conference. + * + * @param {Array} participants - A list of knocking participants. + * @returns {void} + */ +export function admitMultiple(participants: Array) { + return (dispatch: Function, getState: Function) => { + const conference = getCurrentConference(getState); + + participants.forEach(p => { + conference.lobbyApproveAccess(p.id); + }); + }; +} + +/** + * Approves the request of a knocking participant to join the meeting. + * + * @param {string} id - The id of the knocking participant. + * @returns {Function} + */ +export function approveKnockingParticipant(id: string) { + return (dispatch: Dispatch, getState: Function) => { + const conference = getCurrentConference(getState); + + conference && conference.lobbyApproveAccess(id); + }; +} + +/** + * Denies the request of a knocking participant to join the meeting. + * + * @param {string} id - The id of the knocking participant. + * @returns {Function} + */ +export function rejectKnockingParticipant(id: string) { + return (dispatch: Dispatch, getState: Function) => { + const conference = getCurrentConference(getState); + + conference && conference.lobbyDenyAccess(id); + }; +} + +/** + * Action to set the knocking state of the participant. + * + * @param {boolean} knocking - The new state. + * @returns {{ + * state: boolean, + * type: SET_KNOCKING_STATE + * }} + */ +export function setKnockingState(knocking: boolean) { + return { + knocking, + type: SET_KNOCKING_STATE + }; +} + +/** + * Action to set the new state of the lobby mode. + * + * @param {boolean} enabled - The new state to set. + * @returns {{ + * enabled: boolean, + * type: SET_LOBBY_MODE_ENABLED + * }} + */ +export function setLobbyModeEnabled(enabled: boolean) { + return { + enabled, + type: SET_LOBBY_MODE_ENABLED + }; +} + +/** + * Action to be dispatched when we failed to join with a password. + * + * @param {boolean} failed - True of recent password join failed. + * @returns {{ + * failed: boolean, + * type: SET_PASSWORD_JOIN_FAILED + * }} + */ +export function setPasswordJoinFailed(failed: boolean) { + return { + failed, + type: SET_PASSWORD_JOIN_FAILED + }; +} + +/** + * Starts knocking and waiting for approval. + * + * @returns {Function} + */ +export function startKnocking() { + return async (dispatch: Dispatch, getState: Function) => { + const state = getState(); + const { membersOnly } = state['features/base/conference']; + const localParticipant = getLocalParticipant(state); + + dispatch(conferenceWillJoin(membersOnly)); + + // We need to update the conference object with the current display name, if approved + // we want to send that display name, it was not updated in case when pre-join is disabled + sendLocalParticipant(state, membersOnly); + + membersOnly.joinLobby(localParticipant.name, localParticipant.email); + dispatch(setKnockingState(true)); + }; +} /** * Action to toggle lobby mode on or off. diff --git a/react/features/lobby/actions.native.js b/react/features/lobby/actions.native.js index 19f68c80873ef..dd72f76af004d 100644 --- a/react/features/lobby/actions.native.js +++ b/react/features/lobby/actions.native.js @@ -1,10 +1,24 @@ // @flow +import { type Dispatch } from 'redux'; + +import { appNavigate } from '../app/actions'; import { openDialog } from '../base/dialog'; import { DisableLobbyModeDialog, EnableLobbyModeDialog } from './components/native'; -export * from './actions.web'; +export * from './actions.any'; + +/** + * Cancels the ongoing knocking and abandons the join flow. + * + * @returns {Function} + */ +export function cancelKnocking() { + return async (dispatch: Dispatch) => { + dispatch(appNavigate(undefined)); + }; +} /** * Action to show the dialog to disable lobby mode. diff --git a/react/features/lobby/actions.web.js b/react/features/lobby/actions.web.js index 48f37a7788022..63d4b07f002ba 100644 --- a/react/features/lobby/actions.web.js +++ b/react/features/lobby/actions.web.js @@ -2,223 +2,22 @@ import { type Dispatch } from 'redux'; -import { appNavigate, maybeRedirectToWelcomePage } from '../app/actions'; -import { - conferenceWillJoin, - getCurrentConference, - sendLocalParticipant, - setPassword -} from '../base/conference'; -import { getLocalParticipant } from '../base/participants'; -export * from './actions.any'; +import { maybeRedirectToWelcomePage } from '../app/actions'; -import { - KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED, - KNOCKING_PARTICIPANT_LEFT, - SET_KNOCKING_STATE, - SET_LOBBY_MODE_ENABLED, - SET_PASSWORD_JOIN_FAILED -} from './actionTypes'; +export * from './actions.any'; declare var APP: Object; /** - * Cancels the ongoing knocking and abandones the join flow. + * Cancels the ongoing knocking and abandons the join flow. * * @returns {Function} */ export function cancelKnocking() { return async (dispatch: Dispatch) => { - if (typeof APP !== 'undefined') { - // when we are redirecting the library should handle any - // unload and clean of the connection. - APP.API.notifyReadyToClose(); - dispatch(maybeRedirectToWelcomePage()); - - return; - } - - dispatch(appNavigate(undefined)); - }; -} - -/** - * Tries to join with a preset password. - * - * @param {string} password - The password to join with. - * @returns {Function} - */ -export function joinWithPassword(password: string) { - return async (dispatch: Dispatch, getState: Function) => { - const conference = getCurrentConference(getState); - - dispatch(setPassword(conference, conference.join, password)); - }; -} - -/** - * Action to be dispatched when a knocking poarticipant leaves before any response. - * - * @param {string} id - The ID of the participant. - * @returns {{ - * id: string, - * type: KNOCKING_PARTICIPANT_LEFT - * }} - */ -export function knockingParticipantLeft(id: string) { - return { - id, - type: KNOCKING_PARTICIPANT_LEFT - }; -} - -/** - * Action to be executed when a participant starts knocking or an already knocking participant gets updated. - * - * @param {Object} participant - The knocking participant. - * @returns {{ - * participant: Object, - * type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED - * }} - */ -export function participantIsKnockingOrUpdated(participant: Object) { - return { - participant, - type: KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED - }; -} - -/** - * Approves (lets in) or rejects a knocking participant. - * - * @param {string} id - The id of the knocking participant. - * @param {boolean} approved - True if the participant is approved, false otherwise. - * @returns {Function} - */ -export function setKnockingParticipantApproval(id: string, approved: boolean) { - return async (dispatch: Dispatch, getState: Function) => { - const conference = getCurrentConference(getState); - - if (conference) { - if (approved) { - conference.lobbyApproveAccess(id); - } else { - conference.lobbyDenyAccess(id); - } - } - }; -} - -/** - * Action used to admit multiple participants in the conference. - * - * @param {Array} participants - A list of knocking participants. - * @returns {void} - */ -export function admitMultiple(participants: Array) { - return (dispatch: Function, getState: Function) => { - const conference = getCurrentConference(getState); - - participants.forEach(p => { - conference.lobbyApproveAccess(p.id); - }); - }; -} - -/** - * Approves the request of a knocking participant to join the meeting. - * - * @param {string} id - The id of the knocking participant. - * @returns {Function} - */ -export function approveKnockingParticipant(id: string) { - return (dispatch: Dispatch, getState: Function) => { - const conference = getCurrentConference(getState); - - conference && conference.lobbyApproveAccess(id); - }; -} - -/** - * Denies the request of a knocking participant to join the meeting. - * - * @param {string} id - The id of the knocking participant. - * @returns {Function} - */ -export function rejectKnockingParticipant(id: string) { - return (dispatch: Dispatch, getState: Function) => { - const conference = getCurrentConference(getState); - - conference && conference.lobbyDenyAccess(id); - }; -} - -/** - * Action to set the knocking state of the participant. - * - * @param {boolean} knocking - The new state. - * @returns {{ - * state: boolean, - * type: SET_KNOCKING_STATE - * }} - */ -export function setKnockingState(knocking: boolean) { - return { - knocking, - type: SET_KNOCKING_STATE - }; -} - -/** - * Action to set the new state of the lobby mode. - * - * @param {boolean} enabled - The new state to set. - * @returns {{ - * enabled: boolean, - * type: SET_LOBBY_MODE_ENABLED - * }} - */ -export function setLobbyModeEnabled(enabled: boolean) { - return { - enabled, - type: SET_LOBBY_MODE_ENABLED - }; -} - -/** - * Action to be dispatched when we failed to join with a password. - * - * @param {boolean} failed - True of recent password join failed. - * @returns {{ - * failed: boolean, - * type: SET_PASSWORD_JOIN_FAILED - * }} - */ -export function setPasswordJoinFailed(failed: boolean) { - return { - failed, - type: SET_PASSWORD_JOIN_FAILED - }; -} - -/** - * Starts knocking and waiting for approval. - * - * @returns {Function} - */ -export function startKnocking() { - return async (dispatch: Dispatch, getState: Function) => { - const state = getState(); - const { membersOnly } = state['features/base/conference']; - const localParticipant = getLocalParticipant(state); - - dispatch(conferenceWillJoin(membersOnly)); - - // We need to update the conference object with the current display name, if approved - // we want to send that display name, it was not updated in case when pre-join is disabled - sendLocalParticipant(state, membersOnly); - - membersOnly.joinLobby(localParticipant.name, localParticipant.email); - dispatch(setKnockingState(true)); + // when we are redirecting the library should handle any + // unload and clean of the connection. + APP.API.notifyReadyToClose(); + dispatch(maybeRedirectToWelcomePage()); }; } diff --git a/react/features/participants-pane/components/web/LobbyParticipantItem.js b/react/features/participants-pane/components/web/LobbyParticipantItem.js index 45e6bbe8a3a30..4adf9cad45ace 100644 --- a/react/features/participants-pane/components/web/LobbyParticipantItem.js +++ b/react/features/participants-pane/components/web/LobbyParticipantItem.js @@ -33,7 +33,7 @@ export const LobbyParticipantItem = ({ openDrawerForParticipant }: Props) => { const { id } = p; - const [ admit ] = useLobbyActions({ participantID: id }); + const [ admit, reject ] = useLobbyActions({ participantID: id }); const { t } = useTranslation(); return ( @@ -48,6 +48,11 @@ export const LobbyParticipantItem = ({ raisedHand = { p.raisedHand } videoMediaState = { MEDIA_STATE.NONE } youText = { t('chat.you') }> + + {t('lobby.reject')} + diff --git a/react/features/reactions/functions.any.js b/react/features/reactions/functions.any.js index 6fb913814b7ab..7e1952c7dd336 100644 --- a/react/features/reactions/functions.any.js +++ b/react/features/reactions/functions.any.js @@ -55,7 +55,6 @@ export async function sendReactionsWebhook(state: Object, reactions: Array { const dominantSpeakerTime = statsModel.getTotalDominantSpeakerTime(); const hasLeft = statsModel.hasLeft(); - let displayName; - - if (statsModel.isLocalStats()) { - const { t } = this.props; - const meString = t('me'); - - displayName = this.props._localDisplayName; - displayName - = displayName ? `${displayName} (${meString})` : meString; - } else { - displayName - = this.props._stats[userId].getDisplayName() - || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME; - } - return ( { * @private */ _updateStats() { - this.props.dispatch(initUpdateStats(() => this.props.conference.getSpeakerStats())); + this.props.dispatch(initUpdateStats(() => this._getSpeakerStats())); + } + + /** + * Update the internal state with the latest speaker stats. + * + * @returns {Object} + * @private + */ + _getSpeakerStats() { + const stats = { ...this.props.conference.getSpeakerStats() }; + + for (const userId in stats) { + if (stats[userId]) { + if (stats[userId].isLocalStats()) { + const { t } = this.props; + const meString = t('me'); + + stats[userId].setDisplayName( + this.props._localDisplayName + ? `${this.props._localDisplayName} (${meString})` + : meString + ); + } + + if (!stats[userId].getDisplayName()) { + stats[userId].setDisplayName( + interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME + ); + } + } + } + + return stats; } } @@ -198,8 +216,7 @@ class SpeakerStats extends Component { * @private * @returns {{ * _localDisplayName: ?string, - * _stats: Object, - * _criteria: string, + * _stats: Object * }} */ function _mapStateToProps(state) { @@ -213,8 +230,7 @@ function _mapStateToProps(state) { * @type {string|undefined} */ _localDisplayName: localParticipant && localParticipant.name, - _stats: getSpeakerStats(state), - _criteria: getSearchCriteria(state) + _stats: getSpeakerStats(state) }; } diff --git a/react/features/speaker-stats/functions.js b/react/features/speaker-stats/functions.js index 413cce0af747c..ecd81899de19b 100644 --- a/react/features/speaker-stats/functions.js +++ b/react/features/speaker-stats/functions.js @@ -43,10 +43,10 @@ export function getSpeakerStats(state: Object) { * Gets speaker stats search criteria. * * @param {*} state - The redux state. - * @returns {string} - The search criteria. + * @returns {string | null} - The search criteria. */ export function getSearchCriteria(state: Object) { - return state['features/speaker-stats']?.criteria ?? ''; + return state['features/speaker-stats']?.criteria; } /** @@ -161,16 +161,14 @@ export function filterBySearchCriteria(state: Object, stats: ?Object) { const filteredStats = _.cloneDeep(stats ?? getSpeakerStats(state)); const criteria = getSearchCriteria(state); - if (criteria) { + if (criteria !== null) { const searchRegex = new RegExp(criteria, 'gi'); for (const id in filteredStats) { if (filteredStats[id].hasOwnProperty('_isLocalStats')) { const name = filteredStats[id].getDisplayName(); - if (!name || !name.match(searchRegex)) { - filteredStats[id].hidden = true; - } + filteredStats[id].hidden = !name || !name.match(searchRegex); } } } diff --git a/react/features/speaker-stats/reducer.js b/react/features/speaker-stats/reducer.js index d27f94d157d32..29aff93cca50a 100644 --- a/react/features/speaker-stats/reducer.js +++ b/react/features/speaker-stats/reducer.js @@ -18,7 +18,7 @@ import { const INITIAL_STATE = { stats: {}, pendingReorder: true, - criteria: '' + criteria: null }; ReducerRegistry.register('features/speaker-stats', (state = _getInitialState(), action) => { diff --git a/react/features/toolbox/actionTypes.js b/react/features/toolbox/actionTypes.js index e01f0ff1d5fc8..4f23c8d9b8270 100644 --- a/react/features/toolbox/actionTypes.js +++ b/react/features/toolbox/actionTypes.js @@ -55,16 +55,6 @@ export const SET_OVERFLOW_MENU_VISIBLE = 'SET_OVERFLOW_MENU_VISIBLE'; */ export const SET_TOOLBAR_HOVERED = 'SET_TOOLBAR_HOVERED'; -/** - * The type of the action which sets the permanent visibility of the Toolbox. - * - * { - * type: SET_TOOLBOX_ALWAYS_VISIBLE, - * alwaysVisible: boolean - * } - */ -export const SET_TOOLBOX_ALWAYS_VISIBLE = 'SET_TOOLBOX_ALWAYS_VISIBLE'; - /** * The type of the (redux) action which enables/disables the Toolbox. * @@ -87,17 +77,6 @@ export const SET_TOOLBOX_ENABLED = 'SET_TOOLBOX_ENABLED'; */ export const SET_TOOLBOX_TIMEOUT = 'SET_TOOLBOX_TIMEOUT'; -/** - * The type of the action which sets the delay in milliseconds after which - * the Toolbox visibility is to be changed. - * - * { - * type: SET_TOOLBOX_TIMEOUT_MS, - * timeoutMS: number - * } - */ -export const SET_TOOLBOX_TIMEOUT_MS = 'SET_TOOLBOX_TIMEOUT_MS'; - /** * The type of the (redux) action which shows/hides the Toolbox. * diff --git a/react/features/toolbox/actions.any.js b/react/features/toolbox/actions.any.js new file mode 100644 index 0000000000000..0071771726054 --- /dev/null +++ b/react/features/toolbox/actions.any.js @@ -0,0 +1,45 @@ +// @flow + +import type { Dispatch } from 'redux'; + +import { + SET_TOOLBOX_ENABLED, + SET_TOOLBOX_VISIBLE +} from './actionTypes'; + +/** + * Enables/disables the toolbox. + * + * @param {boolean} enabled - True to enable the toolbox or false to disable it. + * @returns {{ + * type: SET_TOOLBOX_ENABLED, + * enabled: boolean + * }} + */ +export function setToolboxEnabled(enabled: boolean): Object { + return { + type: SET_TOOLBOX_ENABLED, + enabled + }; +} + +/** + * Shows/hides the toolbox. + * + * @param {boolean} visible - True to show the toolbox or false to hide it. + * @returns {Function} + */ +export function setToolboxVisible(visible: boolean): Object { + return (dispatch: Dispatch, getState: Function) => { + const { toolbarConfig: { alwaysVisible } } = getState()['features/base/config']; + + if (!visible && alwaysVisible) { + return; + } + + dispatch({ + type: SET_TOOLBOX_VISIBLE, + visible + }); + }; +} diff --git a/react/features/toolbox/actions.native.js b/react/features/toolbox/actions.native.js index 0cb56393d9cd1..0bc714c08ddc6 100644 --- a/react/features/toolbox/actions.native.js +++ b/react/features/toolbox/actions.native.js @@ -1,160 +1,28 @@ -/* @flow */ +// @flow -import { - CLEAR_TOOLBOX_TIMEOUT, - SET_OVERFLOW_MENU_VISIBLE, - SET_TOOLBAR_HOVERED, - SET_TOOLBOX_ALWAYS_VISIBLE, - SET_TOOLBOX_ENABLED, - SET_TOOLBOX_TIMEOUT, - SET_TOOLBOX_TIMEOUT_MS, - SET_TOOLBOX_VISIBLE, - TOGGLE_TOOLBOX_VISIBLE -} from './actionTypes'; +import type { Dispatch } from 'redux'; +import { TOGGLE_TOOLBOX_VISIBLE } from './actionTypes'; -/** - * Signals that toolbox timeout should be cleared. - * - * @returns {{ - * type: CLEAR_TOOLBOX_TIMEOUT - * }} - */ -export function clearToolboxTimeout(): Object { - return { - type: CLEAR_TOOLBOX_TIMEOUT - }; -} - -/** - * Shows/hides the overflow menu. - * - * @param {boolean} visible - True to show it or false to hide it. - * @returns {{ - * type: SET_OVERFLOW_MENU_VISIBLE, - * visible: boolean - * }} - */ -export function setOverflowMenuVisible(visible: boolean): Object { - return { - type: SET_OVERFLOW_MENU_VISIBLE, - visible - }; -} - -/** - * Signals that toolbar is hovered value should be changed. - * - * @param {boolean} hovered - Flag showing whether toolbar is hovered. - * @returns {{ - * type: SET_TOOLBAR_HOVERED, - * hovered: boolean - * }} - */ -export function setToolbarHovered(hovered: boolean): Object { - return { - type: SET_TOOLBAR_HOVERED, - hovered - }; -} - -/** - * Signals that always visible toolbars value should be changed. - * - * @param {boolean} alwaysVisible - Value to be set in redux store. - * @returns {{ - * type: SET_TOOLBOX_ALWAYS_VISIBLE, - * alwaysVisible: boolean - * }} - */ -export function setToolboxAlwaysVisible(alwaysVisible: boolean): Object { - return { - type: SET_TOOLBOX_ALWAYS_VISIBLE, - alwaysVisible - }; -} - -/* eslint-disable flowtype/space-before-type-colon */ - -/** - * Enables/disables the toolbox. - * - * @param {boolean} enabled - True to enable the toolbox or false to disable it. - * @returns {{ - * type: SET_TOOLBOX_ENABLED, - * enabled: boolean - * }} - */ -export function setToolboxEnabled(enabled: boolean): Object { - return { - type: SET_TOOLBOX_ENABLED, - enabled - }; -} - -/** - * Dispatches an action which sets new timeout and clears the previous one. - * - * @param {Function} handler - Function to be invoked after the timeout. - * @param {number} timeoutMS - Delay. - * @returns {{ - * type: SET_TOOLBOX_TIMEOUT, - * handler: Function, - * timeoutMS: number - * }} - */ -export function setToolboxTimeout(handler: Function, timeoutMS: number) - : Object { - return { - type: SET_TOOLBOX_TIMEOUT, - handler, - timeoutMS - }; -} - -/* eslint-enable flowtype/space-before-type-colon */ - -/** - * Dispatches an action which sets new toolbox timeout value. - * - * @param {number} timeoutMS - Delay. - * @returns {{ - * type: SET_TOOLBOX_TIMEOUT_MS, - * timeoutMS: number - * }} - */ -export function setToolboxTimeoutMS(timeoutMS: number): Object { - return { - type: SET_TOOLBOX_TIMEOUT_MS, - timeoutMS - }; -} - -/** - * Shows/hides the toolbox. - * - * @param {boolean} visible - True to show the toolbox or false to hide it. - * @returns {{ - * type: SET_TOOLBOX_VISIBLE, - * visible: boolean - * }} - */ -export function setToolboxVisible(visible: boolean): Object { - return { - type: SET_TOOLBOX_VISIBLE, - visible - }; -} +export * from './actions.any'; /** * Action to toggle the toolbox visibility. * - * @returns {{ - * type: TOGGLE_TOOLBOX_VISIBLE - * }} + * @returns {Function} */ export function toggleToolboxVisible() { - return { - type: TOGGLE_TOOLBOX_VISIBLE + return (dispatch: Dispatch, getState: Function) => { + const state = getState(); + const { toolbarConfig: { alwaysVisible } } = state['features/base/config']; + const { visible } = state['features/toolbox']; + + if (visible && alwaysVisible) { + return; + } + + dispatch({ + type: TOGGLE_TOOLBOX_VISIBLE + }); }; } diff --git a/react/features/toolbox/actions.web.js b/react/features/toolbox/actions.web.js index ec4f4f594a518..bf148d4129be9 100644 --- a/react/features/toolbox/actions.web.js +++ b/react/features/toolbox/actions.web.js @@ -2,23 +2,22 @@ import type { Dispatch } from 'redux'; +import { overwriteConfig } from '../base/config'; import { isLayoutTileView } from '../video-layout'; import { + CLEAR_TOOLBOX_TIMEOUT, FULL_SCREEN_CHANGED, SET_FULL_SCREEN, - SET_OVERFLOW_DRAWER + SET_OVERFLOW_DRAWER, + SET_OVERFLOW_MENU_VISIBLE, + SET_TOOLBAR_HOVERED, + SET_TOOLBOX_TIMEOUT } from './actionTypes'; -import { - clearToolboxTimeout, - setToolboxTimeout, - setToolboxTimeoutMS, - setToolboxVisible -} from './actions.native'; - -declare var interfaceConfig: Object; +import { setToolboxVisible } from './actions'; +import { getToolbarTimeout } from './functions'; -export * from './actions.native'; +export * from './actions.any'; /** * Docks/undocks the Toolbox. @@ -28,7 +27,9 @@ export * from './actions.native'; */ export function dockToolbox(dock: boolean): Function { return (dispatch: Dispatch, getState: Function) => { - const { timeoutMS, visible } = getState()['features/toolbox']; + const state = getState(); + const { visible } = state['features/toolbox']; + const toolbarTimeout = getToolbarTimeout(state); if (dock) { // First make sure the toolbox is shown. @@ -39,7 +40,7 @@ export function dockToolbox(dock: boolean): Function { dispatch( setToolboxTimeout( () => dispatch(hideToolbox()), - timeoutMS)); + toolbarTimeout)); } else { dispatch(showToolbox()); } @@ -73,11 +74,9 @@ export function fullScreenChanged(fullScreen: boolean) { export function hideToolbox(force: boolean = false): Function { return (dispatch: Dispatch, getState: Function) => { const state = getState(); - const { - alwaysVisible, - hovered, - timeoutMS - } = state['features/toolbox']; + const { toolbarConfig: { alwaysVisible } } = state['features/base/config']; + const { hovered } = state['features/toolbox']; + const toolbarTimeout = getToolbarTimeout(state); if (alwaysVisible) { return; @@ -95,7 +94,7 @@ export function hideToolbox(force: boolean = false): Function { dispatch( setToolboxTimeout( () => dispatch(hideToolbox()), - timeoutMS)); + toolbarTimeout)); } else { dispatch(setToolboxVisible(false)); } @@ -128,9 +127,13 @@ export function showToolbox(timeout: number = 0): Object { return (dispatch: Dispatch, getState: Function) => { const state = getState(); const { - alwaysVisible, + toolbarConfig: { initialTimeout, alwaysVisible }, + toolbarConfig + } = state['features/base/config']; + const toolbarTimeout = getToolbarTimeout(state); + + const { enabled, - timeoutMS, visible, overflowDrawer } = state['features/toolbox']; @@ -143,11 +146,17 @@ export function showToolbox(timeout: number = 0): Object { // If the Toolbox is always visible, there's no need for a timeout // to toggle its visibility. if (!alwaysVisible) { + if (typeof initialTimeout === 'number') { + // reset `initialTimeout` once it is consumed once + dispatch(overwriteConfig({ toolbarConfig: { + ...toolbarConfig, + initialTimeout: null + } })); + } dispatch( setToolboxTimeout( () => dispatch(hideToolbox()), - timeout || timeoutMS)); - dispatch(setToolboxTimeoutMS(interfaceConfig.TOOLBAR_TIMEOUT)); + timeout || initialTimeout || toolbarTimeout)); } } }; @@ -180,9 +189,73 @@ export function hideToolboxOnTileView() { const state = getState(); const { overflowDrawer } = state['features/toolbox']; - if (!overflowDrawer && isLayoutTileView(state)) { dispatch(hideToolbox(true)); } }; } + +/** + * Signals that toolbox timeout should be cleared. + * + * @returns {{ + * type: CLEAR_TOOLBOX_TIMEOUT + * }} + */ +export function clearToolboxTimeout(): Object { + return { + type: CLEAR_TOOLBOX_TIMEOUT + }; +} + +/** + * Shows/hides the overflow menu. + * + * @param {boolean} visible - True to show it or false to hide it. + * @returns {{ + * type: SET_OVERFLOW_MENU_VISIBLE, + * visible: boolean + * }} + */ +export function setOverflowMenuVisible(visible: boolean): Object { + return { + type: SET_OVERFLOW_MENU_VISIBLE, + visible + }; +} + +/** + * Signals that toolbar is hovered value should be changed. + * + * @param {boolean} hovered - Flag showing whether toolbar is hovered. + * @returns {{ + * type: SET_TOOLBAR_HOVERED, + * hovered: boolean + * }} + */ +export function setToolbarHovered(hovered: boolean): Object { + return { + type: SET_TOOLBAR_HOVERED, + hovered + }; +} + +/** + * Dispatches an action which sets new timeout and clears the previous one. + * + * @param {Function} handler - Function to be invoked after the timeout. + * @param {number} timeoutMS - Delay. + * @returns {{ + * type: SET_TOOLBOX_TIMEOUT, + * handler: Function, + * timeoutMS: number + * }} + */ +export function setToolboxTimeout(handler: Function, timeoutMS: number): Object { + return { + type: SET_TOOLBOX_TIMEOUT, + handler, + timeoutMS + }; +} + diff --git a/react/features/toolbox/constants.js b/react/features/toolbox/constants.js index 7ef06c6b39ba2..5a58bcd8f54fa 100644 --- a/react/features/toolbox/constants.js +++ b/react/features/toolbox/constants.js @@ -29,3 +29,5 @@ export const THRESHOLDS = [ ]; export const NOT_APPLICABLE = 'N/A'; + +export const TOOLBAR_TIMEOUT = 4000; diff --git a/react/features/toolbox/functions.native.js b/react/features/toolbox/functions.native.js index 194ec89f9a4ca..50f8619c6de33 100644 --- a/react/features/toolbox/functions.native.js +++ b/react/features/toolbox/functions.native.js @@ -60,12 +60,14 @@ export function getMovableButtons(width: number): Set { */ export function isToolboxVisible(stateful: Object | Function) { const state = toState(stateful); - const { alwaysVisible, enabled, visible } = state['features/toolbox']; + const { toolbarConfig: { alwaysVisible } } = state['features/base/config']; + const { enabled, visible } = state['features/toolbox']; const participantCount = getParticipantCountWithFake(state); const alwaysVisibleFlag = getFeatureFlag(state, TOOLBOX_ALWAYS_VISIBLE, false); const enabledFlag = getFeatureFlag(state, TOOLBOX_ENABLED, true); - return enabledFlag && enabled && (alwaysVisible || visible || participantCount === 1 || alwaysVisibleFlag); + return enabledFlag && enabled + && (alwaysVisible || visible || participantCount === 1 || alwaysVisibleFlag); } /** diff --git a/react/features/toolbox/functions.web.js b/react/features/toolbox/functions.web.js index 23b5961c1dae0..81c20c77c22bd 100644 --- a/react/features/toolbox/functions.web.js +++ b/react/features/toolbox/functions.web.js @@ -3,6 +3,8 @@ import { getToolbarButtons } from '../base/config'; import { hasAvailableDevices } from '../base/devices'; +import { TOOLBAR_TIMEOUT } from './constants'; + /** * Helper for getting the height of the toolbox. * @@ -37,9 +39,8 @@ export function isButtonEnabled(name: string, state: Object) { * otherwise. */ export function isToolboxVisible(state: Object) { - const { iAmSipGateway } = state['features/base/config']; + const { iAmSipGateway, toolbarConfig: { alwaysVisible } } = state['features/base/config']; const { - alwaysVisible, timeoutID, visible } = state['features/toolbox']; @@ -101,3 +102,15 @@ export function showOverflowDrawer(state: Object) { export function isToolboxEnabled(state: Object) { return state['features/toolbox'].enabled; } + +/** + * Returns the toolbar timeout from config or the default value. + * + * @param {Object} state - The state from the Redux store. + * @returns {number} - Toolbar timeout in miliseconds. + */ +export function getToolbarTimeout(state: Object) { + const { toolbarConfig: { timeout } } = state['features/base/config']; + + return timeout || TOOLBAR_TIMEOUT; +} diff --git a/react/features/toolbox/reducer.js b/react/features/toolbox/reducer.js index a7740554df1ae..d7641824a6801 100644 --- a/react/features/toolbox/reducer.js +++ b/react/features/toolbox/reducer.js @@ -8,121 +8,66 @@ import { SET_OVERFLOW_DRAWER, SET_OVERFLOW_MENU_VISIBLE, SET_TOOLBAR_HOVERED, - SET_TOOLBOX_ALWAYS_VISIBLE, SET_TOOLBOX_ENABLED, SET_TOOLBOX_TIMEOUT, - SET_TOOLBOX_TIMEOUT_MS, SET_TOOLBOX_VISIBLE, TOGGLE_TOOLBOX_VISIBLE } from './actionTypes'; -declare var interfaceConfig: Object; - /** - * Returns initial state for toolbox's part of Redux store. - * - * @private - * @returns {{ - * alwaysVisible: boolean, - * enabled: boolean, - * hovered: boolean, - * overflowDrawer: boolean, - * overflowMenuVisible: boolean, - * timeoutID: number, - * timeoutMS: number, - * visible: boolean - * }} + * Initial state of toolbox's part of Redux store. */ -function _getInitialState() { - // Does the toolbar eventually fade out, or is it always visible? - let alwaysVisible = false; - - // Toolbar (initial) visibility. - let visible = false; - - // Default toolbox timeout for mobile app. - let timeoutMS = 5000; - - if (typeof interfaceConfig !== 'undefined') { - if (interfaceConfig.INITIAL_TOOLBAR_TIMEOUT) { - timeoutMS = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT; - } - if (typeof interfaceConfig.TOOLBAR_ALWAYS_VISIBLE !== 'undefined') { - alwaysVisible = interfaceConfig.TOOLBAR_ALWAYS_VISIBLE; - } - } - - // When the toolbar is always visible, it must initially be visible too. - if (alwaysVisible === true) { - visible = true; - } - - return { - /** - * The indicator which determines whether the Toolbox should always be - * visible. When false, the toolbar will fade out after timeoutMS. - * - * @type {boolean} - */ - alwaysVisible, - - /** - * The indicator which determines whether the Toolbox is enabled. - * - * @type {boolean} - */ - enabled: true, - - /** - * The indicator which determines whether a Toolbar in the Toolbox is - * hovered. - * - * @type {boolean} - */ - hovered: false, - - /** - * The indicator which determines whether the overflow menu(s) are to be displayed as drawers. - * - * @type {boolean} - */ - overflowDrawer: false, - - /** - * The indicator which determines whether the OverflowMenu is visible. - * - * @type {boolean} - */ - overflowMenuVisible: false, - - /** - * A number, non-zero value which identifies the timer created by a call - * to setTimeout() with timeoutMS. - * - * @type {number|null} - */ - timeoutID: null, - - /** - * The delay in milliseconds before timeoutID executes (after its - * initialization). - * - * @type {number} - */ - timeoutMS, - - /** - * The indicator that determines whether the Toolbox is visible. - * - * @type {boolean} - */ - visible - }; -} +const INITIAL_STATE = { + + /** + * The indicator which determines whether the Toolbox is enabled. + * + * @type {boolean} + */ + enabled: true, + + /** + * The indicator which determines whether a Toolbar in the Toolbox is + * hovered. + * + * @type {boolean} + */ + hovered: false, + + /** + * The indicator which determines whether the overflow menu(s) are to be displayed as drawers. + * + * @type {boolean} + */ + overflowDrawer: false, + + /** + * The indicator which determines whether the OverflowMenu is visible. + * + * @type {boolean} + */ + overflowMenuVisible: false, + + /** + * A number, non-zero value which identifies the timer created by a call + * to setTimeout(). + * + * @type {number|null} + */ + timeoutID: null, + + + /** + * The indicator that determines whether the Toolbox is visible. + * + * @type {boolean} + */ + visible: false +}; ReducerRegistry.register( 'features/toolbox', - (state: Object = _getInitialState(), action: Object) => { + (state: Object = INITIAL_STATE, action: Object) => { switch (action.type) { case CLEAR_TOOLBOX_TIMEOUT: return { @@ -154,13 +99,6 @@ ReducerRegistry.register( hovered: action.hovered }; - case SET_TOOLBOX_ALWAYS_VISIBLE: - return { - ...state, - alwaysVisible: action.alwaysVisible, - visible: action.alwaysVisible === true ? true : state.visible - }; - case SET_TOOLBOX_ENABLED: return { ...state, @@ -170,21 +108,14 @@ ReducerRegistry.register( case SET_TOOLBOX_TIMEOUT: return { ...state, - timeoutID: action.timeoutID, - timeoutMS: action.timeoutMS - }; - - case SET_TOOLBOX_TIMEOUT_MS: - return { - ...state, - timeoutMS: action.timeoutMS + timeoutID: action.timeoutID }; case SET_TOOLBOX_VISIBLE: - return set(state, 'visible', state.alwaysVisible || action.visible); + return set(state, 'visible', action.visible); case TOGGLE_TOOLBOX_VISIBLE: - return set(state, 'visible', state.alwaysVisible || !state.visible); + return set(state, 'visible', !state.visible); } return state; diff --git a/react/features/video-menu/actions.any.js b/react/features/video-menu/actions.any.js index f272c95111bbb..a0c4aa0f9595d 100644 --- a/react/features/video-menu/actions.any.js +++ b/react/features/video-menu/actions.any.js @@ -23,6 +23,7 @@ import { getRemoteParticipants, muteRemoteParticipant } from '../base/participants'; +import { toggleScreensharing } from '../base/tracks'; import { isModerationNotificationDisplayed } from '../notifications'; declare var APP: Object; @@ -34,9 +35,10 @@ const logger = getLogger(__filename); * * @param {boolean} enable - Whether to mute or unmute. * @param {MEDIA_TYPE} mediaType - The type of the media channel to mute. + * @param {boolean} stopScreenSharing - Whether or not to stop the screensharing. * @returns {Function} */ -export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) { +export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE, stopScreenSharing: boolean = false) { return (dispatch: Dispatch, getState: Function) => { const isAudio = mediaType === MEDIA_TYPE.AUDIO; @@ -55,6 +57,10 @@ export function muteLocal(enable: boolean, mediaType: MEDIA_TYPE) { return; } + if (enable && stopScreenSharing) { + dispatch(toggleScreensharing(false, false, true)); + } + sendAnalytics(createToolbarEvent(isAudio ? AUDIO_MUTE : VIDEO_MUTE, { enable })); dispatch(isAudio ? setAudioMuted(enable, /* ensureTrack */ true) : setVideoMuted(enable, mediaType, VIDEO_MUTISM_AUTHORITY.USER, /* ensureTrack */ true)); @@ -97,7 +103,7 @@ export function muteAllParticipants(exclude: Array, mediaType: MEDIA_TYP const localId = getLocalParticipant(state).id; if (!exclude.includes(localId)) { - dispatch(muteLocal(true, mediaType)); + dispatch(muteLocal(true, mediaType, true)); } getRemoteParticipants(state).forEach((p, id) => { diff --git a/react/features/video-menu/components/AbstractMuteEveryoneDialog.js b/react/features/video-menu/components/AbstractMuteEveryoneDialog.js index bf9848d011910..7a80c7e8285ae 100644 --- a/react/features/video-menu/components/AbstractMuteEveryoneDialog.js +++ b/react/features/video-menu/components/AbstractMuteEveryoneDialog.js @@ -105,7 +105,7 @@ export default class AbstractMuteEveryoneDialog extends AbstractMuteRe dispatch(muteAllParticipants(exclude, MEDIA_TYPE.AUDIO)); if (this.state.audioModerationEnabled) { dispatch(requestEnableAudioModeration()); - } else { + } else if (this.state.audioModerationEnabled !== undefined) { dispatch(requestDisableAudioModeration()); } diff --git a/react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js b/react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js index b7185736a1fb8..129977d310764 100644 --- a/react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js +++ b/react/features/video-menu/components/AbstractMuteEveryonesVideoDialog.js @@ -106,7 +106,7 @@ export default class AbstractMuteEveryonesVideoDialog dispatch(muteAllParticipants(exclude, MEDIA_TYPE.VIDEO)); if (this.state.moderationEnabled) { dispatch(requestEnableVideoModeration()); - } else { + } else if (this.state.moderationEnabled !== undefined) { dispatch(requestDisableVideoModeration()); } diff --git a/react/features/virtual-background/components/UploadImageButton.js b/react/features/virtual-background/components/UploadImageButton.js new file mode 100644 index 0000000000000..9f62e38e43300 --- /dev/null +++ b/react/features/virtual-background/components/UploadImageButton.js @@ -0,0 +1,125 @@ +// @flow + +import React, { useCallback, useRef } from 'react'; +import uuid from 'uuid'; + +import { translate } from '../../base/i18n'; +import { Icon, IconPlusCircle } from '../../base/icons'; +import { VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants'; +import { resizeImage } from '../functions'; +import logger from '../logger'; + +type Props = { + + /** + * Callback used to set the 'loading' state of the parent component. + */ + setLoading: Function, + + /** + * Callback used to set the options. + */ + setOptions: Function, + + /** + * Callback used to set the storedImages array. + */ + setStoredImages: Function, + + /** + * A list of images locally stored. + */ + storedImages: Array, + + /** + * If a label should be displayed alongside the button. + */ + showLabel: boolean, + + /** + * Used for translation. + */ + t: Function +} + +/** + * Component used to upload an image. + * + * @param {Object} Props - The props of the component. + * @returns {React$Node} + */ +function UploadImageButton({ + setLoading, + setOptions, + setStoredImages, + showLabel, + storedImages, + t +}: Props) { + const uploadImageButton: Object = useRef(null); + const uploadImageKeyPress = useCallback(e => { + if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) { + e.preventDefault(); + uploadImageButton.current.click(); + } + }, [ uploadImageButton.current ]); + + + const uploadImage = useCallback(async e => { + const reader = new FileReader(); + const imageFile = e.target.files; + + reader.readAsDataURL(imageFile[0]); + reader.onload = async () => { + const url = await resizeImage(reader.result); + const uuId = uuid.v4(); + + setStoredImages([ + ...storedImages, + { + id: uuId, + src: url + } + ]); + setOptions({ + backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE, + enabled: true, + url, + selectedThumbnail: uuId + }); + }; + logger.info('New virtual background image uploaded!'); + + reader.onerror = () => { + setLoading(false); + logger.error('Failed to upload virtual image!'); + }; + }, [ storedImages ]); + + return ( + <> + {showLabel && } + + + + ); +} + +export default translate(UploadImageButton); diff --git a/react/features/virtual-background/components/VirtualBackgroundDialog.js b/react/features/virtual-background/components/VirtualBackgroundDialog.js index 9b3968b5ad708..97459ae34a6ff 100644 --- a/react/features/virtual-background/components/VirtualBackgroundDialog.js +++ b/react/features/virtual-background/components/VirtualBackgroundDialog.js @@ -3,12 +3,11 @@ import Spinner from '@atlaskit/spinner'; import Bourne from '@hapi/bourne'; import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import uuid from 'uuid'; +import React, { useState, useEffect, useCallback } from 'react'; import { Dialog, hideDialog, openDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; -import { Icon, IconCloseSmall, IconPlusCircle, IconShareDesktop } from '../../base/icons'; +import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons'; import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet'; import { createLocalTrack } from '../../base/lib-jitsi-meet/functions'; import { VIDEO_TYPE } from '../../base/media'; @@ -18,62 +17,20 @@ import { Tooltip } from '../../base/tooltip'; import { getLocalVideoTrack } from '../../base/tracks'; import { showErrorNotification } from '../../notifications'; import { toggleBackgroundEffect } from '../actions'; -import { VIRTUAL_BACKGROUND_TYPE } from '../constants'; -import { resizeImage, toDataURL } from '../functions'; +import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants'; +import { toDataURL } from '../functions'; import logger from '../logger'; +import UploadImageButton from './UploadImageButton'; import VirtualBackgroundPreview from './VirtualBackgroundPreview'; - -type Image = { - tooltip?: string, - id: string, - src: string -} - -// The limit of virtual background uploads is 24. When the number -// of uploads is 25 we trigger the deleteStoredImage function to delete -// the first/oldest uploaded background. -const backgroundsLimit = 25; -const images: Array = [ - { - tooltip: 'image1', - id: '1', - src: 'images/virtual-background/background-1.jpg' - }, - { - tooltip: 'image2', - id: '2', - src: 'images/virtual-background/background-2.jpg' - }, - { - tooltip: 'image3', - id: '3', - src: 'images/virtual-background/background-3.jpg' - }, - { - tooltip: 'image4', - id: '4', - src: 'images/virtual-background/background-4.jpg' - }, - { - tooltip: 'image5', - id: '5', - src: 'images/virtual-background/background-5.jpg' - }, - { - tooltip: 'image6', - id: '6', - src: 'images/virtual-background/background-6.jpg' - }, - { - tooltip: 'image7', - id: '7', - src: 'images/virtual-background/background-7.jpg' - } -]; type Props = { + /** + * The list of Images to choose from. + */ + _images: Array, + /** * The current local flip x status. */ @@ -89,6 +46,11 @@ type Props = { */ _selectedThumbnail: string, + /** + * If the upload button should be displayed or not. + */ + _showUploadButton: boolean, + /** * Returns the selected virtual background object. */ @@ -128,11 +90,15 @@ const onError = event => { */ function _mapStateToProps(state): 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: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages), _jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack }; } @@ -145,9 +111,11 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg * @returns {ReactElement} */ function VirtualBackground({ - _localFlipX, + _images, _jitsiTrack, + _localFlipX, _selectedThumbnail, + _showUploadButton, _virtualBackground, dispatch, initialOptions, @@ -158,7 +126,7 @@ function VirtualBackground({ const localImages = jitsiLocalStorage.getItem('virtualBackgrounds'); const [ storedImages, setStoredImages ] = useState>((localImages && Bourne.parse(localImages)) || []); const [ loading, setLoading ] = useState(false); - const uploadImageButton: Object = useRef(null); + const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP ? _virtualBackground.virtualSource : null); @@ -186,7 +154,7 @@ function VirtualBackground({ // Preventing localStorage QUOTA_EXCEEDED_ERR err && setStoredImages(storedImages.slice(1)); } - if (storedImages.length === backgroundsLimit) { + if (storedImages.length === BACKGROUNDS_LIMIT) { setStoredImages(storedImages.slice(1)); } }, [ storedImages ]); @@ -321,61 +289,27 @@ function VirtualBackground({ const setImageBackground = useCallback(async e => { const imageId = e.currentTarget.getAttribute('data-imageid'); - const image = images.find(img => img.id === imageId); + const image = _images.find(img => img.id === imageId); if (image) { - const url = await toDataURL(image.src); - - setOptions({ - backgroundType: 'image', - enabled: true, - url, - selectedThumbnail: image.id - }); - logger.info('Image setted for virtual background preview!'); + try { + const url = await toDataURL(image.src); + + setOptions({ + backgroundType: 'image', + enabled: true, + url, + selectedThumbnail: image.id + }); + logger.info('Image set for virtual background preview!'); + } catch (err) { + logger.error('Could not fetch virtual background image:', err); + } setLoading(false); } }, []); - const uploadImage = useCallback(async e => { - const reader = new FileReader(); - const imageFile = e.target.files; - - reader.readAsDataURL(imageFile[0]); - reader.onload = async () => { - const url = await resizeImage(reader.result); - const uuId = uuid.v4(); - - setStoredImages([ - ...storedImages, - { - id: uuId, - src: url - } - ]); - setOptions({ - backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE, - enabled: true, - url, - selectedThumbnail: uuId - }); - }; - logger.info('New virtual background image uploaded!'); - - reader.onerror = () => { - setLoading(false); - logger.error('Failed to upload virtual image!'); - }; - }, [ dispatch, storedImages ]); - - const uploadImageKeyPress = useCallback(e => { - if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) { - e.preventDefault(); - uploadImageButton.current.click(); - } - }, [ uploadImageButton.current ]); - const setImageBackgroundKeyPress = useCallback(e => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); @@ -448,25 +382,13 @@ function VirtualBackground({ ) : (
- {previewIsLoaded && } - + {_showUploadButton + && }
- {images.map(image => ( + {_images.map(image => ( = [ + { + tooltip: 'image1', + id: '1', + src: 'images/virtual-background/background-1.jpg' + }, + { + tooltip: 'image2', + id: '2', + src: 'images/virtual-background/background-2.jpg' + }, + { + tooltip: 'image3', + id: '3', + src: 'images/virtual-background/background-3.jpg' + }, + { + tooltip: 'image4', + id: '4', + src: 'images/virtual-background/background-4.jpg' + }, + { + tooltip: 'image5', + id: '5', + src: 'images/virtual-background/background-5.jpg' + }, + { + tooltip: 'image6', + id: '6', + src: 'images/virtual-background/background-6.jpg' + }, + { + tooltip: 'image7', + id: '7', + src: 'images/virtual-background/background-7.jpg' + } +];