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:
Hristo Terezov 2020-02-18 16:31:04 +00:00 committed by GitHub
parent 86130c1478
commit bbf1927c70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 199 additions and 161 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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