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:
parent
ed53f54628
commit
31729d7949
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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 = {
|
||||
/**
|
||||
* Creates and appends the contact list to the side panel.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
initHTML();
|
||||
this.lockKey = 'roomLocked';
|
||||
this.unlockKey = 'roomUnlocked';
|
||||
const contactListPanelContainer = document.createElement('div');
|
||||
|
||||
contactListPanelContainer.id = 'contacts_container';
|
||||
contactListPanelContainer.className = 'sideToolbarContainer__inner';
|
||||
|
||||
$('#sideToolbarContainer').append(contactListPanelContainer);
|
||||
|
||||
/* jshint ignore:start */
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<ContactListPanel />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
contactListPanelContainer
|
||||
);
|
||||
/* jshint ignore:end */
|
||||
},
|
||||
|
||||
/**
|
||||
* setup ContactList Model into ContactList View
|
||||
* Indicates if the contact list is currently visible.
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
setup (model) {
|
||||
this.model = model;
|
||||
this.addInviteButton();
|
||||
this.registerListeners();
|
||||
this.setLockDisplay(false);
|
||||
},
|
||||
/**
|
||||
* Adds layout for invite button
|
||||
*/
|
||||
addInviteButton() {
|
||||
let container = document.getElementById('contacts_container');
|
||||
|
||||
container.firstElementChild // this is the title
|
||||
.insertAdjacentHTML('afterend', this.getInviteButtonLayout());
|
||||
|
||||
APP.translation.translateElement($(container));
|
||||
|
||||
$(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);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the view according to the passed in lock state.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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));
|
|
@ -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);
|
|
@ -0,0 +1,2 @@
|
|||
export { default as ContactListPanel } from './ContactListPanel';
|
||||
export { default as ParticipantCounter } from './ParticipantCounter';
|
|
@ -0,0 +1 @@
|
|||
export * from './components';
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue