mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-07-15 18:27:45 +00:00
Compare commits
23 Commits
rm-dead-co
...
release-53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea3f134ebe | ||
|
|
09d07ed423 | ||
|
|
a601742aea | ||
|
|
d6fb444e03 | ||
|
|
66cf348ee1 | ||
|
|
74210baca0 | ||
|
|
968559710b | ||
|
|
a238c96b64 | ||
|
|
343c4fa56c | ||
|
|
fd43918c1e | ||
|
|
8c1209a1d7 | ||
|
|
4709e64e57 | ||
|
|
3109cce8d0 | ||
|
|
b861f77a09 | ||
|
|
4eecfbc2af | ||
|
|
48294aad14 | ||
|
|
92e21a5ca7 | ||
|
|
ff7e107c4f | ||
|
|
c44611fb33 | ||
|
|
83e1e45794 | ||
|
|
cd3a4a9818 | ||
|
|
48890c9603 | ||
|
|
d948a9bedf |
@@ -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);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
68
config.js
68
config.js
@@ -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,
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
4
modules/API/external/external_api.js
vendored
4
modules/API/external/external_api.js
vendored
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
6
react/features/av-moderation/sounds.js
Normal file
6
react/features/av-moderation/sounds.js
Normal 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';
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -213,6 +213,13 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
*/
|
||||
sheetHeader: {
|
||||
backgroundColor: BaseTheme.palette.ui02
|
||||
},
|
||||
|
||||
/**
|
||||
* Bottom sheet's background color with footer.
|
||||
*/
|
||||
sheetFooter: {
|
||||
backgroundColor: BaseTheme.palette.bottomSheet
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@ export type Props = {
|
||||
*/
|
||||
disabledStyles: ?Styles,
|
||||
|
||||
/**
|
||||
* External handler for click action.
|
||||
*/
|
||||
handleClick?: Function,
|
||||
|
||||
/**
|
||||
* Whether to show the label or not.
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
|
||||
12
react/features/conference/components/constants.js
Normal file
12
react/features/conference/components/constants.js
Normal 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'
|
||||
]
|
||||
};
|
||||
22
react/features/conference/components/functions.js
Normal file
22
react/features/conference/components/functions.js
Normal 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;
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
50
react/features/conference/components/web/SubjectText.js
Normal file
50
react/features/conference/components/web/SubjectText.js
Normal 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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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]));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -55,10 +55,10 @@ const useStyles = makeStyles(() => {
|
||||
},
|
||||
text: {
|
||||
color: '#C2C2C2',
|
||||
padding: '10px 16px 10px 52px'
|
||||
padding: '10px 16px'
|
||||
},
|
||||
paddedAction: {
|
||||
marginLeft: '36px;'
|
||||
marginLeft: '36px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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> ({ youText })</span> : null }
|
||||
</ParticipantNameContainer>
|
||||
<ParticipantDetailsContainer>
|
||||
<ParticipantNameContainer>
|
||||
<ParticipantName>
|
||||
{ displayName }
|
||||
</ParticipantName>
|
||||
{ local ? <span> ({ 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);
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -204,7 +204,7 @@ export function showStartedRecordingNotification(
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(showNotification(dialogProps));
|
||||
dispatch(showNotification(dialogProps, NOTIFICATION_TIMEOUT));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -66,6 +66,14 @@ class SharedVideoButton extends AbstractButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._doToggleSharedVideo();
|
||||
}
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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',
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 } />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 } />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
<a
|
||||
className = { `${_SNS}__link` }
|
||||
href = { CHROME } >Chrome</a>
|
||||
{
|
||||
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);
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './DefaultUnsupportedDesktopBrowser';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user