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
pull/1850/head jitsi-meet_2316
virtuacoplenny 7 years ago committed by Дамян Минков
parent 82117a0aef
commit c04ef05058
  1. 3
      conference.js
  2. 18
      css/_filmstrip.scss
  3. 11
      css/_videolayout_default.scss
  4. 66
      modules/UI/videolayout/LargeVideoManager.js
  5. 45
      modules/UI/videolayout/RemoteVideo.js
  6. 38
      modules/UI/videolayout/VideoContainer.js
  7. 20
      react/features/base/participants/actions.js
  8. 1
      react/features/large-video/components/LargeVideo.web.js
  9. 81
      react/features/presence-status/components/PresenceLabel.js
  10. 1
      react/features/presence-status/components/index.js
  11. 1
      react/features/presence-status/index.js

@ -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…
Cancel
Save