Browse Source

feat: convert invite dialog to react and redux

Converting the invite modal includes the following:
- Creating new react components to display InviteDialog. The
  main parent components are ShareLink and PasswordOverview,
  the later handles displaying lock state and password editing.
  These components do not make use of atlaskit as the component
  for input does not yet support readonly, so for consistency
  within the modal content no atlaskit was used.
- Using redux for keeping and accessing lock state instead of
  RoomLocker.
- Publicly exposing the redux action lockStateChanged for direct
  calling on lock events experienced on the web client.
- Removing Invite, InviteDialogView, and RoomLocker and references
  to them.
- Handling errors that occur when setting a password to preserve
  existing web funtionality.
master
Leonard Kim 8 years ago
parent
commit
44b81b20e3
30 changed files with 904 additions and 728 deletions
  1. 19
    15
      conference.js
  2. 2
    0
      css/_variables.scss
  3. 6
    0
      css/modals/_dialog.scss
  4. 41
    1
      css/modals/invite/_invite.scss
  5. 9
    4
      lang/main.json
  6. 0
    187
      modules/UI/invite/Invite.js
  7. 0
    378
      modules/UI/invite/InviteDialogView.js
  8. 0
    103
      modules/UI/invite/RoomLocker.js
  9. 4
    4
      modules/UI/side_pannels/contactlist/ContactList.js
  10. 16
    14
      modules/UI/side_pannels/contactlist/ContactListView.js
  11. 11
    0
      react/features/base/conference/actionTypes.js
  12. 9
    3
      react/features/base/conference/actions.js
  13. 25
    4
      react/features/base/conference/reducer.js
  14. 18
    0
      react/features/invite/actions.js
  15. 137
    0
      react/features/invite/components/AddPasswordForm.js
  16. 104
    0
      react/features/invite/components/InviteDialog.js
  17. 48
    0
      react/features/invite/components/LockStatePanel.js
  18. 147
    0
      react/features/invite/components/PasswordContainer.js
  19. 117
    0
      react/features/invite/components/RemovePasswordForm.js
  20. 110
    0
      react/features/invite/components/ShareLinkForm.js
  21. 1
    0
      react/features/invite/components/index.js
  22. 2
    0
      react/features/invite/index.js
  23. 5
    3
      react/features/room-lock/components/PasswordRequiredPrompt.web.js
  24. 12
    0
      react/features/room-lock/constants.js
  25. 1
    0
      react/features/room-lock/index.js
  26. 56
    5
      react/features/room-lock/middleware.js
  27. 1
    1
      react/features/toolbox/components/Toolbox.native.js
  28. 0
    1
      react/features/toolbox/components/Toolbox.web.js
  29. 3
    1
      react/features/toolbox/defaultToolbarButtons.js
  30. 0
    4
      service/UI/UIEvents.js

+ 19
- 15
conference.js View File

2
 const logger = require("jitsi-meet-logger").getLogger(__filename);
2
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3
 
3
 
4
 import {openConnection} from './connection';
4
 import {openConnection} from './connection';
5
-import Invite from './modules/UI/invite/Invite';
6
 import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
5
 import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
7
 
6
 
8
 import AuthHandler from './modules/UI/authentication/AuthHandler';
7
 import AuthHandler from './modules/UI/authentication/AuthHandler';
28
     conferenceFailed,
27
     conferenceFailed,
29
     conferenceJoined,
28
     conferenceJoined,
30
     conferenceLeft,
29
     conferenceLeft,
31
-    EMAIL_COMMAND
30
+    EMAIL_COMMAND,
31
+    lockStateChanged
32
 } from './react/features/base/conference';
32
 } from './react/features/base/conference';
33
 import {
33
 import {
34
     updateDeviceList
34
     updateDeviceList
373
 }
373
 }
374
 
374
 
375
 class ConferenceConnector {
375
 class ConferenceConnector {
376
-    constructor(resolve, reject, invite) {
376
+    constructor(resolve, reject) {
377
         this._resolve = resolve;
377
         this._resolve = resolve;
378
         this._reject = reject;
378
         this._reject = reject;
379
-        this._invite = invite;
380
         this.reconnectTimeout = null;
379
         this.reconnectTimeout = null;
381
         room.on(ConferenceEvents.CONFERENCE_JOINED,
380
         room.on(ConferenceEvents.CONFERENCE_JOINED,
382
             this._handleConferenceJoined.bind(this));
381
             this._handleConferenceJoined.bind(this));
411
 
410
 
412
             // not enough rights to create conference
411
             // not enough rights to create conference
413
         case ConferenceErrors.AUTHENTICATION_REQUIRED:
412
         case ConferenceErrors.AUTHENTICATION_REQUIRED:
414
-            // schedule reconnect to check if someone else created the room
415
-            this.reconnectTimeout = setTimeout(function () {
416
-                room.join();
417
-            }, 5000);
418
-
419
-            // notify user that auth is required
420
-            AuthHandler.requireAuth(
421
-                room, this._invite.getRoomLocker().password);
413
+            {
414
+                // schedule reconnect to check if someone else created the room
415
+                this.reconnectTimeout = setTimeout(function () {
416
+                    room.join();
417
+                }, 5000);
418
+
419
+                const { password }
420
+                    = APP.store.getState()['features/base/conference'];
421
+
422
+                AuthHandler.requireAuth(room, password);
423
+            }
422
             break;
424
             break;
423
 
425
 
424
         case ConferenceErrors.RESERVATION_ERROR:
426
         case ConferenceErrors.RESERVATION_ERROR:
630
                 // XXX The API will take care of disconnecting from the XMPP
632
                 // XXX The API will take care of disconnecting from the XMPP
631
                 // server (and, thus, leaving the room) on unload.
633
                 // server (and, thus, leaving the room) on unload.
632
                 return new Promise((resolve, reject) => {
634
                 return new Promise((resolve, reject) => {
633
-                    (new ConferenceConnector(
634
-                        resolve, reject, this.invite)).connect();
635
+                    (new ConferenceConnector(resolve, reject)).connect();
635
                 });
636
                 });
636
         });
637
         });
637
     },
638
     },
985
         room = connection.initJitsiConference(APP.conference.roomName,
986
         room = connection.initJitsiConference(APP.conference.roomName,
986
             this._getConferenceOptions());
987
             this._getConferenceOptions());
987
         this._setLocalAudioVideoStreams(localTracks);
988
         this._setLocalAudioVideoStreams(localTracks);
988
-        this.invite = new Invite(room);
989
         this._room = room; // FIXME do not use this
989
         this._room = room; // FIXME do not use this
990
 
990
 
991
         _setupLocalParticipantProperties();
991
         _setupLocalParticipantProperties();
1449
             APP.UI.changeDisplayName(id, formattedDisplayName);
1449
             APP.UI.changeDisplayName(id, formattedDisplayName);
1450
         });
1450
         });
1451
 
1451
 
1452
+        room.on(ConferenceEvents.LOCK_STATE_CHANGED, (...args) => {
1453
+            APP.store.dispatch(lockStateChanged(room, ...args));
1454
+        });
1455
+
1452
         room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
1456
         room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
1453
                 (participant, name, oldValue, newValue) => {
1457
                 (participant, name, oldValue, newValue) => {
1454
             if (name === "raisedHand") {
1458
             if (name === "raisedHand") {

+ 2
- 0
css/_variables.scss View File

83
  * Modals
83
  * Modals
84
  */
84
  */
85
 $modalButtonFontSize: 14px;
85
 $modalButtonFontSize: 14px;
86
+$modalMockAKInputBackground: #fafbfc;
87
+$modalMockAKInputBorder: 1px solid #f4f5f7;
86
 $modalTextColor: #333;
88
 $modalTextColor: #333;
87
 
89
 
88
 /**
90
 /**

+ 6
- 0
css/modals/_dialog.scss View File

79
 
79
 
80
 .modal-dialog-form {
80
 .modal-dialog-form {
81
     color: $modalTextColor;
81
     color: $modalTextColor;
82
+
83
+    .input-control {
84
+        background: $modalMockAKInputBackground;
85
+        border: $modalMockAKInputBorder;
86
+        color: inherit;
87
+    }
82
 }
88
 }
83
 .modal-dialog-footer {
89
 .modal-dialog-footer {
84
     font-size: $modalButtonFontSize;
90
     font-size: $modalButtonFontSize;

+ 41
- 1
css/modals/invite/_invite.scss View File

4
  */
4
  */
5
 #inviteDialogRemovePassword {
5
 #inviteDialogRemovePassword {
6
     cursor: hand;
6
     cursor: hand;
7
-}
7
+}
8
+
9
+.invite-dialog {
10
+    .form-control {
11
+        padding: 0;
12
+    }
13
+
14
+    .inviteLink {
15
+        color: $readOnlyInputColor;
16
+    }
17
+
18
+    .lock-state {
19
+        display: flex;
20
+    }
21
+
22
+    .password-overview {
23
+        .form-control {
24
+            margin-top: 10px;
25
+        }
26
+
27
+        .password-overview-toggle-edit,
28
+        .remove-password-link {
29
+            cursor: pointer;
30
+            text-decoration: none;
31
+        }
32
+
33
+        .password-overview-status,
34
+        .remove-password {
35
+            display: flex;
36
+            justify-content: space-between;
37
+        }
38
+
39
+        .remove-password {
40
+            margin-top: 15px;
41
+        }
42
+    }
43
+
44
+    .remove-password-current {
45
+        color: $inputControlEmColor;
46
+    }
47
+}

+ 9
- 4
lang/main.json View File

153
         "cameraAndMic": "Camera and microphone",
153
         "cameraAndMic": "Camera and microphone",
154
         "moderator": "MODERATOR",
154
         "moderator": "MODERATOR",
155
         "password": "SET PASSWORD",
155
         "password": "SET PASSWORD",
156
-        "audioVideo": "AUDIO AND VIDEO",
157
-        "setPasswordLabel": "Lock your room with a password."
156
+        "audioVideo": "AUDIO AND VIDEO"
158
     },
157
     },
159
     "profile": {
158
     "profile": {
160
         "title": "Profile",
159
         "title": "Profile",
225
         "connecting": "Connecting",
224
         "connecting": "Connecting",
226
         "copy": "Copy",
225
         "copy": "Copy",
227
         "error": "Error",
226
         "error": "Error",
228
-        "roomLocked": "This call is locked. New callers must have the link and enter the password to join",
229
-        "addPassword": "Add a password",
230
         "createPassword": "Create password",
227
         "createPassword": "Create password",
231
         "detectext": "Error when trying to detect desktopsharing extension.",
228
         "detectext": "Error when trying to detect desktopsharing extension.",
232
         "failtoinstall": "Failed to install desktop sharing extension",
229
         "failtoinstall": "Failed to install desktop sharing extension",
429
         "noOtherDevices": "No other devices available",
426
         "noOtherDevices": "No other devices available",
430
         "selectADevice": "Select a device",
427
         "selectADevice": "Select a device",
431
         "testAudio": "Test sound"
428
         "testAudio": "Test sound"
429
+    },
430
+    "invite": {
431
+        "addPassword": "Add password",
432
+        "hidePassword": "Hide password",
433
+        "inviteTo": "Invite people to __conferenceName__",
434
+        "locked": "This call is locked. New callers must have the link and enter the password to join.",
435
+        "showPassword": "Show password",
436
+        "unlocked": "This call is unlocked. Any new caller with the link may join the call."
432
     }
437
     }
433
 }
438
 }

+ 0
- 187
modules/UI/invite/Invite.js View File

1
-/* global JitsiMeetJS, APP */
2
-const logger = require("jitsi-meet-logger").getLogger(__filename);
3
-
4
-import InviteDialogView from './InviteDialogView';
5
-import createRoomLocker from './RoomLocker';
6
-import UIEvents from  '../../../service/UI/UIEvents';
7
-
8
-const ConferenceEvents = JitsiMeetJS.events.conference;
9
-
10
-/**
11
- * Invite module
12
- * Constructor takes conference object giving
13
- * ability to subscribe on its events
14
- */
15
-class Invite {
16
-    constructor(conference) {
17
-        this.conference = conference;
18
-        this.inviteUrl = APP.ConferenceUrl.getInviteUrl();
19
-        this.createRoomLocker(conference);
20
-        this.registerListeners();
21
-    }
22
-
23
-    /**
24
-     * Registering listeners.
25
-     * Primarily listeners for conference events.
26
-     */
27
-    registerListeners() {
28
-
29
-        this.conference.on(ConferenceEvents.LOCK_STATE_CHANGED,
30
-            (locked, error) => {
31
-
32
-            logger.log("Received channel password lock change: ", locked,
33
-                error);
34
-
35
-            if (!locked) {
36
-                this.getRoomLocker().resetPassword();
37
-            }
38
-
39
-            this.setLockedFromElsewhere(locked);
40
-        });
41
-
42
-        this.conference.on(ConferenceEvents.USER_ROLE_CHANGED, (id) => {
43
-            if (APP.conference.isLocalId(id)
44
-                    && this.isModerator !== this.conference.isModerator()) {
45
-
46
-                this.setModerator(this.conference.isModerator());
47
-            }
48
-        });
49
-
50
-        APP.UI.addListener( UIEvents.INVITE_CLICKED,
51
-                            () => { this.openLinkDialog(); });
52
-    }
53
-
54
-    /**
55
-     * Updates the view.
56
-     * If dialog hasn't been defined -
57
-     * creates it and updates
58
-     */
59
-    updateView() {
60
-        if (!this.view) {
61
-            this.initDialog();
62
-        }
63
-
64
-        this.view.updateView();
65
-    }
66
-
67
-    /**
68
-     * Room locker factory
69
-     * @param room
70
-     * @returns {Object} RoomLocker
71
-     * @factory
72
-     */
73
-    createRoomLocker(room = this.conference) {
74
-        let roomLocker = createRoomLocker(room);
75
-        this.roomLocker = roomLocker;
76
-        return this.getRoomLocker();
77
-    }
78
-
79
-    /**
80
-     * Room locker getter
81
-     * @returns {Object} RoomLocker
82
-     */
83
-    getRoomLocker() {
84
-        return this.roomLocker;
85
-    }
86
-
87
-    /**
88
-     * Opens the invite link dialog.
89
-     */
90
-    openLinkDialog () {
91
-        if (!this.view) {
92
-            this.initDialog();
93
-        }
94
-
95
-        this.view.open();
96
-    }
97
-
98
-    /**
99
-     * Dialog initialization.
100
-     * creating view object using as a model this module
101
-     */
102
-    initDialog() {
103
-        this.view = new InviteDialogView(this);
104
-    }
105
-
106
-    /**
107
-     * Password getter
108
-     * @returns {String} password
109
-     */
110
-    getPassword() {
111
-        return this.getRoomLocker().password;
112
-    }
113
-
114
-    /**
115
-     * Switches between the moderator view and normal view.
116
-     *
117
-     * @param isModerator indicates if the participant is moderator
118
-     */
119
-    setModerator(isModerator) {
120
-        this.isModerator = isModerator;
121
-
122
-        this.updateView();
123
-    }
124
-
125
-    /**
126
-     * Allows to unlock the room.
127
-     * If the current user is moderator.
128
-     */
129
-    setRoomUnlocked() {
130
-        if (this.isModerator) {
131
-            this.getRoomLocker().lock().then(() => {
132
-                APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
133
-                this.updateView();
134
-            });
135
-        }
136
-    }
137
-
138
-    /**
139
-     * Allows to lock the room if
140
-     * the current user is moderator.
141
-     * Takes the password.
142
-     * @param {String} newPass
143
-     */
144
-    setRoomLocked(newPass) {
145
-        let isModerator = this.isModerator;
146
-        if (isModerator && (newPass || !this.getRoomLocker().isLocked)) {
147
-            this.getRoomLocker().lock(newPass).then(() => {
148
-                APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
149
-                this.updateView();
150
-            });
151
-        }
152
-    }
153
-
154
-    /**
155
-     * Helper method for encoding
156
-     * Invite URL
157
-     * @returns {string}
158
-     */
159
-    getEncodedInviteUrl() {
160
-        return encodeURI(this.inviteUrl);
161
-    }
162
-
163
-    /**
164
-     * Is locked flag.
165
-     * Delegates to room locker
166
-     * @returns {Boolean} isLocked
167
-     */
168
-    isLocked() {
169
-        return this.getRoomLocker().isLocked;
170
-    }
171
-
172
-    /**
173
-     * Set flag locked from elsewhere to room locker.
174
-     * @param isLocked
175
-     */
176
-    setLockedFromElsewhere(isLocked) {
177
-        let roomLocker = this.getRoomLocker();
178
-        let oldLockState = roomLocker.isLocked;
179
-        if (oldLockState !== isLocked) {
180
-            roomLocker.lockedElsewhere = isLocked;
181
-            APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
182
-            this.updateView();
183
-        }
184
-    }
185
-}
186
-
187
-export default Invite;

+ 0
- 378
modules/UI/invite/InviteDialogView.js View File

1
-/* global $, APP, JitsiMeetJS */
2
-const logger = require("jitsi-meet-logger").getLogger(__filename);
3
-
4
-/**
5
- * Substate for password
6
- * @type {{LOCKED: string, UNLOCKED: string}}
7
- */
8
-const States = {
9
-    LOCKED: 'locked',
10
-    UNLOCKED: 'unlocked'
11
-};
12
-
13
-/**
14
- * Class representing view for Invite dialog
15
- * @class InviteDialogView
16
- */
17
-export default class InviteDialogView {
18
-    constructor(model) {
19
-        this.unlockHint = "unlockHint";
20
-        this.lockHint = "lockHint";
21
-        this.model = model;
22
-
23
-        if (this.model.inviteUrl === null) {
24
-            this.inviteAttributes = `data-i18n="[value]inviteUrlDefaultMsg"`;
25
-        } else {
26
-            this.inviteAttributes
27
-                = `value="${this.model.getEncodedInviteUrl()}"`;
28
-        }
29
-
30
-        this.initDialog();
31
-    }
32
-
33
-    /**
34
-     * Initialization of dialog property
35
-     */
36
-    initDialog() {
37
-        let dialog = {};
38
-        dialog.closeFunction = this.closeFunction.bind(this);
39
-        dialog.submitFunction = this.submitFunction.bind(this);
40
-        dialog.loadedFunction = this.loadedFunction.bind(this);
41
-
42
-        dialog.titleKey = "dialog.shareLink";
43
-        this.dialog = dialog;
44
-
45
-        this.dialog.states = this.getStates();
46
-    }
47
-
48
-    /**
49
-     * Event handler for submitting dialog
50
-     * @param e
51
-     * @param v
52
-     */
53
-    submitFunction(e, v) {
54
-        if (v && this.model.inviteUrl) {
55
-            JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
56
-        } else {
57
-            JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
58
-        }
59
-    }
60
-
61
-    /**
62
-     * Event handler for load dialog
63
-     * @param event
64
-     */
65
-    loadedFunction(event) {
66
-        if (this.model.inviteUrl) {
67
-            document.getElementById('inviteLinkRef').select();
68
-        } else {
69
-            if (event && event.target) {
70
-                $(event.target).find('button[value=true]')
71
-                    .prop('disabled', true);
72
-            }
73
-        }
74
-    }
75
-
76
-    /**
77
-     * Event handler for closing dialog
78
-     * @param e
79
-     * @param v
80
-     * @param m
81
-     * @param f
82
-     */
83
-    closeFunction(e, v, m, f) {
84
-        $(document).off('click', '.copyInviteLink', this.copyToClipboard);
85
-
86
-        if(!v && !m && !f)
87
-            JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
88
-    }
89
-
90
-    /**
91
-     * Returns all states of the dialog
92
-     * @returns {{}}
93
-     */
94
-    getStates() {
95
-        let {
96
-            titleKey
97
-        } = this.dialog;
98
-        let doneMsg = APP.translation.generateTranslationHTML('dialog.done');
99
-        let states = {};
100
-        let buttons = {};
101
-        buttons[`${doneMsg}`] = true;
102
-
103
-        states[States.UNLOCKED] = {
104
-            titleKey,
105
-            html: this.getShareLinkBlock() + this.getAddPasswordBlock(),
106
-            buttons
107
-        };
108
-        states[States.LOCKED] = {
109
-            titleKey,
110
-            html: this.getShareLinkBlock() + this.getPasswordBlock(),
111
-            buttons
112
-        };
113
-
114
-        return states;
115
-    }
116
-
117
-    /**
118
-     * Layout for invite link input
119
-     * @returns {string}
120
-     */
121
-    getShareLinkBlock() {
122
-        let classes = 'button-control button-control_light copyInviteLink';
123
-        return (
124
-            `<div class="form-control">
125
-                <label class="form-control__label" for="inviteLinkRef"
126
-                    data-i18n="${this.dialog.titleKey}"></label>
127
-                <div class="form-control__container">
128
-                    <input class="input-control inviteLink"
129
-                           id="inviteLinkRef" type="text"
130
-                           ${this.inviteAttributes} readonly>
131
-                    <button data-i18n="dialog.copy" class="${classes}"></button>
132
-                </div>
133
-                <p class="form-control__hint ${this.lockHint}">
134
-                   <span class="icon-security-locked"></span>
135
-                   <span data-i18n="dialog.roomLocked"></span>
136
-                </p>
137
-                <p class="form-control__hint ${this.unlockHint}">
138
-                   <span class="icon-security"></span>
139
-                   <span data-i18n="roomUnlocked"></span>
140
-                </p>
141
-            </div>`
142
-        );
143
-    }
144
-
145
-    /**
146
-     * Layout for adding password input
147
-     * @returns {string}
148
-     */
149
-    getAddPasswordBlock() {
150
-        let html;
151
-
152
-        if (this.model.isModerator) {
153
-            html = (`
154
-            <div class="form-control">
155
-                <label class="form-control__label"
156
-                       for="newPasswordInput" data-i18n="dialog.addPassword">
157
-               </label>
158
-                <div class="form-control__container">
159
-                    <input class="input-control"
160
-                           id="newPasswordInput"
161
-                           type="text" 
162
-                           data-i18n="[placeholder]dialog.createPassword">
163
-                    <button id="addPasswordBtn" id="inviteDialogAddPassword"
164
-                            disabled data-i18n="dialog.add"
165
-                            class="button-control button-control_light">
166
-                    </button>
167
-                </div>
168
-            </div>
169
-        `);
170
-        } else {
171
-            html = '';
172
-        }
173
-
174
-        return html;
175
-    }
176
-
177
-    /**
178
-     * Layout for password (when room is locked)
179
-     * @returns {string}
180
-     */
181
-    getPasswordBlock() {
182
-        let password = this.model.getPassword();
183
-        let { isModerator } = this.model;
184
-
185
-        if (isModerator) {
186
-            return (`
187
-                <div class="form-control">
188
-                    <label class="form-control__label"
189
-                           data-i18n="dialog.passwordLabel"></label>
190
-                    <div class="form-control__container">
191
-                        <p>
192
-                            <span class="form-control__text"
193
-                                  data-i18n="dialog.currentPassword"></span>
194
-                            <span id="inviteDialogPassword"
195
-                                  class="form-control__em">
196
-                                ${password}
197
-                            </span>
198
-                        </p>
199
-                        <a class="link form-control__right"
200
-                           id="inviteDialogRemovePassword"
201
-                           data-i18n="dialog.removePassword"></a>
202
-                    </div>
203
-                </div>
204
-            `);
205
-        } else {
206
-            return (`
207
-                <div class="form-control">
208
-                    <p>A participant protected this call with a password.</p>
209
-                </div>
210
-            `);
211
-        }
212
-
213
-    }
214
-
215
-
216
-
217
-    /**
218
-     * Opening the dialog
219
-     */
220
-    open() {
221
-        let {
222
-            submitFunction,
223
-            loadedFunction,
224
-            closeFunction
225
-        } = this.dialog;
226
-
227
-        let states = this.getStates();
228
-        let initial = this.model.roomLocked ? States.LOCKED : States.UNLOCKED;
229
-
230
-        APP.UI.messageHandler.openDialogWithStates(states, {
231
-            submit: submitFunction,
232
-            loaded: loadedFunction,
233
-            close: closeFunction,
234
-            size: 'medium'
235
-        });
236
-        $.prompt.goToState(initial);
237
-
238
-        this.registerListeners();
239
-        this.updateView();
240
-    }
241
-
242
-    /**
243
-     * Setting event handlers
244
-     * used in dialog
245
-     */
246
-    registerListeners() {
247
-        const ENTER_KEY = 13;
248
-        let addPasswordBtn = '#addPasswordBtn';
249
-        let copyInviteLink = '.copyInviteLink';
250
-        let newPasswordInput = '#newPasswordInput';
251
-        let removePassword = '#inviteDialogRemovePassword';
252
-
253
-        $(document).on('click', copyInviteLink, this.copyToClipboard);
254
-        $(removePassword).on('click', () => {
255
-            this.model.setRoomUnlocked();
256
-        });
257
-        let boundSetPassword = this._setPassword.bind(this);
258
-        $(document).on('click', addPasswordBtn, boundSetPassword);
259
-        let boundDisablePass = this.disableAddPassIfInputEmpty.bind(this);
260
-        $(document).on('keypress', newPasswordInput, boundDisablePass);
261
-
262
-        // We need to handle keydown event because impromptu
263
-        // is listening to it too for closing the dialog
264
-        $(newPasswordInput).on('keydown', (e) => {
265
-            if (e.keyCode === ENTER_KEY) {
266
-                e.stopPropagation();
267
-                this._setPassword();
268
-            }
269
-        });
270
-    }
271
-
272
-    /**
273
-     * Marking room as locked
274
-     * @private
275
-     */
276
-    _setPassword() {
277
-        let $passInput = $('#newPasswordInput');
278
-        let newPass = $passInput.val();
279
-
280
-        if(newPass) {
281
-            this.model.setRoomLocked(newPass);
282
-        }
283
-    }
284
-
285
-    /**
286
-     * Checking input and if it's empty then
287
-     * disable add pass button
288
-     */
289
-    disableAddPassIfInputEmpty() {
290
-        let $passInput = $('#newPasswordInput');
291
-        let $addPassBtn = $('#addPasswordBtn');
292
-
293
-        if(!$passInput.val()) {
294
-            $addPassBtn.prop('disabled', true);
295
-        } else {
296
-            $addPassBtn.prop('disabled', false);
297
-        }
298
-    }
299
-
300
-    /**
301
-     * Copying text to clipboard
302
-     */
303
-    copyToClipboard() {
304
-        $('.inviteLink').each(function () {
305
-            let $el = $(this).closest('.jqistate');
306
-
307
-            // TOFIX: We can select only visible elements
308
-            if($el.css('display') === 'block') {
309
-                this.select();
310
-
311
-                try {
312
-                    document.execCommand('copy');
313
-                    this.blur();
314
-                }
315
-                catch (err) {
316
-                    logger.error('error when copy the text');
317
-                }
318
-            }
319
-        });
320
-    }
321
-
322
-    /**
323
-     * Method syncing the view and the model
324
-     */
325
-    updateView() {
326
-        let pass = this.model.getPassword();
327
-        let { isModerator } = this.model;
328
-        if (this.model.getRoomLocker().lockedElsewhere || !pass) {
329
-            $('#inviteDialogPassword').attr("data-i18n", "passwordSetRemotely");
330
-            APP.translation.translateElement($('#inviteDialogPassword'));
331
-        } else {
332
-            $('#inviteDialogPassword').removeAttr("data-i18n");
333
-            $('#inviteDialogPassword').text(pass);
334
-        }
335
-
336
-        // if we are not moderator we cannot remove password
337
-        if (isModerator)
338
-            $('#inviteDialogRemovePassword').show();
339
-        else
340
-            $('#inviteDialogRemovePassword').hide();
341
-
342
-        $('#newPasswordInput').val('');
343
-        this.disableAddPassIfInputEmpty();
344
-
345
-        this.updateInviteLink();
346
-
347
-        $.prompt.goToState(
348
-            (this.model.isLocked())
349
-                ? States.LOCKED
350
-                : States.UNLOCKED);
351
-
352
-        let roomLocked = `.${this.lockHint}`;
353
-        let roomUnlocked = `.${this.unlockHint}`;
354
-
355
-        let showDesc = this.model.isLocked() ? roomLocked : roomUnlocked;
356
-        let hideDesc = !this.model.isLocked() ? roomLocked : roomUnlocked;
357
-
358
-        $(showDesc).show();
359
-        $(hideDesc).hide();
360
-    }
361
-
362
-    /**
363
-     * Updates invite link
364
-     */
365
-    updateInviteLink() {
366
-        // If the invite dialog has been already opened we update the
367
-        // information.
368
-        let inviteLink = document.querySelectorAll('.inviteLink');
369
-        let list = Array.from(inviteLink);
370
-        list.forEach((inviteLink) => {
371
-            inviteLink.value = this.model.inviteUrl;
372
-            inviteLink.select();
373
-        });
374
-
375
-        $('#inviteLinkRef').parent()
376
-            .find('button[value=true]').prop('disabled', false);
377
-    }
378
-}

+ 0
- 103
modules/UI/invite/RoomLocker.js View File

1
-/* global APP, JitsiMeetJS */
2
-const logger = require("jitsi-meet-logger").getLogger(__filename);
3
-
4
-/**
5
- * Show notification that user cannot set password for the conference
6
- * because server doesn't support that.
7
- */
8
-function notifyPasswordNotSupported () {
9
-    logger.warn('room passwords not supported');
10
-    APP.UI.messageHandler.showError(
11
-        "dialog.warning", "dialog.passwordNotSupported");
12
-}
13
-
14
-/**
15
- * Show notification that setting password for the conference failed.
16
- * @param {Error} err error
17
- */
18
-function notifyPasswordFailed(err) {
19
-    logger.warn('setting password failed', err);
20
-    APP.UI.messageHandler.showError(
21
-        "dialog.lockTitle", "dialog.lockMessage");
22
-}
23
-
24
-const ConferenceErrors = JitsiMeetJS.errors.conference;
25
-
26
-/**
27
- * Create new RoomLocker for the conference.
28
- * It allows to set or remove password for the conference,
29
- * or ask for required password.
30
- * @returns {RoomLocker}
31
- */
32
-export default function createRoomLocker (room) {
33
-    let password;
34
-    /**
35
-     * If the room was locked from someone other than us, we indicate it with
36
-     * this property in order to have correct roomLocker state of isLocked.
37
-     * @type {boolean} whether room is locked, but not from us.
38
-     */
39
-    let lockedElsewhere = false;
40
-
41
-    /**
42
-     * @class RoomLocker
43
-     */
44
-    return {
45
-        get isLocked () {
46
-            return !!password || lockedElsewhere;
47
-        },
48
-
49
-        get password () {
50
-            return password;
51
-        },
52
-
53
-        /**
54
-         * Allows to set new password
55
-         * @param newPass
56
-         * @returns {Promise.<TResult>}
57
-         */
58
-        lock (newPass) {
59
-            return room.lock(newPass).then(() => {
60
-                password = newPass;
61
-                // If the password is undefined this means that we're removing
62
-                // it for everyone.
63
-                if (!password)
64
-                    lockedElsewhere = false;
65
-            }).catch(function (err) {
66
-                logger.error(err);
67
-                if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) {
68
-                    notifyPasswordNotSupported();
69
-                } else {
70
-                    notifyPasswordFailed(err);
71
-                }
72
-                throw err;
73
-            });
74
-        },
75
-
76
-        /**
77
-         * Sets that the room is locked from another user, not us.
78
-         * @param {boolean} value locked/unlocked state
79
-         */
80
-        set lockedElsewhere (value) {
81
-            lockedElsewhere = value;
82
-        },
83
-
84
-        /**
85
-         * Whether room is locked from someone else.
86
-         * @returns {boolean} whether room is not locked locally,
87
-         * but it is still locked.
88
-         */
89
-        get lockedElsewhere () {
90
-            return lockedElsewhere;
91
-        },
92
-
93
-        /**
94
-         * Reset the password. Can be useful when room
95
-         * has been unlocked from elsewhere and we can use
96
-         * this method for sync the pass
97
-         */
98
-        resetPassword() {
99
-            password = null;
100
-        },
101
-
102
-    };
103
-}

+ 4
- 4
modules/UI/side_pannels/contactlist/ContactList.js View File

19
     }
19
     }
20
 
20
 
21
     /**
21
     /**
22
-     * Is locked flag.
23
-     * Delegates to Invite module
24
-     * TO FIX: find a better way to access the IS LOCKED state of the invite.
22
+     * Returns true if the current conference is locked.
25
      *
23
      *
26
      * @returns {Boolean}
24
      * @returns {Boolean}
27
      */
25
      */
28
     isLocked() {
26
     isLocked() {
29
-        return APP.conference.invite.isLocked();
27
+        const conference = APP.store.getState()['features/base/conference'];
28
+
29
+        return conference.locked;
30
     }
30
     }
31
 
31
 
32
     /**
32
     /**

+ 16
- 14
modules/UI/side_pannels/contactlist/ContactListView.js View File

1
 /* global $, APP, interfaceConfig */
1
 /* global $, APP, interfaceConfig */
2
 const logger = require("jitsi-meet-logger").getLogger(__filename);
2
 const logger = require("jitsi-meet-logger").getLogger(__filename);
3
 
3
 
4
+import { openInviteDialog } from '../../../../react/features/invite';
5
+
4
 import Avatar from '../../avatar/Avatar';
6
 import Avatar from '../../avatar/Avatar';
5
 import UIEvents from '../../../../service/UI/UIEvents';
7
 import UIEvents from '../../../../service/UI/UIEvents';
6
 import UIUtil from '../../util/UIUtil';
8
 import UIUtil from '../../util/UIUtil';
96
         this.model = model;
98
         this.model = model;
97
         this.addInviteButton();
99
         this.addInviteButton();
98
         this.registerListeners();
100
         this.registerListeners();
99
-        this.toggleLock();
101
+        this.setLockDisplay(false);
100
     },
102
     },
101
     /**
103
     /**
102
      * Adds layout for invite button
104
      * Adds layout for invite button
108
             .insertAdjacentHTML('afterend', this.getInviteButtonLayout());
110
             .insertAdjacentHTML('afterend', this.getInviteButtonLayout());
109
 
111
 
110
         APP.translation.translateElement($(container));
112
         APP.translation.translateElement($(container));
113
+
111
         $(document).on('click', '#addParticipantsBtn', () => {
114
         $(document).on('click', '#addParticipantsBtn', () => {
112
-            APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
115
+            APP.store.dispatch(openInviteDialog());
113
         });
116
         });
114
     },
117
     },
115
     /**
118
     /**
125
 
128
 
126
         return (
129
         return (
127
             `<div class="sideToolbarBlock first">
130
             `<div class="sideToolbarBlock first">
128
-                <button id="addParticipantsBtn" 
129
-                         data-i18n="${key}" 
131
+                <button id="addParticipantsBtn"
132
+                         data-i18n="${key}"
130
                          class="${classes}"></button>
133
                          class="${classes}"></button>
131
                 <div>
134
                 <div>
132
                     ${lockedHtml}
135
                     ${lockedHtml}
158
         let displayNameChange = this.onDisplayNameChange.bind(this);
161
         let displayNameChange = this.onDisplayNameChange.bind(this);
159
 
162
 
160
         APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
163
         APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
161
-                            this.toggleLock.bind(this));
164
+                            this.setLockDisplay.bind(this));
162
         APP.UI.addListener( UIEvents.CONTACT_ADDED,
165
         APP.UI.addListener( UIEvents.CONTACT_ADDED,
163
                             this.onAddContact.bind(this));
166
                             this.onAddContact.bind(this));
164
 
167
 
167
         APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
170
         APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
168
     },
171
     },
169
     /**
172
     /**
170
-     * Updating the view according the model
171
-     * @param type {String} type of change
172
-     * @returns {Promise}
173
+     * Updates the view according to the passed in lock state.
174
+     *
175
+     * @param {boolean} isLocked - True if the locked UI state should display.
173
      */
176
      */
174
-    toggleLock() {
175
-        let isLocked = this.model.isLocked();
176
-        let showKey = isLocked ? this.lockKey : this.unlockKey;
177
-        let hideKey = !isLocked ? this.lockKey : this.unlockKey;
178
-        let showId = `contactList${showKey}`;
179
-        let hideId = `contactList${hideKey}`;
177
+    setLockDisplay(isLocked) {
178
+        const showKey = isLocked ? this.lockKey : this.unlockKey;
179
+        const hideKey = !isLocked ? this.lockKey : this.unlockKey;
180
+        const showId = `contactList${showKey}`;
181
+        const hideId = `contactList${hideKey}`;
180
 
182
 
181
         $(`#${showId}`).show();
183
         $(`#${showId}`).show();
182
         $(`#${hideId}`).hide();
184
         $(`#${hideId}`).hide();

+ 11
- 0
react/features/base/conference/actionTypes.js View File

116
  */
116
  */
117
 export const SET_PASSWORD = Symbol('SET_PASSWORD');
117
 export const SET_PASSWORD = Symbol('SET_PASSWORD');
118
 
118
 
119
+/**
120
+ * The type of Redux action which signals that setting a password on a
121
+ * JitsiConference encountered an error and failed.
122
+ *
123
+ * {
124
+ *     type: SET_PASSWORD_FAILED,
125
+ *     error: string
126
+ * }
127
+ */
128
+export const SET_PASSWORD_FAILED = Symbol('SET_PASSWORD_FAILED');
129
+
119
 /**
130
 /**
120
  * The type of the Redux action which sets the name of the room of the
131
  * The type of the Redux action which sets the name of the room of the
121
  * conference to be joined.
132
  * conference to be joined.

+ 9
- 3
react/features/base/conference/actions.js View File

22
     _SET_AUDIO_ONLY_VIDEO_MUTED,
22
     _SET_AUDIO_ONLY_VIDEO_MUTED,
23
     SET_LASTN,
23
     SET_LASTN,
24
     SET_PASSWORD,
24
     SET_PASSWORD,
25
+    SET_PASSWORD_FAILED,
25
     SET_ROOM
26
     SET_ROOM
26
 } from './actionTypes';
27
 } from './actionTypes';
27
 import {
28
 import {
56
 
57
 
57
     conference.on(
58
     conference.on(
58
             JitsiConferenceEvents.LOCK_STATE_CHANGED,
59
             JitsiConferenceEvents.LOCK_STATE_CHANGED,
59
-            (...args) => dispatch(_lockStateChanged(conference, ...args)));
60
+            (...args) => dispatch(lockStateChanged(conference, ...args)));
60
 
61
 
61
     // Dispatches into features/base/tracks follow:
62
     // Dispatches into features/base/tracks follow:
62
 
63
 
292
  *     locked: boolean
293
  *     locked: boolean
293
  * }}
294
  * }}
294
  */
295
  */
295
-function _lockStateChanged(conference, locked) {
296
+export function lockStateChanged(conference, locked) {
296
     return {
297
     return {
297
         type: LOCK_STATE_CHANGED,
298
         type: LOCK_STATE_CHANGED,
298
         conference,
299
         conference,
440
                             conference,
441
                             conference,
441
                             method,
442
                             method,
442
                             password
443
                             password
443
-                        })));
444
+                        }))
445
+                        .catch(error => dispatch({
446
+                            type: SET_PASSWORD_FAILED,
447
+                            error
448
+                        }))
449
+                );
444
             }
450
             }
445
 
451
 
446
             return Promise.reject();
452
             return Promise.reject();

+ 25
- 4
react/features/base/conference/reducer.js View File

1
+import { LOCKED_LOCALLY, LOCKED_REMOTELY } from '../../room-lock';
2
+
1
 import { JitsiConferenceErrors } from '../lib-jitsi-meet';
3
 import { JitsiConferenceErrors } from '../lib-jitsi-meet';
2
 import {
4
 import {
3
     ReducerRegistry,
5
     ReducerRegistry,
83
             audioOnlyVideoMuted: undefined,
85
             audioOnlyVideoMuted: undefined,
84
             conference: undefined,
86
             conference: undefined,
85
             leaving: undefined,
87
             leaving: undefined,
86
-            locked: undefined,
88
+            locked: passwordRequired ? LOCKED_REMOTELY : undefined,
87
             password: undefined,
89
             password: undefined,
88
 
90
 
89
             /**
91
             /**
112
     // i.e. password-protected is private to lib-jitsi-meet. However, the
114
     // i.e. password-protected is private to lib-jitsi-meet. However, the
113
     // library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
115
     // library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
114
     // with a password.
116
     // with a password.
115
-    const locked = conference.room.locked || undefined;
117
+    const locked = conference.room.locked ? LOCKED_REMOTELY : undefined;
116
 
118
 
117
     return (
119
     return (
118
         setStateProperties(state, {
120
         setStateProperties(state, {
209
         return state;
211
         return state;
210
     }
212
     }
211
 
213
 
212
-    return setStateProperty(state, 'locked', action.locked || undefined);
214
+    let lockState;
215
+
216
+    if (action.locked) {
217
+        lockState = state.locked || LOCKED_REMOTELY;
218
+    }
219
+
220
+    return setStateProperties(state, {
221
+        locked: lockState,
222
+        password: action.locked ? state.password : null
223
+    });
213
 }
224
 }
214
 
225
 
215
 /**
226
 /**
254
     const conference = action.conference;
265
     const conference = action.conference;
255
 
266
 
256
     switch (action.method) {
267
     switch (action.method) {
257
-    case conference.join:
268
+    case conference.join: {
258
         if (state.passwordRequired === conference) {
269
         if (state.passwordRequired === conference) {
259
             return (
270
             return (
260
                 setStateProperties(state, {
271
                 setStateProperties(state, {
272
+                    locked: LOCKED_REMOTELY,
273
+
261
                     /**
274
                     /**
262
                      * The password with which the conference is to be joined.
275
                      * The password with which the conference is to be joined.
263
                      *
276
                      *
267
                     passwordRequired: undefined
280
                     passwordRequired: undefined
268
                 }));
281
                 }));
269
         }
282
         }
283
+
270
         break;
284
         break;
271
     }
285
     }
286
+    case conference.lock: {
287
+        return setStateProperties(state, {
288
+            locked: action.password ? LOCKED_LOCALLY : undefined,
289
+            password: action.password
290
+        });
291
+    }
292
+    }
272
 
293
 
273
     return state;
294
     return state;
274
 }
295
 }

+ 18
- 0
react/features/invite/actions.js View File

1
+/* globals APP */
2
+
3
+import { openDialog } from '../../features/base/dialog';
4
+
5
+import { InviteDialog } from './components';
6
+
7
+/**
8
+ * Opens the Invite Dialog.
9
+ *
10
+ * @returns {Function}
11
+ */
12
+export function openInviteDialog() {
13
+    return dispatch => {
14
+        dispatch(openDialog(InviteDialog, {
15
+            conferenceUrl: encodeURI(APP.ConferenceUrl.getInviteUrl())
16
+        }));
17
+    };
18
+}

+ 137
- 0
react/features/invite/components/AddPasswordForm.js View File

1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { setPassword } from '../../base/conference';
5
+import { translate } from '../../base/i18n';
6
+
7
+/**
8
+ * A React Component for locking a JitsiConference with a password.
9
+ */
10
+class AddPasswordForm extends Component {
11
+    /**
12
+     * AddPasswordForm component's property types.
13
+     *
14
+     * @static
15
+     */
16
+    static propTypes = {
17
+        /**
18
+         * The JitsiConference on which to lock and set a password.
19
+         *
20
+         * @type {JitsiConference}
21
+         */
22
+        conference: React.PropTypes.object,
23
+
24
+        /**
25
+         * Invoked to set a password on the conference.
26
+         */
27
+        dispatch: React.PropTypes.func,
28
+
29
+        /**
30
+         * Invoked to obtain translated strings.
31
+         */
32
+        t: React.PropTypes.func
33
+    }
34
+
35
+    /**
36
+     * Initializes a new AddPasswordForm instance.
37
+     *
38
+     * @param {Object} props - The read-only properties with which the new
39
+     * instance is to be initialized.
40
+     */
41
+    constructor(props) {
42
+        super(props);
43
+
44
+        this.state = {
45
+            password: ''
46
+        };
47
+
48
+        this._onKeyDown = this._onKeyDown.bind(this);
49
+        this._onPasswordChange = this._onPasswordChange.bind(this);
50
+        this._onSubmit = this._onSubmit.bind(this);
51
+    }
52
+
53
+    /**
54
+     * Implements React's {@link Component#render()}.
55
+     *
56
+     * @inheritdoc
57
+     * @returns {ReactElement}
58
+     */
59
+    render() {
60
+        return (
61
+            <div
62
+                className = 'form-control'
63
+                onSubmit = { this._onSubmit } >
64
+                <div className = 'form-control__container'>
65
+                    <input
66
+                        autoFocus = { true }
67
+                        className = 'input-control'
68
+                        id = 'newPasswordInput'
69
+                        onChange = { this._onPasswordChange }
70
+                        onKeyDown = { this._onKeyDown }
71
+                        placeholder
72
+                            = { this.props.t('dialog.createPassword') }
73
+                        type = 'text' />
74
+                    <button
75
+                        className = 'button-control button-control_light'
76
+                        disabled = { !this.state.password }
77
+                        id = 'addPasswordBtn'
78
+                        onClick = { this._onSubmit }
79
+                        type = 'button'>
80
+                        { this.props.t('dialog.add') }
81
+                    </button>
82
+                </div>
83
+            </div>
84
+        );
85
+    }
86
+
87
+    /**
88
+     * Mimics form behavior by listening for enter key press and submitting the
89
+     * entered password.
90
+     *
91
+     * @param {Object} event - DOM Event for keydown.
92
+     * @private
93
+     * @returns {void}
94
+     */
95
+    _onKeyDown(event) {
96
+        event.stopPropagation();
97
+
98
+        if (event.keyCode === /* Enter */ 13) {
99
+            this._onSubmit();
100
+        }
101
+    }
102
+
103
+    /**
104
+     * Updates the internal state of the entered password.
105
+     *
106
+     * @param {Object} event - DOM Event for value change.
107
+     * @private
108
+     * @returns {void}
109
+     */
110
+    _onPasswordChange(event) {
111
+        this.setState({ password: event.target.value });
112
+    }
113
+
114
+    /**
115
+     * Dispatches a request to lock the conference with a password.
116
+     *
117
+     * @private
118
+     * @returns {void}
119
+     */
120
+    _onSubmit() {
121
+        if (!this.state.password) {
122
+            return;
123
+        }
124
+
125
+        const conference = this.props.conference;
126
+
127
+        this.props.dispatch(setPassword(
128
+            conference,
129
+            conference.lock,
130
+            this.state.password
131
+        ));
132
+
133
+        this.setState({ password: '' });
134
+    }
135
+}
136
+
137
+export default translate(connect()(AddPasswordForm));

+ 104
- 0
react/features/invite/components/InviteDialog.js View File

1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { Dialog } from '../../base/dialog';
5
+import { translate } from '../../base/i18n';
6
+import JitsiMeetJS from '../../base/lib-jitsi-meet';
7
+import {
8
+    getLocalParticipant,
9
+    PARTICIPANT_ROLE
10
+} from '../../base/participants';
11
+
12
+import PasswordContainer from './PasswordContainer';
13
+import ShareLinkForm from './ShareLinkForm';
14
+
15
+/**
16
+ * A React Component for displaying other components responsible for copying the
17
+ * current conference url and for setting or removing a conference password.
18
+ */
19
+class InviteDialog extends Component {
20
+    /**
21
+     * InviteDialog component's property types.
22
+     *
23
+     * @static
24
+     */
25
+    static propTypes = {
26
+        /**
27
+         * The redux store representation of the JitsiConference.
28
+         *
29
+         */
30
+        _conference: React.PropTypes.object,
31
+
32
+        /**
33
+         * Whether or not the current user is a conference moderator.
34
+         */
35
+        _isModerator: React.PropTypes.bool,
36
+
37
+        /**
38
+         * The url for the JitsiConference.
39
+         */
40
+        conferenceUrl: React.PropTypes.string,
41
+
42
+        /**
43
+         * Invoked to obtain translated strings.
44
+         */
45
+        t: React.PropTypes.func
46
+    }
47
+
48
+    /**
49
+     * Reports an analytics event for the invite modal being closed.
50
+     *
51
+     * @inheritdoc
52
+     */
53
+    componentWillUnmount() {
54
+        JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
55
+    }
56
+
57
+    /**
58
+     * Implements React's {@link Component#render()}.
59
+     *
60
+     * @inheritdoc
61
+     * @returns {ReactElement}
62
+     */
63
+    render() {
64
+        return (
65
+            <Dialog
66
+                cancelDisabled = { true }
67
+                okTitleKey = 'dialog.done'
68
+                titleString = { this.props.t(
69
+                    'invite.inviteTo',
70
+                    { conferenceName: this.props._conference.room }) } >
71
+                <div className = 'invite-dialog'>
72
+                    <ShareLinkForm toCopy = { this.props.conferenceUrl } />
73
+                    <PasswordContainer
74
+                        conference = { this.props._conference.conference }
75
+                        locked = { this.props._conference.locked }
76
+                        password = { this.props._conference.password }
77
+                        showPasswordEdit = { this.props._isModerator } />
78
+                </div>
79
+            </Dialog>
80
+        );
81
+    }
82
+}
83
+
84
+/**
85
+ * Maps (parts of) the Redux state to the associated InviteDialog's props.
86
+ *
87
+ * @param {Object} state - The Redux state.
88
+ * @private
89
+ * @returns {{
90
+ *     _conference: Object,
91
+ *     _isModerator: boolean
92
+ * }}
93
+ */
94
+function _mapStateToProps(state) {
95
+    const { role }
96
+        = getLocalParticipant(state['features/base/participants']);
97
+
98
+    return {
99
+        _conference: state['features/base/conference'],
100
+        _isModerator: role === PARTICIPANT_ROLE.MODERATOR
101
+    };
102
+}
103
+
104
+export default translate(connect(_mapStateToProps)(InviteDialog));

+ 48
- 0
react/features/invite/components/LockStatePanel.js View File

1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+/**
6
+ * A React Component for displaying the conference lock state.
7
+ */
8
+class LockStatePanel extends Component {
9
+    /**
10
+     * LockStatePanel component's property types.
11
+     *
12
+     * @static
13
+     */
14
+    static propTypes = {
15
+        /**
16
+         * Whether or not the conference is currently locked.
17
+         */
18
+        locked: React.PropTypes.bool,
19
+
20
+        /**
21
+         * Invoked to obtain translated strings.
22
+         */
23
+        t: React.PropTypes.func
24
+    }
25
+
26
+    /**
27
+     * Implements React's {@link Component#render()}.
28
+     *
29
+     * @inheritdoc
30
+     * @returns {ReactElement}
31
+     */
32
+    render() {
33
+        const [ lockStateClass, lockIconClass, lockTextKey ] = this.props.locked
34
+            ? [ 'is-locked', 'icon-security-locked', 'invite.locked' ]
35
+            : [ 'is-unlocked', 'icon-security', 'invite.unlocked' ];
36
+
37
+        return (
38
+            <div className = { `lock-state ${lockStateClass}` }>
39
+                <span className = { lockIconClass } />
40
+                <span>
41
+                    { this.props.t(lockTextKey) }
42
+                </span>
43
+            </div>
44
+        );
45
+    }
46
+}
47
+
48
+export default translate(LockStatePanel);

+ 147
- 0
react/features/invite/components/PasswordContainer.js View File

1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+import { LOCKED_LOCALLY } from '../../room-lock';
5
+
6
+import AddPasswordForm from './AddPasswordForm';
7
+import LockStatePanel from './LockStatePanel';
8
+import RemovePasswordForm from './RemovePasswordForm';
9
+
10
+/**
11
+ * React component for displaying the current room lock state as well as
12
+ * exposing features to modify the room lock.
13
+ */
14
+class PasswordContainer extends Component {
15
+    /**
16
+     * PasswordContainer component's property types.
17
+     *
18
+     * @static
19
+     */
20
+    static propTypes = {
21
+        /**
22
+         * The JitsiConference for which to display a lock state and change the
23
+         * password.
24
+         *
25
+         * @type {JitsiConference}
26
+         */
27
+        conference: React.PropTypes.object,
28
+
29
+        /**
30
+         * The value for how the conference is locked (or undefined if not
31
+         * locked) as defined by room-lock constants.
32
+         */
33
+        locked: React.PropTypes.string,
34
+
35
+        /**
36
+         * The current known password for the JitsiConference.
37
+         */
38
+        password: React.PropTypes.string,
39
+
40
+        /**
41
+         * Whether or not the password editing components should be displayed.
42
+         */
43
+        showPasswordEdit: React.PropTypes.bool,
44
+
45
+        /**
46
+         * Invoked to obtain translated strings.
47
+         */
48
+        t: React.PropTypes.func
49
+    }
50
+
51
+    /**
52
+     * Initializes a new PasswordContainer 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
+        this.state = {
61
+            isEditingPassword: false
62
+        };
63
+
64
+        this._onTogglePasswordEdit = this._onTogglePasswordEdit.bind(this);
65
+    }
66
+
67
+    /**
68
+     * Implements React's {@link Component#render()}.
69
+     *
70
+     * @inheritdoc
71
+     * @returns {ReactElement}
72
+     */
73
+    render() {
74
+        return (
75
+            <div className = 'password-overview'>
76
+                <div className = 'password-overview-status'>
77
+                    <LockStatePanel locked = { Boolean(this.props.locked) } />
78
+                    { this._renderShowPasswordLink() }
79
+                </div>
80
+                { this._renderPasswordEdit() }
81
+            </div>
82
+        );
83
+    }
84
+
85
+    /**
86
+     * Toggles the display of the ReactElements used to edit the password.
87
+     *
88
+     * @private
89
+     * @returns {void}
90
+     */
91
+    _onTogglePasswordEdit() {
92
+        this.setState({
93
+            isEditingPassword: !this.state.isEditingPassword
94
+        });
95
+    }
96
+
97
+    /**
98
+     * Creates a ReactElement used for setting or removing a password.
99
+     *
100
+     * @private
101
+     * @returns {ReactElement|null}
102
+     */
103
+    _renderPasswordEdit() {
104
+        if (!this.state.isEditingPassword) {
105
+            return null;
106
+        }
107
+
108
+        return this.props.locked
109
+            ? <RemovePasswordForm
110
+                conference = { this.props.conference }
111
+                lockedLocally = { this.props.locked === LOCKED_LOCALLY }
112
+                password = { this.props.password } />
113
+            : <AddPasswordForm conference = { this.props.conference } />;
114
+    }
115
+
116
+    /**
117
+     * Creates a ReactElement that toggles displaying password edit components.
118
+     *
119
+     * @private
120
+     * @returns {ReactElement|null}
121
+     */
122
+    _renderShowPasswordLink() {
123
+        if (!this.props.showPasswordEdit) {
124
+            return null;
125
+        }
126
+
127
+        let toggleStatusKey;
128
+
129
+        if (this.state.isEditingPassword) {
130
+            toggleStatusKey = 'invite.hidePassword';
131
+        } else if (this.props.locked) {
132
+            toggleStatusKey = 'invite.showPassword';
133
+        } else {
134
+            toggleStatusKey = 'invite.addPassword';
135
+        }
136
+
137
+        return (
138
+            <a
139
+                className = 'password-overview-toggle-edit'
140
+                onClick = { this._onTogglePasswordEdit }>
141
+                { this.props.t(toggleStatusKey) }
142
+            </a>
143
+        );
144
+    }
145
+}
146
+
147
+export default translate(PasswordContainer);

+ 117
- 0
react/features/invite/components/RemovePasswordForm.js View File

1
+import React, { Component } from 'react';
2
+import { connect } from 'react-redux';
3
+
4
+import { setPassword } from '../../base/conference';
5
+import { translate } from '../../base/i18n';
6
+
7
+/**
8
+ * A React Component for removing a lock from a JitsiConference.
9
+ */
10
+class RemovePasswordForm extends Component {
11
+    /**
12
+     * RemovePasswordForm component's property types.
13
+     *
14
+     * @static
15
+     */
16
+    static propTypes = {
17
+        /**
18
+         * The JitsiConference on which remove a lock.
19
+         *
20
+         * @type {JitsiConference}
21
+         */
22
+        conference: React.PropTypes.object,
23
+
24
+        /**
25
+         * Invoked to send a password removal request.
26
+         */
27
+        dispatch: React.PropTypes.func,
28
+
29
+        /**
30
+         * Whether or not the room lock, if any, was set by the local user.
31
+         */
32
+        lockedLocally: React.PropTypes.bool,
33
+
34
+        /**
35
+         * The current known password for the JitsiConference.
36
+         */
37
+        password: React.PropTypes.string,
38
+
39
+        /**
40
+         * Invoked to obtain translated strings.
41
+         */
42
+        t: React.PropTypes.func
43
+    }
44
+
45
+    /**
46
+     * Initializes a new RemovePasswordForm instance.
47
+     *
48
+     * @param {Object} props - The read-only properties with which the new
49
+     * instance is to be initialized.
50
+     */
51
+    constructor(props) {
52
+        super(props);
53
+
54
+        this._onClick = this._onClick.bind(this);
55
+    }
56
+
57
+    /**
58
+     * Implements React's {@link Component#render()}.
59
+     *
60
+     * @private
61
+     * @returns {ReactElement}
62
+     */
63
+    render() {
64
+        return (
65
+            <div className = 'remove-password'>
66
+                <div className = 'remove-password-description'>
67
+                    { this._getPasswordPreviewText() }
68
+                </div>
69
+                <a
70
+                    className = 'remove-password-link'
71
+                    id = 'inviteDialogRemovePassword'
72
+                    onClick = { this._onClick }>
73
+                    { this.props.t('dialog.removePassword') }
74
+                </a>
75
+            </div>
76
+        );
77
+    }
78
+
79
+    /**
80
+     * Creates a ReactElement for displaying the current password.
81
+     *
82
+     * @private
83
+     * @returns {ReactElement}
84
+     */
85
+    _getPasswordPreviewText() {
86
+        return (
87
+            <span>
88
+                <span>
89
+                    { `${this.props.t('dialog.currentPassword')} ` }
90
+                </span>
91
+                <span className = 'remove-password-current'>
92
+                    { this.props.lockedLocally
93
+                        ? this.props.password
94
+                        : this.props.t('passwordSetRemotely') }
95
+                </span>
96
+            </span>
97
+        );
98
+    }
99
+
100
+    /**
101
+     * Dispatches a request to remove any set password on the JitsiConference.
102
+     *
103
+     * @private
104
+     * @returns {void}
105
+     */
106
+    _onClick() {
107
+        const conference = this.props.conference;
108
+
109
+        this.props.dispatch(setPassword(
110
+            conference,
111
+            conference.lock,
112
+            ''
113
+        ));
114
+    }
115
+}
116
+
117
+export default translate(connect()(RemovePasswordForm));

+ 110
- 0
react/features/invite/components/ShareLinkForm.js View File

1
+import React, { Component } from 'react';
2
+
3
+import { translate } from '../../base/i18n';
4
+
5
+const logger = require('jitsi-meet-logger').getLogger(__filename);
6
+
7
+/**
8
+ * A React Component for displaying a value with a copy button that can be
9
+ * clicked to copy the value onto the clipboard.
10
+ */
11
+class ShareLinkForm extends Component {
12
+    /**
13
+     * ShareLinkForm component's property types.
14
+     *
15
+     * @static
16
+     */
17
+    static propTypes = {
18
+        /**
19
+         * Invoked to obtain translated strings.
20
+         */
21
+        t: React.PropTypes.func,
22
+
23
+       /**
24
+         * The value to be displayed and copied onto the clipboard.
25
+         */
26
+        toCopy: React.PropTypes.string
27
+    }
28
+
29
+    /**
30
+     * Initializes a new ShareLinkForm instance.
31
+     *
32
+     * @param {Object} props - The read-only properties with which the new
33
+     * instance is to be initialized.
34
+     */
35
+    constructor(props) {
36
+        super(props);
37
+
38
+        this._inputElement = null;
39
+
40
+        this._onClick = this._onClick.bind(this);
41
+        this._setInput = this._setInput.bind(this);
42
+    }
43
+
44
+    /**
45
+     * Implements React's {@link Component#render()}.
46
+     *
47
+     * @inheritdoc
48
+     * @returns {ReactElement}
49
+     */
50
+    render() {
51
+        const inputValue = this.props.toCopy
52
+            || this.props.t('inviteUrlDefaultMsg');
53
+
54
+        // FIXME input is used here instead of atlaskit field-text because
55
+        // field-text does not currently support readonly
56
+        return (
57
+            <div className = 'form-control'>
58
+                <label className = 'form-control__label'>
59
+                    { this.props.t('dialog.shareLink') }
60
+                </label>
61
+                <div className = 'form-control__container'>
62
+                    <input
63
+                        className = 'input-control inviteLink'
64
+                        id = 'inviteLinkRef'
65
+                        readOnly = { true }
66
+                        ref = { this._setInput }
67
+                        type = 'text'
68
+                        value = { inputValue } />
69
+                    <button
70
+                        className =
71
+                            'button-control button-control_light copyInviteLink'
72
+                        onClick = { this._onClick }
73
+                        type = 'button'>
74
+                        { this.props.t('dialog.copy') }
75
+                    </button>
76
+                </div>
77
+            </div>
78
+        );
79
+    }
80
+
81
+    /**
82
+     * Copies the passed in value to the clipboard.
83
+     *
84
+     * @private
85
+     * @returns {void}
86
+     */
87
+    _onClick() {
88
+        try {
89
+            this._inputElement.select();
90
+            document.execCommand('copy');
91
+            this._inputElement.blur();
92
+        } catch (err) {
93
+            logger.error('error when copying the text', err);
94
+        }
95
+    }
96
+
97
+    /**
98
+     * Sets the internal reference to the DOM element for the input field so it
99
+     * may be accessed directly.
100
+     *
101
+     * @param {Object} element - DOM element for the component's input.
102
+     * @private
103
+     * @returns {void}
104
+     */
105
+    _setInput(element) {
106
+        this._inputElement = element;
107
+    }
108
+}
109
+
110
+export default translate(ShareLinkForm);

+ 1
- 0
react/features/invite/components/index.js View File

1
+export { default as InviteDialog } from './InviteDialog';

+ 2
- 0
react/features/invite/index.js View File

1
+export * from './actions';
2
+export * from './components';

+ 5
- 3
react/features/room-lock/components/PasswordRequiredPrompt.web.js View File

3
 import { connect } from 'react-redux';
3
 import { connect } from 'react-redux';
4
 import AKFieldText from '@atlaskit/field-text';
4
 import AKFieldText from '@atlaskit/field-text';
5
 
5
 
6
+import UIEvents from '../../../../service/UI/UIEvents';
7
+
6
 import { setPassword } from '../../base/conference';
8
 import { setPassword } from '../../base/conference';
7
 import { Dialog } from '../../base/dialog';
9
 import { Dialog } from '../../base/dialog';
8
 import { translate } from '../../base/i18n';
10
 import { translate } from '../../base/i18n';
109
         // password required will be received and the room again
111
         // password required will be received and the room again
110
         // will be marked as locked.
112
         // will be marked as locked.
111
         if (!this.state.password || this.state.password === '') {
113
         if (!this.state.password || this.state.password === '') {
112
-            // XXX temporary solution till we move the whole invite logic
113
-            // in react
114
-            APP.conference.invite.setLockedFromElsewhere(false);
114
+            // XXX temporary solution while some components are not listening
115
+            // for lock state updates in redux
116
+            APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, false);
115
         }
117
         }
116
 
118
 
117
         this.props.dispatch(setPassword(
119
         this.props.dispatch(setPassword(

+ 12
- 0
react/features/room-lock/constants.js View File

1
+/**
2
+ * The room lock state where the password was set by the current user.
3
+ *
4
+ * @type {string}
5
+ */
6
+export const LOCKED_LOCALLY = 'LOCKED_LOCALLY';
7
+
8
+/**
9
+ * The room lock state where the password was set by a remote user.
10
+ * @type {string}
11
+ */
12
+export const LOCKED_REMOTELY = 'LOCKED_REMOTELY';

+ 1
- 0
react/features/room-lock/index.js View File

1
 export * from './actions';
1
 export * from './actions';
2
 export * from './components';
2
 export * from './components';
3
+export * from './constants';
3
 
4
 
4
 import './middleware';
5
 import './middleware';

+ 56
- 5
react/features/room-lock/middleware.js View File

1
 /* global APP */
1
 /* global APP */
2
-import JitsiMeetJS from '../base/lib-jitsi-meet';
2
+const logger = require('jitsi-meet-logger').getLogger(__filename);
3
+
4
+import UIEvents from '../../../service/UI/UIEvents';
3
 
5
 
4
-import { CONFERENCE_FAILED } from '../base/conference';
6
+import {
7
+    CONFERENCE_FAILED,
8
+    LOCK_STATE_CHANGED,
9
+    SET_PASSWORD_FAILED
10
+} from '../base/conference';
11
+import JitsiMeetJS from '../base/lib-jitsi-meet';
5
 import { MiddlewareRegistry } from '../base/redux';
12
 import { MiddlewareRegistry } from '../base/redux';
13
+
6
 import { _showPasswordDialog } from './actions';
14
 import { _showPasswordDialog } from './actions';
7
 
15
 
8
 /**
16
 /**
20
 
28
 
21
         if (action.conference
29
         if (action.conference
22
             && JitsiConferenceErrors.PASSWORD_REQUIRED === action.error) {
30
             && JitsiConferenceErrors.PASSWORD_REQUIRED === action.error) {
23
-            // XXX temporary solution till we move the whole invite
24
-            // logic in react
31
+            // XXX temporary solution while some components are not listening
32
+            // for lock state updates in redux
25
             if (typeof APP !== 'undefined') {
33
             if (typeof APP !== 'undefined') {
26
-                APP.conference.invite.setLockedFromElsewhere(true);
34
+                APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, true);
27
             }
35
             }
28
 
36
 
29
             store.dispatch(_showPasswordDialog(action.conference));
37
             store.dispatch(_showPasswordDialog(action.conference));
30
         }
38
         }
31
         break;
39
         break;
32
     }
40
     }
41
+    case LOCK_STATE_CHANGED: {
42
+        // TODO Remove this logic when all components interested in the lock
43
+        // state change event are moved into react/redux.
44
+        if (typeof APP !== 'undefined') {
45
+            APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK, action.locked);
46
+        }
47
+
48
+        break;
49
+    }
50
+    case SET_PASSWORD_FAILED:
51
+        return _notifySetPasswordError(store, next, action);
33
     }
52
     }
34
 
53
 
35
     return next(action);
54
     return next(action);
36
 });
55
 });
56
+
57
+/**
58
+ * Handles errors that occur when a password is failed to be set.
59
+ *
60
+ * @param {Store} store - The Redux store in which the specified action is being
61
+ * dispatched.
62
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
63
+ * specified action to the specified store.
64
+ * @param {Action} action - The Redux action SET_PASSWORD_ERROR which has the
65
+ * error type that should be handled.
66
+ * @private
67
+ * @returns {Object} The new state that is the result of the reduction of the
68
+ * specified action.
69
+ */
70
+function _notifySetPasswordError(store, next, action) {
71
+    if (typeof APP !== 'undefined') {
72
+        // TODO remove this logic when displaying of error messages on web is
73
+        // handled through react/redux
74
+        if (action.error
75
+            === JitsiMeetJS.errors.conference.PASSWORD_NOT_SUPPORTED) {
76
+            logger.warn('room passwords not supported');
77
+            APP.UI.messageHandler.showError(
78
+                'dialog.warning', 'dialog.passwordNotSupported');
79
+        } else {
80
+            logger.warn('setting password failed', action.error);
81
+            APP.UI.messageHandler.showError(
82
+                'dialog.lockTitle', 'dialog.lockMessage');
83
+        }
84
+    }
85
+
86
+    return next(action);
87
+}

+ 1
- 1
react/features/toolbox/components/Toolbox.native.js View File

322
          * @protected
322
          * @protected
323
          * @type {boolean}
323
          * @type {boolean}
324
          */
324
          */
325
-        _locked: conference.locked
325
+        _locked: Boolean(conference.locked)
326
     };
326
     };
327
 }
327
 }
328
 
328
 

+ 0
- 1
react/features/toolbox/components/Toolbox.web.js View File

223
  * @returns {{
223
  * @returns {{
224
  *     _alwaysVisible: boolean,
224
  *     _alwaysVisible: boolean,
225
  *     _audioMuted: boolean,
225
  *     _audioMuted: boolean,
226
- *     _locked: boolean,
227
  *     _subjectSlideIn: boolean,
226
  *     _subjectSlideIn: boolean,
228
  *     _videoMuted: boolean
227
  *     _videoMuted: boolean
229
  * }}
228
  * }}

+ 3
- 1
react/features/toolbox/defaultToolbarButtons.js View File

4
 
4
 
5
 import UIEvents from '../../../service/UI/UIEvents';
5
 import UIEvents from '../../../service/UI/UIEvents';
6
 
6
 
7
+import { openInviteDialog } from '../invite';
8
+
7
 declare var APP: Object;
9
 declare var APP: Object;
8
 declare var config: Object;
10
 declare var config: Object;
9
 declare var JitsiMeetJS: Object;
11
 declare var JitsiMeetJS: Object;
222
         id: 'toolbar_button_link',
224
         id: 'toolbar_button_link',
223
         onClick() {
225
         onClick() {
224
             JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
226
             JitsiMeetJS.analytics.sendEvent('toolbar.invite.clicked');
225
-            APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
227
+            APP.store.dispatch(openInviteDialog());
226
         },
228
         },
227
         tooltipKey: 'toolbar.invite'
229
         tooltipKey: 'toolbar.invite'
228
     },
230
     },

+ 0
- 4
service/UI/UIEvents.js View File

22
     VIDEO_MUTED: "UI.video_muted",
22
     VIDEO_MUTED: "UI.video_muted",
23
     ETHERPAD_CLICKED: "UI.etherpad_clicked",
23
     ETHERPAD_CLICKED: "UI.etherpad_clicked",
24
     SHARED_VIDEO_CLICKED: "UI.start_shared_video",
24
     SHARED_VIDEO_CLICKED: "UI.start_shared_video",
25
-    /**
26
-     * Indicates that an invite button has been clicked.
27
-     */
28
-    INVITE_CLICKED: "UI.invite_clicked",
29
     /**
25
     /**
30
      * Updates shared video with params: url, state, time(optional)
26
      * Updates shared video with params: url, state, time(optional)
31
      * Where url is the video link, state is stop/start/pause and time is the
27
      * Where url is the video link, state is stop/start/pause and time is the

Loading…
Cancel
Save