diff --git a/css/_vertical_filmstrip_overrides.scss b/css/_vertical_filmstrip_overrides.scss
index 49c8a683f..d3ac48dd7 100644
--- a/css/_vertical_filmstrip_overrides.scss
+++ b/css/_vertical_filmstrip_overrides.scss
@@ -82,6 +82,7 @@
.toolbar-icon {
float: none;
+ margin: auto;
}
}
}
diff --git a/modules/UI/videolayout/RemoteVideo.js b/modules/UI/videolayout/RemoteVideo.js
index 3d31f05bc..7ada86ea0 100644
--- a/modules/UI/videolayout/RemoteVideo.js
+++ b/modules/UI/videolayout/RemoteVideo.js
@@ -2,6 +2,7 @@
/* eslint-disable no-unused-vars */
import React from 'react';
+import ReactDOM from 'react-dom';
import {
MuteButton,
@@ -515,6 +516,13 @@ RemoteVideo.prototype.remove = function () {
this.removeAudioLevelIndicator();
+ const toolbarContainer
+ = this.container.querySelector('.videocontainer__toolbar');
+
+ if (toolbarContainer) {
+ ReactDOM.unmountComponentAtNode(toolbarContainer);
+ }
+
this.removeConnectionIndicator();
this.removeDisplayName();
diff --git a/modules/UI/videolayout/SmallVideo.js b/modules/UI/videolayout/SmallVideo.js
index 2c4dfb95d..b07d65540 100644
--- a/modules/UI/videolayout/SmallVideo.js
+++ b/modules/UI/videolayout/SmallVideo.js
@@ -16,7 +16,12 @@ import {
ConnectionIndicator
} from '../../../react/features/connection-indicator';
import { DisplayName } from '../../../react/features/display-name';
-/* eslint-disable no-unused-vars */
+import {
+ AudioMutedIndicator,
+ ModeratorIndicator,
+ VideoMutedIndicator
+} from '../../../react/features/filmstrip';
+/* eslint-enable no-unused-vars */
const logger = require("jitsi-meet-logger").getLogger(__filename);
@@ -64,6 +69,7 @@ const DISPLAY_VIDEO_WITH_NAME = 3;
const DISPLAY_AVATAR_WITH_NAME = 4;
function SmallVideo(VideoLayout) {
+ this._isModerator = false;
this.isAudioMuted = false;
this.hasAvatar = false;
this.isVideoMuted = false;
@@ -270,43 +276,8 @@ SmallVideo.prototype.updateConnectionStatus = function (connectionStatus) {
* or hidden
*/
SmallVideo.prototype.showAudioIndicator = function (isMuted) {
- let mutedIndicator = this.getAudioMutedIndicator();
-
- UIUtil.setVisible(mutedIndicator, isMuted);
-
this.isAudioMuted = isMuted;
-};
-
-/**
- * Returns the audio muted indicator jquery object. If it doesn't exists -
- * creates it.
- *
- * @returns {HTMLElement} the audio muted indicator
- */
-SmallVideo.prototype.getAudioMutedIndicator = function () {
- let selector = '#' + this.videoSpanId + ' .audioMuted';
- let audioMutedSpan = document.querySelector(selector);
-
- if (audioMutedSpan) {
- return audioMutedSpan;
- }
-
- audioMutedSpan = document.createElement('span');
- audioMutedSpan.className = 'audioMuted toolbar-icon';
-
- UIUtil.setTooltip(audioMutedSpan,
- "videothumbnail.mute",
- "top");
-
- let mutedIndicator = document.createElement('i');
- mutedIndicator.className = 'icon-mic-disabled';
- audioMutedSpan.appendChild(mutedIndicator);
-
- this.container
- .querySelector('.videocontainer__toolbar')
- .appendChild(audioMutedSpan);
-
- return audioMutedSpan;
+ this.updateStatusBar();
};
/**
@@ -320,75 +291,38 @@ SmallVideo.prototype.setVideoMutedView = function(isMuted) {
this.isVideoMuted = isMuted;
this.updateView();
- let element = this.getVideoMutedIndicator();
-
- UIUtil.setVisible(element, isMuted);
+ this.updateStatusBar();
};
/**
- * Returns the video muted indicator jquery object. If it doesn't exists -
- * creates it.
+ * Create or updates the ReactElement for displaying status indicators about
+ * audio mute, video mute, and moderator status.
*
- * @returns {jQuery|HTMLElement} the video muted indicator
+ * @returns {void}
*/
-SmallVideo.prototype.getVideoMutedIndicator = function () {
- var selector = '#' + this.videoSpanId + ' .videoMuted';
- var videoMutedSpan = document.querySelector(selector);
+SmallVideo.prototype.updateStatusBar = function () {
+ const statusBarContainer
+ = this.container.querySelector('.videocontainer__toolbar');
- if (videoMutedSpan) {
- return videoMutedSpan;
- }
-
- videoMutedSpan = document.createElement('span');
- videoMutedSpan.className = 'videoMuted toolbar-icon';
-
- this.container
- .querySelector('.videocontainer__toolbar')
- .appendChild(videoMutedSpan);
-
- var mutedIndicator = document.createElement('i');
- mutedIndicator.className = 'icon-camera-disabled';
-
- UIUtil.setTooltip(mutedIndicator,
- "videothumbnail.videomute",
- "top");
-
- videoMutedSpan.appendChild(mutedIndicator);
-
- return videoMutedSpan;
+ /* jshint ignore:start */
+ ReactDOM.render(
+
+ { this.isAudioMuted ?
: null }
+ { this.isVideoMuted ?
: null }
+ { this._isModerator
+ && !interfaceConfig.DISABLE_FOCUS_INDICATOR
+ ?
: null }
+
,
+ statusBarContainer);
+ /* jshint ignore:end */
};
/**
* Adds the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.addModeratorIndicator = function () {
-
- // Don't create moderator indicator if DISABLE_FOCUS_INDICATOR is true
- if (interfaceConfig.DISABLE_FOCUS_INDICATOR)
- return false;
-
- // Show moderator indicator
- var indicatorSpan = $('#' + this.videoSpanId + ' .focusindicator');
-
- if (indicatorSpan.length) {
- return;
- }
-
- indicatorSpan = document.createElement('span');
- indicatorSpan.className = 'focusindicator toolbar-icon right';
-
- this.container
- .querySelector('.videocontainer__toolbar')
- .appendChild(indicatorSpan);
-
- var moderatorIndicator = document.createElement('i');
- moderatorIndicator.className = 'icon-star';
-
- UIUtil.setTooltip(moderatorIndicator,
- "videothumbnail.moderator",
- "top-left");
-
- indicatorSpan.appendChild(moderatorIndicator);
+ this._isModerator = true;
+ this.updateStatusBar();
};
/**
@@ -456,7 +390,8 @@ SmallVideo.prototype._getAudioLevelContainer = function () {
* Removes the element indicating the moderator(owner) of the conference.
*/
SmallVideo.prototype.removeModeratorIndicator = function () {
- $('#' + this.videoSpanId + ' .focusindicator').remove();
+ this._isModerator = false;
+ this.updateStatusBar();
};
/**
diff --git a/react/features/filmstrip/components/_.web.js b/react/features/filmstrip/components/_.web.js
index e69de29bb..b80c83af3 100644
--- a/react/features/filmstrip/components/_.web.js
+++ b/react/features/filmstrip/components/_.web.js
@@ -0,0 +1 @@
+export * from './web';
diff --git a/react/features/filmstrip/components/index.js b/react/features/filmstrip/components/index.js
index ea1fd9926..0205f7fc6 100644
--- a/react/features/filmstrip/components/index.js
+++ b/react/features/filmstrip/components/index.js
@@ -1 +1,3 @@
+export * from './_';
+
export { default as Filmstrip } from './Filmstrip';
diff --git a/react/features/filmstrip/components/web/AudioMutedIndicator.js b/react/features/filmstrip/components/web/AudioMutedIndicator.js
new file mode 100644
index 000000000..c49d11f1b
--- /dev/null
+++ b/react/features/filmstrip/components/web/AudioMutedIndicator.js
@@ -0,0 +1,24 @@
+import BaseIndicator from './BaseIndicator';
+
+/**
+ * React {@code Component} for showing an audio muted icon with a tooltip.
+ *
+ * @extends BaseIndicator
+ */
+class AudioMutedIndicator extends BaseIndicator {
+ /**
+ * Initializes a new AudioMutedIcon instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this._classNames = 'audioMuted toolbar-icon';
+ this._iconClass = 'icon-mic-disabled';
+ this._tooltipKey = 'videothumbnail.mute';
+ }
+}
+
+export default AudioMutedIndicator;
diff --git a/react/features/filmstrip/components/web/BaseIndicator.js b/react/features/filmstrip/components/web/BaseIndicator.js
new file mode 100644
index 000000000..30f21142a
--- /dev/null
+++ b/react/features/filmstrip/components/web/BaseIndicator.js
@@ -0,0 +1,110 @@
+import React, { Component } from 'react';
+
+import UIUtil from '../../../../../modules/UI/util/UIUtil';
+
+/**
+ * React {@code Component} for showing an icon with a tooltip.
+ *
+ * @extends Component
+ */
+class BaseIndicator extends Component {
+ /**
+ * Initializes a new {@code BaseIndicator} instance.
+ *
+ * @param {Object} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ /**
+ * The CSS classes to apply to the root HTML element of the component.
+ *
+ * @type {string}
+ */
+ this._classNames = '';
+
+ /**
+ * The CSS class which will display an icon.
+ *
+ * @type {string}
+ */
+ this._iconClass = '';
+
+ /**
+ * An internal reference to the HTML element at the top of the
+ * component's DOM hierarchy. The reference is needed for attaching a
+ * tooltip.
+ *
+ * @type {HTMLElement}
+ */
+ this._rootElement = null;
+
+ /**
+ * The translation key for the text to display in the tooltip.
+ *
+ * @type {string}
+ */
+ this._tooltipKey = '';
+
+ // Bind event handler so it is only bound once for every instance.
+ this._setRootElementRef = this._setRootElementRef.bind(this);
+ }
+
+ /**
+ * Sets a tooltip which will display when hovering over the component.
+ *
+ * @inheritdoc
+ * @returns {void}
+ */
+ componentDidMount() {
+ this._setTooltip();
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ return (
+
+
+
+ );
+ }
+
+ /**
+ * Sets the internal reference to the root HTML element for the component.
+ *
+ * @param {HTMLIconElement} element - The root HTML element of the
+ * component.
+ * @private
+ * @returns {void}
+ */
+ _setRootElementRef(element) {
+ this._rootElement = element;
+ }
+
+ /**
+ * Associate the component as a tooltip trigger so a tooltip may display on
+ * hover.
+ *
+ * @private
+ * @returns {void}
+ */
+ _setTooltip() {
+ // TODO Replace UIUtil with an AtlasKit component when a suitable one
+ // becomes available for tooltips.
+ UIUtil.setTooltip(
+ this._rootElement,
+ this._tooltipKey,
+ 'top'
+ );
+ }
+}
+
+export default BaseIndicator;
diff --git a/react/features/filmstrip/components/web/ModeratorIndicator.js b/react/features/filmstrip/components/web/ModeratorIndicator.js
new file mode 100644
index 000000000..7fc552aaf
--- /dev/null
+++ b/react/features/filmstrip/components/web/ModeratorIndicator.js
@@ -0,0 +1,24 @@
+import BaseIndicator from './BaseIndicator';
+
+/**
+ * React {@code Component} for showing a moderator icon with a tooltip.
+ *
+ * @extends BaseIndicator
+ */
+class ModeratorIndicator extends BaseIndicator {
+ /**
+ * Initializes a new ModeratorIndicator instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this._classNames = 'focusindicator toolbar-icon right';
+ this._iconClass = 'icon-star';
+ this._tooltipKey = 'videothumbnail.moderator';
+ }
+}
+
+export default ModeratorIndicator;
diff --git a/react/features/filmstrip/components/web/VideoMutedIndicator.js b/react/features/filmstrip/components/web/VideoMutedIndicator.js
new file mode 100644
index 000000000..f5dcd10d3
--- /dev/null
+++ b/react/features/filmstrip/components/web/VideoMutedIndicator.js
@@ -0,0 +1,24 @@
+import BaseIndicator from './BaseIndicator';
+
+/**
+ * React {@code Component} for showing a video muted icon with a tooltip.
+ *
+ * @extends BaseIndicator
+ */
+class VideoMutedIndicator extends BaseIndicator {
+ /**
+ * Initializes a new VideoMutedIndicator instance.
+ *
+ * @param {Object} props - The read-only React Component props with which
+ * the new instance is to be initialized.
+ */
+ constructor(props) {
+ super(props);
+
+ this._classNames = 'videoMuted toolbar-icon';
+ this._iconClass = 'icon-camera-disabled';
+ this._tooltipKey = 'videothumbnail.videomute';
+ }
+}
+
+export default VideoMutedIndicator;
diff --git a/react/features/filmstrip/components/web/index.js b/react/features/filmstrip/components/web/index.js
new file mode 100644
index 000000000..e47ee6edd
--- /dev/null
+++ b/react/features/filmstrip/components/web/index.js
@@ -0,0 +1,3 @@
+export { default as AudioMutedIndicator } from './AudioMutedIndicator';
+export { default as ModeratorIndicator } from './ModeratorIndicator';
+export { default as VideoMutedIndicator } from './VideoMutedIndicator';