fix(thumbnail): Optimize status bar moderator icon (#5076)
* fix(thumbnail): Optimize status bar moderator icon Moved all moderator functionality to react to optimize the number of status bar updates. * fix(RemoteVideoMenuTriggerButton): Use nullish coalescing Co-Authored-By: Saúl Ibarra Corretgé <saghul@jitsi.org> * ref(StatusBar): rename to StatusIndicators * fix(RemoteVideoMenu): isModerator value. * fix(notification): mobile. Co-authored-by: Saúl Ibarra Corretgé <s@saghul.net>
This commit is contained in:
parent
86130c1478
commit
bbf1927c70
|
@ -435,7 +435,6 @@ export default {
|
|||
* the tracks won't exist).
|
||||
*/
|
||||
_localTracksInitialized: false,
|
||||
isModerator: false,
|
||||
isSharingScreen: false,
|
||||
|
||||
/**
|
||||
|
@ -926,14 +925,6 @@ export default {
|
|||
this.muteVideo(!this.isLocalVideoMuted(), showUI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve list of conference participants (without local user).
|
||||
* @returns {JitsiParticipant[]}
|
||||
*/
|
||||
listMembers() {
|
||||
return room.getParticipants();
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve list of ids of conference participants (without local user).
|
||||
* @returns {string[]}
|
||||
|
@ -1894,9 +1885,6 @@ export default {
|
|||
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
APP.UI.addUser(user);
|
||||
|
||||
// check the roles for the new user and reflect them
|
||||
APP.UI.updateUserRole(user);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.USER_LEFT, (id, user) => {
|
||||
|
@ -1927,19 +1915,8 @@ export default {
|
|||
logger.info(`My role changed, new role: ${role}`);
|
||||
|
||||
APP.store.dispatch(localParticipantRoleChanged(role));
|
||||
|
||||
if (this.isModerator !== room.isModerator()) {
|
||||
this.isModerator = room.isModerator();
|
||||
APP.UI.updateLocalRole(room.isModerator());
|
||||
}
|
||||
} else {
|
||||
APP.store.dispatch(participantRoleChanged(id, role));
|
||||
|
||||
const user = room.getParticipantById(id);
|
||||
|
||||
if (user) {
|
||||
APP.UI.updateUserRole(user);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -126,10 +126,6 @@ UI.initConference = function() {
|
|||
const { getState } = APP.store;
|
||||
const { id, name } = getLocalParticipant(getState);
|
||||
|
||||
// Update default button states before showing the toolbar
|
||||
// if local role changes buttons state will be again updated.
|
||||
UI.updateLocalRole(APP.conference.isModerator);
|
||||
|
||||
UI.showToolbar();
|
||||
|
||||
const displayName = config.displayJids ? id : name;
|
||||
|
@ -279,44 +275,6 @@ UI.addUser = function(user) {
|
|||
UI.onPeerVideoTypeChanged
|
||||
= (id, newVideoType) => VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
|
||||
/**
|
||||
* Update local user role and show notification if user is moderator.
|
||||
* @param {boolean} isModerator if local user is moderator or not
|
||||
*/
|
||||
UI.updateLocalRole = isModerator => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
if (isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
||||
messageHandler.participantNotification(
|
||||
null, 'notify.me', 'connected', 'notify.moderator');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the role for the user and reflect it in the UI, moderator ui indication
|
||||
* and notifies user who is the moderator
|
||||
* @param user to check for moderator
|
||||
*/
|
||||
UI.updateUserRole = user => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
// We don't need to show moderator notifications when the focus (moderator)
|
||||
// indicator is disabled.
|
||||
if (!user.isModerator() || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayName = user.getDisplayName();
|
||||
|
||||
messageHandler.participantNotification(
|
||||
displayName,
|
||||
'notify.somebody',
|
||||
'connected',
|
||||
'notify.grantedTo',
|
||||
{ to: displayName
|
||||
? UIUtil.escapeHtml(displayName) : '$t(notify.somebody)' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the user status.
|
||||
*
|
||||
|
|
|
@ -129,6 +129,7 @@ export default class RemoteVideo extends SmallVideo {
|
|||
this._setThumbnailSize();
|
||||
this.initBrowserSpecificProperties();
|
||||
this.updateRemoteVideoMenu();
|
||||
this.updateStatusBar();
|
||||
this.addAudioLevelIndicator();
|
||||
this.addPresenceLabel();
|
||||
|
||||
|
@ -187,7 +188,6 @@ export default class RemoteVideo extends SmallVideo {
|
|||
// hide volume when in silent mode
|
||||
const onVolumeChange
|
||||
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
|
||||
const { isModerator } = APP.conference;
|
||||
const participantID = this.id;
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
let remoteMenuPosition;
|
||||
|
@ -207,7 +207,6 @@ export default class RemoteVideo extends SmallVideo {
|
|||
<RemoteVideoMenuTriggerButton
|
||||
initialVolumeValue = { initialVolumeValue }
|
||||
isAudioMuted = { this.isAudioMuted }
|
||||
isModerator = { isModerator }
|
||||
menuPosition = { remoteMenuPosition }
|
||||
onMenuDisplay
|
||||
= {this._onRemoteVideoMenuDisplay.bind(this)}
|
||||
|
|
|
@ -18,11 +18,9 @@ import {
|
|||
import { ConnectionIndicator } from '../../../react/features/connection-indicator';
|
||||
import { DisplayName } from '../../../react/features/display-name';
|
||||
import {
|
||||
AudioMutedIndicator,
|
||||
DominantSpeakerIndicator,
|
||||
ModeratorIndicator,
|
||||
RaisedHandIndicator,
|
||||
VideoMutedIndicator
|
||||
StatusIndicators
|
||||
} from '../../../react/features/filmstrip';
|
||||
import {
|
||||
LAYOUTS,
|
||||
|
@ -84,7 +82,6 @@ export default class SmallVideo {
|
|||
* Constructor.
|
||||
*/
|
||||
constructor(VideoLayout) {
|
||||
this._isModerator = false;
|
||||
this.isAudioMuted = false;
|
||||
this.hasAvatar = false;
|
||||
this.isVideoMuted = false;
|
||||
|
@ -286,45 +283,18 @@ export default class SmallVideo {
|
|||
return;
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
let tooltipPosition;
|
||||
|
||||
if (currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
tooltipPosition = 'right';
|
||||
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
|
||||
tooltipPosition = 'left';
|
||||
} else {
|
||||
tooltipPosition = 'top';
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<div>
|
||||
{ this.isAudioMuted
|
||||
? <AudioMutedIndicator
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
: null }
|
||||
{ this.isVideoMuted
|
||||
? <VideoMutedIndicator
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
: null }
|
||||
{ this._isModerator && !interfaceConfig.DISABLE_FOCUS_INDICATOR
|
||||
? <ModeratorIndicator
|
||||
tooltipPosition = { tooltipPosition } />
|
||||
: null }
|
||||
</div>
|
||||
</I18nextProvider>,
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<StatusIndicators
|
||||
showAudioMutedIndicator = { this.isAudioMuted }
|
||||
showVideoMutedIndicator = { this.isVideoMuted }
|
||||
participantID = { this.id } />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
statusBarContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the element indicating the moderator(owner) of the conference.
|
||||
*/
|
||||
addModeratorIndicator() {
|
||||
this._isModerator = true;
|
||||
this.updateStatusBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the element indicating the audio level of the participant.
|
||||
*
|
||||
|
@ -380,14 +350,6 @@ export default class SmallVideo {
|
|||
return this.container.querySelector('.audioindicator-container');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the element indicating the moderator(owner) of the conference.
|
||||
*/
|
||||
removeModeratorIndicator() {
|
||||
this._isModerator = false;
|
||||
this.updateStatusBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an especially interesting function. A naive reader might think that
|
||||
* it returns this SmallVideo's "video" element. But it is much more exciting.
|
||||
|
|
|
@ -174,9 +174,9 @@ const VideoLayout = {
|
|||
|
||||
// Make sure track's muted state is reflected
|
||||
if (stream.getType() === 'audio') {
|
||||
this.onAudioMute(stream.getParticipantId(), stream.isMuted());
|
||||
this.onAudioMute(id, stream.isMuted());
|
||||
} else {
|
||||
this.onVideoMute(stream.getParticipantId(), stream.isMuted());
|
||||
this.onVideoMute(id, stream.isMuted());
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -204,8 +204,7 @@ const VideoLayout = {
|
|||
updateMutedForNoTracks(participantId, mediaType) {
|
||||
const participant = APP.conference.getParticipantById(participantId);
|
||||
|
||||
if (participant
|
||||
&& !participant.getTracksByMediaType(mediaType).length) {
|
||||
if (participant && !participant.getTracksByMediaType(mediaType).length) {
|
||||
if (mediaType === 'audio') {
|
||||
APP.UI.setAudioMuted(participantId, true);
|
||||
} else if (mediaType === 'video') {
|
||||
|
@ -328,35 +327,6 @@ const VideoLayout = {
|
|||
this._updateLargeVideoIfDisplayed(resourceJid, true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a visual indicator for the moderator of the conference.
|
||||
* On local or remote participants.
|
||||
*/
|
||||
showModeratorIndicator() {
|
||||
const isModerator = APP.conference.isModerator;
|
||||
|
||||
if (isModerator) {
|
||||
localVideoThumbnail.addModeratorIndicator();
|
||||
} else {
|
||||
localVideoThumbnail.removeModeratorIndicator();
|
||||
}
|
||||
|
||||
APP.conference.listMembers().forEach(member => {
|
||||
const id = member.getId();
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (!remoteVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.isModerator()) {
|
||||
remoteVideo.addModeratorIndicator();
|
||||
}
|
||||
|
||||
remoteVideo.updateRemoteVideoMenu();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* On audio muted event.
|
||||
*/
|
||||
|
@ -371,7 +341,7 @@ const VideoLayout = {
|
|||
}
|
||||
|
||||
remoteVideo.showAudioIndicator(isMuted);
|
||||
remoteVideo.updateRemoteVideoMenu(isMuted);
|
||||
remoteVideo.updateRemoteVideoMenu();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/* @flow */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { getLocalParticipant, getParticipantById, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
|
||||
import AudioMutedIndicator from './AudioMutedIndicator';
|
||||
import ModeratorIndicator from './ModeratorIndicator';
|
||||
import VideoMutedIndicator from './VideoMutedIndicator';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link StatusIndicators}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The current layout of the filmstrip.
|
||||
*/
|
||||
_currentLayout: string,
|
||||
|
||||
/**
|
||||
* Indicates if the moderator indicator should be visible or not.
|
||||
*/
|
||||
_showModeratorIndicator: Boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the audio muted indicator should be visible or not.
|
||||
*/
|
||||
showAudioMutedIndicator: Boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the video muted indicator should be visible or not.
|
||||
*/
|
||||
showVideoMutedIndicator: Boolean,
|
||||
|
||||
/**
|
||||
* The ID of the participant for which the status bar is rendered.
|
||||
*/
|
||||
participantID: String
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} for showing the status bar in a thumbnail.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class StatusIndicators extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_currentLayout,
|
||||
_showModeratorIndicator,
|
||||
showAudioMutedIndicator,
|
||||
showVideoMutedIndicator
|
||||
} = this.props;
|
||||
let tooltipPosition;
|
||||
|
||||
switch (_currentLayout) {
|
||||
case LAYOUTS.TILE_VIEW:
|
||||
tooltipPosition = 'right';
|
||||
break;
|
||||
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
|
||||
tooltipPosition = 'left';
|
||||
break;
|
||||
default:
|
||||
tooltipPosition = 'top';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ showAudioMutedIndicator ? <AudioMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
|
||||
{ showVideoMutedIndicator ? <VideoMutedIndicator tooltipPosition = { tooltipPosition } /> : null }
|
||||
{ _showModeratorIndicator ? <ModeratorIndicator tooltipPosition = { tooltipPosition } /> : null }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code StatusIndicators}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _currentLayout: string,
|
||||
* _showModeratorIndicator: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participantID } = ownProps;
|
||||
|
||||
// Only the local participant won't have id for the time when the conference is not yet joined.
|
||||
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_currentLayout: getCurrentLayout(state),
|
||||
_showModeratorIndicator:
|
||||
!interfaceConfig.DISABLE_FOCUS_INDICATOR && participant && participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(StatusIndicators);
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
|
||||
export { default as AudioMutedIndicator } from './AudioMutedIndicator';
|
||||
export { default as DominantSpeakerIndicator }
|
||||
from './DominantSpeakerIndicator';
|
||||
export { default as DominantSpeakerIndicator } from './DominantSpeakerIndicator';
|
||||
export { default as Filmstrip } from './Filmstrip';
|
||||
export { default as ModeratorIndicator } from './ModeratorIndicator';
|
||||
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
|
||||
export { default as StatusIndicators } from './StatusIndicators';
|
||||
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
|
||||
|
|
|
@ -4,6 +4,8 @@ import { getCurrentConference } from '../base/conference';
|
|||
import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_ROLE,
|
||||
PARTICIPANT_UPDATED,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../base/participants';
|
||||
|
@ -29,15 +31,29 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
switch (action.type) {
|
||||
case PARTICIPANT_JOINED: {
|
||||
const result = next(action);
|
||||
|
||||
const { participant: p } = action;
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
if (!p.local && !joinLeaveNotificationsDisabled()) {
|
||||
store.dispatch(showParticipantJoinedNotification(
|
||||
getParticipantDisplayName(store.getState, p.id)
|
||||
dispatch(showParticipantJoinedNotification(
|
||||
getParticipantDisplayName(getState, p.id)
|
||||
));
|
||||
}
|
||||
|
||||
if (typeof interfaceConfig === 'object'
|
||||
&& !interfaceConfig.DISABLE_FOCUS_INDICATOR && p.role === PARTICIPANT_ROLE.MODERATOR) {
|
||||
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
||||
const displayName = getParticipantDisplayName(getState, p.id);
|
||||
|
||||
dispatch(showNotification({
|
||||
descriptionArguments: { to: displayName || '$t(notify.somebody)' },
|
||||
descriptionKey: 'notify.grantedTo',
|
||||
titleKey: 'notify.somebody',
|
||||
title: displayName
|
||||
},
|
||||
NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
|
@ -60,6 +76,30 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
|
||||
return next(action);
|
||||
}
|
||||
case PARTICIPANT_UPDATED: {
|
||||
if (typeof interfaceConfig === 'undefined' || interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
||||
// Do not show the notification for mobile and also when the focus indicator is disabled.
|
||||
return next(action);
|
||||
}
|
||||
|
||||
const { id, role } = action.participant;
|
||||
const state = store.getState();
|
||||
const { role: oldRole } = getParticipantById(state, id);
|
||||
|
||||
if (oldRole !== role && role === PARTICIPANT_ROLE.MODERATOR) {
|
||||
const displayName = getParticipantDisplayName(state, id);
|
||||
|
||||
store.dispatch(showNotification({
|
||||
descriptionArguments: { to: displayName || '$t(notify.somebody)' },
|
||||
descriptionKey: 'notify.grantedTo',
|
||||
titleKey: 'notify.somebody',
|
||||
title: displayName
|
||||
},
|
||||
NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
import React, { Component } from 'react';
|
||||
|
||||
import { Icon, IconMenuThumb } from '../../../base/icons';
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
|
||||
import { Popover } from '../../../base/popover';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
||||
import {
|
||||
MuteButton,
|
||||
|
@ -23,6 +25,11 @@ declare var interfaceConfig: Object;
|
|||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether or not the participant is a conference moderator.
|
||||
*/
|
||||
_isModerator: boolean,
|
||||
|
||||
/**
|
||||
* A value between 0 and 1 indicating the volume of the participant's
|
||||
* audio element.
|
||||
|
@ -34,11 +41,6 @@ type Props = {
|
|||
*/
|
||||
isAudioMuted: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the participant is a conference moderator.
|
||||
*/
|
||||
isModerator: boolean,
|
||||
|
||||
/**
|
||||
* Callback to invoke when the popover has been displayed.
|
||||
*/
|
||||
|
@ -154,9 +156,9 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
*/
|
||||
_renderRemoteVideoMenu() {
|
||||
const {
|
||||
_isModerator,
|
||||
initialVolumeValue,
|
||||
isAudioMuted,
|
||||
isModerator,
|
||||
onRemoteControlToggle,
|
||||
onVolumeChange,
|
||||
remoteControlState,
|
||||
|
@ -165,7 +167,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
|
||||
const buttons = [];
|
||||
|
||||
if (isModerator) {
|
||||
if (_isModerator) {
|
||||
buttons.push(
|
||||
<MuteButton
|
||||
isAudioMuted = { isAudioMuted }
|
||||
|
@ -216,4 +218,22 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default RemoteVideoMenuTriggerButton;
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code RemoteVideoMenuTriggerButton}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The own props of the component.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isModerator: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const participant = getLocalParticipant(state);
|
||||
|
||||
return {
|
||||
_isModerator: Boolean(participant?.role === PARTICIPANT_ROLE.MODERATOR)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(RemoteVideoMenuTriggerButton);
|
||||
|
|
Loading…
Reference in New Issue