Compare commits

...

23 Commits

Author SHA1 Message Date
robertpin
ea3f134ebe fix(av-moderation) Only stop screensharing on Stop everyone's video 2021-09-24 08:53:53 -05:00
Hristo Terezov
09d07ed423 Show reactions buttons at all times
Don't send reactions via the channel if there's only one participant
in the meeting
2021-09-23 11:29:05 -05:00
Hristo Terezov
a601742aea Update lib-jitsi-meet
* ref(JitsiConference): don't crash on wrong oldTrack
* fix(replaceTrack): Don't wrap Error in Error.
2021-09-23 11:08:04 -05:00
Vlad Piersec
d6fb444e03 fix(context-menus) Don't show volume slider on iOS web 2021-09-23 18:21:04 +03:00
Avram Tudor
66cf348ee1 fix(jaas) do not show overriden unsupported browser page for jaas users (#9962) 2021-09-23 18:21:04 +03:00
Vlad Piersec
74210baca0 feat(api): Expose event used for sending browser support 2021-09-23 18:21:04 +03:00
Vlad Piersec
968559710b fix(feedback): Scroll to the top when opening feedback dialog 2021-09-23 18:21:04 +03:00
Дамян Минков
a238c96b64 fix: Fixes undefined error causing to stop reloads.
Jibri was hitting a problem where it reloads and in certain cases (remote user is screensharing) we hit this participant undefined, which stops reload and stops recording.
It is still not obvious why we try to render this on leaving the conference and for a participant that is not in the conference ... this re-render should not happen as this component should be removed from its parent when the participant is not existing.
2021-09-23 10:09:46 -05:00
Saúl Ibarra Corretgé
343c4fa56c fix(participants-pane) bring back lobby reject button
It got accidentally removed in:
1dc8bfa631 (diff-ef66b8ab7c899b16d6c1102b3c812aa45509f95e11267cbfdbd0674143e94575)
2021-09-23 16:12:25 +02:00
robertpin
fd43918c1e Fix stop everyone's video
Stop everyone's video now also stops screensharing for the moderator that did the action, not just video
2021-09-23 10:47:17 +03:00
robertpin
8c1209a1d7 fix(av-moderation) - Mute/ stop video except no longer change moderation status
Mute everyone else no longer disables moderation
2021-09-23 10:47:17 +03:00
robertpin
4709e64e57 fix(av-moderation) Display Allow Video instead of Ask to Unmute (#9991)
When the user is allowed audio but not video display Allow Video button instead of Ask to Unmute
2021-09-23 10:47:17 +03:00
robertpin
3109cce8d0 fix(config,notifications) fix rendering moderator notifications (#9986)
Move DISABLE_FOCUS_INDICATOR from interface_config.js to config.js (disableModeratorIndicator).
2021-09-23 10:47:17 +03:00
robertpin
b861f77a09 fix(reactions) Reactions improvements (#9964)
* Register shortcuts on mount

* Enable reactions by default

When having unread messages and sending a reaction the unread count now shows the correct count

* Fix overflow menu bottom color when reactions are enabled

* Update raise hand functionality

On desktop show raise button with arrow for reactions. Only show raise hand in the reactions menu on mobile

* Fix unreadCount with reactions

UnreadCount ignores all reactions messages

* Fix background color
2021-09-23 10:08:26 +03:00
Hristo Terezov
4eecfbc2af fix(recorder): tile view
In the case of the recorder we were not taking into account that the
local thumbnail is not visible. This was braking the rendering  and
positioning of the thumbnails in tile view.
2021-09-22 10:34:17 -05:00
Jaya Allamsetty
48294aad14 fix(device-selection) Do not create preview when mic selection is disabled.
This fixes an issue on mobile Safari when audio is lost after the user opens the device selection menu.
2021-09-22 11:28:14 -04:00
Vlad Piersec
92e21a5ca7 fix(responsive-ui): Shrink self view when in portrait mode on mobile web 2021-09-22 18:22:39 +03:00
Horatiu Muresan
ff7e107c4f feat(conference-info-header) Make conference info header configurable. (#9638) 2021-09-22 18:22:39 +03:00
hmuresan
c44611fb33 fix(notifications) Fix recording start notif not disappearing 2021-09-22 18:22:39 +03:00
Hristo Terezov
83e1e45794 fix(Filmstrip) fix not being able to scroll 2021-09-22 18:22:39 +03:00
Horatiu Muresan
cd3a4a9818 fix(context-menus) Fix participant context menus/toolbar overflow menu
- on ipads, long touch open dialog now opens the context menu to the left of the thumbnail as expected
- on ipads, now we close context menus on tap out
- fix case when participant context menu's height > tileview videos' height causing scroll on videos pane
- keep toolbox open while the overflow menu is shown
- keep remote participant video thumbnail in filmstrip visible even if toolbox is hidden, if context menu is opened
- Fix bug where toolbox could be completely disabled
2021-09-22 18:22:39 +03:00
Horatiu Muresan
48890c9603 feat(toolbar-buttons): Add event for notify overwritten toolbar buttons 2021-09-22 18:22:39 +03:00
robertpin
d948a9bedf fix(av-moderation) Advanced moderation improvements (#9935)
* Update moderation in effect notifications

Only display one notification for each media type. Display notification for keyboard shortcuts as well

* Update muted remotely notification

Display name of moderator in the notification

* Fix indentation on moderation menu

* Update text for video moderation

* Added moderator label in participant pane

* Update microphone icon in participant list

For participants that speak, or are noisy, but aren't dominant speaker, the icon in the participant list will look the same as the dominant speaker icon but will not change their position in the list

* Added sound for asked to unmute notification

* Code review changes

* Code review changes

Use simple var instead of function for audio media state

* Move constants to constants file

* Moved constants from notifications to av-moderation
2021-09-20 10:16:30 +03:00
112 changed files with 1561 additions and 652 deletions

View File

@@ -24,6 +24,8 @@ import {
redirectToStaticPage,
reloadWithStoredParams
} from './react/features/app/actions';
import { showModeratedNotification } from './react/features/av-moderation/actions';
import { shouldShowModeratedNotification } from './react/features/av-moderation/functions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -119,7 +121,7 @@ import {
maybeOpenFeedbackDialog,
submitFeedback
} from './react/features/feedback';
import { showNotification } from './react/features/notifications';
import { isModerationNotificationDisplayed, showNotification } from './react/features/notifications';
import { mediaPermissionPromptVisibilityChanged, toggleSlowGUMOverlay } from './react/features/overlay';
import { suspendDetected } from './react/features/power-monitor';
import {
@@ -887,13 +889,24 @@ export default {
* dialogs in case of media permissions error.
*/
muteAudio(mute, showUI = true) {
const state = APP.store.getState();
if (!mute
&& isUserInteractionRequiredForUnmute(APP.store.getState())) {
&& isUserInteractionRequiredForUnmute(state)) {
logger.error('Unmuting audio requires user interaction');
return;
}
// check for A/V Moderation when trying to unmute
if (!mute && shouldShowModeratedNotification(MEDIA_TYPE.AUDIO, state)) {
if (!isModerationNotificationDisplayed(MEDIA_TYPE.AUDIO, state)) {
APP.store.dispatch(showModeratedNotification(MEDIA_TYPE.AUDIO));
}
return;
}
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
// This will only modify base/media.audio.muted which is then synced
@@ -1511,14 +1524,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;
@@ -1560,7 +1573,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`);
@@ -1611,9 +1624,10 @@ export default {
* @param {Array<string>} [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.<T>}
*/
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.');
@@ -1639,7 +1653,7 @@ export default {
}
return this._untoggleScreenSharing
? this._untoggleScreenSharing()
? this._untoggleScreenSharing(ignoreDidHaveVideo)
: Promise.resolve();
},
@@ -2476,8 +2490,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);
}
);
},

View File

@@ -71,8 +71,11 @@ var config = {
// callStatsThreshold: 5 // enable callstats for 5% of the users.
},
// Enables reactions feature.
// enableReactions: false,
// Disables the reactions feature.
// disableReactions: true,
// Disables moderator indicators.
// disableModeratorIndicator: false,
// Disables polls feature.
// disablePolls: false,
@@ -539,6 +542,43 @@ var config = {
// '__end'
// ],
// Toolbar buttons which have their click event exposed through the API on
// `toolbarButtonClicked` event instead of executing the normal click routine.
// buttonsWithNotifyClick: [
// 'camera',
// 'chat',
// 'closedcaptions',
// 'desktop',
// 'download',
// 'embedmeeting',
// 'etherpad',
// 'feedback',
// 'filmstrip',
// 'fullscreen',
// 'hangup',
// 'help',
// 'invite',
// 'livestreaming',
// 'microphone',
// 'mute-everyone',
// 'mute-video-everyone',
// 'participants-pane',
// 'profile',
// 'raisehand',
// 'recording',
// 'security',
// 'select-background',
// 'settings',
// 'shareaudio',
// 'sharedvideo',
// 'shortcuts',
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'videoquality',
// '__end'
// ],
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [],
@@ -700,6 +740,7 @@ var config = {
// Array<string> of disabled sounds.
// Possible values:
// - 'ASKED_TO_UNMUTE_SOUND'
// - 'E2EE_OFF_SOUND'
// - 'E2EE_ON_SOUND'
// - 'INCOMING_MSG_SOUND'
@@ -863,15 +904,32 @@ var config = {
// If true, tile view will not be enabled automatically when the participants count threshold is reached.
// disableTileView: true,
// Controls the visibility and behavior of the top header conference info labels.
// If a label's id is not in any of the 2 arrays, it will not be visible at all on the header.
// conferenceInfo: {
// // those labels will not be hidden in tandem with the toolbox.
// alwaysVisible: ['recording', 'local-recording'],
// // those labels will be auto-hidden in tandem with the toolbox buttons.
// autoHide: [
// 'subject',
// 'conference-timer',
// 'participants-count',
// 'e2ee',
// 'transcribing',
// 'video-quality',
// 'insecure-room'
// ]
// },
// Hides the conference subject
// hideConferenceSubject: true,
// Hides the recording label
// hideRecordingLabel: false,
// Hides the conference timer.
// hideConferenceTimer: true,
// Hides the recording label
// hideRecordingLabel: false,
// Hides the participants stats
// hideParticipantsStats: true,

View File

@@ -2,7 +2,7 @@
.reactions-menu {
width: 280px;
background: #292929;
background: $menuBG;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
border-radius: 3px;
padding: 16px;

View File

@@ -1,54 +1,22 @@
.subject {
box-sizing: border-box;
color: #fff;
margin-top: 20px;
position: absolute;
top: -120px;
transition: top .3s ease-in;
width: 100%;
margin-top: -120px;
transition: margin-top .3s ease-in;
z-index: $zindex3;
&.visible {
top: 0;
margin-top: 20px;
}
&.recording {
top: 0;
.subject-details-container {
opacity: 0;
transition: opacity .3s ease-in;
}
.subject-info-container .show-always {
transition: margin-left .3s ease-in;
}
&.visible {
.subject-details-container {
opacity: 1;
}
}
}
}
.subject-details-container {
display: flex;
}
.subject-info-container {
display: flex;
justify-content: center;
max-width: calc(100% - 280px);
margin: 0 auto;
&--full-width {
max-width: 100%;
}
height: 28px;
@media (max-width: 500px) {
flex-wrap: wrap;
max-width: 100%;
}
}
@@ -63,21 +31,47 @@
.subject-text {
background: rgba(0, 0, 0, 0.6);
border-radius: 3px 0px 0px 3px;
box-sizing: border-box;
font-size: 14px;
line-height: 24px;
padding: 2px 16px;
height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 28px;
padding: 0 16px;
height: 28px;
@media (max-width: 700px) {
max-width: 100px;
}
@media (max-width: 300px) {
display: none;
}
&--content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.subject-timer {
background: rgba(0, 0, 0, 0.8);
border-radius: 0px 3px 3px 0px;
display: inline-block;
box-sizing: border-box;
font-size: 12px;
line-height: 16px;
line-height: 28px;
min-width: 34px;
padding: 6px 8px;
padding: 0 8px;
height: 28px;
@media (max-width: 300px) {
display: none;
}
}
.details-container {
width: 100%;
display: flex;
justify-content: center;
position: absolute;
top: 0;
height: 48px;
}

View File

@@ -84,6 +84,10 @@
margin-bottom: 3px;
margin-left: $remoteVideoMenuIconMargin;
}
.self-view-mobile-portrait video {
object-fit: contain;
}
}
/**
@@ -112,3 +116,11 @@
transition-delay: 0.1s;
}
}
/**
* Overrides for self view when in portrait mode on mobile.
* This is done in order to keep the aspect ratio.
*/
.vertical-filmstrip .self-view-mobile-portrait #localVideo_container {
object-fit: contain;
}

View File

@@ -39,7 +39,8 @@ var interfaceConfig = {
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
DISABLE_FOCUS_INDICATOR: false,
// Deprecated. Please use disableModeratorIndicator from config.js
// DISABLE_FOCUS_INDICATOR: false,
/**
* If true, notifications regarding joining/leaving are no longer displayed.

View File

@@ -563,9 +563,9 @@
"moderator": "You're now a moderator",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You've been muted by the moderator",
"mutedRemotelyTitle": "You've been muted by {{moderator}}",
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
"videoMutedRemotelyTitle": "Your camera has been turned off by the moderator",
"videoMutedRemotelyTitle": "Your video has been turned off by {{moderator}}",
"videoMutedRemotelyDescription": "You can always turn it on again.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
@@ -610,6 +610,7 @@
},
"actions": {
"allow": "Allow attendees to:",
"allowVideo": "Allow video",
"audioModeration": "Unmute themselves",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"invite": "Invite Someone",
@@ -620,7 +621,7 @@
"stopEveryonesVideo": "Stop everyone's video",
"stopVideo": "Stop video",
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
"videoModeration": "Start video"
"videoModeration": "Start their video"
}
},
"passwordSetRemotely": "Set by another participant",

View File

@@ -15,6 +15,7 @@ import {
} from '../../react/features/base/conference';
import { overwriteConfig, getWhitelistedJSON } from '../../react/features/base/config';
import { toggleDialog } from '../../react/features/base/dialog/actions';
import { isSupportedBrowser } from '../../react/features/base/environment';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../react/features/base/media';
@@ -692,6 +693,7 @@ class API {
this._enabled = true;
initCommands();
this.notifyBrowserSupport(isSupportedBrowser());
}
/**
@@ -1342,6 +1344,32 @@ class API {
});
}
/**
* Notify external application ( if API is enabled) that a toolbar button was clicked.
*
* @param {string} key - The key of the toolbar button.
* @returns {void}
*/
notifyToolbarButtonClicked(key: string) {
this._sendEvent({
name: 'toolbar-button-clicked',
key
});
}
/**
* Notify external application (if API is enabled) wether the used browser is supported or not.
*
* @param {boolean} supported - If browser is supported or not.
* @returns {void}
*/
notifyBrowserSupport(supported: boolean) {
this._sendEvent({
name: 'browser-support',
supported
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -76,6 +76,7 @@ const events = {
'avatar-changed': 'avatarChanged',
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'browser-support': 'browserSupport',
'camera-error': 'cameraError',
'chat-updated': 'chatUpdated',
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
@@ -112,7 +113,8 @@ const events = {
'dominant-speaker-changed': 'dominantSpeakerChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged'
'tile-view-changed': 'tileViewChanged',
'toolbar-button-clicked': 'toolbarButtonClicked'
};
/**

4
package-lock.json generated
View File

@@ -11117,8 +11117,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#ad1f06d76833e5d7233c86bab36e76bcd5ce44e0",
"from": "github:jitsi/lib-jitsi-meet#ad1f06d76833e5d7233c86bab36e76bcd5ce44e0",
"version": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
"from": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#98cd62cc00f92c8c2430e52ca746a86813658e83",

View File

@@ -59,7 +59,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ad1f06d76833e5d7233c86bab36e76bcd5ce44e0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fbf85bdcec64185431cd6012060f4d4e922c573f",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",

View File

@@ -2,6 +2,8 @@
import { getConferenceState } from '../base/conference';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import { getParticipantById } from '../base/participants';
import { isForceMuted } from '../participants-pane/functions';
import {
DISMISS_PENDING_PARTICIPANT,
@@ -27,11 +29,15 @@ import { isEnabledFromState } from './functions';
export const approveParticipant = (id: string) => (dispatch: Function, getState: Function) => {
const state = getState();
const { conference } = getConferenceState(state);
const participant = getParticipantById(state, id);
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state)) {
const isAudioForceMuted = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
const isVideoForceMuted = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
if (isEnabledFromState(MEDIA_TYPE.AUDIO, state) && isAudioForceMuted) {
conference.avModerationApprove(MEDIA_TYPE.AUDIO, id);
}
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state)) {
if (isEnabledFromState(MEDIA_TYPE.VIDEO, state) && isVideoForceMuted) {
conference.avModerationApprove(MEDIA_TYPE.VIDEO, id);
}
};

View File

@@ -17,3 +17,15 @@ export const MEDIA_TYPE_TO_PENDING_STORE_KEY: {[key: MediaType]: string} = {
[MEDIA_TYPE.AUDIO]: 'pendingAudio',
[MEDIA_TYPE.VIDEO]: 'pendingVideo'
};
export const ASKED_TO_UNMUTE_SOUND_ID = 'ASKED_TO_UNMUTE_SOUND';
export const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
export const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
export const CS_MODERATION_NOTIFICATION_ID = 'screensharing-moderation';
export const MODERATION_NOTIFICATIONS = {
[MEDIA_TYPE.AUDIO]: AUDIO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.VIDEO]: VIDEO_MODERATION_NOTIFICATION_ID,
[MEDIA_TYPE.PRESENTER]: CS_MODERATION_NOTIFICATION_ID
};

View File

@@ -1,6 +1,7 @@
// @flow
import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { getConferenceState } from '../base/conference';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../base/media';
@@ -13,6 +14,7 @@ import {
raiseHand
} from '../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import {
hideNotification,
showNotification
@@ -35,21 +37,31 @@ import {
participantApproved,
participantPendingAudio
} from './actions';
import {
ASKED_TO_UNMUTE_SOUND_ID, AUDIO_MODERATION_NOTIFICATION_ID,
CS_MODERATION_NOTIFICATION_ID,
VIDEO_MODERATION_NOTIFICATION_ID
} from './constants';
import {
isEnabledFromState,
isParticipantApproved,
isParticipantPending
} from './functions';
const VIDEO_MODERATION_NOTIFICATION_ID = 'video-moderation';
const AUDIO_MODERATION_NOTIFICATION_ID = 'audio-moderation';
const CS_MODERATION_NOTIFICATION_ID = 'video-moderation';
import { ASKED_TO_UNMUTE_FILE } from './sounds';
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const { type } = action;
const { conference } = getConferenceState(getState());
switch (type) {
case APP_WILL_MOUNT: {
dispatch(registerSound(ASKED_TO_UNMUTE_SOUND_ID, ASKED_TO_UNMUTE_FILE));
break;
}
case APP_WILL_UNMOUNT: {
dispatch(unregisterSound(ASKED_TO_UNMUTE_SOUND_ID));
break;
}
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
let descriptionKey;
let titleKey;
@@ -160,6 +172,7 @@ StateListenerRegistry.register(
customActionNameKey: 'notify.unmute',
customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
}));
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
}
});

View File

@@ -0,0 +1,6 @@
/**
* The name of the bundled audio file which will be played for the raise hand sound.
*
* @type {string}
*/
export const ASKED_TO_UNMUTE_FILE = 'asked-unmute.mp3';

View File

@@ -19,6 +19,7 @@ export default [
'apiLogLevels',
'avgRtpStatsN',
'backgroundAlpha',
'buttonsWithNotifyClick',
/**
* The display name of the CallKit call representing the conference/meeting
@@ -69,6 +70,7 @@ export default [
*/
'callUUID',
'conferenceInfo',
'channelLastN',
'connectionIndicators',
'constraints',
@@ -94,9 +96,11 @@ export default [
'disableIncomingMessageSound',
'disableJoinLeaveSounds',
'disableLocalVideoFlip',
'disableModeratorIndicator',
'disableNS',
'disablePolls',
'disableProfile',
'disableReactions',
'disableRecordAudioNotification',
'disableRemoteControl',
'disableRemoteMute',
@@ -124,7 +128,6 @@ export default [
'enableLayerSuspension',
'enableLipSync',
'enableOpusRed',
'enableReactions',
'enableRemb',
'enableSaveLogs',
'enableScreenshotCapture',

View File

@@ -2,6 +2,7 @@
import _ from 'lodash';
import { CONFERENCE_INFO } from '../../conference/components/constants';
import { equals, ReducerRegistry } from '../redux';
import {
@@ -56,6 +57,17 @@ const INITIAL_RN_STATE = {
}
};
/**
* Mapping between old configs controlling the conference info headers visibility and the
* new configs. Needed in order to keep backwards compatibility.
*/
const CONFERENCE_HEADER_MAPPING = {
hideConferenceTimer: [ 'conference-timer' ],
hideConferenceSubject: [ 'subject' ],
hideParticipantsStats: [ 'participants-count' ],
hideRecordingLabel: [ 'recording', 'local-recording' ]
};
ReducerRegistry.register('features/base/config', (state = _getInitialState(), action) => {
switch (action.type) {
case UPDATE_CONFIG:
@@ -172,6 +184,27 @@ function _setConfig(state, { config }) {
return equals(state, newState) ? state : newState;
}
/**
* Processes the conferenceInfo object against the defaults.
*
* @param {Object} config - The old config.
* @returns {Object} The processed conferenceInfo object.
*/
function _getConferenceInfo(config) {
const { conferenceInfo } = config;
if (conferenceInfo) {
return {
alwaysVisible: conferenceInfo.alwaysVisible ?? [ ...CONFERENCE_INFO.alwaysVisible ],
autoHide: conferenceInfo.autoHide ?? [ ...CONFERENCE_INFO.autoHide ]
};
}
return {
...CONFERENCE_INFO
};
}
/**
* Constructs a new config {@code Object}, if necessary, out of a specific
* config {@code Object} which is in the latest format supported by jitsi-meet.
@@ -194,6 +227,27 @@ function _translateLegacyConfig(oldValue: Object) {
newValue.toolbarButtons = interfaceConfig.TOOLBAR_BUTTONS;
}
const filteredConferenceInfo = Object.keys(CONFERENCE_HEADER_MAPPING).filter(key => oldValue[key]);
if (filteredConferenceInfo.length) {
newValue.conferenceInfo = _getConferenceInfo(oldValue);
filteredConferenceInfo.forEach(key => {
// hideRecordingLabel does not mean not render it at all, but autoHide it
if (key === 'hideRecordingLabel') {
newValue.conferenceInfo.alwaysVisible
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= _.union(newValue.conferenceInfo.autoHide, CONFERENCE_HEADER_MAPPING[key]);
} else {
newValue.conferenceInfo.alwaysVisible
= newValue.conferenceInfo.alwaysVisible.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
newValue.conferenceInfo.autoHide
= newValue.conferenceInfo.autoHide.filter(c => !CONFERENCE_HEADER_MAPPING[key].includes(c));
}
});
}
if (!oldValue.connectionIndicators
&& typeof interfaceConfig === 'object'
&& (interfaceConfig.hasOwnProperty('CONNECTION_INDICATOR_DISABLED')
@@ -232,6 +286,12 @@ function _translateLegacyConfig(oldValue: Object) {
};
}
if (oldValue.disableModeratorIndicator === undefined
&& typeof interfaceConfig === 'object'
&& interfaceConfig.hasOwnProperty('DISABLE_FOCUS_INDICATOR')) {
newValue.disableModeratorIndicator = interfaceConfig.DISABLE_FOCUS_INDICATOR;
}
return newValue;
}

View File

@@ -54,4 +54,13 @@ export const CONNECTION_WILL_CONNECT = 'CONNECTION_WILL_CONNECT';
*/
export const SET_LOCATION_URL = 'SET_LOCATION_URL';
/**
* The type of (redux) action which tells whether connection info should be displayed
* on context menu.
*
* {
* type: SHOW_CONNECTION_INFO,
* showConnectionInfo: boolean
* }
*/
export const SHOW_CONNECTION_INFO = 'SHOW_CONNECTION_INFO';

View File

@@ -145,6 +145,7 @@ class BottomSheet extends PureComponent<Props> {
renderHeader
? _styles.sheetHeader
: _styles.sheet,
renderFooter && _styles.sheetFooter,
style,
{
maxHeight: _height - 100
@@ -154,7 +155,10 @@ class BottomSheet extends PureComponent<Props> {
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { addScrollViewPadding && styles.scrollView } >
style = { [
renderFooter && _styles.sheet,
addScrollViewPadding && styles.scrollView
] } >
{ this.props.children }
</ScrollView>
{ renderFooter && renderFooter() }

View File

@@ -213,6 +213,13 @@ ColorSchemeRegistry.register('BottomSheet', {
*/
sheetHeader: {
backgroundColor: BaseTheme.palette.ui02
},
/**
* Bottom sheet's background color with footer.
*/
sheetFooter: {
backgroundColor: BaseTheme.palette.bottomSheet
}
});

View File

@@ -28,8 +28,7 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
*
* @static
*/
type Props = {
...DialogProps,
type Props = DialogProps & {
/**
* Custom dialog header that replaces the standard heading.
@@ -77,6 +76,11 @@ type Props = {
*/
onDecline?: Function,
/**
* Callback invoked when setting the ref of the Dialog.
*/
onDialogRef?: Function,
/**
* Disables rendering of the submit button.
*/
@@ -127,7 +131,7 @@ class StatelessDialog extends Component<Props> {
this._onKeyPress = this._onKeyPress.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._renderFooter = this._renderFooter.bind(this);
this._setDialogElement = this._setDialogElement.bind(this);
this._onDialogRef = this._onDialogRef.bind(this);
}
/**
@@ -166,7 +170,7 @@ class StatelessDialog extends Component<Props> {
width = { width || 'medium' }>
<div
onKeyPress = { this._onKeyPress }
ref = { this._setDialogElement }>
ref = { this._onDialogRef }>
<form
className = 'modal-dialog-form'
id = 'modal-dialog-form'
@@ -319,19 +323,18 @@ class StatelessDialog extends Component<Props> {
);
}
_setDialogElement: (?HTMLElement) => void;
_onDialogRef: (?Element) => void;
/**
* Sets the instance variable for the div containing the component's dialog
* element so it can be accessed directly.
* Callback invoked when setting the ref of the dialog's child passing the Modal ref.
* It is done this way because we cannot directly access the ref of the Modal component.
*
* @param {HTMLElement} element - The DOM element for the component's
* dialog.
* @param {HTMLElement} element - The DOM element for the dialog.
* @private
* @returns {void}
*/
_setDialogElement(element: ?HTMLElement) {
this._dialogElement = element;
_onDialogRef(element: ?Element) {
this.props.onDialogRef && this.props.onDialogRef(element && element.parentNode);
}
_onKeyPress: (Object) => void;

View File

@@ -4,6 +4,7 @@ import type { Dispatch } from 'redux';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { isModerationNotificationDisplayed } from '../../notifications';
import {
SET_AUDIO_MUTED,
@@ -113,7 +114,9 @@ export function setVideoMuted(
// check for A/V Moderation when trying to unmute
if (!muted && shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
if (!isModerationNotificationDisplayed(MEDIA_TYPE.VIDEO, state)) {
ensureTrack && dispatch(showModeratedNotification(MEDIA_TYPE.VIDEO));
}
return;
}

View File

@@ -466,7 +466,7 @@ export function participantUpdated(participant = {}) {
* @returns {Promise}
*/
export function participantMutedUs(participant, track) {
return dispatch => {
return (dispatch, getState) => {
if (!participant) {
return;
}
@@ -474,7 +474,10 @@ export function participantMutedUs(participant, track) {
const isAudio = track.isAudioTrack();
dispatch(showNotification({
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle'
titleKey: isAudio ? 'notify.mutedRemotelyTitle' : 'notify.videoMutedRemotelyTitle',
titleArguments: {
moderator: getParticipantDisplayName(getState, participant.getId())
}
}));
};
}

View File

@@ -113,6 +113,12 @@ class Popover extends Component<Props, State> {
id: ''
};
/**
* Reference to the dialog container.
*/
_containerRef: Object;
/**
* Initializes a new {@code Popover} instance.
*
@@ -130,8 +136,10 @@ class Popover extends Component<Props, State> {
this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this);
this._onKeyPress = this._onKeyPress.bind(this);
this._containerRef = React.createRef();
this._onEscKey = this._onEscKey.bind(this);
this._onThumbClick = this._onThumbClick.bind(this);
this._onTouchStart = this._onTouchStart.bind(this);
}
/**
@@ -141,7 +149,27 @@ class Popover extends Component<Props, State> {
* @public
*/
showDialog() {
this.setState({ showDialog: true });
this._onShowDialog();
}
/**
* Sets up a touch event listener to attach.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
window.addEventListener('touchstart', this._onTouchStart);
}
/**
* Removes the listener set up in the {@code componentDidMount} method.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
window.removeEventListener('touchstart', this._onTouchStart);
}
/**
@@ -178,7 +206,8 @@ class Popover extends Component<Props, State> {
onClick = { this._onThumbClick }
onKeyPress = { this._onKeyPress }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
onMouseLeave = { this._onHideDialog }
ref = { this._containerRef }>
<InlineDialog
content = { this._renderContent() }
isOpen = { this.state.showDialog }
@@ -189,6 +218,25 @@ class Popover extends Component<Props, State> {
);
}
_onTouchStart: (event: TouchEvent) => void;
/**
* Hide dialog on touch outside of the context menu.
*
* @param {TouchEvent} event - The touch event.
* @private
* @returns {void}
*/
_onTouchStart(event) {
if (this.state.showDialog
&& !this.props.overflowDrawer
&& this._containerRef
&& this._containerRef.current
&& !this._containerRef.current.contains(event.target)) {
this._onHideDialog();
}
}
_onHideDialog: () => void;
/**
@@ -216,7 +264,7 @@ class Popover extends Component<Props, State> {
* @returns {void}
*/
_onShowDialog(event) {
event.stopPropagation();
event && event.stopPropagation();
if (!this.props.disablePopover) {
this.setState({ showDialog: true });

View File

@@ -30,3 +30,15 @@ export const SET_ASPECT_RATIO = 'SET_ASPECT_RATIO';
* @public
*/
export const SET_REDUCED_UI = 'SET_REDUCED_UI';
/**
* The type of (redux) action which tells whether a local or remote participant
* context menu is open.
*
* {
* type: SET_CONTEXT_MENU_OPEN,
* showConnectionInfo: boolean
* }
*/
export const SET_CONTEXT_MENU_OPEN = 'SET_CONTEXT_MENU_OPEN';

View File

@@ -7,7 +7,7 @@ import { CHAT_SIZE } from '../../chat/constants';
import { getParticipantsPaneOpen } from '../../participants-pane/functions';
import theme from '../../participants-pane/theme.json';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_CONTEXT_MENU_OPEN, SET_REDUCED_UI } from './actionTypes';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
/**
@@ -110,3 +110,16 @@ export function setReducedUI(width: number, height: number): Function {
}
};
}
/**
* Sets whether the local or remote participant context menu is open.
*
* @param {boolean} isOpen - Whether local or remote context menu is open.
* @returns {Object}
*/
export function setParticipantContextMenuOpen(isOpen: boolean) {
return {
type: SET_CONTEXT_MENU_OPEN,
isOpen
};
}

View File

@@ -2,7 +2,7 @@
import { ReducerRegistry, set } from '../redux';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_REDUCED_UI } from './actionTypes';
import { CLIENT_RESIZED, SET_ASPECT_RATIO, SET_CONTEXT_MENU_OPEN, SET_REDUCED_UI } from './actionTypes';
import { ASPECT_RATIO_NARROW } from './constants';
const {
@@ -17,7 +17,8 @@ const DEFAULT_STATE = {
aspectRatio: ASPECT_RATIO_NARROW,
clientHeight: innerHeight,
clientWidth: innerWidth,
reducedUI: false
reducedUI: false,
contextMenuOpened: false
};
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
@@ -34,6 +35,9 @@ ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE,
case SET_REDUCED_UI:
return set(state, 'reducedUI', action.reducedUI);
case SET_CONTEXT_MENU_OPEN:
return set(state, 'contextMenuOpened', action.isOpen);
}
return state;

View File

@@ -23,6 +23,14 @@ export default class AbstractAudioMuteButton<P: Props, S: *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setAudioMuted(!this._isAudioMuted());
}

View File

@@ -20,6 +20,11 @@ export type Props = {
*/
disabledStyles: ?Styles,
/**
* External handler for click action.
*/
handleClick?: Function,
/**
* Whether to show the label or not.
*/

View File

@@ -22,6 +22,14 @@ export default class AbstractVideoMuteButton<P : Props, S : *>
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._setVideoMuted(!this._isVideoMuted());
}

View File

@@ -258,17 +258,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
};
}

View File

@@ -3,7 +3,7 @@
import UIEvents from '../../../../service/UI/UIEvents';
import { showModeratedNotification } from '../../av-moderation/actions';
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
import { hideNotification } from '../../notifications';
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getAvailableDevices } from '../devices/actions';
import {
@@ -142,15 +142,18 @@ MiddlewareRegistry.register(store => next => action => {
// check for A/V Moderation when trying to start screen sharing
if ((action.enabled || action.enabled === undefined)
&& shouldShowModeratedNotification(MEDIA_TYPE.VIDEO, store.getState())) {
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
if (!isModerationNotificationDisplayed(MEDIA_TYPE.PRESENTER, store.getState())) {
store.dispatch(showModeratedNotification(MEDIA_TYPE.PRESENTER));
}
return;
}
const { enabled, audioOnly } = action;
const { enabled, audioOnly, ignoreDidHaveVideo } = action;
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING, { enabled,
audioOnly });
audioOnly,
ignoreDidHaveVideo });
}
break;

View File

@@ -22,6 +22,8 @@ import {
* "error" or "local" or "remote".
* @param {string} messageDetails.timestamp - A timestamp to display for when
* the message was received.
* @param {string} messageDetails.isReaction - Whether or not the
* message is a reaction message.
* @returns {{
* type: ADD_MESSAGE,
* displayName: string,
@@ -29,6 +31,7 @@ import {
* message: string,
* messageType: string,
* timestamp: string,
* isReaction: boolean
* }}
*/
export function addMessage(messageDetails: Object) {

View File

@@ -18,11 +18,6 @@ type Props = AbstractButtonProps & {
* Whether or not the chat feature is currently displayed.
*/
_chatOpen: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -61,7 +56,13 @@ class ChatButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -69,14 +69,30 @@ export function getUnreadCount(state: Object) {
return 0;
}
let reactionMessages = 0;
if (navigator.product === 'ReactNative') {
// React native stores the messages in a reversed order.
return messages.indexOf(lastReadMessage);
const lastReadIndex = messages.indexOf(lastReadMessage);
for (let i = 0; i < lastReadIndex; i++) {
if (messages[i].isReaction) {
reactionMessages++;
}
}
return lastReadIndex - reactionMessages;
}
const lastReadIndex = messages.lastIndexOf(lastReadMessage);
return messagesCount - (lastReadIndex + 1);
for (let i = lastReadIndex + 1; i < messagesCount; i++) {
if (messages[i].isReaction) {
reactionMessages++;
}
}
return messagesCount - (lastReadIndex + 1) - reactionMessages;
}
/**

View File

@@ -68,7 +68,12 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case ADD_MESSAGE:
unreadCount = action.hasRead ? 0 : getUnreadCount(getState()) + 1;
unreadCount = getUnreadCount(getState());
if (action.isReaction) {
action.hasRead = false;
} else {
unreadCount = action.hasRead ? 0 : unreadCount + 1;
}
isOpen = getState()['features/chat'].isOpen;
if (typeof APP !== 'undefined') {
@@ -171,7 +176,7 @@ MiddlewareRegistry.register(store => next => action => {
message: action.message,
privateMessage: false,
timestamp: Date.now()
}, false);
}, false, true);
}
}
@@ -270,7 +275,7 @@ function _addChatMsgListener(conference, store) {
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
timestamp: eventData.timestamp
}, false);
}, false, true);
}
}
});
@@ -304,11 +309,13 @@ function _handleChatError({ dispatch }, error) {
* @param {Store} store - The Redux store.
* @param {Object} message - The message object.
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
* @param {boolean} isReaction - Whether or not the message is a reaction message.
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState },
{ id, message, privateMessage, timestamp },
shouldPlaySound = true
shouldPlaySound = true,
isReaction = false
) {
// Logic for all platforms:
const state = getState();
@@ -337,7 +344,8 @@ function _handleReceivedMessage({ dispatch, getState },
message,
privateMessage,
recipient: getParticipantDisplayName(state, localParticipant.id),
timestamp: millisecondsTimestamp
timestamp: millisecondsTimestamp,
isReaction
}));
if (typeof APP !== 'undefined') {

View File

@@ -28,6 +28,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
displayName: action.displayName,
error: action.error,
id: action.id,
isReaction: action.isReaction,
messageType: action.messageType,
message: action.message,
privateMessage: action.privateMessage,

View File

@@ -0,0 +1,12 @@
export const CONFERENCE_INFO = {
alwaysVisible: [ 'recording', 'local-recording' ],
autoHide: [
'subject',
'conference-timer',
'participants-count',
'e2ee',
'transcribing',
'video-quality',
'insecure-room'
]
};

View File

@@ -0,0 +1,22 @@
// @flow
import { CONFERENCE_INFO } from './constants';
/**
* Retrieves the conference info labels based on config values and defaults.
*
* @param {Object} state - The redux state.
* @returns {Object} The conferenceInfo object.
*/
export const getConferenceInfo = (state: Object) => {
const { conferenceInfo } = state['features/base/config'];
if (conferenceInfo) {
return {
alwaysVisible: conferenceInfo.alwaysVisible ?? CONFERENCE_INFO.alwaysVisible,
autoHide: conferenceInfo.autoHide ?? CONFERENCE_INFO.autoHide
};
}
return CONFERENCE_INFO;
};

View File

@@ -1,22 +1,22 @@
/* @flow */
import React from 'react';
import React, { Component } from 'react';
import { getConferenceName } from '../../../base/conference/functions';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { getParticipantCount } from '../../../base/participants/functions';
import { connect } from '../../../base/redux';
import { E2EELabel } from '../../../e2ee';
import { LocalRecordingLabel } from '../../../local-recording';
import { getSessionStatusToShow, RecordingLabel } from '../../../recording';
import { RecordingLabel } from '../../../recording';
import { isToolboxVisible } from '../../../toolbox/functions.web';
import { TranscribingLabel } from '../../../transcribing';
import { VideoQualityLabel } from '../../../video-quality';
import ConferenceTimer from '../ConferenceTimer';
import { getConferenceInfo } from '../functions';
import ConferenceInfoContainer from './ConferenceInfoContainer';
import InsecureRoomNameLabel from './InsecureRoomNameLabel';
import ParticipantsCount from './ParticipantsCount';
import { InsecureRoomNameLabel } from '.';
import SubjectText from './SubjectText';
/**
* The type of the React {@code Component} props of {@link Subject}.
@@ -24,54 +24,59 @@ import { InsecureRoomNameLabel } from '.';
type Props = {
/**
* Whether the info should span across the full width.
* The conference info labels to be shown in the conference header.
*/
_fullWidth: boolean,
/**
* Whether the conference name and timer should be displayed or not.
*/
_hideConferenceNameAndTimer: boolean,
/**
* Whether the conference timer should be shown or not.
*/
_hideConferenceTimer: boolean,
/**
* Whether the recording label should be shown or not.
*/
_hideRecordingLabel: boolean,
/**
* Whether the participant count should be shown or not.
*/
_showParticipantCount: boolean,
/**
* The subject or the of the conference.
* Falls back to conference name.
*/
_subject: string,
_conferenceInfo: Object,
/**
* Indicates whether the component should be visible or not.
*/
_visible: boolean,
/**
* Whether or not the recording label is visible.
*/
_recordingLabel: boolean
_visible: boolean
};
const getLeftMargin = () => {
const subjectContainerWidth = document.getElementById('subject-container')?.clientWidth ?? 0;
const recContainerWidth = document.getElementById('rec-container')?.clientWidth ?? 0;
const subjectDetailsContainer = document.getElementById('subject-details-container')?.clientWidth ?? 0;
return (subjectContainerWidth - recContainerWidth - subjectDetailsContainer) / 2;
};
const COMPONENTS = [
{
Component: SubjectText,
id: 'subject'
},
{
Component: ConferenceTimer,
id: 'conference-timer'
},
{
Component: ParticipantsCount,
id: 'participants-count'
},
{
Component: E2EELabel,
id: 'e2ee'
},
{
Component: () => (
<>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
</>
),
id: 'recording'
},
{
Component: LocalRecordingLabel,
id: 'local-recording'
},
{
Component: TranscribingLabel,
id: 'transcribing'
},
{
Component: VideoQualityLabel,
id: 'video-quality'
},
{
Component: InsecureRoomNameLabel,
id: 'insecure-room'
}
];
/**
* The upper band of the meeing containing the conference name, timer and labels.
@@ -79,61 +84,90 @@ const getLeftMargin = () => {
* @param {Object} props - The props of the component.
* @returns {React$None}
*/
function ConferenceInfo(props: Props) {
const {
_hideConferenceNameAndTimer,
_hideConferenceTimer,
_showParticipantCount,
_hideRecordingLabel,
_subject,
_fullWidth,
_visible,
_recordingLabel
} = props;
class ConferenceInfo extends Component<Props> {
/**
* Initializes a new {@code ConferenceInfo} instance.
*
* @param {Props} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
return (
<div className = { `subject ${_recordingLabel ? 'recording' : ''} ${_visible ? 'visible' : ''}` }>
<div
className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }
id = 'subject-container'>
{!_hideRecordingLabel && <div
className = 'show-always'
id = 'rec-container'
// eslint-disable-next-line react-native/no-inline-styles
style = {{
marginLeft: !_recordingLabel || _visible ? 0 : getLeftMargin()
}}>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
<LocalRecordingLabel />
</div>
this._renderAutoHide = this._renderAutoHide.bind(this);
this._renderAlwaysVisible = this._renderAlwaysVisible.bind(this);
}
_renderAutoHide: () => void;
/**
* Renders auto-hidden info header labels.
*
* @returns {void}
*/
_renderAutoHide() {
const { autoHide } = this.props._conferenceInfo;
if (!autoHide || !autoHide.length) {
return null;
}
return (
<ConferenceInfoContainer visible = { this.props._visible } >
{
COMPONENTS
.filter(comp => autoHide.includes(comp.id))
.map(c =>
<c.Component key = { c.id } />
)
}
<div
className = 'subject-details-container'
id = 'subject-details-container'>
{
!_hideConferenceNameAndTimer
&& <div className = 'subject-info'>
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
{ !_hideConferenceTimer && <ConferenceTimer /> }
</div>
}
{ _showParticipantCount && <ParticipantsCount /> }
<E2EELabel />
{_hideRecordingLabel && (
<>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
<LocalRecordingLabel />
</>
)}
<TranscribingLabel />
<VideoQualityLabel />
<InsecureRoomNameLabel />
</div>
</ConferenceInfoContainer>
);
}
_renderAlwaysVisible: () => void;
/**
* Renders the always visible info header labels.
*
* @returns {void}
*/
_renderAlwaysVisible() {
const { alwaysVisible } = this.props._conferenceInfo;
if (!alwaysVisible || !alwaysVisible.length) {
return null;
}
return (
<ConferenceInfoContainer visible = { true } >
{
COMPONENTS
.filter(comp => alwaysVisible.includes(comp.id))
.map(c =>
<c.Component key = { c.id } />
)
}
</ConferenceInfoContainer>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div className = 'details-container' >
{ [
this._renderAlwaysVisible(),
this._renderAutoHide()
] }
</div>
</div>
);
);
}
}
/**
@@ -143,40 +177,14 @@ function ConferenceInfo(props: Props) {
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _hideConferenceTimer: boolean,
* _showParticipantCount: boolean,
* _subject: string,
* _visible: boolean
* _visible: boolean,
* _conferenceInfo: Object
* }}
*/
function _mapStateToProps(state) {
const participantCount = getParticipantCount(state);
const {
hideConferenceTimer,
hideConferenceSubject,
hideParticipantsStats,
hideRecordingLabel,
iAmRecorder
} = state['features/base/config'];
const { clientWidth } = state['features/base/responsive-ui'];
const shouldHideRecordingLabel = hideRecordingLabel || iAmRecorder;
const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE);
const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM);
const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
const isStreamRecording = streamRecordingStatus
? streamRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
const { isEngaged } = state['features/local-recording'];
return {
_hideConferenceNameAndTimer: clientWidth < 300,
_hideConferenceTimer: Boolean(hideConferenceTimer),
_hideRecordingLabel: shouldHideRecordingLabel,
_fullWidth: state['features/video-layout'].tileViewEnabled,
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
_subject: hideConferenceSubject ? '' : getConferenceName(state),
_visible: isToolboxVisible(state),
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !shouldHideRecordingLabel
_conferenceInfo: getConferenceInfo(state)
};
}

View File

@@ -0,0 +1,24 @@
/* @flow */
import React from 'react';
type Props = {
/**
* The children components.
*/
children: React$Node,
/**
* Whether this conference info container should be visible or not.
*/
visible: boolean
}
export default ({ visible, children }: Props) => (
<div className = { `subject${visible ? ' visible' : ''}` }>
<div className = { 'subject-info-container' }>
{children}
</div>
</div>
);

View File

@@ -19,7 +19,7 @@ type Props = {
/**
* Number of the conference participants.
*/
count: string,
count: number,
/**
* Conference data.
@@ -72,6 +72,12 @@ class ParticipantsCount extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const { count } = this.props;
if (count <= 2) {
return null;
}
return (
<div
className = 'participants-count'
@@ -79,7 +85,7 @@ class ParticipantsCount extends PureComponent<Props> {
<Label
className = 'label--white'
icon = { IconUserGroups }
text = { this.props.count } />
text = { count } />
</div>
);
}

View File

@@ -0,0 +1,50 @@
/* @flow */
import React from 'react';
import { getConferenceName } from '../../../base/conference/functions';
import { connect } from '../../../base/redux';
import { Tooltip } from '../../../base/tooltip';
type Props = {
/**
* The conference display name.
*/
_subject: string
}
/**
* Label for the conference name.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
const SubjectText = ({ _subject }: Props) => (
<div className = 'subject-text'>
<Tooltip
content = { _subject }
position = 'bottom'>
<div className = 'subject-text--content'>{ _subject }</div>
</Tooltip>
</div>
);
/**
* Maps (parts of) the Redux state to the associated
* {@code Subject}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _subject: string,
* }}
*/
function _mapStateToProps(state) {
return {
_subject: getConferenceName(state)
};
}
export default connect(_mapStateToProps)(SubjectText);

View File

@@ -5,3 +5,4 @@ export { default as renderConferenceTimer } from './ConferenceTimerDisplay';
export { default as InsecureRoomNameLabel } from './InsecureRoomNameLabel';
export { default as InviteMore } from './InviteMore';
export { default as ConferenceInfo } from './ConferenceInfo';
export { default as SubjectText } from './SubjectText';

View File

@@ -258,6 +258,12 @@ class DeviceSelection extends AbstractDialogTab<Props, State> {
* @returns {void}
*/
_createAudioInputTrack(deviceId) {
const { hideAudioInputPreview } = this.props;
if (hideAudioInputPreview) {
return;
}
return this._disposeAudioInputPreview()
.then(() => createLocalTrack('audio', deviceId, 5000))
.then(jitsiLocalTrack => {

View File

@@ -36,7 +36,13 @@ class EmbedMeetingButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('embed.meeting'));
dispatch(openDialog(EmbedMeetingDialog));

View File

@@ -59,12 +59,20 @@ class SharedDocumentButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _editing, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent(
'toggle.etherpad',
{
enable: !this.props._editing
enable: !_editing
}));
this.props.dispatch(toggleDocument());
dispatch(toggleDocument());
}
/**

View File

@@ -40,7 +40,13 @@ class FeedbackButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _conference, dispatch } = this.props;
const { _conference, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('feedback'));
dispatch(openFeedbackDialog(_conference));

View File

@@ -36,6 +36,10 @@ const SCORES = [
'feedback.veryGood'
];
type Scrollable = {
scroll: Function
}
/**
* The type of the React {@code Component} props of {@link FeedbackDialog}.
*/
@@ -171,6 +175,12 @@ class FeedbackDialog extends Component<Props, State> {
this._onScoreContainerMouseLeave
= this._onScoreContainerMouseLeave.bind(this);
this._onSubmit = this._onSubmit.bind(this);
// 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);
};
}
/**
@@ -244,6 +254,7 @@ class FeedbackDialog extends Component<Props, State> {
<Dialog
okKey = 'dialog.Submit'
onCancel = { this._onCancel }
onDialogRef = { this._onScrollTop }
onSubmit = { this._onSubmit }
titleKey = 'feedback.rateExperience'>
<div className = 'feedback-dialog'>
@@ -364,6 +375,8 @@ class FeedbackDialog extends Component<Props, State> {
return true;
}
_onScrollTop: (node: ?Scrollable) => void;
}
/**

View File

@@ -64,6 +64,11 @@ type Props = {
*/
_filmstripHeight: number,
/**
* Whether this is a recorder or not.
*/
_iAmRecorder: boolean,
/**
* Whether the filmstrip button is enabled.
*/
@@ -235,14 +240,14 @@ class Filmstrip extends PureComponent <Props> {
* @returns {Object}
*/
_calculateIndices(startIndex, stopIndex) {
const { _currentLayout, _thumbnailsReordered } = this.props;
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
let start = startIndex;
let stop = stopIndex;
if (_thumbnailsReordered) {
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
// endpoint. The remote participants start from index 1.
if (_currentLayout === LAYOUTS.TILE_VIEW) {
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
start = Math.max(startIndex - 1, 0);
stop = stopIndex - 1;
}
@@ -294,18 +299,24 @@ class Filmstrip extends PureComponent <Props> {
* @returns {string} - The key.
*/
_gridItemKey({ columnIndex, rowIndex }) {
const { _columns, _remoteParticipants, _remoteParticipantsLength, _thumbnailsReordered } = this.props;
const {
_columns,
_iAmRecorder,
_remoteParticipants,
_remoteParticipantsLength,
_thumbnailsReordered
} = this.props;
const index = (rowIndex * _columns) + columnIndex;
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
const remoteIndex = _thumbnailsReordered ? index - 1 : index;
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
if (index > _remoteParticipantsLength) {
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
return `empty-${index}`;
}
if (index === localIndex) {
if (!_iAmRecorder && index === localIndex) {
return 'local';
}
@@ -528,7 +539,7 @@ class Filmstrip extends PureComponent <Props> {
*/
function _mapStateToProps(state) {
const toolbarButtons = getToolbarButtons(state);
const { testing = {} } = state['features/base/config'];
const { testing = {}, iAmRecorder } = state['features/base/config'];
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
const { visible, remoteParticipants } = state['features/filmstrip'];
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
@@ -596,6 +607,7 @@ function _mapStateToProps(state) {
_currentLayout,
_filmstripHeight: remoteFilmstripHeight,
_filmstripWidth: remoteFilmstripWidth,
_iAmRecorder: Boolean(iAmRecorder),
_isFilmstripButtonEnabled: isButtonEnabled('filmstrip', state),
_remoteParticipantsLength: remoteParticipants.length,
_remoteParticipants: remoteParticipants,

View File

@@ -129,11 +129,13 @@ function _mapStateToProps(state, ownProps) {
isAudioMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.AUDIO, participantID);
}
const { disableModeratorIndicator } = state['features/base/config'];
return {
_currentLayout: getCurrentLayout(state),
_showAudioMutedIndicator: isAudioMuted,
_showModeratorIndicator:
!interfaceConfig.DISABLE_FOCUS_INDICATOR && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
!disableModeratorIndicator && participant && participant.role === PARTICIPANT_ROLE.MODERATOR,
_showScreenShareIndicator: isScreenSharing,
_showVideoMutedIndicator: isVideoMuted
};

View File

@@ -34,7 +34,6 @@ import {
DISPLAY_MODE_TO_STRING,
DISPLAY_VIDEO,
DISPLAY_VIDEO_WITH_NAME,
MOBILE_FILMSTRIP_PORTRAIT_RATIO,
VIDEO_TEST_EVENTS,
SHOW_TOOLBAR_CONTEXT_MENU_AFTER
} from '../../constants';
@@ -771,7 +770,6 @@ class Thumbnail extends Component<Props, State> {
const {
_defaultLocalDisplayName,
_disableLocalVideoFlip,
_height,
_isMobile,
_isMobilePortrait,
_isScreenSharing,
@@ -783,13 +781,14 @@ class Thumbnail extends Component<Props, State> {
const { id } = _participant || {};
const { audioLevel } = this.state;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
let containerClassName = this._getContainerClassName();
const videoTrackClassName
= !_disableLocalVideoFlip && _videoTrack && !_isScreenSharing && _localFlipX ? 'flipVideoX' : '';
styles.thumbnail.height = _isMobilePortrait
? `${Math.floor(_height * MOBILE_FILMSTRIP_PORTRAIT_RATIO)}px`
: styles.thumbnail.height;
if (_isMobilePortrait) {
styles.thumbnail.height = styles.thumbnail.width;
containerClassName = `${containerClassName} self-view-mobile-portrait`;
}
return (
<span

View File

@@ -113,26 +113,27 @@ function _mapStateToProps(state, ownProps) {
const { columns, rows } = gridDimensions;
const index = (rowIndex * columns) + columnIndex;
let horizontalOffset;
const { iAmRecorder } = state['features/base/config'];
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1);
if (rowIndex === rows - 1) { // center the last row
const { width: thumbnailWidth } = thumbnailSize;
const { iAmRecorder } = state['features/base/config'];
const partialLastRowParticipantsNumber = (remoteParticipantsLength + (iAmRecorder ? 0 : 1)) % columns;
const partialLastRowParticipantsNumber = participantsLenght % columns;
if (partialLastRowParticipantsNumber > 0) {
horizontalOffset = Math.floor((columns - partialLastRowParticipantsNumber) * (thumbnailWidth + 4) / 2);
}
}
if (index > remoteParticipantsLength) {
if (index > participantsLenght - 1) {
return {};
}
// When the thumbnails are reordered, local participant is inserted at index 0.
const localIndex = enableThumbnailReordering ? 0 : remoteParticipantsLength;
const remoteIndex = enableThumbnailReordering ? index - 1 : index;
const remoteIndex = enableThumbnailReordering && !iAmRecorder ? index - 1 : index;
if (index === localIndex) {
if (!iAmRecorder && index === localIndex) {
return {
_participantID: 'local',
_horizontalOffset: horizontalOffset

View File

@@ -221,14 +221,6 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
*/
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
/**
* The ratio for filmstrip self view on mobile portrait mode.
*
* @type {number}
*/
export const MOBILE_FILMSTRIP_PORTRAIT_RATIO = 2.5;
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.

View File

@@ -70,9 +70,11 @@ export function shouldRemoteVideosBeVisible(state: Object) {
const participantCount = getParticipantCountWithFake(state);
let pinnedParticipant;
const { disable1On1Mode } = state['features/base/config'];
const { contextMenuOpened } = state['features/base/responsive-ui'];
return Boolean(
participantCount > 2
contextMenuOpened
|| participantCount > 2
// Always show the filmstrip when there is another participant to
// show and the local video is pinned, or the toolbar is displayed.

View File

@@ -34,7 +34,13 @@ class InviteButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('invite'));
dispatch(beginAddPeople());

View File

@@ -34,7 +34,13 @@ class KeyboardShortcutsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('shortcuts'));
dispatch(openKeyboardShortcutsDialog());

View File

@@ -36,7 +36,13 @@ class LocalRecording extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('local.recording'));
dispatch(openDialog(LocalRecordingInfoDialog));

View File

@@ -13,15 +13,20 @@ import { Tooltip } from '../../base/tooltip';
*/
type Props = {
/**
* Whether this is the Jibri recorder participant.
*/
_iAmRecorder: boolean,
/**
* Whether local recording is engaged or not.
*/
_isEngaged: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* Whether local recording is engaged or not.
*/
isEngaged: boolean
};
/**
@@ -38,7 +43,7 @@ class LocalRecordingLabel extends Component<Props> {
* @returns {ReactElement}
*/
render() {
if (!this.props.isEngaged) {
if (!this.props._isEngaged || this.props._iAmRecorder) {
return null;
}
@@ -66,9 +71,11 @@ class LocalRecordingLabel extends Component<Props> {
*/
function _mapStateToProps(state) {
const { isEngaged } = state['features/local-recording'];
const { iAmRecorder } = state['features/base/config'];
return {
isEngaged
_isEngaged: isEngaged,
_iAmRecorder: iAmRecorder
};
}

View File

@@ -1,5 +1,7 @@
// @flow
import { MODERATION_NOTIFICATIONS } from '../av-moderation/constants';
import { MEDIA_TYPE } from '../base/media';
import { toState } from '../base/redux';
declare var interfaceConfig: Object;
@@ -26,3 +28,18 @@ export function areThereNotifications(stateful: Object | Function) {
export function joinLeaveNotificationsDisabled() {
return Boolean(typeof interfaceConfig !== 'undefined' && interfaceConfig?.DISABLE_JOIN_LEAVE_NOTIFICATIONS);
}
/**
* Returns whether or not the moderation notification for the given type is displayed.
*
* @param {MEDIA_TYPE} mediaType - The media type to check.
* @param {Object | Function} stateful - The redux store state.
* @returns {boolean}
*/
export function isModerationNotificationDisplayed(mediaType: MEDIA_TYPE, stateful: Object | Function) {
const state = toState(stateful);
const { notifications } = state['features/notifications'];
return Boolean(notifications.find(n => n.uid === MODERATION_NOTIFICATIONS[mediaType]));
}

View File

@@ -69,13 +69,14 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
}
case PARTICIPANT_UPDATED: {
if (typeof interfaceConfig === 'undefined') {
// Do not show the notification for mobile and also when the focus indicator is disabled.
const state = store.getState();
const { disableModeratorIndicator } = state['features/base/config'];
if (disableModeratorIndicator) {
return next(action);
}
const { id, role } = action.participant;
const state = store.getState();
const localParticipant = getLocalParticipant(state);
if (localParticipant.id !== id) {

View File

@@ -55,10 +55,10 @@ const useStyles = makeStyles(() => {
},
text: {
color: '#C2C2C2',
padding: '10px 16px 10px 52px'
padding: '10px 16px'
},
paddedAction: {
marginLeft: '36px;'
marginLeft: '36px'
}
};
});

View File

@@ -14,11 +14,6 @@ type Props = AbstractButtonProps & {
* Whether or not the participants pane is open.
*/
_isOpen: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -37,7 +32,13 @@ class ParticipantsPaneButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -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') }>
<ParticipantActionButton
onClick = { reject }
primary = { false }>
{t('lobby.reject')}
</ParticipantActionButton>
<ParticipantActionButton
onClick = { admit }
primary = { true }>

View File

@@ -5,6 +5,7 @@ import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { isToolbarButtonEnabled } from '../../../base/config/functions.web';
import { openDialog } from '../../../base/dialog';
import { isIosMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import {
IconCloseCircle,
@@ -395,6 +396,11 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
return null;
}
const showVolumeSlider = !isIosMobileBrowser()
&& overflowDrawer
&& typeof _volume === 'number'
&& !isNaN(_volume);
const actions
= _participant?.isFakeParticipant ? (
<>
@@ -463,7 +469,7 @@ class MeetingParticipantContextMenu extends Component<Props, State> {
)
}
</ContextMenuItemGroup>
{ overflowDrawer && typeof _volume === 'number' && !isNaN(_volume)
{ showVolumeSlider
&& <ContextMenuItemGroup>
<VolumeSlider
initialValue = { _volume }

View File

@@ -1,15 +1,24 @@
// @flow
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { translate } from '../../../base/i18n';
import { JitsiTrackEvents } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../base/media';
import {
getLocalParticipant,
getParticipantByIdOrUndefined,
getParticipantDisplayName
getParticipantDisplayName,
isParticipantModerator
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks';
import { ACTION_TRIGGER, type MediaState } from '../../constants';
import {
getLocalAudioTrack,
getTrackByMediaTypeAndParticipant,
isParticipantAudioMuted,
isParticipantVideoMuted
} from '../../../base/tracks';
import { ACTION_TRIGGER, type MediaState, MEDIA_STATE } from '../../constants';
import {
getParticipantAudioMediaState,
getParticipantVideoMediaState,
@@ -27,6 +36,11 @@ type Props = {
*/
_audioMediaState: MediaState,
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object,
/**
* Media state for video.
*/
@@ -122,6 +136,11 @@ type Props = {
*/
participantID: ?string,
/**
* The translate function.
*/
t: Function,
/**
* The translated "you" text.
*/
@@ -136,6 +155,7 @@ type Props = {
*/
function MeetingParticipantItem({
_audioMediaState,
_audioTrack,
_videoMediaState,
_displayName,
_local,
@@ -153,14 +173,55 @@ function MeetingParticipantItem({
openDrawerForParticipant,
overflowDrawer,
participantActionEllipsisLabel,
t,
youText
}: Props) {
const [ hasAudioLevels, setHasAudioLevel ] = useState(false);
const [ registeredEvent, setRegisteredEvent ] = useState(false);
const _updateAudioLevel = useCallback(level => {
const audioLevel = typeof level === 'number' && !isNaN(level)
? level : 0;
setHasAudioLevel(audioLevel > 0.009);
}, []);
useEffect(() => {
if (_audioTrack && !registeredEvent) {
const { jitsiTrack } = _audioTrack;
if (jitsiTrack) {
jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, _updateAudioLevel);
setRegisteredEvent(true);
}
}
return () => {
if (_audioTrack && registeredEvent) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, _updateAudioLevel);
}
};
}, [ _audioTrack ]);
const audioMediaState = _audioMediaState === MEDIA_STATE.UNMUTED && hasAudioLevels
? MEDIA_STATE.DOMINANT_SPEAKER : _audioMediaState;
let askToUnmuteText = askUnmuteText;
if (_audioMediaState !== MEDIA_STATE.FORCE_MUTED && _videoMediaState === MEDIA_STATE.FORCE_MUTED) {
askToUnmuteText = t('participantsPane.actions.allowVideo');
}
return (
<ParticipantItem
actionsTrigger = { ACTION_TRIGGER.HOVER }
audioMediaState = { _audioMediaState }
audioMediaState = { audioMediaState }
displayName = { _displayName }
isHighlighted = { isHighlighted }
isModerator = { isParticipantModerator(_participant) }
local = { _local }
onLeave = { onLeave }
openDrawerForParticipant = { openDrawerForParticipant }
@@ -173,7 +234,7 @@ function MeetingParticipantItem({
{!overflowDrawer && !_participant?.isFakeParticipant
&& <>
<ParticipantQuickAction
askUnmuteText = { askUnmuteText }
askUnmuteText = { askToUnmuteText }
buttonType = { _quickActionButtonType }
muteAudio = { muteAudio }
muteParticipantButtonText = { muteParticipantButtonText }
@@ -181,7 +242,7 @@ function MeetingParticipantItem({
<ParticipantActionEllipsis
aria-label = { participantActionEllipsisLabel }
onClick = { onContextMenu } />
</>
</>
}
{!overflowDrawer && _localVideoOwner && _participant?.isFakeParticipant && (
@@ -214,8 +275,13 @@ function _mapStateToProps(state, ownProps): Object {
const _videoMediaState = getParticipantVideoMediaState(participant, _isVideoMuted, state);
const _quickActionButtonType = getQuickActionButtonType(participant, _isAudioMuted, state);
const tracks = state['features/base/tracks'];
const _audioTrack = participantID === localParticipantId
? getLocalAudioTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantID);
return {
_audioMediaState,
_audioTrack,
_videoMediaState,
_displayName: getParticipantDisplayName(state, participant?.id),
_local: Boolean(participant?.local),
@@ -227,4 +293,4 @@ function _mapStateToProps(state, ownProps): Object {
};
}
export default connect(_mapStateToProps)(MeetingParticipantItem);
export default translate(connect(_mapStateToProps)(MeetingParticipantItem));

View File

@@ -3,6 +3,7 @@
import React, { type Node, useCallback } from 'react';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import {
ACTION_TRIGGER,
AudioStateIcons,
@@ -14,10 +15,12 @@ import {
import { RaisedHandIndicator } from './RaisedHandIndicator';
import {
ModeratorLabel,
ParticipantActionsHover,
ParticipantActionsPermanent,
ParticipantContainer,
ParticipantContent,
ParticipantDetailsContainer,
ParticipantName,
ParticipantNameContainer,
ParticipantStates
@@ -58,6 +61,11 @@ type Props = {
*/
isHighlighted?: boolean,
/**
* Whether or not the participant is a moderator.
*/
isModerator: boolean,
/**
* True if the participant is local.
*/
@@ -93,6 +101,11 @@ type Props = {
*/
videoMediaState: MediaState,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* The translated "you" text.
*/
@@ -105,9 +118,10 @@ type Props = {
* @param {Props} props - The props of the component.
* @returns {ReactNode}
*/
export default function ParticipantItem({
function ParticipantItem({
children,
isHighlighted,
isModerator,
onLeave,
actionsTrigger = ACTION_TRIGGER.HOVER,
audioMediaState = MEDIA_STATE.NONE,
@@ -118,6 +132,7 @@ export default function ParticipantItem({
openDrawerForParticipant,
overflowDrawer,
raisedHand,
t,
youText
}: Props) {
const ParticipantActions = Actions[actionsTrigger];
@@ -140,12 +155,17 @@ export default function ParticipantItem({
participantId = { participantID }
size = { 32 } />
<ParticipantContent>
<ParticipantNameContainer>
<ParticipantName>
{ displayName }
</ParticipantName>
{ local ? <span>&nbsp;({ youText })</span> : null }
</ParticipantNameContainer>
<ParticipantDetailsContainer>
<ParticipantNameContainer>
<ParticipantName>
{ displayName }
</ParticipantName>
{ local ? <span>&nbsp;({ youText })</span> : null }
</ParticipantNameContainer>
{isModerator && <ModeratorLabel>
{t('videothumbnail.moderator')}
</ModeratorLabel>}
</ParticipantDetailsContainer>
{ !local && <ParticipantActions children = { children } /> }
<ParticipantStates>
{ raisedHand && <RaisedHandIndicator /> }
@@ -156,3 +176,5 @@ export default function ParticipantItem({
</ParticipantContainer>
);
}
export default translate(ParticipantItem);

View File

@@ -282,6 +282,7 @@ export const ParticipantContainer = styled.div`
color: white;
display: flex;
font-size: 13px;
font-weight: normal;
height: ${props => props.theme.participantItemHeight}px;
margin: 0 -${props => props.theme.panePadding}px;
padding-left: ${props => props.theme.panePadding}px;
@@ -341,10 +342,24 @@ export const ParticipantName = styled.div`
`;
export const ParticipantNameContainer = styled.div`
display: flex;
flex: 1;
overflow: hidden;
`;
export const ModeratorLabel = styled.div`
font-size: 12px;
line-height: 16px;
color: #858585;
`;
export const ParticipantDetailsContainer = styled.div`
display: flex;
flex: 1;
margin-right: 8px;
overflow: hidden;
flex-direction: column;
justify-content: flex-start;
`;
export const RaisedHandIndicatorBackground = styled.div`

View File

@@ -5,7 +5,6 @@ import { View } from 'react-native';
import { useSelector } from 'react-redux';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { getParticipantCount } from '../../../base/participants';
import { REACTIONS } from '../../constants';
import RaiseHandButton from './RaiseHandButton';
@@ -37,20 +36,17 @@ function ReactionMenu({
overflowMenu
}: Props) {
const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
const _participantCount = useSelector(state => getParticipantCount(state));
return (
<View style = { overflowMenu ? _styles.overflowReactionMenu : _styles.reactionMenu }>
{_participantCount > 1
&& <View style = { _styles.reactionRow }>
{Object.keys(REACTIONS).map(key => (
<ReactionButton
key = { key }
reaction = { key }
styles = { _styles.reactionButton } />
))}
</View>
}
<View style = { _styles.reactionRow }>
{Object.keys(REACTIONS).map(key => (
<ReactionButton
key = { key }
reaction = { key }
styles = { _styles.reactionButton } />
))}
</View>
<RaiseHandButton onCancel = { onCancel } />
</View>
);

View File

@@ -8,8 +8,9 @@ import {
createToolbarEvent,
sendAnalytics
} from '../../../analytics';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { getLocalParticipant, getParticipantCount, participantUpdated } from '../../../base/participants';
import { getLocalParticipant, participantUpdated } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { dockToolbox } from '../../../toolbox/actions.web';
import { addReactionToBuffer } from '../../actions.any';
@@ -21,39 +22,39 @@ import ReactionButton from './ReactionButton';
type Props = {
/**
* The number of conference participants.
* Docks the toolbox
*/
_participantCount: number,
_dockToolbox: Function,
/**
* Used for translation.
* Whether or not it's a mobile browser.
*/
t: Function,
/**
* Whether or not the local participant's hand is raised.
*/
_raisedHand: boolean,
_isMobile: boolean,
/**
* The ID of the local participant.
*/
_localParticipantID: String,
/**
* Whether or not the local participant's hand is raised.
*/
_raisedHand: boolean,
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* Docks the toolbox
*/
_dockToolbox: Function,
/**
* Whether or not it's displayed in the overflow menu.
*/
overflowMenu: boolean
overflowMenu: boolean,
/**
* Used for translation.
*/
t: Function
};
declare var APP: Object;
@@ -182,25 +183,27 @@ class ReactionsMenu extends Component<Props> {
* @inheritdoc
*/
render() {
const { _participantCount, _raisedHand, t, overflowMenu } = this.props;
const { _raisedHand, t, overflowMenu, _isMobile } = this.props;
return (
<div className = { `reactions-menu ${overflowMenu ? 'overflow' : ''}` }>
{ _participantCount > 1 && <div className = 'reactions-row'>
<div className = 'reactions-row'>
{ this._getReactionButtons() }
</div> }
<div className = 'raise-hand-row'>
<ReactionButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = '✋'
key = 'raisehand'
label = {
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
${overflowMenu ? '' : ' (R)'}`
}
onClick = { this._onToolbarToggleRaiseHand }
toggled = { true } />
</div>
{_isMobile && (
<div className = 'raise-hand-row'>
<ReactionButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = '✋'
key = 'raisehand'
label = {
`${t(`toolbar.${_raisedHand ? 'lowerYourHand' : 'raiseYourHand'}`)}
${overflowMenu ? '' : ' (R)'}`
}
onClick = { this._onToolbarToggleRaiseHand }
toggled = { true } />
</div>
)}
</div>
);
}
@@ -217,8 +220,8 @@ function mapStateToProps(state) {
return {
_localParticipantID: localParticipant.id,
_raisedHand: localParticipant.raisedHand,
_participantCount: getParticipantCount(state)
_isMobile: isMobileBrowser(),
_raisedHand: localParticipant.raisedHand
};
}

View File

@@ -2,14 +2,16 @@
import React from 'react';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { IconRaisedHand } from '../../../base/icons';
import { IconArrowUp, IconRaisedHand } from '../../../base/icons';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ToolboxButtonWithIcon } from '../../../base/toolbox/components';
import ToolbarButton from '../../../toolbox/components/web/ToolbarButton';
import { toggleReactionsMenuVisibility } from '../../actions.web';
import { type ReactionEmojiProps } from '../../constants';
import { getReactionsQueue } from '../../functions.any';
import { getReactionsQueue, isReactionsEnabled } from '../../functions.any';
import { getReactionsMenuVisibility } from '../../functions.web';
import ReactionEmoji from './ReactionEmoji';
@@ -18,34 +20,44 @@ import ReactionsMenuPopup from './ReactionsMenuPopup';
type Props = {
/**
* Used for translation.
* Whether or not reactions are enabled.
*/
t: Function,
_reactionsEnabled: Boolean,
/**
* Whether or not the local participant's hand is raised.
* Redux dispatch function.
*/
raisedHand: boolean,
dispatch: Function,
/**
* Click handler for the reaction button. Toggles the reactions menu.
* Click handler for raise hand functionality.
*/
onReactionsClick: Function,
handleClick: Function,
/**
* Whether or not the reactions menu is open.
*/
isOpen: boolean,
/**
* Whether or not it's a mobile browser.
*/
isMobile: boolean,
/**
* Whether or not the local participant's hand is raised.
*/
raisedHand: boolean,
/**
* The array of reactions to be displayed.
*/
reactionsQueue: Array<ReactionEmojiProps>,
/**
* Redux dispatch function.
* Used for translation.
*/
dispatch: Function
t: Function
};
@@ -57,11 +69,14 @@ declare var APP: Object;
* @returns {ReactElement}
*/
function ReactionsMenuButton({
t,
raisedHand,
_reactionsEnabled,
dispatch,
handleClick,
isOpen,
isMobile,
raisedHand,
reactionsQueue,
dispatch
t
}: Props) {
/**
@@ -73,16 +88,32 @@ function ReactionsMenuButton({
dispatch(toggleReactionsMenuVisibility());
}
const raiseHandButton = (<ToolbarButton
accessibilityLabel = { t('toolbar.accessibilityLabel.raiseHand') }
icon = { IconRaisedHand }
key = 'raise-hand'
onClick = { handleClick }
toggled = { raisedHand }
tooltip = { t('toolbar.raiseHand') } />);
return (
<div className = 'reactions-menu-popup-container'>
<ReactionsMenuPopup>
<ToolbarButton
accessibilityLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconRaisedHand }
key = 'reactions'
onClick = { toggleReactionsMenu }
toggled = { raisedHand }
tooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) } />
{!_reactionsEnabled || isMobile ? raiseHandButton
: (
<ToolboxButtonWithIcon
ariaControls = 'reactions-menu-dialog'
ariaExpanded = { isOpen }
ariaHasPopup = { true }
ariaLabel = { t('toolbar.accessibilityLabel.reactionsMenu') }
icon = { IconArrowUp }
iconDisabled = { false }
iconId = 'reactions-menu-button'
iconTooltip = { t(`toolbar.${isOpen ? 'closeReactionsMenu' : 'openReactionsMenu'}`) }
onIconClick = { toggleReactionsMenu }>
{raiseHandButton}
</ToolboxButtonWithIcon>
)}
</ReactionsMenuPopup>
{reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
@@ -103,7 +134,9 @@ function mapStateToProps(state) {
const localParticipant = getLocalParticipant(state);
return {
_reactionsEnabled: isReactionsEnabled(state),
isOpen: getReactionsMenuVisibility(state),
isMobile: isMobileBrowser(),
reactionsQueue: getReactionsQueue(state),
raisedHand: localParticipant?.raisedHand
};

View File

@@ -151,11 +151,11 @@ export function getReactionsSoundsThresholds(reactions: Array<string>) {
* @returns {boolean}
*/
export function isReactionsEnabled(state: Object) {
const { enableReactions } = state['features/base/config'];
const { disableReactions } = state['features/base/config'];
if (navigator.product === 'ReactNative') {
return enableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
return !disableReactions && getFeatureFlag(state, REACTIONS_ENABLED, true);
}
return enableReactions;
return !disableReactions;
}

View File

@@ -3,6 +3,7 @@
import { batch } from 'react-redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import { getParticipantCount } from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { updateSettings } from '../base/settings';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
@@ -91,9 +92,12 @@ MiddlewareRegistry.register(store => next => action => {
case FLUSH_REACTION_BUFFER: {
const state = getState();
const { buffer } = state['features/reactions'];
const participantCount = getParticipantCount(state);
batch(() => {
dispatch(sendReactions());
if (participantCount > 1) {
dispatch(sendReactions());
}
dispatch(addReactionsToChat(getReactionMessageFromBuffer(buffer)));
dispatch(pushReactions(buffer));
});

View File

@@ -204,7 +204,7 @@ export function showStartedRecordingNotification(
}
}
dispatch(showNotification(dialogProps));
dispatch(showNotification(dialogProps, NOTIFICATION_TIMEOUT));
};
}

View File

@@ -15,6 +15,11 @@ import { getSessionStatusToShow } from '../functions';
*/
type Props = {
/**
* Whether this is the Jibri recorder participant.
*/
_iAmRecorder: boolean,
/**
* The status of the highermost priority session.
*/
@@ -100,7 +105,7 @@ export default class AbstractRecordingLabel
* @inheritdoc
*/
render() {
return this.props._status && !this.state.staleLabel
return this.props._status && !this.state.staleLabel && !this.props._iAmRecorder
? this._renderLabel() : null;
}
@@ -172,6 +177,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const { mode } = ownProps;
return {
_iAmRecorder: state['features/base/config'].iAmRecorder,
_status: getSessionStatusToShow(state, mode)
};
}

View File

@@ -76,7 +76,13 @@ export default class AbstractLiveStreamButton<P: Props> extends AbstractButton<P
* @returns {void}
*/
async _handleClick() {
const { _isLiveStreamRunning, dispatch } = this.props;
const { _isLiveStreamRunning, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
const dialogShown = await dispatch(maybeShowPremiumFeatureDialog(FEATURES.RECORDING));

View File

@@ -77,7 +77,13 @@ export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *>
* @returns {void}
*/
async _handleClick() {
const { _isRecordingRunning, dispatch } = this.props;
const { _isRecordingRunning, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent(
'recording.button',

View File

@@ -47,8 +47,16 @@ class ShareAudioButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.dispatch(startAudioScreenShareFlow());
this.props.dispatch(setOverflowMenuVisible(false));
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
dispatch(startAudioScreenShareFlow());
dispatch(setOverflowMenuVisible(false));
}
/**

View File

@@ -48,8 +48,16 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked }));
this.props.dispatch(toggleSecurityDialog());
const { _locked, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('toggle.security', { enable: !_locked }));
dispatch(toggleSecurityDialog());
}
/**

View File

@@ -42,7 +42,15 @@ class SettingsButton extends AbstractButton<Props, *> {
_handleClick() {
const {
defaultTab = SETTINGS_TABS.DEVICES,
dispatch } = this.props;
dispatch,
handleClick
} = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab));

View File

@@ -15,6 +15,11 @@ declare var APP: Object;
export type Props = {
...$Exact<AbstractDialogTabProps>,
/**
* Whether or not the reactions feature is enabled.
*/
enableReactions: Boolean,
/**
* Whether or not the sound for the incoming message should play.
*/
@@ -40,11 +45,6 @@ export type Props = {
*/
soundsReactions: Boolean,
/**
* Whether or not the reactions feature is enabled.
*/
enableReactions: Boolean,
/**
* Invoked to obtain translated strings.
*/

View File

@@ -10,6 +10,7 @@ import {
import { toState } from '../base/redux';
import { parseStandardURIString } from '../base/util';
import { isFollowMeActive } from '../follow-me';
import { isReactionsEnabled } from '../reactions/functions.any';
import { SS_DEFAULT_FRAME_RATE, SS_SUPPORTED_FRAMERATES } from './constants';
@@ -174,7 +175,7 @@ export function getSoundsTabProps(stateful: Object | Function) {
soundsTalkWhileMuted,
soundsReactions
} = state['features/base/settings'];
const { enableReactions } = state['features/base/config'];
const enableReactions = isReactionsEnabled(state);
return {
soundsIncomingMessage,

View File

@@ -66,6 +66,14 @@ class SharedVideoButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
this._doToggleSharedVideo();
}

View File

@@ -41,7 +41,13 @@ class SpeakerStatsButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _conference, dispatch } = this.props;
const { _conference, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('speaker.stats'));
dispatch(openDialog(SpeakerStats, {

View File

@@ -38,7 +38,13 @@ export class AbstractClosedCaptionButton
* @returns {void}
*/
async _handleClick() {
const { _requestingSubtitles, dispatch } = this.props;
const { _requestingSubtitles, dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('transcribing.ccButton',
{

View File

@@ -13,8 +13,7 @@ import {
clearToolboxTimeout,
setToolboxTimeout,
setToolboxTimeoutMS,
setToolboxVisible,
setToolboxEnabled
setToolboxVisible
} from './actions.native';
declare var interfaceConfig: Object;
@@ -132,10 +131,13 @@ export function showToolbox(timeout: number = 0): Object {
alwaysVisible,
enabled,
timeoutMS,
visible
visible,
overflowDrawer
} = state['features/toolbox'];
const { contextMenuOpened } = state['features/base/responsive-ui'];
const contextMenuOpenedInTileview = isLayoutTileView(state) && contextMenuOpened && !overflowDrawer;
if (enabled && !visible) {
if (enabled && !visible && !contextMenuOpenedInTileview) {
dispatch(setToolboxVisible(true));
// If the Toolbox is always visible, there's no need for a timeout
@@ -167,18 +169,20 @@ export function setOverflowDrawer(displayAsDrawer: boolean) {
};
}
/**
* Disables and hides the toolbox on demand when in tile view.
*
* @returns {void}
*/
export function disableToolboxOnTileView() {
export function hideToolboxOnTileView() {
return (dispatch: Dispatch<any>, getState: Function) => {
if (!isLayoutTileView(getState())) {
return;
}
const state = getState();
const { overflowDrawer } = state['features/toolbox'];
dispatch(setToolboxEnabled(false));
dispatch(hideToolbox(true));
if (!overflowDrawer && isLayoutTileView(state)) {
dispatch(hideToolbox(true));
}
};
}

View File

@@ -32,8 +32,16 @@ class DownloadButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _downloadAppsUrl, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('download.pressed'));
openURLInBrowser(this.props._downloadAppsUrl);
openURLInBrowser(_downloadAppsUrl);
}
}

View File

@@ -33,8 +33,16 @@ class HelpButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { _userDocumentationURL, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('help.pressed'));
openURLInBrowser(this.props._userDocumentationURL);
openURLInBrowser(_userDocumentationURL);
}
}

View File

@@ -39,7 +39,13 @@ class MuteEveryoneButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId } = this.props;
const { dispatch, localParticipantId, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryoneDialog, {

View File

@@ -39,7 +39,13 @@ class MuteEveryonesVideoButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, localParticipantId } = this.props;
const { dispatch, localParticipantId, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
sendAnalytics(createToolbarEvent('mute.everyone.pressed'));
dispatch(openDialog(MuteEveryonesVideoDialog, {

View File

@@ -15,6 +15,11 @@ import AudioMuteButton from '../AudioMuteButton';
type Props = {
/**
* External handler for click action.
*/
handleClick: Function,
/**
* Indicates whether audio permissions have been granted or denied.
*/
@@ -62,6 +67,7 @@ class AudioSettingsButton extends Component<Props> {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
_onEscClick: (KeyboardEvent) => void;
@@ -76,17 +82,36 @@ class AudioSettingsButton extends Component<Props> {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this.props.onAudioOptionsClick();
this._onClick();
}
}
_onClick: () => void;
/**
* Click handler for the more actions entries.
*
* @returns {void}
*/
_onClick() {
const { handleClick, onAudioOptionsClick } = this.props;
if (handleClick) {
handleClick();
return;
}
onAudioOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { hasPermissions, isDisabled, onAudioOptionsClick, visible, isOpen, t } = this.props;
const { handleClick, hasPermissions, isDisabled, visible, isOpen, t } = this.props;
const settingsDisabled = !hasPermissions
|| isDisabled
|| !JitsiMeetJS.mediaDevices.isMultipleAudioInputSupported();
@@ -102,12 +127,12 @@ class AudioSettingsButton extends Component<Props> {
iconDisabled = { settingsDisabled }
iconId = 'audio-settings-button'
iconTooltip = { t('toolbar.audioSettings') }
onIconClick = { onAudioOptionsClick }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<AudioMuteButton />
<AudioMuteButton handleClick = { handleClick } />
</ToolboxButtonWithIcon>
</AudioSettingsPopup>
) : <AudioMuteButton />;
) : <AudioMuteButton handleClick = { handleClick } />;
}
}

View File

@@ -11,11 +11,6 @@ type Props = AbstractButtonProps & {
* Whether or not the app is currently in full screen.
*/
_fullScreen: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -73,7 +68,13 @@ class FullscreenButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -87,7 +87,13 @@ class ProfileButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
const { dispatch, _unclickable } = this.props;
const { dispatch, _unclickable, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
if (!_unclickable) {
sendAnalytics(createToolbarEvent('profile'));

View File

@@ -12,11 +12,6 @@ type Props = AbstractButtonProps & {
* Whether or not the local participant's hand is raised.
*/
_raisedHand: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -51,7 +46,13 @@ class RaiseHandButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -29,11 +29,6 @@ type Props = AbstractButtonProps & {
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
@@ -79,7 +74,13 @@ class ShareDesktopButton extends AbstractButton<Props, *> {
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
const { handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
}
/**

View File

@@ -41,7 +41,15 @@ class ToggleCameraButton extends AbstractButton<Props, any> {
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleCamera());
const { dispatch, handleClick } = this.props;
if (handleClick) {
handleClick();
return;
}
dispatch(toggleCamera());
}
/**

View File

@@ -17,7 +17,6 @@ import { translate } from '../../../base/i18n';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipantCount,
haveParticipantWithScreenSharingFeature,
raiseHand
} from '../../../base/participants';
@@ -87,7 +86,6 @@ import AudioSettingsButton from './AudioSettingsButton';
import FullscreenButton from './FullscreenButton';
import OverflowMenuButton from './OverflowMenuButton';
import ProfileButton from './ProfileButton';
import RaiseHandButton from './RaiseHandButton';
import Separator from './Separator';
import ShareDesktopButton from './ShareDesktopButton';
import ToggleCameraButton from './ToggleCameraButton';
@@ -103,6 +101,11 @@ type Props = {
*/
_backgroundType: String,
/**
* Toolbar buttons which have their click exposed through the API.
*/
_buttonsWithNotifyClick: Array<string>,
/**
* Whether or not the chat feature is currently displayed.
*/
@@ -175,11 +178,6 @@ type Props = {
*/
_overflowMenuVisible: boolean,
/**
* Number of participants in the conference.
*/
_participantCount: number,
/**
* Whether or not the participants pane is open.
*/
@@ -249,16 +247,12 @@ type Props = {
declare var APP: Object;
type State = {
reactionsShortcutsRegistered: boolean
};
/**
* Implements the conference toolbox on React/Web.
*
* @extends Component
*/
class Toolbox extends Component<Props, State> {
class Toolbox extends Component<Props> {
/**
* Initializes a new {@code Toolbox} instance.
*
@@ -268,10 +262,6 @@ class Toolbox extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
reactionsShortcutsRegistered: false
};
// Bind event handlers so they are only bound once per instance.
this._onMouseOut = this._onMouseOut.bind(this);
this._onMouseOver = this._onMouseOver.bind(this);
@@ -301,7 +291,7 @@ class Toolbox extends Component<Props, State> {
* @returns {void}
*/
componentDidMount() {
const { _toolbarButtons, t, dispatch, _reactionsEnabled, _participantCount } = this.props;
const { _toolbarButtons, t, dispatch, _reactionsEnabled } = this.props;
const KEYBOARD_SHORTCUTS = [
isToolbarButtonEnabled('videoquality', _toolbarButtons) && {
character: 'A',
@@ -350,7 +340,7 @@ class Toolbox extends Component<Props, State> {
}
});
if (_reactionsEnabled && _participantCount > 1) {
if (_reactionsEnabled) {
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
dispatch(addReactionToBuffer(key));
@@ -384,51 +374,14 @@ class Toolbox extends Component<Props, State> {
* @inheritdoc
*/
componentDidUpdate(prevProps) {
// Ensure the dialog is closed when the toolbox becomes hidden.
if (prevProps._overflowMenuVisible && !this.props._visible) {
this._onSetOverflowVisible(false);
}
const { _dialog, dispatch } = this.props;
if (prevProps._overflowMenuVisible
&& !prevProps._dialog
&& this.props._dialog) {
&& _dialog) {
this._onSetOverflowVisible(false);
this.props.dispatch(setToolbarHovered(false));
}
if (!this.state.reactionsShortcutsRegistered
&& (prevProps._reactionsEnabled !== this.props._reactionsEnabled
|| prevProps._participantCount !== this.props._participantCount)) {
if (this.props._reactionsEnabled && this.props._participantCount > 1) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
reactionsShortcutsRegistered: true
});
const REACTION_SHORTCUTS = Object.keys(REACTIONS).map(key => {
const onShortcutSendReaction = () => {
this.props.dispatch(addReactionToBuffer(key));
sendAnalytics(createShortcutEvent(
`reaction.${key}`
));
};
return {
character: REACTIONS[key].shortcutChar,
exec: onShortcutSendReaction,
helpDescription: this.props.t(`toolbar.reaction${key.charAt(0).toUpperCase()}${key.slice(1)}`),
altKey: true
};
});
REACTION_SHORTCUTS.forEach(shortcut => {
APP.keyboardshortcut.registerShortcut(
shortcut.character,
null,
shortcut.exec,
shortcut.helpDescription,
shortcut.altKey);
});
}
dispatch(setToolbarHovered(false));
}
}
@@ -442,7 +395,7 @@ class Toolbox extends Component<Props, State> {
[ 'A', 'C', 'D', 'R', 'S' ].forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter));
if (this.props._reactionsEnabled && this.state.reactionsShortcutsRegistered) {
if (this.props._reactionsEnabled) {
Object.keys(REACTIONS).map(key => REACTIONS[key].shortcutChar)
.forEach(letter =>
APP.keyboardshortcut.unregisterShortcut(letter, true));
@@ -610,8 +563,7 @@ class Toolbox extends Component<Props, State> {
const {
_feedbackConfigured,
_isMobile,
_screenSharing,
_reactionsEnabled
_screenSharing
} = this.props;
const microphone = {
@@ -648,8 +600,8 @@ class Toolbox extends Component<Props, State> {
const raisehand = {
key: 'raisehand',
Content: _reactionsEnabled ? ReactionsMenuButton : RaiseHandButton,
handleClick: _reactionsEnabled ? null : this._onToolbarToggleRaiseHand,
Content: ReactionsMenuButton,
handleClick: this._onToolbarToggleRaiseHand,
group: 2
};
@@ -835,6 +787,24 @@ class Toolbox extends Component<Props, State> {
};
}
/**
* Overwrites click handlers for buttons in case click is exposed through the iframe API.
*
* @param {Object} buttons - The list of toolbar buttons.
* @returns {void}
*/
_overwriteButtonsClickHandlers(buttons) {
if (typeof APP === 'undefined' || !this.props._buttonsWithNotifyClick?.length) {
return;
}
Object.values(buttons).forEach((button: any) => {
if (this.props._buttonsWithNotifyClick.includes(button.key)) {
button.handleClick = () => APP.API.notifyToolbarButtonClicked(button.key);
}
});
}
/**
* Returns all buttons that need to be rendered.
*
@@ -849,6 +819,8 @@ class Toolbox extends Component<Props, State> {
const buttons = this._getAllButtons();
this._overwriteButtonsClickHandlers(buttons);
const isHangupVisible = isToolbarButtonEnabled('hangup', _toolbarButtons);
const { order } = THRESHOLDS.find(({ width }) => _clientWidth > width)
|| THRESHOLDS[THRESHOLDS.length - 1];
@@ -886,7 +858,9 @@ class Toolbox extends Component<Props, State> {
* @returns {void}
*/
_onMouseOut() {
this.props.dispatch(setToolbarHovered(false));
const { _overflowMenuVisible, dispatch } = this.props;
!_overflowMenuVisible && dispatch(setToolbarHovered(false));
}
_onMouseOver: () => void;
@@ -914,6 +888,7 @@ class Toolbox extends Component<Props, State> {
*/
_onSetOverflowVisible(visible) {
this.props.dispatch(setOverflowMenuVisible(visible));
this.props.dispatch(setToolbarHovered(visible));
}
_onShortcutToggleChat: () => void;
@@ -1313,7 +1288,9 @@ function _mapStateToProps(state, ownProps) {
let desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
const {
callStatsID,
enableFeaturesBasedOnToken
disableProfile,
enableFeaturesBasedOnToken,
buttonsWithNotifyClick
} = state['features/base/config'];
const {
fullScreen,
@@ -1345,6 +1322,7 @@ function _mapStateToProps(state, ownProps) {
return {
_backgroundType: state['features/virtual-background'].backgroundType,
_buttonsWithNotifyClick: buttonsWithNotifyClick,
_chatOpen: state['features/chat'].isOpen,
_clientWidth: clientWidth,
_conference: conference,
@@ -1353,13 +1331,12 @@ function _mapStateToProps(state, ownProps) {
_dialog: Boolean(state['features/base/dialog'].component),
_feedbackConfigured: Boolean(callStatsID),
_fullScreen: fullScreen,
_isProfileDisabled: Boolean(state['features/base/config'].disableProfile),
_isProfileDisabled: Boolean(disableProfile),
_isMobile: isMobileBrowser(),
_isVpaasMeeting: isVpaasMeeting(state),
_localParticipantID: localParticipant?.id,
_localVideo: localVideo,
_overflowMenuVisible: overflowMenuVisible,
_participantCount: getParticipantCount(state),
_participantsPaneOpen: getParticipantsPaneOpen(state),
_raisedHand: localParticipant?.raisedHand,
_reactionsEnabled: isReactionsEnabled(state),

View File

@@ -16,6 +16,11 @@ import VideoMuteButton from '../VideoMuteButton';
type Props = {
/**
* External handler for click action.
*/
handleClick: Function,
/**
* Click handler for the small icon. Opens video options.
*/
@@ -62,7 +67,7 @@ type Props = {
*/
class VideoSettingsButton extends Component<Props> {
/**
* Initializes a new {@code AudioSettingsButton} instance.
* Initializes a new {@code VideoSettingsButton} instance.
*
* @inheritdoc
*/
@@ -70,6 +75,7 @@ class VideoSettingsButton extends Component<Props> {
super(props);
this._onEscClick = this._onEscClick.bind(this);
this._onClick = this._onClick.bind(this);
}
/**
@@ -94,17 +100,36 @@ class VideoSettingsButton extends Component<Props> {
if (event.key === 'Escape' && this.props.isOpen) {
event.preventDefault();
event.stopPropagation();
this.props.onVideoOptionsClick();
this._onClick();
}
}
_onClick: () => void;
/**
* Click handler for the more actions entries.
*
* @returns {void}
*/
_onClick() {
const { handleClick, onVideoOptionsClick } = this.props;
if (handleClick) {
handleClick();
return;
}
onVideoOptionsClick();
}
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
render() {
const { onVideoOptionsClick, t, visible, isOpen } = this.props;
const { handleClick, t, visible, isOpen } = this.props;
return visible ? (
<VideoSettingsPopup>
@@ -117,12 +142,12 @@ class VideoSettingsButton extends Component<Props> {
iconDisabled = { this._isIconDisabled() }
iconId = 'video-settings-button'
iconTooltip = { t('toolbar.videoSettings') }
onIconClick = { onVideoOptionsClick }
onIconClick = { this._onClick }
onIconKeyDown = { this._onEscClick }>
<VideoMuteButton />
<VideoMuteButton handleClick = { handleClick } />
</ToolboxButtonWithIcon>
</VideoSettingsPopup>
) : <VideoMuteButton />;
) : <VideoMuteButton handleClick = { handleClick } />;
}
}

View File

@@ -0,0 +1,73 @@
/* @flow */
import React, { Component } from 'react';
import { isBrowsersOptimal } from '../../base/environment';
import { translate } from '../../base/i18n';
import { CHROME, FIREFOX } from './browserLinks';
/**
* The namespace of the CSS styles of UnsupportedDesktopBrowser.
*
* @private
* @type {string}
*/
const _SNS = 'unsupported-desktop-browser';
/**
* The type of the React {@code Component} props of
* {@link JaasUnsupportedDesktopBrowser}.
*/
type Props = {
/**
* The function to translate human-readable text.
*/
t: Function
};
/**
* React component representing unsupported browser page.
*
* @class UnsupportedDesktopBrowser
*/
class JaasUnsupportedDesktopBrowser extends Component<Props> {
/**
* Renders the component.
*
* @returns {ReactElement}
*/
render() {
return (
<div className = { _SNS }>
<h2 className = { `${_SNS}__title` }>
It looks like you're using a browser we don't support.
</h2>
<p className = { `${_SNS}__description` }>
Please try again with the latest version of&nbsp;
<a
className = { `${_SNS}__link` }
href = { CHROME } >Chrome</a>&nbsp;
{
this._showFirefox() && <>or <a
className = { `${_SNS}__link` }
href = { FIREFOX }>Firefox</a></>
}
</p>
</div>
);
}
/**
* Returns whether or not a link to download Firefox is displayed.
*
* @private
* @returns {boolean}
*/
_showFirefox() {
return isBrowsersOptimal('firefox');
}
}
export default translate(JaasUnsupportedDesktopBrowser);

View File

@@ -0,0 +1 @@
export { default } from './DefaultUnsupportedDesktopBrowser';

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