feat(contact-list): convert to react

- Remove references to the model ContactList.
- Replace ContactListView with an empty element for attaching
  the React Component ContactListPanel, which has the same
  features as the old ContactListView.
- Create new selector for getting non-fake participants for
  ContactListPanel's props.
- Create a ParticipantCounter component to place in the contact
  list button. Previously ContactListView updated that but now
  it's a react component hooked into the participant state.
- Remove pub/sub that was used only by ContactListView.
This commit is contained in:
Leonard Kim 2017-08-30 16:17:55 -07:00 committed by yanas
parent ed53f54628
commit 31729d7949
20 changed files with 433 additions and 472 deletions

View File

@ -2,7 +2,6 @@
const logger = require("jitsi-meet-logger").getLogger(__filename);
import {openConnection} from './connection';
import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
import AuthHandler from './modules/UI/authentication/AuthHandler';
import Recorder from './modules/recorder/Recorder';
@ -72,10 +71,7 @@ import {
mediaPermissionPromptVisibilityChanged,
suspendDetected
} from './react/features/overlay';
import {
isButtonEnabled,
showDesktopSharingButton
} from './react/features/toolbox';
import { showDesktopSharingButton } from './react/features/toolbox';
const { participantConnectionStatus } = JitsiMeetJS.constants;
@ -710,11 +706,6 @@ export default {
this._createRoom(tracks);
APP.remoteControl.init();
if (isButtonEnabled('contacts')
&& !interfaceConfig.filmStripOnly) {
APP.UI.ContactList = new ContactList(room);
}
// if user didn't give access to mic or camera or doesn't have
// them at all, we mark corresponding toolbar buttons as muted,
// so that the user can try unmute later on and add audio/video

View File

@ -1,7 +1,19 @@
#contacts_container {
cursor: default;
> ul#contacts {
/**
* Override generic side toolbar styles to compensate for AtlasKit Button
* being used instead of custom button styling.
*/
.sideToolbarBlock {
.contact-list-panel-invite-button {
font-size: $modalButtonFontSize;
justify-content: center;
margin: 9px 0;
}
}
#contacts {
font-size: 12px;
bottom: 0px;
margin: 0;
@ -21,8 +33,7 @@
}
#contacts {
>li {
.contact-list-item {
align-items: center;
border-radius: 3px;
color: $baseLight;
@ -39,7 +50,7 @@
background: $toolbarSelectBackground;
}
> p {
.contact-list-item-name {
overflow: hidden;
text-overflow: ellipsis;
}
@ -62,4 +73,4 @@
border-radius: 20px;
max-height: 30px;
max-width: 30px;
}
}

View File

@ -155,8 +155,6 @@ UI.showChatError = function (err, msg) {
* @param {string} displayName new nickname
*/
UI.changeDisplayName = function (id, displayName) {
if (UI.ContactList)
UI.ContactList.onDisplayNameChange(id, displayName);
VideoLayout.onDisplayNameChanged(id, displayName);
if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
@ -201,9 +199,6 @@ UI.setLocalRaisedHandStatus
*/
UI.initConference = function () {
let id = APP.conference.getMyUserId();
// Add myself to the contact list.
if (UI.ContactList)
UI.ContactList.addContact(id, true);
// Update default button states before showing the toolbar
// if local role changes buttons state will be again updated.
@ -427,9 +422,6 @@ UI.addUser = function (user) {
var id = user.getId();
var displayName = user.getDisplayName();
if (UI.ContactList)
UI.ContactList.addContact(id);
messageHandler.participantNotification(
displayName,'notify.somebody', 'connected', 'notify.connected'
);
@ -455,9 +447,6 @@ UI.addUser = function (user) {
* @param {string} displayName user nickname
*/
UI.removeUser = function (id, displayName) {
if (UI.ContactList)
UI.ContactList.removeContact(id);
messageHandler.participantNotification(
displayName,'notify.somebody', 'disconnected', 'notify.disconnected'
);
@ -737,8 +726,6 @@ UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
*/
function changeAvatar(id, avatarUrl) {
VideoLayout.changeUserAvatar(id, avatarUrl);
if (UI.ContactList)
UI.ContactList.changeUserAvatar(id, avatarUrl);
if (APP.conference.isLocalId(id)) {
Profile.changeAvatar(avatarUrl);
}

View File

@ -1,19 +0,0 @@
/**
* Class representing Contact model
* @class Contact
*/
export default class Contact {
constructor(opts) {
let {
id,
avatar,
name,
isLocal
} = opts;
this.id = id;
this.avatar = avatar || '';
this.name = name || '';
this.isLocal = isLocal || false;
}
}

View File

@ -1,93 +0,0 @@
/* global APP */
import UIEvents from '../../../../service/UI/UIEvents';
import ContactListView from './ContactListView';
import Contact from './Contact';
/**
* Model for the Contact list.
*
* @class ContactList
*/
class ContactList {
constructor(conference) {
this.conference = conference;
this.contacts = [];
this.roomLocked = false;
//setup ContactList Model into ContactList View
ContactListView.setup(this);
}
/**
* Returns true if the current conference is locked.
*
* @returns {Boolean}
*/
isLocked() {
return APP.store.getState()['features/base/conference'].locked;
}
/**
* Adding new participant.
*
* @param id
* @param isLocal
*/
addContact(id, isLocal) {
const exists = this.contacts.some(el => el.id === id);
if (!exists) {
let newContact = new Contact({ id, isLocal });
this.contacts.push(newContact);
APP.UI.emitEvent(UIEvents.CONTACT_ADDED, { id, isLocal });
}
}
/**
* Removing participant.
*
* @param id
* @returns {Array|*}
*/
removeContact(id) {
this.contacts = this.contacts.filter((el) => el.id !== id);
APP.UI.emitEvent(UIEvents.CONTACT_REMOVED, { id });
return this.contacts;
}
/**
* Changing the display name.
*
* @param id
* @param name
*/
onDisplayNameChange (id, name) {
if(!name)
return;
if (id === 'localVideoContainer') {
id = APP.conference.getMyUserId();
}
let contacts = this.contacts.filter((el) => el.id === id);
contacts.forEach((el) => {
el.name = name;
});
APP.UI.emitEvent(UIEvents.DISPLAY_NAME_CHANGED, { id, name });
}
/**
* Changing the avatar.
*
* @param id
* @param avatar
*/
changeUserAvatar (id, avatar) {
let contacts = this.contacts.filter((el) => el.id === id);
contacts.forEach((el) => {
el.avatar = avatar;
});
APP.UI.emitEvent(UIEvents.USER_AVATAR_CHANGED, { id, avatar });
}
}
export default ContactList;

View File

@ -1,288 +1,57 @@
/* global $, APP, interfaceConfig */
/* global $, APP */
import { openInviteDialog } from '../../../../react/features/invite';
/* 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 { ContactListPanel } from '../../../../react/features/contact-list';
/* eslint-enable no-unused-vars */
import Avatar from '../../avatar/Avatar';
import UIEvents from '../../../../service/UI/UIEvents';
import UIUtil from '../../util/UIUtil';
const logger = require('jitsi-meet-logger').getLogger(__filename);
let numberOfContacts = 0;
const sidePanelsContainerId = 'sideToolbarContainer';
const htmlStr = `
<div id="contacts_container" class="sideToolbarContainer__inner">
<div class="title" data-i18n="contactlist"
data-i18n-options='{"pcount":"1"}'></div>
<ul id="contacts"></ul>
</div>`;
function initHTML() {
$(`#${sidePanelsContainerId}`)
.append(htmlStr);
}
/**
* Updates the number of participants in the contact list button and sets
* the glow
* @param delta indicates whether a new user has joined (1) or someone has
* left(-1)
*/
function updateNumberOfParticipants(delta) {
numberOfContacts += delta;
if (numberOfContacts <= 0) {
logger.error("Invalid number of participants: " + numberOfContacts);
return;
}
$("#numberOfParticipants").text(numberOfContacts);
APP.translation.translateElement(
$("#contacts_container>div.title"), {pcount: numberOfContacts});
}
/**
* Creates the avatar element.
*
* @return {object} the newly created avatar element
*/
function createAvatar(jid) {
let avatar = document.createElement('img');
avatar.className = "icon-avatar avatar";
avatar.src = Avatar.getAvatarUrl(jid);
return avatar;
}
/**
* Creates the display name paragraph.
*
* @param displayName the display name to set
*/
function createDisplayNameParagraph(key, displayName) {
let p = document.createElement('p');
if (displayName) {
p.innerHTML = displayName;
} else if(key) {
p.setAttribute("data-i18n",key);
}
return p;
}
/**
* Getter for current contact element
* @param id
* @returns {JQuery}
*/
function getContactEl (id) {
return $(`#contacts>li[id="${id}"]`);
}
/**
* Contact list.
*
* FIXME: One day this view should no longer be called "contact list" because
* the term "contact" is not used elsewhere. Normally people in the conference
* are internally refered to as "participants" or externally as "members".
*/
var ContactListView = {
init () {
initHTML();
this.lockKey = 'roomLocked';
this.unlockKey = 'roomUnlocked';
},
/**
* setup ContactList Model into ContactList View
* Creates and appends the contact list to the side panel.
*
* @param model
* @returns {void}
*/
setup (model) {
this.model = model;
this.addInviteButton();
this.registerListeners();
this.setLockDisplay(false);
},
/**
* Adds layout for invite button
*/
addInviteButton() {
let container = document.getElementById('contacts_container');
init() {
const contactListPanelContainer = document.createElement('div');
container.firstElementChild // this is the title
.insertAdjacentHTML('afterend', this.getInviteButtonLayout());
contactListPanelContainer.id = 'contacts_container';
contactListPanelContainer.className = 'sideToolbarContainer__inner';
APP.translation.translateElement($(container));
$('#sideToolbarContainer').append(contactListPanelContainer);
$(document).on('click', '#addParticipantsBtn', () => {
APP.store.dispatch(openInviteDialog());
});
},
/**
* Returns layout for invite button
*/
getInviteButtonLayout() {
let classes = 'button-control button-control_primary';
classes += ' button-control_full-width';
let key = 'addParticipants';
let lockedHtml = this.getLockDescriptionLayout(this.lockKey);
let unlockedHtml = this.getLockDescriptionLayout(this.unlockKey);
return (
`<div class="sideToolbarBlock first">
<button id="addParticipantsBtn"
data-i18n="${key}"
class="${classes}"></button>
<div>
${lockedHtml}
${unlockedHtml}
</div>
</div>`);
},
/**
* Adds layout for lock description
*/
getLockDescriptionLayout(key) {
let classes = "form-control__hint form-control_full-width";
let padlockSuffix = '';
if (key === this.lockKey) {
padlockSuffix = '-locked';
}
return `<p id="contactList${key}" class="${classes}">
<span class="icon-security${padlockSuffix}"></span>
<span data-i18n="${key}"></span>
</p>`;
},
/**
* Setup listeners
*/
registerListeners() {
let removeContact = this.onRemoveContact.bind(this);
let changeAvatar = this.changeUserAvatar.bind(this);
let displayNameChange = this.onDisplayNameChange.bind(this);
APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
this.setLockDisplay.bind(this));
APP.UI.addListener( UIEvents.CONTACT_ADDED,
this.onAddContact.bind(this));
APP.UI.addListener(UIEvents.CONTACT_REMOVED, removeContact);
APP.UI.addListener(UIEvents.USER_AVATAR_CHANGED, changeAvatar);
APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
/* jshint ignore:start */
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<ContactListPanel />
</I18nextProvider>
</Provider>,
contactListPanelContainer
);
/* jshint ignore:end */
},
/**
* Updates the view according to the passed in lock state.
* Indicates if the contact list is currently visible.
*
* @param {boolean} locked - True to display the locked UI state or false to
* display the unlocked UI state.
*/
setLockDisplay(locked) {
let hideKey, showKey;
if (locked) {
hideKey = this.unlockKey;
showKey = this.lockKey;
} else {
hideKey = this.lockKey;
showKey = this.unlockKey;
}
$(`#contactList${hideKey}`).hide();
$(`#contactList${showKey}`).show();
},
/**
* Indicates if the chat is currently visible.
*
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
* otherwise
* @return {boolean) true if the contact list is currently visible.
*/
isVisible () {
return UIUtil.isVisible(document.getElementById("contactlist"));
},
/**
* Handler for Adding a contact for the given id.
* @param isLocal is an id for the local user.
*/
onAddContact (data) {
let { id, isLocal } = data;
let contactlist = $('#contacts');
let newContact = document.createElement('li');
newContact.id = id;
newContact.className = "clickable";
newContact.onclick = (event) => {
if (event.currentTarget.className === "clickable") {
APP.UI.emitEvent(UIEvents.CONTACT_CLICKED, id);
}
};
if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
newContact.appendChild(createAvatar(id));
newContact.appendChild(
createDisplayNameParagraph(
isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
APP.translation.translateElement($(newContact));
if (APP.conference.isLocalId(id)) {
contactlist.prepend(newContact);
} else {
contactlist.append(newContact);
}
updateNumberOfParticipants(1);
},
/**
* Handler for removing
* a contact for the given id.
*/
onRemoveContact (data) {
let { id } = data;
let contact = getContactEl(id);
if (contact.length > 0) {
contact.remove();
updateNumberOfParticipants(-1);
}
},
setClickable (id, isClickable) {
getContactEl(id).toggleClass('clickable', isClickable);
},
/**
* Changes display name of the user
* defined by its id
* @param data
*/
onDisplayNameChange (data) {
let { id, name } = data;
if(!name)
return;
if (id === 'localVideoContainer') {
id = APP.conference.getMyUserId();
}
let contactName = $(`#contacts #${id}>p`);
if (contactName.text() !== name) {
contactName.text(name);
}
},
/**
* Changes user avatar
* @param data
*/
changeUserAvatar (data) {
let { id, avatar } = data;
// set the avatar in the contact list
let contact = $(`#${id}>img`);
if (contact.length > 0) {
contact.attr('src', avatar);
}
}
};

View File

@ -84,7 +84,7 @@ export function getAvatarURL({ avatarID, avatarURL, email, id }: {
* @returns {(Participant|undefined)}
*/
export function getLocalParticipant(stateOrGetState: Object | Function) {
const participants = _getParticipants(stateOrGetState);
const participants = _getAllParticipants(stateOrGetState);
return participants.find(p => p.local);
}
@ -103,7 +103,7 @@ export function getLocalParticipant(stateOrGetState: Object | Function) {
export function getParticipantById(
stateOrGetState: Object | Function,
id: string) {
const participants = _getParticipants(stateOrGetState);
const participants = _getAllParticipants(stateOrGetState);
return participants.find(p => p.id === id);
}
@ -119,10 +119,22 @@ export function getParticipantById(
* @returns {number}
*/
export function getParticipantCount(stateOrGetState: Object | Function) {
const participants = _getParticipants(stateOrGetState);
const realParticipants = participants.filter(p => !p.isBot);
return getParticipants(stateOrGetState).length;
}
return realParticipants.length;
/**
* Selectors for getting all known participants with fake participants filtered
* out.
*
* @param {(Function|Object|Participant[])} stateOrGetState - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the
* features/base/participants state.
* @returns {Participant[]}
*/
export function getParticipants(stateOrGetState: Object | Function) {
return _getAllParticipants(stateOrGetState).filter(p => !p.isBot);
}
/**
@ -135,9 +147,7 @@ export function getParticipantCount(stateOrGetState: Object | Function) {
* @returns {(Participant|undefined)}
*/
export function getPinnedParticipant(stateOrGetState: Object | Function) {
const participants = _getParticipants(stateOrGetState);
return participants.find(p => p.pinned);
return _getAllParticipants(stateOrGetState).find(p => p.pinned);
}
/**
@ -150,7 +160,7 @@ export function getPinnedParticipant(stateOrGetState: Object | Function) {
* @private
* @returns {Participant[]}
*/
function _getParticipants(stateOrGetState) {
function _getAllParticipants(stateOrGetState) {
return (
Array.isArray(stateOrGetState)
? stateOrGetState

View File

@ -0,0 +1,100 @@
/* global APP */
import React, { Component } from 'react';
import UIEvents from '../../../../service/UI/UIEvents';
import { Avatar } from '../../base/participants';
/**
* Implements a React {@code Component} for showing a participant's avatar and
* name and emits an event when it has been clicked.
*
* @extends Component
*/
class ContactListItem extends Component {
/**
* Default values for {@code ContactListItem} component's properties.
*
* @static
*/
static propTypes = {
/**
* The link to the participant's avatar image.
*/
avatarURI: React.PropTypes.string,
/**
* An id attribute to set on the root of {@code ContactListItem}. Used
* by the torture tests.
*/
id: React.PropTypes.string,
/**
* The participant's display name.
*/
name: React.PropTypes.string
};
/**
* Initializes new {@code ContactListItem} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onClick = this._onClick.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<li
className = 'clickable contact-list-item'
id = { this.props.id }
onClick = { this._onClick }>
{ this.props.avatarURI ? this._renderAvatar() : null }
<p className = 'contact-list-item-name'>
{ this.props.name }
</p>
</li>
);
}
/**
* Emits an event notifying the contact list item for the passed in
* participant ID has been clicked.
*
* @private
* @returns {void}
*/
_onClick() {
// FIXME move this call to a pinning action, which is what's happening
// on the listener end, when the listener is properly hooked into redux.
APP.UI.emitEvent(UIEvents.CONTACT_CLICKED, this.props.id);
}
/**
* Renders the React Element for displaying the participant's avatar image.
*
* @private
* @returns {ReactElement}
*/
_renderAvatar() {
return (
<Avatar
className = 'icon-avatar avatar'
uri = { this.props.avatarURI } />
);
}
}
export default ContactListItem;

View File

@ -0,0 +1,182 @@
import Button from '@atlaskit/button';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Avatar from '../../../../modules/UI/avatar/Avatar';
import { translate } from '../../base/i18n';
import { getParticipants } from '../../base/participants';
import { openInviteDialog } from '../../invite';
import ContactListItem from './ContactListItem';
const { PropTypes } = React;
declare var interfaceConfig: Object;
/**
* React component for showing a list of current conference participants, the
* current conference lock state, and a button to open the invite dialog.
*
* @extends Component
*/
class ContactListPanel extends Component {
/**
* Default values for {@code ContactListPanel} component's properties.
*
* @static
*/
static propTypes = {
/**
* Whether or not the conference is currently locked with a password.
*/
_locked: PropTypes.bool,
/**
* The participants to show in the contact list.
*/
_participants: PropTypes.array,
/**
* Invoked to open an invite dialog.
*/
dispatch: PropTypes.func,
/**
* Invoked to obtain translated strings.
*/
t: PropTypes.func
};
/**
* Initializes a new {@code ContactListPanel} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onOpenInviteDialog = this._onOpenInviteDialog.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { _locked, _participants, t } = this.props;
return (
<div className = 'contact-list-panel'>
<div className = 'title'>
{ t('contactlist', { pcount: _participants.length }) }
</div>
<div className = 'sideToolbarBlock first'>
<Button
appearance = 'primary'
className = 'contact-list-panel-invite-button'
id = 'addParticipantsBtn'
onClick = { this._onOpenInviteDialog }
type = 'button'>
{ t('addParticipants') }
</Button>
<div>
{ _locked
? this._renderLockedMessage()
: this._renderUnlockedMessage() }
</div>
</div>
<ul id = 'contacts'>
{ this._renderContacts() }
</ul>
</div>
);
}
/**
* Dispatches an action to open an invite dialog.
*
* @private
* @returns {void}
*/
_onOpenInviteDialog() {
this.props.dispatch(openInviteDialog());
}
/**
* Renders React Elements for displaying information about each participant
* in the contact list.
*
* @private
* @returns {ReactElement[]}
*/
_renderContacts() {
return this.props._participants.map(({ avatarId, id, name }) =>
( // eslint-disable-line no-extra-parens
<ContactListItem
avatarURI = { interfaceConfig.SHOW_CONTACTLIST_AVATARS
? Avatar.getAvatarUrl(avatarId) : null }
id = { id }
key = { id }
name = { name } />
));
}
/**
* Renders a React Element for informing the conference is currently locked.
*
* @private
* @returns {ReactElement}
*/
_renderLockedMessage() {
return (
<p
className = 'form-control__hint form-control_full-width'
id = 'contactListroomLocked'>
<span className = 'icon-security-locked' />
<span>{ this.props.t('roomLocked') }</span>
</p>
);
}
/**
* Renders a React Element for informing the conference is currently not
* locked.
*
* @private
* @returns {ReactElement}
*/
_renderUnlockedMessage() {
return (
<p
className = 'form-control__hint form-control_full-width'
id = 'contactListroomUnlocked'>
<span className = 'icon-security' />
<span>{ this.props.t('roomUnlocked') }</span>
</p>
);
}
}
/**
* Maps (parts of) the Redux state to the associated {@code ContactListPanel}'s
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _locked: boolean,
* _participants: Array
* }}
*/
function _mapStateToProps(state) {
return {
_locked: state['features/base/conference'].locked,
_participants: getParticipants(state)
};
}
export default translate(connect(_mapStateToProps)(ContactListPanel));

View File

@ -0,0 +1,58 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getParticipantCount } from '../../base/participants';
/**
* React component for showing a badge with the current count of conference
* participants.
*
* @extends Component
*/
class ParticipantCounter extends Component {
/**
* {@code ParticipantCounter} component's property types.
*
* @static
*/
static propTypes = {
/**
* The number of participants in the conference.
*/
_count: React.PropTypes.number
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<span className = 'badge-round'>
<span id = 'numberOfParticipants'>
{ this.props._count }
</span>
</span>
);
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code ParticipantCounter}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _count: number
* }}
*/
function _mapStateToProps(state) {
return {
_count: getParticipantCount(state)
};
}
export default connect(_mapStateToProps)(ParticipantCounter);

View File

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

View File

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

View File

@ -1,11 +1,7 @@
/* global APP */
import AKFieldText from '@atlaskit/field-text';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { setPassword } from '../../base/conference';
import { Dialog } from '../../base/dialog';
import { translate } from '../../base/i18n';
@ -114,12 +110,6 @@ class PasswordRequiredPrompt extends Component {
// succeeds (maybe someone removed the password meanwhile). If it is
// still locked, another password required will be received and the room
// again will be marked as locked.
if (!this.state.password || this.state.password === '') {
// XXX Temporary solution while some components are not listening
// for lock state updates in redux.
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, false);
}
this.props.dispatch(
setPassword(conference, conference.join, this.state.password));

View File

@ -27,12 +27,6 @@ MiddlewareRegistry.register(store => next => action => {
const { conference, error } = action;
if (conference && error === JitsiConferenceErrors.PASSWORD_REQUIRED) {
// XXX Temporary solution while some components are not listening
// for lock state updates in redux.
if (typeof APP !== 'undefined') {
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, true);
}
store.dispatch(_showPasswordDialog(conference));
}
break;

View File

@ -105,6 +105,7 @@ export default class StatelessToolbarButton extends AbstractToolbarButton {
onClick = { this._onClick }
ref = { this.props.createRefToButton }>
{ this._renderInnerElementsIfRequired() }
{ this._renderChildComponentIfRequired() }
</a>
);
}
@ -131,6 +132,22 @@ export default class StatelessToolbarButton extends AbstractToolbarButton {
}
}
/**
* Render any configured child component for the toolbar button.
*
* @returns {ReactElement|null}
* @private
*/
_renderChildComponentIfRequired(): ReactElement<*> | null {
if (this.props.button.childComponent) {
const Child = this.props.button.childComponent;
return <Child />;
}
return null;
}
/**
* If toolbar button should contain children elements
* renders them.

View File

@ -10,6 +10,8 @@ import { VideoQualityButton } from '../video-quality';
import UIEvents from '../../../service/UI/UIEvents';
import { ParticipantCounter } from '../contact-list';
declare var APP: Object;
declare var interfaceConfig: Object;
declare var JitsiMeetJS: Object;
@ -101,20 +103,9 @@ const buttons: Object = {
* The descriptor of the contact list toolbar button.
*/
contacts: {
childComponent: ParticipantCounter,
classNames: [ 'button', 'icon-contactList' ],
enabled: true,
// XXX: Hotfix to solve race condition between toolbox rendering and
// contact list view that updates the number of active participants
// via jQuery. There is case when contact list view updates number of
// participants but toolbox has not been rendered yet. Since this issue
// is reproducible only for conferences with the only participant let's
// use 1 participant as a default value for this badge. Later after
// reactification of contact list let's use the value of active
// paricipants from Redux store.
html: <span className = 'badge-round'>
<span id = 'numberOfParticipants'>1</span>
</span>,
id: 'toolbar_contact_list',
onClick() {
JitsiMeetJS.analytics.sendEvent(

View File

@ -118,35 +118,5 @@ export default {
/**
* Notifies that the displayed particpant id on the largeVideo is changed.
*/
LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed",
/**
* Toggling room lock
*/
TOGGLE_ROOM_LOCK: "UI.toggle_room_lock",
/**
* Adding contact to contact list
*/
CONTACT_ADDED: "UI.contact_added",
/**
* Removing the contact from contact list
*/
CONTACT_REMOVED: "UI.contact_removed",
/**
* Indicates that a user avatar has changed.
*/
USER_AVATAR_CHANGED: "UI.user_avatar_changed",
/**
* Display name changed.
*/
DISPLAY_NAME_CHANGED: "UI.display_name_changed",
/**
* Show custom popup/tooltip for a specified button.
*/
SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP: "UI.show_custom_toolbar_button_popup"
LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed"
};