feat(display-name): convert to React (#1672)

* feat(display-name): convert to React

- Create a new React Component for displaying and updating display
  names on small videos
- The updating of the Component is defined in the parent class
  SmallVideo, which children will get access to through prototype
  copying
- Create a new actionType and middleware so name changes that occur
  in DisplayName can be propogated to outside redux
- Update the local video's DisplayName when a conference is joined
  or else the component may keep an undefined user id

* squash: query for the container, not the el owned by react
This commit is contained in:
virtuacoplenny 2017-06-28 20:35:43 -07:00 committed by yanas
parent e7a4318e8c
commit 928181cd7a
14 changed files with 396 additions and 150 deletions

View File

@ -37,6 +37,7 @@ import {
} from './react/features/base/lib-jitsi-meet';
import {
localParticipantRoleChanged,
MAX_DISPLAY_NAME_LENGTH,
participantJoined,
participantLeft,
participantRoleChanged,
@ -103,12 +104,6 @@ const commands = {
SHARED_VIDEO: "shared-video"
};
/**
* Max length of the display names. If we receive longer display name the
* additional chars are going to be cut.
*/
const MAX_DISPLAY_NAME_LENGTH = 50;
/**
* Open Connection. When authentication failed it shows auth dialog.
* @param roomName the room name to use

View File

@ -253,6 +253,10 @@ UI.initConference = function () {
UI.mucJoined = function () {
VideoLayout.mucJoined();
// Update local video now that a conference is joined a user ID should be
// set.
UI.changeDisplayName('localVideoContainer', APP.settings.getDisplayName());
};
/***

View File

@ -659,6 +659,10 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
var remotes = document.getElementById('filmstripRemoteVideosContainer');
return remotes.appendChild(container);
};
@ -696,23 +700,11 @@ SharedVideoThumb.prototype.setDisplayName = function(displayName) {
return;
}
var nameSpan = $('#' + this.videoSpanId + '>span.displayname');
// If we already have a display name for this video.
if (nameSpan.length > 0) {
if (displayName && displayName.length > 0) {
$('#' + this.videoSpanId + '_name').text(displayName);
}
} else {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0].appendChild(nameSpan);
if (displayName && displayName.length > 0)
$(nameSpan).text(displayName);
nameSpan.id = this.videoSpanId + '_name';
}
this.updateDisplayName({
displayName: displayName || '',
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
/**

View File

@ -47,116 +47,38 @@ LocalVideo.prototype.setDisplayName = function(displayName) {
return;
}
var nameSpan = $('#' + this.videoSpanId + ' .displayname');
var defaultLocalDisplayName = APP.translation.generateTranslationHTML(
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME);
var meHTML;
// If we already have a display name for this video.
if (nameSpan.length > 0) {
if (nameSpan.text() !== displayName) {
if (displayName && displayName.length > 0) {
meHTML = APP.translation.generateTranslationHTML("me");
$('#localDisplayName').html(
`${UIUtil.escapeHtml(displayName)} (${meHTML})`
);
$('#editDisplayName').val(
`${UIUtil.escapeHtml(displayName)}`
);
} else {
$('#localDisplayName').html(defaultLocalDisplayName);
}
}
this.updateView();
} else {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
document.getElementById(this.videoSpanId)
.appendChild(nameSpan);
if (displayName && displayName.length > 0) {
meHTML = APP.translation.generateTranslationHTML("me");
nameSpan.innerHTML = UIUtil.escapeHtml(displayName) + meHTML;
}
else {
nameSpan.innerHTML = defaultLocalDisplayName;
}
nameSpan.id = 'localDisplayName';
//translates popover of edit button
APP.translation.translateElement($("a.displayname"));
var editableText = document.createElement('input');
editableText.className = 'editdisplayname';
editableText.type = 'text';
editableText.id = 'editDisplayName';
if (displayName && displayName.length) {
editableText.value = displayName;
}
editableText.setAttribute('style', 'display:none;');
editableText.setAttribute('data-i18n',
'[placeholder]defaultNickname');
editableText.setAttribute("data-i18n-options",
JSON.stringify({name: "Jane Pink"}));
APP.translation.translateElement($(editableText));
this.container
.appendChild(editableText);
var self = this;
$('#localVideoContainer .displayname')
.bind("click", function (e) {
let $editDisplayName = $('#editDisplayName');
e.preventDefault();
e.stopPropagation();
// we set display to be hidden
self.hideDisplayName = true;
// update the small video vide to hide the display name
self.updateView();
// disables further updates in the thumbnail to stay in the
// edit mode
self.disableUpdateView = true;
$editDisplayName.show();
$editDisplayName.focus();
$editDisplayName.select();
$editDisplayName.one("focusout", function () {
self.emitter.emit(UIEvents.NICKNAME_CHANGED, this.value);
$editDisplayName.hide();
// stop editing, display displayName and resume updating
// the thumbnail
self.hideDisplayName = false;
self.disableUpdateView = false;
self.updateView();
});
$editDisplayName.on('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
$('#editDisplayName').hide();
// focusout handler will save display name
}
});
});
}
this.updateDisplayName({
allowEditing: true,
displayName: displayName,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
};
LocalVideo.prototype.changeVideo = function (stream) {
this.videoStream = stream;
let localVideoClick = (event) => {
// TODO Checking the classList is a workround to allow events to bubble
// into the DisplayName component if it was clicked. React's synthetic
// events will fire after jQuery handlers execute, so stop propogation
// at this point will prevent DisplayName from getting click events.
// This workaround should be removeable once LocalVideo is a React
// Component because then the components share the same eventing system.
const { classList } = event.target;
const clickedOnDisplayName = classList.contains('displayname')
|| classList.contains('editdisplayname');
// FIXME: with Temasys plugin event arg is not an event, but
// the clicked object itself, so we have to skip this call
if (event.stopPropagation) {
if (event.stopPropagation && !clickedOnDisplayName) {
event.stopPropagation();
}
this.VideoLayout.handleVideoThumbClicked(this.id);
if (!clickedOnDisplayName) {
this.VideoLayout.handleVideoThumbClicked(this.id);
}
};
let localVideoContainerSelector = $('#localVideoContainer');

View File

@ -516,6 +516,7 @@ RemoteVideo.prototype.remove = function () {
this.removeAudioLevelIndicator();
this.removeConnectionIndicator();
this.removeDisplayName();
// Make sure that the large video is updated if are removing its
// corresponding small video.
this.VideoLayout.updateAfterThumbRemoved(this.id);
@ -640,31 +641,11 @@ RemoteVideo.prototype.setDisplayName = function(displayName) {
return;
}
var nameSpan = $('#' + this.videoSpanId + ' .displayname');
// If we already have a display name for this video.
if (nameSpan.length > 0) {
if (displayName && displayName.length > 0) {
var displaynameSpan = $('#' + this.videoSpanId + '_name');
if (displaynameSpan.text() !== displayName)
displaynameSpan.text(displayName);
}
else
$('#' + this.videoSpanId + '_name').text(
interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
} else {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
$('#' + this.videoSpanId)[0]
.appendChild(nameSpan);
if (displayName && displayName.length > 0) {
$(nameSpan).text(displayName);
} else {
nameSpan.innerHTML = interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
}
nameSpan.id = this.videoSpanId + '_name';
}
this.updateDisplayName({
displayName: displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME,
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
};
/**
@ -707,6 +688,10 @@ RemoteVideo.createContainer = function (spanId) {
overlay.className = "videocontainer__hoverOverlay";
container.appendChild(overlay);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
var remotes = document.getElementById('filmstripRemoteVideosContainer');
return remotes.appendChild(container);
};

View File

@ -3,13 +3,17 @@
/* eslint-disable no-unused-vars */
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { AudioLevelIndicator }
from '../../../react/features/audio-level-indicator';
import {
ConnectionIndicator
} from '../../../react/features/connection-indicator';
/* eslint-enable no-unused-vars */
import { DisplayName } from '../../../react/features/display-name';
/* eslint-disable no-unused-vars */
const logger = require("jitsi-meet-logger").getLogger(__filename);
@ -483,7 +487,45 @@ SmallVideo.prototype.$avatar = function () {
* the video thumbnail
*/
SmallVideo.prototype.$displayName = function () {
return $('#' + this.videoSpanId + ' .displayname');
return $('#' + this.videoSpanId + ' .displayNameContainer');
};
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
* @returns {void}
*/
SmallVideo.prototype.updateDisplayName = function (props) {
const displayNameContainer
= this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
/* jshint ignore:start */
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<DisplayName { ...props } />
</I18nextProvider>
</Provider>,
displayNameContainer);
/* jshint ignore:end */
}
};
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
SmallVideo.prototype.removeDisplayName = function () {
const displayNameContainer
= this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
};
/**

View File

@ -10,6 +10,18 @@
*/
export const DOMINANT_SPEAKER_CHANGED = Symbol('DOMINANT_SPEAKER_CHANGED');
/**
* Create an action for when the local participant's display name is updated.
*
* {
* type: PARTICIPANT_DISPLAY_NAME_CHANGED,
* id: string,
* name: string
* }
*/
export const PARTICIPANT_DISPLAY_NAME_CHANGED
= Symbol('PARTICIPANT_DISPLAY_NAME_CHANGED');
/**
* Action to signal that ID of participant has changed. This happens when
* local participant joins a new conference or quits one.

View File

@ -1,11 +1,13 @@
import {
DOMINANT_SPEAKER_CHANGED,
PARTICIPANT_DISPLAY_NAME_CHANGED,
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
} from './actionTypes';
import { MAX_DISPLAY_NAME_LENGTH } from './constants';
import { getLocalParticipant } from './functions';
/**
@ -123,6 +125,29 @@ export function localParticipantLeft() {
};
}
/**
* Action to signal that a participant's display name has changed.
*
* @param {string} id - The id of the participant being changed.
* @param {string} displayName - The new display name.
* @returns {{
* type: PARTICIPANT_DISPLAY_NAME_CHANGED,
* id: string,
* name: string
* }}
*/
export function participantDisplayNameChanged(id, displayName = '') {
// FIXME Do not use this action over participantUpdated. This action exists
// as a a bridge for local name updates. Once other components responsible
// for updating the local user's display name are in react/redux, this
// action should be replaceable with the participantUpdated action.
return {
type: PARTICIPANT_DISPLAY_NAME_CHANGED,
id,
name: displayName.substr(0, MAX_DISPLAY_NAME_LENGTH)
};
}
/**
* Action to signal that a participant has joined.
*

View File

@ -6,6 +6,13 @@
*/
export const LOCAL_PARTICIPANT_DEFAULT_ID = 'local';
/**
* Max length of the display names.
*
* @type {string}
*/
export const MAX_DISPLAY_NAME_LENGTH = 50;
/**
* The set of possible XMPP MUC roles for conference participants.
*

View File

@ -1,3 +1,5 @@
import UIEvents from '../../../../service/UI/UIEvents';
import {
CONFERENCE_JOINED,
CONFERENCE_LEFT
@ -5,7 +7,11 @@ import {
import { MiddlewareRegistry } from '../redux';
import { localParticipantIdChanged } from './actions';
import { PARTICIPANT_DISPLAY_NAME_CHANGED } from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID } from './constants';
import { getLocalParticipant } from './functions';
declare var APP: Object;
/**
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
@ -23,6 +29,20 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_LEFT:
store.dispatch(localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
break;
// TODO Remove this middleware when the local display name update flow is
// fully brought into redux.
case PARTICIPANT_DISPLAY_NAME_CHANGED: {
if (typeof APP !== 'undefined') {
const participant = getLocalParticipant(store.getState());
if (participant && participant.id === action.id) {
APP.UI.emitEvent(UIEvents.NICKNAME_CHANGED, action.name);
}
}
break;
}
}
return next(action);

View File

@ -0,0 +1,239 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { translate } from '../../base/i18n';
import { participantDisplayNameChanged } from '../../base/participants';
/**
* React {@code Component} for displaying and editing a participant's name.
*
* @extends Component
*/
class DisplayName extends Component {
/**
* {@code DisplayName}'s property types.
*
* @static
*/
static propTypes = {
/**
* Whether or not the display name should be editable on click.
*/
allowEditing: React.PropTypes.bool,
/**
* Invoked to update the participant's display name.
*/
dispatch: React.PropTypes.func,
/**
* The participant's current display name.
*/
displayName: React.PropTypes.string,
/**
* A string to append to the displayName, if provided.
*/
displayNameSuffix: React.PropTypes.string,
/**
* The ID attribute to add to the component. Useful for global querying
* for the component by legacy components and torture tests.
*/
elementID: React.PropTypes.string,
/**
* The ID of the participant whose name is being displayed.
*/
participantID: React.PropTypes.string,
/**
* Invoked to obtain translated strings.
*/
t: React.PropTypes.func
};
/**
* Initializes a new {@code DisplayName} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
this.state = {
/**
* The current value of the display name in the edit field.
*
* @type {string}
*/
editDisplayNameValue: '',
/**
* Whether or not the component should be displaying an editable
* input.
*
* @type {boolean}
*/
isEditing: false
};
/**
* The internal reference to the HTML element backing the React
* {@code Component} input with id {@code editDisplayName}. It is
* necessary for automatically selecting the display name input field
* when starting to edit the display name.
*
* @private
* @type {HTMLInputElement}
*/
this._nameInput = null;
// Bind event handlers so they are only bound once for every instance.
this._onChange = this._onChange.bind(this);
this._onKeyDown = this._onKeyDown.bind(this);
this._onStartEditing = this._onStartEditing.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._setNameInputRef = this._setNameInputRef.bind(this);
}
/**
* Automatically selects the input field's value after starting to edit the
* display name.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(previousProps, previousState) {
if (!previousState.isEditing && this.state.isEditing) {
this._nameInput.select();
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
allowEditing,
displayName,
displayNameSuffix,
elementID,
t
} = this.props;
if (allowEditing && this.state.isEditing) {
return (
<input
autoFocus = { true }
className = 'editdisplayname'
id = 'editDisplayName'
onBlur = { this._onSubmit }
onChange = { this._onChange }
onKeyDown = { this._onKeyDown }
placeholder = { t('defaultNickname') }
ref = { this._setNameInputRef }
type = 'text'
value = { this.state.editDisplayNameValue } />
);
}
const suffix
= displayName && displayNameSuffix ? ` (${displayNameSuffix})` : '';
return (
<span
className = 'displayname'
id = { elementID }
onClick = { this._onStartEditing }>
{ `${displayName || displayNameSuffix || ''}${suffix}` }
</span>
);
}
/**
* Updates the internal state of the display name entered into the edit
* field.
*
* @param {Object} event - DOM Event for value change.
* @private
* @returns {void}
*/
_onChange(event) {
this.setState({
editDisplayNameValue: event.target.value
});
}
/**
* Submits the editted display name update if the enter key is pressed.
*
* @param {Event} event - Key down event object.
* @private
* @returns {void}
*/
_onKeyDown(event) {
if (event.key === 'Enter') {
this._onSubmit();
}
}
/**
* Updates the component to display an editable input field and sets the
* initial value to the current display name.
*
* @private
* @returns {void}
*/
_onStartEditing() {
if (this.props.allowEditing) {
this.setState({
isEditing: true,
editDisplayNameValue: this.props.displayName || ''
});
}
}
/**
* Dispatches an action to update the display name if any change has
* occurred after editing. Clears any temporary state used to keep track
* of pending display name changes and exits editing mode.
*
* @param {Event} event - Key down event object.
* @private
* @returns {void}
*/
_onSubmit() {
const { editDisplayNameValue } = this.state;
const { dispatch, participantID } = this.props;
dispatch(participantDisplayNameChanged(
participantID, editDisplayNameValue));
this.setState({
isEditing: false,
editDisplayNameValue: ''
});
this._nameInput = null;
}
/**
* Sets the internal reference to the HTML element backing the React
* {@code Component} input with id {@code editDisplayName}.
*
* @param {HTMLInputElement} element - The DOM/HTML element for this
* {@code Component}'s input.
* @private
* @returns {void}
*/
_setNameInputRef(element) {
this._nameInput = element;
}
}
export default translate(connect()(DisplayName));

View File

@ -0,0 +1 @@
export { default as DisplayName } from './DisplayName';

View File

@ -0,0 +1 @@
export * from './components';

View File

@ -40,6 +40,7 @@ export default class Filmstrip extends Component {
= 'connection-indicator-container' />
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer' />
</span>
</div>
<div