feat(presence): display status in thumbnail and large video (#1828)
* feat(presence): display status in thumbnail and large video - Create a React Component for displaying presence. It currently connects to the store for participant updates but in the future should not be as smart once more reactification occurs. - Modify filmstrip css so the presence status displays horizontal center and below the avatar. - Modify videolayout css so the presence status displays horizontal centered and with a rounded background. - Dispatch presence updates so the participant state can be update. - Update message position on large video update to ensure message positioning is correct. * squash: do not show presence message if connection message is displayed
This commit is contained in:
parent
82117a0aef
commit
c04ef05058
|
@ -44,6 +44,7 @@ import {
|
|||
participantConnectionStatusChanged,
|
||||
participantJoined,
|
||||
participantLeft,
|
||||
participantPresenceChanged,
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
} from './react/features/base/participants';
|
||||
|
@ -1627,6 +1628,8 @@ export default {
|
|||
});
|
||||
|
||||
room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
|
||||
APP.store.dispatch(participantPresenceChanged(id, status));
|
||||
|
||||
let user = room.getParticipantById(id);
|
||||
if (user) {
|
||||
APP.UI.updateUserStatus(user, status);
|
||||
|
|
|
@ -103,6 +103,24 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.presence-label {
|
||||
color: $participantNameColor;
|
||||
font-size: 12px;
|
||||
font-weight: 100;
|
||||
left: 0;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
top: calc(50% + 30px);
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
z-index: $zindex3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hovered video thumbnail.
|
||||
*/
|
||||
|
|
|
@ -487,8 +487,8 @@
|
|||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
#remotePresenceMessage,
|
||||
#remoteConnectionMessage {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
z-index: $zindex2;
|
||||
|
@ -496,6 +496,11 @@
|
|||
font-size: 14px;
|
||||
text-align: center;
|
||||
color: #FFF;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
#remotePresenceMessage .presence-label,
|
||||
#remoteConnectionMessage {
|
||||
opacity: .80;
|
||||
text-shadow: 0px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 1px 1px rgba(0,0,0,0.3),
|
||||
|
@ -508,6 +513,10 @@
|
|||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
#remotePresenceMessage .no-presence,
|
||||
#remoteConnectionMessage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#localConnectionMessage {
|
||||
display: none;
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
/* global $, APP, config, JitsiMeetJS */
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { setLargeVideoHDStatus } from '../../../react/features/base/conference';
|
||||
|
@ -101,8 +109,8 @@ export default class LargeVideoManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Stops any polling intervals on the instance and and removes any
|
||||
* listeners registered on child components.
|
||||
* Stops any polling intervals on the instance and removes any
|
||||
* listeners registered on child components, including React Components.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
|
@ -110,6 +118,8 @@ export default class LargeVideoManager {
|
|||
window.clearInterval(this._updateVideoResolutionInterval);
|
||||
this.videoContainer.removeResizeListener(
|
||||
this._onVideoResolutionUpdate);
|
||||
|
||||
this.removePresenceLabel();
|
||||
}
|
||||
|
||||
onHoverIn (e) {
|
||||
|
@ -252,6 +262,11 @@ export default class LargeVideoManager {
|
|||
!overrideAndHide && isConnectionInterrupted,
|
||||
!overrideAndHide && messageKey);
|
||||
|
||||
// Change the participant id the presence label is listening to.
|
||||
this.updatePresenceLabel(id);
|
||||
|
||||
this.videoContainer.positionRemoteStatusMessages();
|
||||
|
||||
// resolve updateLargeVideo promise after everything is done
|
||||
promise.then(resolve);
|
||||
|
||||
|
@ -385,6 +400,51 @@ export default class LargeVideoManager {
|
|||
AudioLevels.updateLargeVideoAudioLevel("dominantSpeaker", lvl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a message of the passed in participant id's presence status. The
|
||||
* message will not display if the remote connection message is displayed.
|
||||
*
|
||||
* @param {string} id - The participant ID whose associated user's presence
|
||||
* status should be displayed.
|
||||
* @returns {void}
|
||||
*/
|
||||
updatePresenceLabel(id) {
|
||||
const isConnectionMessageVisible
|
||||
= $('#remoteConnectionMessage').is(':visible');
|
||||
|
||||
if (isConnectionMessageVisible) {
|
||||
this.removePresenceLabel();
|
||||
return;
|
||||
}
|
||||
|
||||
const presenceLabelContainer = $('#remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer.length) {
|
||||
/* jshint ignore:start */
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<PresenceLabel participantID = { id } />
|
||||
</Provider>,
|
||||
presenceLabelContainer.get(0));
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the messages about the displayed participant's presence status.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
removePresenceLabel() {
|
||||
const presenceLabelContainer = $('#remotePresenceMessage');
|
||||
|
||||
if (presenceLabelContainer.length) {
|
||||
/* jshint ignore:start */
|
||||
ReactDOM.unmountComponentAtNode(presenceLabelContainer.get(0));
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide watermark.
|
||||
* @param {boolean} show
|
||||
|
@ -463,8 +523,6 @@ export default class LargeVideoManager {
|
|||
APP.translation.translateElement(
|
||||
$('#remoteConnectionMessage'), msgOptions);
|
||||
}
|
||||
|
||||
this.videoContainer.positionRemoteConnectionMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { PresenceLabel } from '../../../react/features/presence-status';
|
||||
import {
|
||||
MuteButton,
|
||||
KickButton,
|
||||
|
@ -100,6 +102,8 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
|||
|
||||
this.addAudioLevelIndicator();
|
||||
|
||||
this.addPresenceLabel();
|
||||
|
||||
return this.container;
|
||||
};
|
||||
|
||||
|
@ -530,6 +534,8 @@ RemoteVideo.prototype.remove = function () {
|
|||
|
||||
this.removeAvatar();
|
||||
|
||||
this.removePresenceLabel();
|
||||
|
||||
this._unmountIndicators();
|
||||
|
||||
// Make sure that the large video is updated if are removing its
|
||||
|
@ -666,6 +672,41 @@ RemoteVideo.prototype.removeRemoteVideoMenu = function() {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mounts the {@code PresenceLabel} for displaying the participant's current
|
||||
* presence status.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
RemoteVideo.prototype.addPresenceLabel = function () {
|
||||
const presenceLabelContainer
|
||||
= this.container.querySelector('.presence-label-container');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
/* jshint ignore:start */
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<PresenceLabel participantID = { this.id } />
|
||||
</Provider>,
|
||||
presenceLabelContainer);
|
||||
/* jshint ignore:end */
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Unmounts the {@code PresenceLabel} component.
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
RemoteVideo.prototype.removePresenceLabel = function () {
|
||||
const presenceLabelContainer
|
||||
= this.container.querySelector('.presence-label-container');
|
||||
|
||||
if (presenceLabelContainer) {
|
||||
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteVideo.createContainer = function (spanId) {
|
||||
let container = document.createElement('span');
|
||||
container.id = spanId;
|
||||
|
@ -695,6 +736,10 @@ RemoteVideo.createContainer = function (spanId) {
|
|||
avatarContainer.className = 'avatar-container';
|
||||
container.appendChild(avatarContainer);
|
||||
|
||||
const presenceLabelContainer = document.createElement('div');
|
||||
presenceLabelContainer.className = 'presence-label-container';
|
||||
container.appendChild(presenceLabelContainer);
|
||||
|
||||
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
||||
return remotes.appendChild(container);
|
||||
};
|
||||
|
|
|
@ -189,6 +189,8 @@ export class VideoContainer extends LargeContainer {
|
|||
*/
|
||||
this.$remoteConnectionMessage = $('#remoteConnectionMessage');
|
||||
|
||||
this.$remotePresenceMessage = $('#remotePresenceMessage');
|
||||
|
||||
/**
|
||||
* Indicates whether or not the video stream attached to the video
|
||||
* element has started(which means that there is any image rendered
|
||||
|
@ -321,27 +323,35 @@ export class VideoContainer extends LargeContainer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Update position of the remote connection message which describes that
|
||||
* the remote user is having connectivity issues.
|
||||
* Updates the positioning of the remote connection presence message and the
|
||||
* connection status message which escribes that the remote user is having
|
||||
* connectivity issues.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
positionRemoteConnectionMessage () {
|
||||
positionRemoteStatusMessages() {
|
||||
this._positionParticipantStatus(this.$remoteConnectionMessage);
|
||||
this._positionParticipantStatus(this.$remotePresenceMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the position of the passed in jQuery object so it displays
|
||||
* in the middle of the video container or below the avatar.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_positionParticipantStatus($element) {
|
||||
if (this.avatarDisplayed) {
|
||||
let $avatarImage = $("#dominantSpeakerAvatar");
|
||||
this.$remoteConnectionMessage.css(
|
||||
$element.css(
|
||||
'top',
|
||||
$avatarImage.offset().top + $avatarImage.height() + 10);
|
||||
} else {
|
||||
let height = this.$remoteConnectionMessage.height();
|
||||
let parentHeight = this.$remoteConnectionMessage.parent().height();
|
||||
this.$remoteConnectionMessage.css(
|
||||
'top', (parentHeight/2) - (height/2));
|
||||
let height = $element.height();
|
||||
let parentHeight = $element.parent().height();
|
||||
$element.css('top', (parentHeight/2) - (height/2));
|
||||
}
|
||||
|
||||
let width = this.$remoteConnectionMessage.width();
|
||||
let parentWidth = this.$remoteConnectionMessage.parent().width();
|
||||
this.$remoteConnectionMessage.css(
|
||||
'left', ((parentWidth/2) - (width/2)));
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight, animate = false) {
|
||||
|
@ -372,7 +382,7 @@ export class VideoContainer extends LargeContainer {
|
|||
|
||||
this.$avatar.css('top', top);
|
||||
|
||||
this.positionRemoteConnectionMessage();
|
||||
this.positionRemoteStatusMessages();
|
||||
|
||||
this.$wrapper.animate({
|
||||
width: width,
|
||||
|
|
|
@ -204,6 +204,26 @@ export function participantLeft(id) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant's presence status has changed.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {string} presence - Participant's new presence status.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* presence: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function participantPresenceChanged(id, presence) {
|
||||
return participantUpdated({
|
||||
id,
|
||||
presence
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant's role has changed.
|
||||
*
|
||||
|
|
|
@ -36,6 +36,7 @@ export default class LargeVideo extends Component {
|
|||
id = 'dominantSpeakerAvatar'
|
||||
src = '' />
|
||||
</div>
|
||||
<div id = 'remotePresenceMessage' />
|
||||
<span id = 'remoteConnectionMessage' />
|
||||
<div>
|
||||
<div className = 'video_blurred_container'>
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getParticipantById } from '../../base/participants';
|
||||
|
||||
/**
|
||||
* React {@code Component} for displaying the current presence status of a
|
||||
* participant.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class PresenceLabel extends Component {
|
||||
/**
|
||||
* The default values for {@code PresenceLabel} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static defaultProps = {
|
||||
_presence: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* {@code PresenceLabel} component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The current present status associated with the passed in
|
||||
* participantID prop.
|
||||
*/
|
||||
_presence: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The ID of the participant whose presence status shoul display.
|
||||
*/
|
||||
participantID: React.PropTypes.string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _presence } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className
|
||||
= { `presence-label ${_presence ? '' : 'no-presence'}` }>
|
||||
{ _presence }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated {@code PresenceLabel}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {Object} ownProps - The React Component props passed to the associated
|
||||
* instance of {@code PresenceLabel}.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _presence: (string|undefined)
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const participant
|
||||
= getParticipantById(
|
||||
state['features/base/participants'], ownProps.participantID);
|
||||
|
||||
return {
|
||||
_presence: participant && participant.presence
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(PresenceLabel);
|
|
@ -0,0 +1 @@
|
|||
export { default as PresenceLabel } from './PresenceLabel';
|
|
@ -0,0 +1 @@
|
|||
export * from './components';
|
Loading…
Reference in New Issue