Переглянути джерело

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.
j8
Leonard Kim 7 роки тому
джерело
коміт
31729d7949

+ 1
- 10
conference.js Переглянути файл

@@ -2,7 +2,6 @@
2 2
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3 3
 
4 4
 import {openConnection} from './connection';
5
-import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
6 5
 
7 6
 import AuthHandler from './modules/UI/authentication/AuthHandler';
8 7
 import Recorder from './modules/recorder/Recorder';
@@ -72,10 +71,7 @@ import {
72 71
     mediaPermissionPromptVisibilityChanged,
73 72
     suspendDetected
74 73
 } from './react/features/overlay';
75
-import {
76
-    isButtonEnabled,
77
-    showDesktopSharingButton
78
-} from './react/features/toolbox';
74
+import { showDesktopSharingButton } from './react/features/toolbox';
79 75
 
80 76
 const { participantConnectionStatus } = JitsiMeetJS.constants;
81 77
 
@@ -710,11 +706,6 @@ export default {
710 706
                 this._createRoom(tracks);
711 707
                 APP.remoteControl.init();
712 708
 
713
-                if (isButtonEnabled('contacts')
714
-                    && !interfaceConfig.filmStripOnly) {
715
-                    APP.UI.ContactList = new ContactList(room);
716
-                }
717
-
718 709
                 // if user didn't give access to mic or camera or doesn't have
719 710
                 // them at all, we mark corresponding toolbar buttons as muted,
720 711
                 // so that the user can try unmute later on and add audio/video

+ 16
- 5
css/_contact_list.scss Переглянути файл

@@ -1,7 +1,19 @@
1 1
 #contacts_container {
2 2
     cursor: default;
3 3
 
4
-    > ul#contacts {
4
+    /**
5
+     * Override generic side toolbar styles to compensate for AtlasKit Button
6
+     * being used instead of custom button styling.
7
+     */
8
+    .sideToolbarBlock {
9
+        .contact-list-panel-invite-button {
10
+            font-size: $modalButtonFontSize;
11
+            justify-content: center;
12
+            margin: 9px 0;
13
+        }
14
+    }
15
+
16
+    #contacts {
5 17
         font-size: 12px;
6 18
         bottom: 0px;
7 19
         margin: 0;
@@ -21,8 +33,7 @@
21 33
 }
22 34
 
23 35
 #contacts {
24
-
25
-    >li {
36
+    .contact-list-item {
26 37
         align-items: center;
27 38
         border-radius: 3px;
28 39
         color: $baseLight;
@@ -39,7 +50,7 @@
39 50
             background: $toolbarSelectBackground;
40 51
         }
41 52
 
42
-        > p {
53
+        .contact-list-item-name {
43 54
             overflow: hidden;
44 55
             text-overflow: ellipsis;
45 56
         }
@@ -62,4 +73,4 @@
62 73
     border-radius: 20px;
63 74
     max-height: 30px;
64 75
     max-width: 30px;
65
-}
76
+}

+ 0
- 13
modules/UI/UI.js Переглянути файл

@@ -155,8 +155,6 @@ UI.showChatError = function (err, msg) {
155 155
  * @param {string} displayName new nickname
156 156
  */
157 157
 UI.changeDisplayName = function (id, displayName) {
158
-    if (UI.ContactList)
159
-        UI.ContactList.onDisplayNameChange(id, displayName);
160 158
     VideoLayout.onDisplayNameChanged(id, displayName);
161 159
 
162 160
     if (APP.conference.isLocalId(id) || id === 'localVideoContainer') {
@@ -201,9 +199,6 @@ UI.setLocalRaisedHandStatus
201 199
  */
202 200
 UI.initConference = function () {
203 201
     let id = APP.conference.getMyUserId();
204
-    // Add myself to the contact list.
205
-    if (UI.ContactList)
206
-        UI.ContactList.addContact(id, true);
207 202
 
208 203
     // Update default button states before showing the toolbar
209 204
     // if local role changes buttons state will be again updated.
@@ -427,9 +422,6 @@ UI.addUser = function (user) {
427 422
     var id = user.getId();
428 423
     var displayName = user.getDisplayName();
429 424
 
430
-    if (UI.ContactList)
431
-        UI.ContactList.addContact(id);
432
-
433 425
     messageHandler.participantNotification(
434 426
         displayName,'notify.somebody', 'connected', 'notify.connected'
435 427
     );
@@ -455,9 +447,6 @@ UI.addUser = function (user) {
455 447
  * @param {string} displayName user nickname
456 448
  */
457 449
 UI.removeUser = function (id, displayName) {
458
-    if (UI.ContactList)
459
-        UI.ContactList.removeContact(id);
460
-
461 450
     messageHandler.participantNotification(
462 451
         displayName,'notify.somebody', 'disconnected', 'notify.disconnected'
463 452
     );
@@ -737,8 +726,6 @@ UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
737 726
  */
738 727
 function changeAvatar(id, avatarUrl) {
739 728
     VideoLayout.changeUserAvatar(id, avatarUrl);
740
-    if (UI.ContactList)
741
-        UI.ContactList.changeUserAvatar(id, avatarUrl);
742 729
     if (APP.conference.isLocalId(id)) {
743 730
         Profile.changeAvatar(avatarUrl);
744 731
     }

+ 0
- 19
modules/UI/side_pannels/contactlist/Contact.js Переглянути файл

@@ -1,19 +0,0 @@
1
-/**
2
- * Class representing Contact model
3
- * @class Contact
4
- */
5
-export default class Contact {
6
-    constructor(opts) {
7
-        let {
8
-            id,
9
-            avatar,
10
-            name,
11
-            isLocal
12
-        } = opts;
13
-
14
-        this.id = id;
15
-        this.avatar = avatar || '';
16
-        this.name = name || '';
17
-        this.isLocal = isLocal || false;
18
-    }
19
-}

+ 0
- 93
modules/UI/side_pannels/contactlist/ContactList.js Переглянути файл

@@ -1,93 +0,0 @@
1
-/* global APP */
2
-
3
-import UIEvents from '../../../../service/UI/UIEvents';
4
-import ContactListView from './ContactListView';
5
-import Contact from './Contact';
6
-
7
-/**
8
- * Model for the Contact list.
9
- *
10
- * @class ContactList
11
- */
12
-class ContactList {
13
-    constructor(conference) {
14
-        this.conference = conference;
15
-        this.contacts = [];
16
-        this.roomLocked = false;
17
-        //setup ContactList Model into ContactList View
18
-        ContactListView.setup(this);
19
-    }
20
-
21
-    /**
22
-     * Returns true if the current conference is locked.
23
-     *
24
-     * @returns {Boolean}
25
-     */
26
-    isLocked() {
27
-        return APP.store.getState()['features/base/conference'].locked;
28
-    }
29
-
30
-    /**
31
-     * Adding new participant.
32
-     *
33
-     * @param id
34
-     * @param isLocal
35
-     */
36
-    addContact(id, isLocal) {
37
-        const exists = this.contacts.some(el => el.id === id);
38
-
39
-        if (!exists) {
40
-            let newContact = new Contact({ id, isLocal });
41
-            this.contacts.push(newContact);
42
-            APP.UI.emitEvent(UIEvents.CONTACT_ADDED, { id, isLocal });
43
-        }
44
-    }
45
-
46
-    /**
47
-     * Removing participant.
48
-     *
49
-     * @param id
50
-     * @returns {Array|*}
51
-     */
52
-    removeContact(id) {
53
-        this.contacts = this.contacts.filter((el) => el.id !== id);
54
-        APP.UI.emitEvent(UIEvents.CONTACT_REMOVED, { id });
55
-        return this.contacts;
56
-    }
57
-
58
-    /**
59
-     * Changing the display name.
60
-     *
61
-     * @param id
62
-     * @param name
63
-     */
64
-    onDisplayNameChange (id, name) {
65
-        if(!name)
66
-            return;
67
-        if (id === 'localVideoContainer') {
68
-            id = APP.conference.getMyUserId();
69
-        }
70
-
71
-        let contacts = this.contacts.filter((el) => el.id === id);
72
-        contacts.forEach((el) => {
73
-            el.name = name;
74
-        });
75
-        APP.UI.emitEvent(UIEvents.DISPLAY_NAME_CHANGED, { id, name });
76
-    }
77
-
78
-    /**
79
-     * Changing the avatar.
80
-     *
81
-     * @param id
82
-     * @param avatar
83
-     */
84
-    changeUserAvatar (id, avatar) {
85
-        let contacts = this.contacts.filter((el) => el.id === id);
86
-        contacts.forEach((el) => {
87
-            el.avatar = avatar;
88
-        });
89
-        APP.UI.emitEvent(UIEvents.USER_AVATAR_CHANGED, { id, avatar });
90
-    }
91
-}
92
-
93
-export default ContactList;

+ 33
- 264
modules/UI/side_pannels/contactlist/ContactListView.js Переглянути файл

@@ -1,288 +1,57 @@
1
-/* global $, APP, interfaceConfig */
1
+/* global $, APP */
2 2
 
3
-import { openInviteDialog } from '../../../../react/features/invite';
3
+/* eslint-disable no-unused-vars */
4
+import React from 'react';
5
+import ReactDOM from 'react-dom';
6
+import { I18nextProvider } from 'react-i18next';
7
+import { Provider } from 'react-redux';
4 8
 
5
-import Avatar from '../../avatar/Avatar';
6
-import UIEvents from '../../../../service/UI/UIEvents';
7
-import UIUtil from '../../util/UIUtil';
8
-
9
-const logger = require('jitsi-meet-logger').getLogger(__filename);
10
-
11
-let numberOfContacts = 0;
12
-const sidePanelsContainerId = 'sideToolbarContainer';
13
-const htmlStr = `
14
-    <div id="contacts_container" class="sideToolbarContainer__inner">
15
-        <div class="title" data-i18n="contactlist"
16
-            data-i18n-options='{"pcount":"1"}'></div>
17
-        <ul id="contacts"></ul>
18
-    </div>`;
19
-
20
-function initHTML() {
21
-    $(`#${sidePanelsContainerId}`)
22
-        .append(htmlStr);
23
-}
24
-
25
-/**
26
- * Updates the number of participants in the contact list button and sets
27
- * the glow
28
- * @param delta indicates whether a new user has joined (1) or someone has
29
- * left(-1)
30
- */
31
-function updateNumberOfParticipants(delta) {
32
-    numberOfContacts += delta;
33
-
34
-    if (numberOfContacts <= 0) {
35
-        logger.error("Invalid number of participants: " + numberOfContacts);
36
-        return;
37
-    }
38
-
39
-    $("#numberOfParticipants").text(numberOfContacts);
40
-
41
-    APP.translation.translateElement(
42
-        $("#contacts_container>div.title"), {pcount: numberOfContacts});
43
-}
44
-
45
-/**
46
- * Creates the avatar element.
47
- *
48
- * @return {object} the newly created avatar element
49
- */
50
-function createAvatar(jid) {
51
-    let avatar = document.createElement('img');
52
-    avatar.className = "icon-avatar avatar";
53
-    avatar.src = Avatar.getAvatarUrl(jid);
54
-
55
-    return avatar;
56
-}
57
-
58
-/**
59
- * Creates the display name paragraph.
60
- *
61
- * @param displayName the display name to set
62
- */
63
-function createDisplayNameParagraph(key, displayName) {
64
-    let p = document.createElement('p');
65
-    if (displayName) {
66
-        p.innerHTML = displayName;
67
-    } else if(key) {
68
-        p.setAttribute("data-i18n",key);
69
-    }
70
-
71
-    return p;
72
-}
9
+import { i18next } from '../../../../react/features/base/i18n';
10
+import { ContactListPanel } from '../../../../react/features/contact-list';
11
+/* eslint-enable no-unused-vars */
73 12
 
74
-/**
75
- * Getter for current contact element
76
- * @param id
77
- * @returns {JQuery}
78
- */
79
-function getContactEl (id) {
80
-    return $(`#contacts>li[id="${id}"]`);
81
-}
13
+import UIUtil from '../../util/UIUtil';
82 14
 
83 15
 /**
84 16
  * Contact list.
17
+ *
18
+ * FIXME: One day this view should no longer be called "contact list" because
19
+ * the term "contact" is not used elsewhere. Normally people in the conference
20
+ * are internally refered to as "participants" or externally as "members".
85 21
  */
86 22
 var ContactListView = {
87
-    init () {
88
-        initHTML();
89
-        this.lockKey = 'roomLocked';
90
-        this.unlockKey = 'roomUnlocked';
91
-    },
92
-
93 23
     /**
94
-     * setup ContactList Model into ContactList View
24
+     * Creates and appends the contact list to the side panel.
95 25
      *
96
-     * @param model
26
+     * @returns {void}
97 27
      */
98
-    setup (model) {
99
-        this.model = model;
100
-        this.addInviteButton();
101
-        this.registerListeners();
102
-        this.setLockDisplay(false);
103
-    },
104
-    /**
105
-     * Adds layout for invite button
106
-     */
107
-    addInviteButton() {
108
-        let container = document.getElementById('contacts_container');
28
+    init() {
29
+        const contactListPanelContainer = document.createElement('div');
109 30
 
110
-        container.firstElementChild // this is the title
111
-            .insertAdjacentHTML('afterend', this.getInviteButtonLayout());
31
+        contactListPanelContainer.id = 'contacts_container';
32
+        contactListPanelContainer.className = 'sideToolbarContainer__inner';
112 33
 
113
-        APP.translation.translateElement($(container));
34
+        $('#sideToolbarContainer').append(contactListPanelContainer);
114 35
 
115
-        $(document).on('click', '#addParticipantsBtn', () => {
116
-            APP.store.dispatch(openInviteDialog());
117
-        });
36
+        /* jshint ignore:start */
37
+        ReactDOM.render(
38
+            <Provider store = { APP.store }>
39
+                <I18nextProvider i18n = { i18next }>
40
+                    <ContactListPanel />
41
+                </I18nextProvider>
42
+            </Provider>,
43
+            contactListPanelContainer
44
+        );
45
+        /* jshint ignore:end */
118 46
     },
119
-    /**
120
-     *  Returns layout for invite button
121
-     */
122
-    getInviteButtonLayout() {
123
-        let classes = 'button-control button-control_primary';
124
-        classes += ' button-control_full-width';
125
-        let key = 'addParticipants';
126
-
127
-        let lockedHtml = this.getLockDescriptionLayout(this.lockKey);
128
-        let unlockedHtml = this.getLockDescriptionLayout(this.unlockKey);
129 47
 
130
-        return (
131
-            `<div class="sideToolbarBlock first">
132
-                <button id="addParticipantsBtn"
133
-                         data-i18n="${key}"
134
-                         class="${classes}"></button>
135
-                <div>
136
-                    ${lockedHtml}
137
-                    ${unlockedHtml}
138
-                </div>
139
-            </div>`);
140
-    },
141 48
     /**
142
-     * Adds layout for lock description
143
-     */
144
-    getLockDescriptionLayout(key) {
145
-        let classes = "form-control__hint form-control_full-width";
146
-        let padlockSuffix = '';
147
-        if (key === this.lockKey) {
148
-            padlockSuffix = '-locked';
149
-        }
150
-
151
-        return `<p id="contactList${key}" class="${classes}">
152
-                    <span class="icon-security${padlockSuffix}"></span>
153
-                    <span data-i18n="${key}"></span>
154
-                </p>`;
155
-    },
156
-    /**
157
-     * Setup listeners
158
-     */
159
-    registerListeners() {
160
-        let removeContact = this.onRemoveContact.bind(this);
161
-        let changeAvatar = this.changeUserAvatar.bind(this);
162
-        let displayNameChange = this.onDisplayNameChange.bind(this);
163
-
164
-        APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
165
-                            this.setLockDisplay.bind(this));
166
-        APP.UI.addListener( UIEvents.CONTACT_ADDED,
167
-                            this.onAddContact.bind(this));
168
-
169
-        APP.UI.addListener(UIEvents.CONTACT_REMOVED, removeContact);
170
-        APP.UI.addListener(UIEvents.USER_AVATAR_CHANGED, changeAvatar);
171
-        APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
172
-    },
173
-
174
-    /**
175
-     * Updates the view according to the passed in lock state.
49
+     * Indicates if the contact list is currently visible.
176 50
      *
177
-     * @param {boolean} locked - True to display the locked UI state or false to
178
-     * display the unlocked UI state.
179
-     */
180
-    setLockDisplay(locked) {
181
-        let hideKey, showKey;
182
-
183
-        if (locked) {
184
-            hideKey = this.unlockKey;
185
-            showKey = this.lockKey;
186
-        } else {
187
-            hideKey = this.lockKey;
188
-            showKey = this.unlockKey;
189
-        }
190
-
191
-        $(`#contactList${hideKey}`).hide();
192
-        $(`#contactList${showKey}`).show();
193
-    },
194
-
195
-    /**
196
-     * Indicates if the chat is currently visible.
197
-     *
198
-     * @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
199
-     * otherwise
51
+     * @return {boolean) true if the contact list is currently visible.
200 52
      */
201 53
     isVisible () {
202 54
         return UIUtil.isVisible(document.getElementById("contactlist"));
203
-    },
204
-
205
-    /**
206
-     * Handler for Adding a contact for the given id.
207
-     * @param isLocal is an id for the local user.
208
-     */
209
-    onAddContact (data) {
210
-        let { id, isLocal } = data;
211
-        let contactlist = $('#contacts');
212
-        let newContact = document.createElement('li');
213
-        newContact.id = id;
214
-        newContact.className = "clickable";
215
-        newContact.onclick = (event) => {
216
-            if (event.currentTarget.className === "clickable") {
217
-                APP.UI.emitEvent(UIEvents.CONTACT_CLICKED, id);
218
-            }
219
-        };
220
-
221
-        if (interfaceConfig.SHOW_CONTACTLIST_AVATARS)
222
-            newContact.appendChild(createAvatar(id));
223
-
224
-        newContact.appendChild(
225
-            createDisplayNameParagraph(
226
-                isLocal ? interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME : null,
227
-                isLocal ? null : interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME));
228
-        APP.translation.translateElement($(newContact));
229
-
230
-        if (APP.conference.isLocalId(id)) {
231
-            contactlist.prepend(newContact);
232
-        } else {
233
-            contactlist.append(newContact);
234
-        }
235
-        updateNumberOfParticipants(1);
236
-    },
237
-
238
-    /**
239
-     * Handler for removing
240
-     * a contact for the given id.
241
-     */
242
-    onRemoveContact (data) {
243
-        let { id } = data;
244
-        let contact = getContactEl(id);
245
-
246
-        if (contact.length > 0) {
247
-            contact.remove();
248
-            updateNumberOfParticipants(-1);
249
-        }
250
-    },
251
-
252
-    setClickable (id, isClickable) {
253
-        getContactEl(id).toggleClass('clickable', isClickable);
254
-    },
255
-
256
-    /**
257
-     * Changes display name of the user
258
-     * defined by its id
259
-     * @param data
260
-     */
261
-    onDisplayNameChange (data) {
262
-        let { id, name } = data;
263
-        if(!name)
264
-            return;
265
-        if (id === 'localVideoContainer') {
266
-            id = APP.conference.getMyUserId();
267
-        }
268
-        let contactName = $(`#contacts #${id}>p`);
269
-
270
-        if (contactName.text() !== name) {
271
-            contactName.text(name);
272
-        }
273
-    },
274
-
275
-    /**
276
-     * Changes user avatar
277
-     * @param data
278
-     */
279
-    changeUserAvatar (data) {
280
-        let { id, avatar } = data;
281
-        // set the avatar in the contact list
282
-        let contact = $(`#${id}>img`);
283
-        if (contact.length > 0) {
284
-            contact.attr('src', avatar);
285
-        }
286 55
     }
287 56
 };
288 57
 

+ 19
- 9
react/features/base/participants/functions.js Переглянути файл

@@ -84,7 +84,7 @@ export function getAvatarURL({ avatarID, avatarURL, email, id }: {
84 84
  * @returns {(Participant|undefined)}
85 85
  */
86 86
 export function getLocalParticipant(stateOrGetState: Object | Function) {
87
-    const participants = _getParticipants(stateOrGetState);
87
+    const participants = _getAllParticipants(stateOrGetState);
88 88
 
89 89
     return participants.find(p => p.local);
90 90
 }
@@ -103,7 +103,7 @@ export function getLocalParticipant(stateOrGetState: Object | Function) {
103 103
 export function getParticipantById(
104 104
         stateOrGetState: Object | Function,
105 105
         id: string) {
106
-    const participants = _getParticipants(stateOrGetState);
106
+    const participants = _getAllParticipants(stateOrGetState);
107 107
 
108 108
     return participants.find(p => p.id === id);
109 109
 }
@@ -119,10 +119,22 @@ export function getParticipantById(
119 119
  * @returns {number}
120 120
  */
121 121
 export function getParticipantCount(stateOrGetState: Object | Function) {
122
-    const participants = _getParticipants(stateOrGetState);
123
-    const realParticipants = participants.filter(p => !p.isBot);
122
+    return getParticipants(stateOrGetState).length;
123
+}
124
+
124 125
 
125
-    return realParticipants.length;
126
+/**
127
+ * Selectors for getting all known participants with fake participants filtered
128
+ * out.
129
+ *
130
+ * @param {(Function|Object|Participant[])} stateOrGetState - The redux state
131
+ * features/base/participants, the (whole) redux state, or redux's
132
+ * {@code getState} function to be used to retrieve the
133
+ * features/base/participants state.
134
+ * @returns {Participant[]}
135
+ */
136
+export function getParticipants(stateOrGetState: Object | Function) {
137
+    return _getAllParticipants(stateOrGetState).filter(p => !p.isBot);
126 138
 }
127 139
 
128 140
 /**
@@ -135,9 +147,7 @@ export function getParticipantCount(stateOrGetState: Object | Function) {
135 147
  * @returns {(Participant|undefined)}
136 148
  */
137 149
 export function getPinnedParticipant(stateOrGetState: Object | Function) {
138
-    const participants = _getParticipants(stateOrGetState);
139
-
140
-    return participants.find(p => p.pinned);
150
+    return _getAllParticipants(stateOrGetState).find(p => p.pinned);
141 151
 }
142 152
 
143 153
 /**
@@ -150,7 +160,7 @@ export function getPinnedParticipant(stateOrGetState: Object | Function) {
150 160
  * @private
151 161
  * @returns {Participant[]}
152 162
  */
153
-function _getParticipants(stateOrGetState) {
163
+function _getAllParticipants(stateOrGetState) {
154 164
     return (
155 165
         Array.isArray(stateOrGetState)
156 166
             ? stateOrGetState

+ 0
- 0
react/features/contact-list/components/ContactListItem.native.js Переглянути файл


+ 100
- 0
react/features/contact-list/components/ContactListItem.web.js Переглянути файл

@@ -0,0 +1,100 @@
1
+/* global APP */
2
+
3
+import React, { Component } from 'react';
4
+
5
+import UIEvents from '../../../../service/UI/UIEvents';
6
+
7
+import { Avatar } from '../../base/participants';
8
+
9
+/**
10
+ * Implements a React {@code Component} for showing a participant's avatar and
11
+ * name and emits an event when it has been clicked.
12
+ *
13
+ * @extends Component
14
+ */
15
+class ContactListItem extends Component {
16
+    /**
17
+     * Default values for {@code ContactListItem} component's properties.
18
+     *
19
+     * @static
20
+     */
21
+    static propTypes = {
22
+        /**
23
+         * The link to the participant's avatar image.
24
+         */
25
+        avatarURI: React.PropTypes.string,
26
+
27
+        /**
28
+         * An id attribute to set on the root of {@code ContactListItem}. Used
29
+         * by the torture tests.
30
+         */
31
+        id: React.PropTypes.string,
32
+
33
+        /**
34
+         * The participant's display name.
35
+         */
36
+        name: React.PropTypes.string
37
+    };
38
+
39
+    /**
40
+     * Initializes new {@code ContactListItem} instance.
41
+     *
42
+     * @param {Object} props - The read-only properties with which the new
43
+     * instance is to be initialized.
44
+     */
45
+    constructor(props) {
46
+        super(props);
47
+
48
+        // Bind event handler so it is only bound once for every instance.
49
+        this._onClick = this._onClick.bind(this);
50
+    }
51
+
52
+    /**
53
+     * Implements React's {@link Component#render()}.
54
+     *
55
+     * @inheritdoc
56
+     * @returns {ReactElement}
57
+     */
58
+    render() {
59
+        return (
60
+            <li
61
+                className = 'clickable contact-list-item'
62
+                id = { this.props.id }
63
+                onClick = { this._onClick }>
64
+                { this.props.avatarURI ? this._renderAvatar() : null }
65
+                <p className = 'contact-list-item-name'>
66
+                    { this.props.name }
67
+                </p>
68
+            </li>
69
+        );
70
+    }
71
+
72
+    /**
73
+     * Emits an event notifying the contact list item for the passed in
74
+     * participant ID has been clicked.
75
+     *
76
+     * @private
77
+     * @returns {void}
78
+     */
79
+    _onClick() {
80
+        // FIXME move this call to a pinning action, which is what's happening
81
+        // on the listener end, when the listener is properly hooked into redux.
82
+        APP.UI.emitEvent(UIEvents.CONTACT_CLICKED, this.props.id);
83
+    }
84
+
85
+    /**
86
+     * Renders the React Element for displaying the participant's avatar image.
87
+     *
88
+     * @private
89
+     * @returns {ReactElement}
90
+     */
91
+    _renderAvatar() {
92
+        return (
93
+            <Avatar
94
+                className = 'icon-avatar avatar'
95
+                uri = { this.props.avatarURI } />
96
+        );
97
+    }
98
+}
99
+
100
+export default ContactListItem;

+ 182
- 0
react/features/contact-list/components/ContactListPanel.web.js Переглянути файл

@@ -0,0 +1,182 @@
1
+import Button from '@atlaskit/button';
2
+import React, { Component } from 'react';
3
+import { connect } from 'react-redux';
4
+
5
+import Avatar from '../../../../modules/UI/avatar/Avatar';
6
+
7
+import { translate } from '../../base/i18n';
8
+import { getParticipants } from '../../base/participants';
9
+import { openInviteDialog } from '../../invite';
10
+
11
+import ContactListItem from './ContactListItem';
12
+
13
+const { PropTypes } = React;
14
+
15
+declare var interfaceConfig: Object;
16
+
17
+/**
18
+ * React component for showing a list of current conference participants, the
19
+ * current conference lock state, and a button to open the invite dialog.
20
+ *
21
+ * @extends Component
22
+ */
23
+class ContactListPanel extends Component {
24
+    /**
25
+     * Default values for {@code ContactListPanel} component's properties.
26
+     *
27
+     * @static
28
+     */
29
+    static propTypes = {
30
+        /**
31
+         * Whether or not the conference is currently locked with a password.
32
+         */
33
+        _locked: PropTypes.bool,
34
+
35
+        /**
36
+         * The participants to show in the contact list.
37
+         */
38
+        _participants: PropTypes.array,
39
+
40
+        /**
41
+         * Invoked to open an invite dialog.
42
+         */
43
+        dispatch: PropTypes.func,
44
+
45
+        /**
46
+         * Invoked to obtain translated strings.
47
+         */
48
+        t: PropTypes.func
49
+    };
50
+
51
+    /**
52
+     * Initializes a new {@code ContactListPanel} instance.
53
+     *
54
+     * @param {Object} props - The read-only properties with which the new
55
+     * instance is to be initialized.
56
+     */
57
+    constructor(props) {
58
+        super(props);
59
+
60
+        // Bind event handler so it is only bound once for every instance.
61
+        this._onOpenInviteDialog = this._onOpenInviteDialog.bind(this);
62
+    }
63
+
64
+    /**
65
+     * Implements React's {@link Component#render()}.
66
+     *
67
+     * @inheritdoc
68
+     */
69
+    render() {
70
+        const { _locked, _participants, t } = this.props;
71
+
72
+        return (
73
+            <div className = 'contact-list-panel'>
74
+                <div className = 'title'>
75
+                    { t('contactlist', { pcount: _participants.length }) }
76
+                </div>
77
+                <div className = 'sideToolbarBlock first'>
78
+                    <Button
79
+                        appearance = 'primary'
80
+                        className = 'contact-list-panel-invite-button'
81
+                        id = 'addParticipantsBtn'
82
+                        onClick = { this._onOpenInviteDialog }
83
+                        type = 'button'>
84
+                        { t('addParticipants') }
85
+                    </Button>
86
+                    <div>
87
+                        { _locked
88
+                            ? this._renderLockedMessage()
89
+                            : this._renderUnlockedMessage() }
90
+                    </div>
91
+                </div>
92
+                <ul id = 'contacts'>
93
+                    { this._renderContacts() }
94
+                </ul>
95
+            </div>
96
+        );
97
+    }
98
+
99
+    /**
100
+     * Dispatches an action to open an invite dialog.
101
+     *
102
+     * @private
103
+     * @returns {void}
104
+     */
105
+    _onOpenInviteDialog() {
106
+        this.props.dispatch(openInviteDialog());
107
+    }
108
+
109
+    /**
110
+     * Renders React Elements for displaying information about each participant
111
+     * in the contact list.
112
+     *
113
+     * @private
114
+     * @returns {ReactElement[]}
115
+     */
116
+    _renderContacts() {
117
+        return this.props._participants.map(({ avatarId, id, name }) =>
118
+            ( // eslint-disable-line no-extra-parens
119
+                <ContactListItem
120
+                    avatarURI = { interfaceConfig.SHOW_CONTACTLIST_AVATARS
121
+                        ? Avatar.getAvatarUrl(avatarId) : null }
122
+                    id = { id }
123
+                    key = { id }
124
+                    name = { name } />
125
+            ));
126
+    }
127
+
128
+    /**
129
+     * Renders a React Element for informing the conference is currently locked.
130
+     *
131
+     * @private
132
+     * @returns {ReactElement}
133
+     */
134
+    _renderLockedMessage() {
135
+        return (
136
+            <p
137
+                className = 'form-control__hint form-control_full-width'
138
+                id = 'contactListroomLocked'>
139
+                <span className = 'icon-security-locked' />
140
+                <span>{ this.props.t('roomLocked') }</span>
141
+            </p>
142
+        );
143
+    }
144
+
145
+    /**
146
+     * Renders a React Element for informing the conference is currently not
147
+     * locked.
148
+     *
149
+     * @private
150
+     * @returns {ReactElement}
151
+     */
152
+    _renderUnlockedMessage() {
153
+        return (
154
+            <p
155
+                className = 'form-control__hint form-control_full-width'
156
+                id = 'contactListroomUnlocked'>
157
+                <span className = 'icon-security' />
158
+                <span>{ this.props.t('roomUnlocked') }</span>
159
+            </p>
160
+        );
161
+    }
162
+}
163
+
164
+/**
165
+ * Maps (parts of) the Redux state to the associated {@code ContactListPanel}'s
166
+ * props.
167
+ *
168
+ * @param {Object} state - The Redux state.
169
+ * @private
170
+ * @returns {{
171
+ *     _locked: boolean,
172
+ *     _participants: Array
173
+ * }}
174
+ */
175
+function _mapStateToProps(state) {
176
+    return {
177
+        _locked: state['features/base/conference'].locked,
178
+        _participants: getParticipants(state)
179
+    };
180
+}
181
+
182
+export default translate(connect(_mapStateToProps)(ContactListPanel));

+ 0
- 0
react/features/contact-list/components/ContactlistPanel.native.js Переглянути файл


+ 0
- 0
react/features/contact-list/components/ParticipantCounter.native.js Переглянути файл


+ 58
- 0
react/features/contact-list/components/ParticipantCounter.web.js Переглянути файл

@@ -0,0 +1,58 @@
1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { getParticipantCount } from '../../base/participants';
5
+
6
+/**
7
+ * React component for showing a badge with the current count of conference
8
+ * participants.
9
+ *
10
+ * @extends Component
11
+ */
12
+class ParticipantCounter extends Component {
13
+    /**
14
+     * {@code ParticipantCounter} component's property types.
15
+     *
16
+     * @static
17
+     */
18
+    static propTypes = {
19
+        /**
20
+         * The number of participants in the conference.
21
+         */
22
+        _count: React.PropTypes.number
23
+    };
24
+
25
+    /**
26
+     * Implements React's {@link Component#render()}.
27
+     *
28
+     * @inheritdoc
29
+     * @returns {ReactElement}
30
+     */
31
+    render() {
32
+        return (
33
+            <span className = 'badge-round'>
34
+                <span id = 'numberOfParticipants'>
35
+                    { this.props._count }
36
+                </span>
37
+            </span>
38
+        );
39
+    }
40
+}
41
+
42
+/**
43
+ * Maps (parts of) the Redux state to the associated
44
+ * {@code ParticipantCounter}'s props.
45
+ *
46
+ * @param {Object} state - The Redux state.
47
+ * @private
48
+ * @returns {{
49
+ *     _count: number
50
+ * }}
51
+ */
52
+function _mapStateToProps(state) {
53
+    return {
54
+        _count: getParticipantCount(state)
55
+    };
56
+}
57
+
58
+export default connect(_mapStateToProps)(ParticipantCounter);

+ 2
- 0
react/features/contact-list/components/index.js Переглянути файл

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

+ 1
- 0
react/features/contact-list/index.js Переглянути файл

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

+ 0
- 10
react/features/room-lock/components/PasswordRequiredPrompt.web.js Переглянути файл

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

+ 0
- 6
react/features/room-lock/middleware.js Переглянути файл

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

+ 17
- 0
react/features/toolbox/components/StatelessToolbarButton.js Переглянути файл

@@ -105,6 +105,7 @@ export default class StatelessToolbarButton extends AbstractToolbarButton {
105 105
                 onClick = { this._onClick }
106 106
                 ref = { this.props.createRefToButton }>
107 107
                 { this._renderInnerElementsIfRequired() }
108
+                { this._renderChildComponentIfRequired() }
108 109
             </a>
109 110
         );
110 111
     }
@@ -131,6 +132,22 @@ export default class StatelessToolbarButton extends AbstractToolbarButton {
131 132
         }
132 133
     }
133 134
 
135
+    /**
136
+     * Render any configured child component for the toolbar button.
137
+     *
138
+     * @returns {ReactElement|null}
139
+     * @private
140
+     */
141
+    _renderChildComponentIfRequired(): ReactElement<*> | null {
142
+        if (this.props.button.childComponent) {
143
+            const Child = this.props.button.childComponent;
144
+
145
+            return <Child />;
146
+        }
147
+
148
+        return null;
149
+    }
150
+
134 151
     /**
135 152
      * If toolbar button should contain children elements
136 153
      * renders them.

+ 3
- 12
react/features/toolbox/defaultToolbarButtons.js Переглянути файл

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

+ 1
- 31
service/UI/UIEvents.js Переглянути файл

@@ -118,35 +118,5 @@ export default {
118 118
     /**
119 119
      * Notifies that the displayed particpant id on the largeVideo is changed.
120 120
      */
121
-    LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed",
122
-
123
-    /**
124
-     * Toggling room lock
125
-     */
126
-    TOGGLE_ROOM_LOCK: "UI.toggle_room_lock",
127
-
128
-    /**
129
-     * Adding contact to contact list
130
-     */
131
-    CONTACT_ADDED: "UI.contact_added",
132
-
133
-    /**
134
-     * Removing the contact from contact list
135
-     */
136
-    CONTACT_REMOVED: "UI.contact_removed",
137
-
138
-    /**
139
-     * Indicates that a user avatar has changed.
140
-     */
141
-    USER_AVATAR_CHANGED: "UI.user_avatar_changed",
142
-
143
-    /**
144
-     * Display name changed.
145
-     */
146
-    DISPLAY_NAME_CHANGED: "UI.display_name_changed",
147
-
148
-    /**
149
-     * Show custom popup/tooltip for a specified button.
150
-     */
151
-    SHOW_CUSTOM_TOOLBAR_BUTTON_POPUP: "UI.show_custom_toolbar_button_popup"
121
+    LARGE_VIDEO_ID_CHANGED: "UI.large_video_id_changed"
152 122
 };

Завантаження…
Відмінити
Зберегти