Ver código fonte

feat(AddPeopleDialog): Update design; feat(SecurityDialog): Implement

master
Mihai Uscat 4 anos atrás
pai
commit
75c836c70c
52 arquivos alterados com 2251 adições e 1481 exclusões
  1. 2
    0
      css/main.scss
  2. 1
    0
      css/modals/invite/_add-people.scss
  3. 0
    93
      css/modals/invite/_info.scss
  4. 252
    0
      css/modals/invite/_invite_more.scss
  5. 37
    0
      css/modals/security/_security.scss
  6. 2
    2
      interface_config.js
  7. 22
    2
      lang/main.json
  8. 11
    2
      react/features/base/dialog/components/web/StatelessDialog.js
  9. 3
    0
      react/features/base/icons/svg/arrow-down-small.svg
  10. 3
    0
      react/features/base/icons/svg/envelope.svg
  11. 3
    0
      react/features/base/icons/svg/google.svg
  12. 8
    1
      react/features/base/icons/svg/index.js
  13. 0
    5
      react/features/base/icons/svg/invite.svg
  14. 3
    0
      react/features/base/icons/svg/lock.svg
  15. 3
    0
      react/features/base/icons/svg/office365.svg
  16. 3
    0
      react/features/base/icons/svg/unlock.svg
  17. 3
    0
      react/features/base/icons/svg/user-plus.svg
  18. 3
    0
      react/features/base/icons/svg/yahoo.svg
  19. 6
    0
      react/features/base/toolbox/components/AbstractToolboxItem.js
  20. 2
    2
      react/features/base/toolbox/components/ToolboxItem.web.js
  21. 6
    3
      react/features/conference/components/web/Conference.js
  22. 94
    0
      react/features/conference/components/web/InviteMore.js
  23. 1
    1
      react/features/conference/components/web/Subject.js
  24. 1
    1
      react/features/invite/actions.any.js
  25. 150
    414
      react/features/invite/components/add-people-dialog/web/AddPeopleDialog.js
  26. 111
    0
      react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.js
  27. 39
    0
      react/features/invite/components/add-people-dialog/web/DialInNumber.js
  28. 76
    0
      react/features/invite/components/add-people-dialog/web/DialInSection.js
  29. 38
    0
      react/features/invite/components/add-people-dialog/web/Header.js
  30. 156
    0
      react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js
  31. 501
    0
      react/features/invite/components/add-people-dialog/web/InviteContactsForm.js
  32. 32
    0
      react/features/invite/components/add-people-dialog/web/InviteContactsSection.js
  33. 111
    0
      react/features/invite/components/add-people-dialog/web/LiveStreamSection.js
  34. 1
    0
      react/features/invite/components/add-people-dialog/web/index.js
  35. 23
    0
      react/features/invite/components/add-people-dialog/web/utils.js
  36. 0
    1
      react/features/invite/components/index.js
  37. 0
    0
      react/features/invite/components/info-dialog/index.native.js
  38. 0
    3
      react/features/invite/components/info-dialog/index.web.js
  39. 0
    644
      react/features/invite/components/info-dialog/web/InfoDialog.js
  40. 0
    268
      react/features/invite/components/info-dialog/web/InfoDialogButton.js
  41. 0
    4
      react/features/invite/components/info-dialog/web/index.js
  42. 52
    0
      react/features/invite/functions.js
  43. 15
    0
      react/features/security/actions.js
  44. 3
    0
      react/features/security/components/index.js
  45. 38
    0
      react/features/security/components/security-dialog/Header.js
  46. 2
    2
      react/features/security/components/security-dialog/PasswordForm.js
  47. 190
    0
      react/features/security/components/security-dialog/PasswordSection.js
  48. 126
    0
      react/features/security/components/security-dialog/SecurityDialog.js
  49. 83
    0
      react/features/security/components/security-dialog/SecurityDialogButton.js
  50. 4
    0
      react/features/security/components/security-dialog/index.js
  51. 4
    0
      react/features/security/index.js
  52. 27
    33
      react/features/toolbox/components/web/Toolbox.js

+ 2
- 0
css/main.scss Ver arquivo

@@ -94,5 +94,7 @@ $flagsImagePath: "../images/";
94 94
 @import 'prejoin';
95 95
 @import 'prejoin-dialog';
96 96
 @import 'country-picker';
97
+@import 'modals/invite/invite_more';
98
+@import 'modals/security/security';
97 99
 
98 100
 /* Modules END */

+ 1
- 0
css/modals/invite/_add-people.scss Ver arquivo

@@ -3,6 +3,7 @@
3 3
  */
4 4
 .modal-dialog-form {
5 5
     .add-people-form-wrap {
6
+        margin-top: 8px;
6 7
 
7 8
         .error {
8 9
             padding-left: 5px;

+ 0
- 93
css/modals/invite/_info.scss Ver arquivo

@@ -3,47 +3,6 @@
3 3
     display: flex;
4 4
     font-size: 14px;
5 5
 
6
-    .info-dialog-action-link {
7
-        display: inline-block;
8
-        line-height: 1.5em;
9
-
10
-        a {
11
-            cursor: pointer;
12
-            vertical-align: middle;
13
-        }
14
-    }
15
-
16
-    .info-dialog-action-link:before {
17
-        color: $linkFontColor;
18
-        content: '\2022';
19
-        font-size: 1.5em;
20
-        padding: 0 10px;
21
-        vertical-align: middle;
22
-    }
23
-
24
-    .info-dialog-action-link:first-child:before {
25
-        content: '';
26
-        padding: 0;
27
-    }
28
-
29
-    .info-dialog-action-links {
30
-        font-weight: bold;
31
-        margin-top: 10px;
32
-        white-space: nowrap;
33
-    }
34
-
35
-    .info-dialog-action-separator {
36
-        display: inline-block;
37
-    }
38
-
39
-    .info-dialog-copy-element {
40
-        opacity: 0;
41
-        pointer-events: none;
42
-        position: absolute;
43
-        -webkit-user-select: text;
44
-        user-select: text;
45
-    }
46
-
47 6
     .info-dialog-column {
48 7
         margin-right: 10px;
49 8
         overflow: hidden;
@@ -56,52 +15,6 @@
56 15
         }
57 16
     }
58 17
 
59
-    .info-dialog-conference-url,
60
-    .info-dialog-live-stream-url {
61
-        width: max-content;
62
-        width: -moz-max-content;
63
-        width: -webkit-max-content;
64
-        word-break: break-all;
65
-        max-width: 400px;
66
-        display: flex;
67
-        align-items: center;
68
-    }
69
-
70
-    .info-dialog-dial-in {
71
-        word-break: break-all;
72
-
73
-        .conference-id,
74
-        .phone-number {
75
-            user-select: text;
76
-        }
77
-    }
78
-
79
-    .info-dialog-icon {
80
-        color: #6453C0;
81
-        font-size: 16px;
82
-        min-width: 30px;
83
-    }
84
-
85
-    .info-dialog-url-text,
86
-    .info-dialog-url-text:hover {
87
-        color: inherit;
88
-        cursor: inherit;
89
-    }
90
-
91
-    .info-dialog-url-icon {
92
-        display: inline-block;
93
-        margin-left: 5px;
94
-
95
-        svg {
96
-            cursor: pointer;
97
-        }
98
-    }
99
-
100
-    .info-dialog-title {
101
-        font-weight: bold;
102
-        margin-bottom: 10px;
103
-    }
104
-
105 18
     .info-dialog-password,
106 19
     .info-password,
107 20
     .info-password-form {
@@ -223,10 +136,4 @@
223 136
         -moz-user-select: text;
224 137
         -webkit-user-select: text;
225 138
     }
226
-
227
-    .info-dialog-url-text-unselectable {
228
-        user-select: none;
229
-        -moz-user-select: none;
230
-        -webkit-user-select: none;
231
-    }
232 139
 }

+ 252
- 0
css/modals/invite/_invite_more.scss Ver arquivo

@@ -0,0 +1,252 @@
1
+.invite-more {
2
+    &-container {
3
+        color: #fff;
4
+        font-weight: 600;
5
+        position: absolute;
6
+        width: 100%;
7
+        text-align: center;
8
+        z-index: $zindex2;
9
+        background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
10
+
11
+        &.elevated {
12
+            z-index: $filmstripVideosZ + 1;
13
+        }
14
+    }
15
+
16
+    &-header {
17
+        font-size: 19px;
18
+        line-height: 28px;
19
+        margin: 24px 0 16px 0;
20
+    }
21
+
22
+    &-button {
23
+        display: flex;
24
+        justify-content: space-between;
25
+        align-items: center;
26
+        margin: auto;
27
+        padding: 8px 16px;
28
+        width: 152px;
29
+        height: 24px;
30
+        background: #0376DA;
31
+        border-radius: 3px;
32
+        font-size: 14px;
33
+        line-height: 24px;
34
+        cursor: pointer;
35
+
36
+        &:hover {
37
+            background: #278ADF;
38
+        }
39
+
40
+        &-text {
41
+            font-size: 15px;
42
+            line-height: 24px;
43
+        }
44
+    }
45
+    &-dialog {
46
+        color: #fff;
47
+        font-size: 15px;
48
+        line-height: 24px;
49
+
50
+        & > span {
51
+            font-weight: 600;
52
+        }
53
+
54
+        &.header {
55
+            display: flex;
56
+            justify-content: space-between;
57
+            margin: 16px 16px 24px;
58
+            width: calc(100% - 32px);
59
+            color: #fff;
60
+            font-weight: 600;
61
+            font-size: 24px;
62
+            line-height: 32px;
63
+
64
+            & > div > svg {
65
+                cursor: pointer;
66
+                fill: #A4B8D1;
67
+            }
68
+        }
69
+
70
+        &.copy-link {
71
+            display: flex;
72
+            justify-content: space-between;
73
+            align-items: center;
74
+            padding: 8px 8px 8px 16px;
75
+            margin-top: 8px;
76
+            width: calc(100% - 24px);
77
+            height: 24px;
78
+
79
+            background: #0376DA;
80
+            border-radius: 4px;
81
+            cursor: pointer;
82
+
83
+            &:hover {
84
+                background: #278ADF;
85
+                font-weight: 600;
86
+            }
87
+
88
+            &-text {
89
+                overflow: hidden;
90
+                text-overflow: ellipsis;
91
+                white-space: nowrap;
92
+                max-width: 292px;
93
+
94
+                &.selected {
95
+                    font-weight: 600;
96
+                }
97
+            }
98
+
99
+            &.clicked {
100
+                background: #31B76A;
101
+            }
102
+
103
+            & > div > svg > path {
104
+                fill: #fff;
105
+            }
106
+        }
107
+
108
+        &.separator {
109
+            margin: 24px 0 24px -20px;
110
+            padding: 0 20px;
111
+            width: 100%;
112
+            height: 1px;
113
+            background: #5E6D7A;
114
+        }
115
+
116
+        &.email-container {
117
+            display: flex;
118
+            justify-content: space-between;
119
+            align-items: center;
120
+            padding: 8px 8px 8px 16px;
121
+            margin-top: 24px;
122
+            width: calc(100% - 26px);
123
+            height: 22px;
124
+
125
+            background: #2A3A4B;
126
+            border: 1px solid #5E6D7A;
127
+            border-radius: 3px;
128
+            cursor: pointer;
129
+
130
+            &.active {
131
+                border-radius: 3px 3px 0 0;
132
+            }
133
+        }
134
+
135
+        &.icon-container {
136
+            display: none;
137
+
138
+            &.active {
139
+                display: flex;
140
+                width: calc(100% - 26px);
141
+                padding: 8px 8px 8px 16px;
142
+
143
+                background: #2A3A4B;
144
+                border: 1px solid #5E6D7A;
145
+                border-top: none;
146
+                border-radius: 0 0 3px 3px;
147
+
148
+                & > * {
149
+                    display: flex;
150
+                    justify-content: center;
151
+                    align-items: center;
152
+                    height: 40px;
153
+                    width: 40px;
154
+                    border-radius: 4px;
155
+                    cursor: pointer;
156
+                }
157
+    
158
+                &:hover > div:hover {
159
+                    background-color: rgba(255, 255, 255, 0.2);
160
+                }
161
+    
162
+                & > :not(:last-child) {
163
+                    margin-right: 16px;
164
+                }
165
+
166
+                .copy-invite-icon > div > svg > path {
167
+                    fill: #A4B8D1;
168
+                }
169
+            }
170
+        }
171
+
172
+        &.dial-in-display {
173
+            .info-label {
174
+                color: #A4B8D1;
175
+            }
176
+
177
+            .dial-in-copy {
178
+                display: inline-block;
179
+                vertical-align: middle;
180
+                margin-left: 21px;
181
+                cursor: pointer;
182
+            }
183
+        }
184
+
185
+        &.invite-buttons {
186
+            width: 100%;
187
+            text-align: right;
188
+            margin-top: 8px;
189
+
190
+            & > a {
191
+                display: inline-block;
192
+                height: 24px;
193
+                width: 48px;
194
+                border-radius: 3px;
195
+                text-align: center;
196
+                text-decoration: none;
197
+                cursor: pointer;
198
+            }
199
+
200
+            &-cancel {
201
+                margin-right: 16px;
202
+                padding: 7px 15px;
203
+                background: #2A3A4B;
204
+                border: 1px solid #5E6D7A;
205
+            }
206
+
207
+            &-add {
208
+                padding: 8px 16px;
209
+                background: #0376DA;
210
+            }
211
+        }
212
+
213
+        &.stream {
214
+            display: flex;
215
+            justify-content: space-between;
216
+            align-items: center;
217
+            padding: 8px 8px 8px 16px;
218
+            margin-top: 8px;
219
+            width: calc(100% - 26px);
220
+            height: 22px;
221
+
222
+            background: #2A3A4B;
223
+            border: 1px solid #5E6D7A;
224
+            border-radius: 3px;
225
+            cursor: pointer;
226
+
227
+            &:hover {
228
+                font-weight: 600;
229
+            }
230
+
231
+            &-text {
232
+                overflow: hidden;
233
+                text-overflow: ellipsis;
234
+                white-space: nowrap;
235
+                max-width: 292px;
236
+
237
+                &.selected {
238
+                    font-weight: 600;
239
+                }
240
+            }
241
+
242
+            &.clicked {
243
+                background: #31B76A;
244
+                border: 1px solid #31B76A;
245
+            }
246
+
247
+            & > div > svg > path {
248
+                fill: #fff;
249
+            }
250
+        }
251
+    }
252
+}

+ 37
- 0
css/modals/security/_security.scss Ver arquivo

@@ -0,0 +1,37 @@
1
+.security {
2
+    &-dialog {
3
+        color: #fff;
4
+        font-size: 15px;
5
+        line-height: 24px;
6
+        
7
+        &.password {
8
+            display: flex;
9
+            justify-content: space-between;
10
+            align-items: center;
11
+
12
+            &-actions {
13
+                a {
14
+                    cursor: pointer;
15
+                    text-decoration: none;
16
+                    font-size: 14px;
17
+                    color: #6FB1EA;
18
+                }
19
+
20
+                & > a + a {
21
+                    margin-left: 24px;
22
+                }
23
+            }
24
+        }
25
+    }
26
+}
27
+
28
+.new-toolbox .toolbox-content .toolbox-icon.security-toolbar-button,
29
+.new-toolbox .toolbox-content .toolbox-icon.toggled.security-toolbar-button {
30
+    background: rgba(241, 173, 51, 0.7);
31
+    border: 1px solid rgba(255, 255, 255, 0.4);
32
+
33
+    &:hover {
34
+        background: rgba(241, 173, 51, 0.7);
35
+        border: 1px solid rgba(255, 255, 255, 0.4);
36
+    }
37
+}

+ 2
- 2
interface_config.js Ver arquivo

@@ -48,11 +48,11 @@ var interfaceConfig = {
48 48
      */
49 49
     TOOLBAR_BUTTONS: [
50 50
         'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
51
-        'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
51
+        'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
52 52
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
53 53
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
54 54
         'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone',
55
-        'e2ee'
55
+        'e2ee', 'security'
56 56
     ],
57 57
 
58 58
     SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

+ 22
- 2
lang/main.json Ver arquivo

@@ -1,21 +1,36 @@
1 1
 {
2 2
     "addPeople": {
3 3
         "add": "Invite",
4
+        "addContacts": "Invite your contacts",
5
+        "copyInvite": "Copy meeting invitation",
6
+        "copyLink": "Copy meeting link",
7
+        "copyStream": "Copy live streaming link",
4 8
         "countryNotSupported": "We do not support this destination yet.",
5 9
         "countryReminder": "Calling outside the US? Please make sure you start with the country code!",
10
+        "defaultEmail": "Your Default Email",
6 11
         "disabled": "You can't invite people.",
7 12
         "failedToAdd": "Failed to add participants",
8 13
         "footerText": "Dialing out is disabled.",
14
+        "googleEmail": "Google Email",
15
+        "inviteMoreHeader": "You are the only one in the meeting",
16
+        "inviteMoreMailSubject": "Join {{appName}} meeting",
17
+        "inviteMorePrompt": "Invite more people",
18
+        "linkCopied": "Link copied to clipboard",
9 19
         "loading": "Searching for people and phone numbers",
10 20
         "loadingNumber": "Validating phone number",
11 21
         "loadingPeople": "Searching for people to invite",
12 22
         "noResults": "No matching search results",
13 23
         "noValidNumbers": "Please enter a phone number",
24
+        "outlookEmail": "Outlook Email",
14 25
         "searchNumbers": "Add phone numbers",
15 26
         "searchPeople": "Search for people",
16 27
         "searchPeopleAndNumbers": "Search for people or add their phone numbers",
28
+        "shareInvite": "Share meeting invitation",
29
+        "shareLink": "Share the meeting link to invite others",
30
+        "shareStream": "Share the live streaming link",
17 31
         "telephone": "Telephone: {{number}}",
18
-        "title": "Invite people to this meeting"
32
+        "title": "Invite people to this meeting",
33
+        "yahooEmail": "Yahoo Email"
19 34
     },
20 35
     "audioDevices": {
21 36
         "bluetooth": "Bluetooth",
@@ -146,6 +161,7 @@
146 161
         "accessibilityLabel": {
147 162
             "liveStreaming": "Live Stream"
148 163
         },
164
+        "add": "Add",
149 165
         "allow": "Allow",
150 166
         "alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
151 167
         "alreadySharedVideoTitle": "Only one shared video is allowed at a time",
@@ -562,7 +578,9 @@
562 578
         "pullToRefresh": "Pull to refresh"
563 579
     },
564 580
     "security": {
565
-        "insecureRoomNameWarning": "The room name is insecure. Unwanted participants may join your conference."
581
+        "about": "You can add a passcode to your meeting. Participants will need to provide the passcode before they are allowed to join the meeting.",
582
+        "insecureRoomNameWarning": "The room name is insecure. Unwanted participants may join your conference.",
583
+        "securityOptions": "Security options"
566 584
     },
567 585
     "settings": {
568 586
         "calendar": {
@@ -662,6 +680,7 @@
662 680
             "raiseHand": "Toggle raise hand",
663 681
             "recording": "Toggle recording",
664 682
             "remoteMute": "Mute participant",
683
+            "security": "Security options",
665 684
             "Settings": "Toggle settings",
666 685
             "sharedvideo": "Toggle Youtube video sharing",
667 686
             "shareRoom": "Invite someone",
@@ -715,6 +734,7 @@
715 734
         "profile": "Edit your profile",
716 735
         "raiseHand": "Raise / Lower your hand",
717 736
         "raiseYourHand": "Raise your hand",
737
+        "security": "Security options",
718 738
         "Settings": "Settings",
719 739
         "sharedvideo": "Share a YouTube video",
720 740
         "shareRoom": "Invite someone",

+ 11
- 2
react/features/base/dialog/components/web/StatelessDialog.js Ver arquivo

@@ -29,7 +29,10 @@ const OK_BUTTON_ID = 'modal-dialog-ok-button';
29 29
 type Props = {
30 30
     ...DialogProps,
31 31
 
32
-    i18n: Object,
32
+    /**
33
+     * Custom dialog header that replaces the standard heading.
34
+     */
35
+    customHeader?: React$Element<any> | Function,
33 36
 
34 37
     /**
35 38
      * Disables dismissing the dialog when the blanket is clicked. Enabled
@@ -43,6 +46,8 @@ type Props = {
43 46
      */
44 47
     hideCancelButton: boolean,
45 48
 
49
+    i18n: Object,
50
+
46 51
     /**
47 52
      * Whether the dialog is modal. This means clicking on the blanket will
48 53
      * leave the dialog open. No cancel button.
@@ -106,6 +111,7 @@ class StatelessDialog extends Component<Props> {
106 111
      */
107 112
     render() {
108 113
         const {
114
+            customHeader,
109 115
             children,
110 116
             t /* The following fixes a flow error: */ = _.identity,
111 117
             titleString,
@@ -116,8 +122,11 @@ class StatelessDialog extends Component<Props> {
116 122
         return (
117 123
             <Modal
118 124
                 autoFocus = { true }
125
+                components = {{
126
+                    Header: customHeader
127
+                }}
119 128
                 footer = { this._renderFooter }
120
-                heading = { titleString || t(titleKey) }
129
+                heading = { customHeader ? undefined : titleString || t(titleKey) }
121 130
                 i18n = { this.props.i18n }
122 131
                 onClose = { this._onDialogDismissed }
123 132
                 onDialogDismissed = { this._onDialogDismissed }

+ 3
- 0
react/features/base/icons/svg/arrow-down-small.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.0933 8.33104C16.4628 7.92053 17.0951 7.88726 17.5056 8.25672C17.9161 8.62617 17.9494 9.25846 17.5799 9.66897L12.8749 14.9247C12.4777 15.3661 11.7856 15.3661 11.3883 14.9247L6.75666 9.66897C6.3872 9.25846 6.42048 8.62617 6.83099 8.25672C7.2415 7.88726 7.87379 7.92053 8.24325 8.33104L12.1316 12.7609L16.0933 8.33104Z" fill="white"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/envelope.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4H20C21.1046 4 22 4.89543 22 6V18C22 19.1046 21.1046 20 20 20H4C2.89543 20 2 19.1046 2 18V6C2 4.89543 2.89543 4 4 4ZM4 8V18H20V8L12 12L4 8ZM20 6H4L12 10L20 6Z" fill="#A4B8D1"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/google.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M20.2359 10.3544H19.56V10.32H12V13.68H16.7479C16.054 15.6356 14.1935 17.04 12 17.04C9.21583 17.04 6.95998 14.7841 6.95998 12C6.95998 9.21583 9.21583 6.95998 12 6.95998C13.2846 6.95998 14.4543 7.44396 15.3436 8.23638L17.7192 5.86076C16.2197 4.46294 14.2132 3.59998 12 3.59998C7.36029 3.59998 3.59998 7.36029 3.59998 12C3.59998 16.6397 7.36029 20.4 12 20.4C16.6397 20.4 20.4 16.6397 20.4 12C20.4 11.4372 20.3426 10.8876 20.2359 10.3544Z" fill="#A4B8D1"/>
3
+</svg>

+ 8
- 1
react/features/base/icons/svg/index.js Ver arquivo

@@ -4,6 +4,7 @@ export { default as IconAdd } from './add.svg';
4 4
 export { default as IconAddPeople } from './link.svg';
5 5
 export { default as IconArrowBack } from './arrow_back.svg';
6 6
 export { default as IconArrowDown } from './arrow_down.svg';
7
+export { default as IconArrowDownSmall } from './arrow-down-small.svg';
7 8
 export { default as IconArrowLeft } from './arrow-left.svg';
8 9
 export { default as IconAudioOnly } from './visibility.svg';
9 10
 export { default as IconAudioOnlyOff } from './visibility-off.svg';
@@ -30,18 +31,21 @@ export { default as IconDominantSpeaker } from './dominant-speaker.svg';
30 31
 export { default as IconDownload } from './download.svg';
31 32
 export { default as IconDragHandle } from './drag-handle.svg';
32 33
 export { default as IconE2EE } from './e2ee.svg';
34
+export { default as IconEmail } from './envelope.svg';
33 35
 export { default as IconEventNote } from './event_note.svg';
34 36
 export { default as IconExclamation } from './exclamation.svg';
35 37
 export { default as IconExclamationSolid } from './exclamation-solid.svg';
36 38
 export { default as IconExitFullScreen } from './exit-full-screen.svg';
37 39
 export { default as IconFeedback } from './feedback.svg';
38 40
 export { default as IconFullScreen } from './full-screen.svg';
41
+export { default as IconGoogle } from './google.svg';
39 42
 export { default as IconHangup } from './hangup.svg';
40 43
 export { default as IconHelp } from './help.svg';
41 44
 export { default as IconInfo } from './info.svg';
42
-export { default as IconInvite } from './invite.svg';
45
+export { default as IconInviteMore } from './user-plus.svg';
43 46
 export { default as IconKick } from './kick.svg';
44 47
 export { default as IconLiveStreaming } from './public.svg';
48
+export { default as IconLockPassword } from './lock.svg';
45 49
 export { default as IconMenu } from './menu.svg';
46 50
 export { default as IconMenuDown } from './menu-down.svg';
47 51
 export { default as IconMenuThumb } from './thumb-menu.svg';
@@ -56,6 +60,7 @@ export { default as IconMuteEveryone } from './mute-everyone.svg';
56 60
 export { default as IconMuteEveryoneElse } from './mute-everyone-else.svg';
57 61
 export { default as IconNotificationJoin } from './navigate_next.svg';
58 62
 export { default as IconOpenInNew } from './open_in_new.svg';
63
+export { default as IconOutlook } from './office365.svg';
59 64
 export { default as IconPhone } from './phone.svg';
60 65
 export { default as IconPin } from './enlarge.svg';
61 66
 export { default as IconPresentation } from './presentation.svg';
@@ -79,6 +84,7 @@ export { default as IconShareVideo } from './shared-video.svg';
79 84
 export { default as IconSwitchCamera } from './switch-camera.svg';
80 85
 export { default as IconTileView } from './tiles-many.svg';
81 86
 export { default as IconToggleRecording } from './camera-take-picture.svg';
87
+export { default as IconUnlockPassword } from './unlock.svg';
82 88
 export { default as IconVideoQualityAudioOnly } from './AUD.svg';
83 89
 export { default as IconVideoQualityHD } from './HD.svg';
84 90
 export { default as IconVideoQualityLD } from './LD.svg';
@@ -87,3 +93,4 @@ export { default as IconVolume } from './volume.svg';
87 93
 export { default as IconVolumeEmpty } from './volume-empty.svg';
88 94
 export { default as IconVolumeOff } from './volume-off.svg';
89 95
 export { default as IconWarning } from './warning.svg';
96
+export { default as IconYahoo } from './yahoo.svg';

+ 0
- 5
react/features/base/icons/svg/invite.svg Ver arquivo

@@ -1,5 +0,0 @@
1
-<!-- Generated by IcoMoon.io -->
2
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
3
-<title>invite</title>
4
-<path d="M18.984 12.984h-6v6h-1.969v-6h-6v-1.969h6v-6h1.969v6h6v1.969z"></path>
5
-</svg>

+ 3
- 0
react/features/base/icons/svg/lock.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10H7H17H19V20H5V10ZM19 8H17V7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7V8H5C3.89543 8 3 8.89543 3 10V20C3 21.1046 3.89543 22 5 22H19C20.1046 22 21 21.1046 21 20V10C21 8.89543 20.1046 8 19 8ZM12.9686 15.7502C13.5837 15.4091 14 14.7532 14 14C14 12.8954 13.1046 12 12 12C10.8954 12 10 12.8954 10 14C10 14.7532 10.4163 15.4091 11.0314 15.7502C11.0109 15.8301 11 15.9138 11 16V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V16C13 15.9138 12.9891 15.8301 12.9686 15.7502ZM12 4C13.6569 4 15 5.34315 15 7V8H9V7C9 5.34315 10.3431 4 12 4Z" fill="white"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/office365.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M3 6L14.0138 2L20.0213 3.5V20.5L14.0138 22L3 18L14.0138 19.5V5L7.00501 6.5V16.5L3 18V6Z" fill="#A4B8D1"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/unlock.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M7 8V7C7 4.23858 9.23858 2 12 2C14.0608 2 15.8304 3.24676 16.5957 5.02716L14.7583 5.81698C14.2882 4.72339 13.2108 4 12 4C10.3431 4 9 5.34315 9 7V8H12H16.8374H16.8818H19C20.1046 8 21 8.89543 21 10V20C21 21.1046 20.1046 22 19 22H5C3.89543 22 3 21.1046 3 20V10C3 8.89543 3.89543 8 5 8H7ZM5 20V10H19V20H5ZM12.9686 15.7502C13.5837 15.4091 14 14.7532 14 14C14 12.8954 13.1046 12 12 12C10.8954 12 10 12.8954 10 14C10 14.7532 10.4163 15.4091 11.0314 15.7502C11.0109 15.8301 11 15.9138 11 16V17C11 17.5523 11.4477 18 12 18C12.5523 18 13 17.5523 13 17V16C13 15.9138 12.9891 15.8301 12.9686 15.7502Z" fill="white"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/user-plus.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7C14 8.10457 13.1046 9 12 9C10.8954 9 10 8.10457 10 7C10 5.89543 10.8954 5 12 5C13.1046 5 14 5.89543 14 7ZM8 7C8 9.20914 9.79086 11 12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7ZM12 12C5.98633 12 4 14.25 4 18.75C4 20.25 4.66667 21 6 21H14.7578C15.565 22.206 16.9398 23 18.5 23C20.9853 23 23 20.9853 23 18.5C23 16.0381 21.0231 14.038 18.5701 14.0005C17.3541 12.6668 15.2739 12 12 12ZM16.1639 14.6531C15.2365 14.1909 13.8943 14 12 14C7.30232 14 6 15.1737 6 18.75C6 18.8592 6.00376 18.9414 6.00693 19H14.0275C14.0093 18.8358 14 18.669 14 18.5C14 16.8702 14.8665 15.4427 16.1639 14.6531ZM19 18H21V19H19V21H18V19H16V18H18V16H19V18Z" fill="white"/>
3
+</svg>

+ 3
- 0
react/features/base/icons/svg/yahoo.svg Ver arquivo

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path d="M4 3H9.10397L12.076 10.6035L15.0865 3H20.0561L12.573 21H7.57197L9.62033 16.2303L4 3Z" fill="#A4B8D1"/>
3
+</svg>

+ 6
- 0
react/features/base/toolbox/components/AbstractToolboxItem.js Ver arquivo

@@ -35,6 +35,12 @@ export type Props = {
35 35
      */
36 36
     accessibilityLabel: string,
37 37
 
38
+    /**
39
+     * An extra class name to be added at the end of the element's class name
40
+     * in order to enable custom styling.
41
+     */
42
+    customClass?: string,
43
+
38 44
     /**
39 45
      * Whether this item is disabled or not. When disabled, clicking an the item
40 46
      * has no effect, and it may reflect on its style.

+ 2
- 2
react/features/base/toolbox/components/ToolboxItem.web.js Ver arquivo

@@ -67,11 +67,11 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
67 67
      * @returns {ReactElement}
68 68
      */
69 69
     _renderIcon() {
70
-        const { disabled, icon, showLabel, toggled } = this.props;
70
+        const { customClass, disabled, icon, showLabel, toggled } = this.props;
71 71
         const iconComponent = <Icon src = { icon } />;
72 72
         const elementType = showLabel ? 'span' : 'div';
73 73
         const className = `${showLabel ? 'overflow-menu-item-icon' : 'toolbox-icon'} ${
74
-            toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''}`;
74
+            toggled ? 'toggled' : ''} ${disabled ? 'disabled' : ''} ${customClass ?? ''}`;
75 75
 
76 76
         return React.createElement(elementType, { className }, iconComponent);
77 77
     }

+ 6
- 3
react/features/conference/components/web/Conference.js Ver arquivo

@@ -25,15 +25,17 @@ import {
25 25
 
26 26
 import { maybeShowSuboptimalExperienceNotification } from '../../functions';
27 27
 
28
-import Labels from './Labels';
29
-import { default as Notice } from './Notice';
30
-import { default as Subject } from './Subject';
31 28
 import {
32 29
     AbstractConference,
33 30
     abstractMapStateToProps
34 31
 } from '../AbstractConference';
35 32
 import type { AbstractProps } from '../AbstractConference';
36 33
 
34
+import InviteMore from './InviteMore';
35
+import Labels from './Labels';
36
+import { default as Notice } from './Notice';
37
+import { default as Subject } from './Subject';
38
+
37 39
 declare var APP: Object;
38 40
 declare var config: Object;
39 41
 declare var interfaceConfig: Object;
@@ -202,6 +204,7 @@ class Conference extends AbstractConference<Props, *> {
202 204
 
203 205
                 <Notice />
204 206
                 <Subject />
207
+                <InviteMore />
205 208
                 <div id = 'videospace'>
206 209
                     <LargeVideo />
207 210
                     { hideVideoQualityLabel

+ 94
- 0
react/features/conference/components/web/InviteMore.js Ver arquivo

@@ -0,0 +1,94 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { Icon, IconInviteMore } from '../../../base/icons';
7
+import { getParticipantCount } from '../../../base/participants';
8
+import { connect } from '../../../base/redux';
9
+import { beginAddPeople } from '../../../invite';
10
+import { isToolboxVisible } from '../../../toolbox';
11
+
12
+type Props = {
13
+
14
+    /**
15
+     * Whether tile view is enabled.
16
+     */
17
+    _tileViewEnabled: Boolean,
18
+
19
+    /**
20
+     * Whether to show the option to invite more people
21
+     * instead of the subject.
22
+     */
23
+    _visible: boolean,
24
+
25
+    /**
26
+     * Handler to open the invite dialog.
27
+     */
28
+    onClick: Function,
29
+
30
+    /**
31
+     * Invoked to obtain translated strings.
32
+     */
33
+    t: Function
34
+}
35
+
36
+/**
37
+ * Represents a replacement for the subject, prompting the
38
+ * sole participant to invite more participants.
39
+ *
40
+ * @param {Object} props - The props of the component.
41
+ * @returns {React$Element<any>}
42
+ */
43
+function InviteMore({
44
+    _tileViewEnabled,
45
+    _visible,
46
+    onClick,
47
+    t
48
+}: Props) {
49
+    return (
50
+        _visible
51
+            ? <div className = { `invite-more-container${_tileViewEnabled ? ' elevated' : ''}` }>
52
+                <div className = 'invite-more-header'>
53
+                    {t('addPeople.inviteMoreHeader')}
54
+                </div>
55
+                <div
56
+                    className = 'invite-more-button'
57
+                    onClick = { onClick }>
58
+                    <Icon src = { IconInviteMore } />
59
+                    <div className = 'invite-more-text'>
60
+                        {t('addPeople.inviteMorePrompt')}
61
+                    </div>
62
+                </div>
63
+            </div> : null
64
+    );
65
+}
66
+
67
+/**
68
+ * Maps (parts of) the Redux state to the associated
69
+ * {@code Subject}'s props.
70
+ *
71
+ * @param {Object} state - The Redux state.
72
+ * @private
73
+ * @returns {Props}
74
+ */
75
+function mapStateToProps(state) {
76
+    const participantCount = getParticipantCount(state);
77
+
78
+    return {
79
+        _tileViewEnabled: state['features/video-layout'].tileViewEnabled,
80
+        _visible: isToolboxVisible(state) && participantCount === 1
81
+    };
82
+}
83
+
84
+/**
85
+ * Maps dispatching of some action to React component props.
86
+ *
87
+ * @param {Function} dispatch - Redux action dispatcher.
88
+ * @returns {Props}
89
+ */
90
+const mapDispatchToProps = {
91
+    onClick: () => beginAddPeople()
92
+};
93
+
94
+export default translate(connect(mapStateToProps, mapDispatchToProps)(InviteMore));

+ 1
- 1
react/features/conference/components/web/Subject.js Ver arquivo

@@ -75,7 +75,7 @@ function _mapStateToProps(state) {
75 75
     return {
76 76
         _showParticipantCount: participantCount > 2,
77 77
         _subject: getConferenceName(state),
78
-        _visible: isToolboxVisible(state)
78
+        _visible: isToolboxVisible(state) && participantCount > 1
79 79
     };
80 80
 }
81 81
 

+ 1
- 1
react/features/invite/actions.any.js Ver arquivo

@@ -3,8 +3,8 @@
3 3
 import type { Dispatch } from 'redux';
4 4
 
5 5
 import { getInviteURL } from '../base/connection';
6
-import { inviteVideoRooms } from '../videosipgw';
7 6
 import { getParticipants } from '../base/participants';
7
+import { inviteVideoRooms } from '../videosipgw';
8 8
 
9 9
 import {
10 10
     ADD_PENDING_INVITE_REQUEST,

+ 150
- 414
react/features/invite/components/add-people-dialog/web/AddPeopleDialog.js Ver arquivo

@@ -1,478 +1,214 @@
1 1
 // @flow
2 2
 
3
-import InlineMessage from '@atlaskit/inline-message';
4
-import React from 'react';
5
-import type { Dispatch } from 'redux';
3
+import React, { useState, useEffect } from 'react';
6 4
 
7 5
 import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
8
-import { Avatar } from '../../../../base/avatar';
9
-import { Dialog, hideDialog } from '../../../../base/dialog';
10
-import { translate, translateToHTML } from '../../../../base/i18n';
11
-import { Icon, IconPhone } from '../../../../base/icons';
6
+import { getRoomName } from '../../../../base/conference';
7
+import { getInviteURL } from '../../../../base/connection';
8
+import { Dialog } from '../../../../base/dialog';
9
+import { translate } from '../../../../base/i18n';
10
+import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
12 11
 import { getLocalParticipant } from '../../../../base/participants';
13
-import { MultiSelectAutocomplete } from '../../../../base/react';
14 12
 import { connect } from '../../../../base/redux';
13
+import { getActiveSession } from '../../../../recording';
15 14
 
16
-import AbstractAddPeopleDialog, {
17
-    type Props as AbstractProps,
18
-    type State,
19
-    _mapStateToProps as _abstractMapStateToProps
20
-} from '../AbstractAddPeopleDialog';
15
+import { updateDialInNumbers } from '../../../actions';
16
+import { _getDefaultPhoneNumber, getInviteText, isAddPeopleEnabled, isDialOutEnabled } from '../../../functions';
17
+
18
+import CopyMeetingLinkSection from './CopyMeetingLinkSection';
19
+import DialInSection from './DialInSection';
20
+import Header from './Header';
21
+import InviteByEmailSection from './InviteByEmailSection';
22
+import InviteContactsSection from './InviteContactsSection';
23
+import LiveStreamSection from './LiveStreamSection';
21 24
 
22 25
 declare var interfaceConfig: Object;
23 26
 
24
-/**
25
- * The type of the React {@code Component} props of {@link AddPeopleDialog}.
26
- */
27
-type Props = AbstractProps & {
27
+type Props = {
28 28
 
29 29
     /**
30
-     * The {@link JitsiMeetConference} which will be used to invite "room"
31
-     * participants through the SIP Jibri (Video SIP gateway).
30
+     * The name of the current conference. Used as part of inviting users.
32 31
      */
33
-    _conference: Object,
32
+    _conferenceName: string,
34 33
 
35 34
     /**
36
-     * Whether to show a footer text after the search results as a last element.
35
+     * The object representing the dialIn feature.
37 36
      */
38
-    _footerTextEnabled: boolean,
37
+    _dialIn: Object,
39 38
 
40 39
     /**
41
-     * The redux {@code dispatch} function.
40
+     * Whether or not invite should be hidden.
42 41
      */
43
-    dispatch: Dispatch<any>,
42
+    _hideInviteContacts: boolean,
44 43
 
45 44
     /**
46
-     * Invoked to obtain translated strings.
45
+     * The current url of the conference to be copied onto the clipboard.
47 46
      */
48
-    t: Function,
49
-};
50
-
51
-/**
52
- * The dialog that allows to invite people to the call.
53
- */
54
-class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
55
-    _multiselect = null;
56
-
57
-    _resourceClient: Object;
58
-
59
-    state = {
60
-        addToCallError: false,
61
-        addToCallInProgress: false,
62
-        inviteItems: []
63
-    };
47
+    _inviteUrl: string,
64 48
 
65 49
     /**
66
-     * Initializes a new {@code AddPeopleDialog} instance.
67
-     *
68
-     * @param {Object} props - The read-only properties with which the new
69
-     * instance is to be initialized.
50
+     * The current known URL for a live stream in progress.
70 51
      */
71
-    constructor(props: Props) {
72
-        super(props);
73
-
74
-        // Bind event handlers so they are only bound once per instance.
75
-        this._onItemSelected = this._onItemSelected.bind(this);
76
-        this._onSelectionChange = this._onSelectionChange.bind(this);
77
-        this._onSubmit = this._onSubmit.bind(this);
78
-        this._parseQueryResults = this._parseQueryResults.bind(this);
79
-        this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
80
-
81
-        this._resourceClient = {
82
-            makeQuery: this._query,
83
-            parseResults: this._parseQueryResults
84
-        };
85
-    }
52
+    _liveStreamViewURL: string,
86 53
 
87 54
     /**
88
-     * Sends an analytics event to record the dialog has been shown.
89
-     *
90
-     * @inheritdoc
91
-     * @returns {void}
55
+     * The redux representation of the local participant.
92 56
      */
93
-    componentDidMount() {
94
-        sendAnalytics(createInviteDialogEvent(
95
-            'invite.dialog.opened', 'dialog'));
96
-    }
57
+    _localParticipantName: ?string,
97 58
 
98 59
     /**
99
-     * React Component method that executes once component is updated.
100
-     *
101
-     * @param {Object} prevProps - The state object before the update.
102
-     * @param {Object} prevState - The state object before the update.
103
-     * @returns {void}
60
+     * The current location url of the conference.
104 61
      */
105
-    componentDidUpdate(prevProps, prevState) {
106
-        /**
107
-         * Clears selected items from the multi select component on successful
108
-         * invite.
109
-         */
110
-        if (prevState.addToCallError
111
-                && !this.state.addToCallInProgress
112
-                && !this.state.addToCallError
113
-                && this._multiselect) {
114
-            this._multiselect.setSelectedItems([]);
115
-        }
116
-    }
62
+    _locationUrl: Object,
117 63
 
118 64
     /**
119
-     * Sends an analytics event to record the dialog has been closed.
120
-     *
121
-     * @inheritdoc
122
-     * @returns {void}
65
+     * Invoked to obtain translated strings.
123 66
      */
124
-    componentWillUnmount() {
125
-        sendAnalytics(createInviteDialogEvent(
126
-            'invite.dialog.closed', 'dialog'));
127
-    }
67
+    t: Function,
128 68
 
129 69
     /**
130
-     * Renders the content of this component.
131
-     *
132
-     * @returns {ReactElement}
70
+     * Method to update the dial in numbers.
133 71
      */
134
-    render() {
135
-        const {
136
-            _addPeopleEnabled,
137
-            _dialOutEnabled,
138
-            _footerTextEnabled,
139
-            t
140
-        } = this.props;
141
-        let isMultiSelectDisabled = this.state.addToCallInProgress || false;
142
-        let placeholder;
143
-        let loadingMessage;
144
-        let noMatches;
145
-        let footerText;
146
-
147
-        if (_addPeopleEnabled && _dialOutEnabled) {
148
-            loadingMessage = 'addPeople.loading';
149
-            noMatches = 'addPeople.noResults';
150
-            placeholder = 'addPeople.searchPeopleAndNumbers';
151
-        } else if (_addPeopleEnabled) {
152
-            loadingMessage = 'addPeople.loadingPeople';
153
-            noMatches = 'addPeople.noResults';
154
-            placeholder = 'addPeople.searchPeople';
155
-        } else if (_dialOutEnabled) {
156
-            loadingMessage = 'addPeople.loadingNumber';
157
-            noMatches = 'addPeople.noValidNumbers';
158
-            placeholder = 'addPeople.searchNumbers';
159
-        } else {
160
-            isMultiSelectDisabled = true;
161
-            noMatches = 'addPeople.noResults';
162
-            placeholder = 'addPeople.disabled';
163
-        }
164
-
165
-        if (_footerTextEnabled) {
166
-            footerText = {
167
-                content: <div className = 'footer-text-wrap'>
168
-                    <div>
169
-                        <span className = 'footer-telephone-icon'>
170
-                            <Icon src = { IconPhone } />
171
-                        </span>
172
-                    </div>
173
-                    { translateToHTML(t, 'addPeople.footerText') }
174
-                </div>
175
-            };
176
-        }
177
-
178
-        return (
179
-            <Dialog
180
-                okDisabled = { this._isAddDisabled() }
181
-                okKey = 'addPeople.add'
182
-                onSubmit = { this._onSubmit }
183
-                titleKey = 'addPeople.title'
184
-                width = 'medium'>
185
-                <div className = 'add-people-form-wrap'>
186
-                    { this._renderErrorMessage() }
187
-                    <MultiSelectAutocomplete
188
-                        footer = { footerText }
189
-                        isDisabled = { isMultiSelectDisabled }
190
-                        loadingMessage = { t(loadingMessage) }
191
-                        noMatchesFound = { t(noMatches) }
192
-                        onItemSelected = { this._onItemSelected }
193
-                        onSelectionChange = { this._onSelectionChange }
194
-                        placeholder = { t(placeholder) }
195
-                        ref = { this._setMultiSelectElement }
196
-                        resourceClient = { this._resourceClient }
197
-                        shouldFitContainer = { true }
198
-                        shouldFocus = { true } />
199
-                </div>
200
-            </Dialog>
201
-        );
202
-    }
203
-
204
-    _invite: Array<Object> => Promise<*>
205
-
206
-    _isAddDisabled: () => boolean;
207
-
208
-    _onItemSelected: (Object) => Object;
72
+    updateNumbers: Function
73
+};
209 74
 
210
-    /**
211
-     * Callback invoked when a selection has been made but before it has been
212
-     * set as selected.
213
-     *
214
-     * @param {Object} item - The item that has just been selected.
215
-     * @private
216
-     * @returns {Object} The item to display as selected in the input.
217
-     */
218
-    _onItemSelected(item) {
219
-        if (item.item.type === 'phone') {
220
-            item.content = item.item.number;
75
+/**
76
+ * Invite More component.
77
+ *
78
+ * @returns {React$Element<any>}
79
+ */
80
+function AddPeopleDialog({
81
+    _conferenceName,
82
+    _dialIn,
83
+    _hideInviteContacts,
84
+    _inviteUrl,
85
+    _liveStreamViewURL,
86
+    _localParticipantName,
87
+    _locationUrl,
88
+    t,
89
+    updateNumbers }: Props) {
90
+    const [ phoneNumber, setPhoneNumber ] = useState(undefined);
91
+
92
+    /**
93
+     * Updates the dial-in numbers.
94
+     */
95
+    useEffect(() => {
96
+        if (!_dialIn.numbers) {
97
+            updateNumbers();
221 98
         }
222
-
223
-        return item;
224
-    }
225
-
226
-    _onSelectionChange: (Map<*, *>) => void;
99
+    }, []);
227 100
 
228 101
     /**
229
-     * Handles a selection change.
102
+     * Sends analytics events when the dialog opens/closes.
230 103
      *
231
-     * @param {Map} selectedItems - The list of selected items.
232
-     * @private
233 104
      * @returns {void}
234 105
      */
235
-    _onSelectionChange(selectedItems) {
236
-        this.setState({
237
-            inviteItems: selectedItems
238
-        });
239
-    }
106
+    useEffect(() => {
107
+        sendAnalytics(createInviteDialogEvent(
108
+            'invite.dialog.opened', 'dialog'));
240 109
 
241
-    _onSubmit: () => void;
110
+        return () => {
111
+            sendAnalytics(createInviteDialogEvent(
112
+                'invite.dialog.closed', 'dialog'));
113
+        };
114
+    }, []);
242 115
 
243 116
     /**
244
-     * Submits the selection for inviting.
117
+     * Updates the phone number in the state once the dial-in numbers are fetched.
245 118
      *
246
-     * @private
247 119
      * @returns {void}
248 120
      */
249
-    _onSubmit() {
250
-        const { inviteItems } = this.state;
251
-        const invitees = inviteItems.map(({ item }) => item);
252
-
253
-        this._invite(invitees)
254
-            .then(invitesLeftToSend => {
255
-                if (invitesLeftToSend.length) {
256
-                    const unsentInviteIDs
257
-                        = invitesLeftToSend.map(invitee =>
258
-                            invitee.id || invitee.user_id || invitee.number);
259
-                    const itemsToSelect
260
-                        = inviteItems.filter(({ item }) =>
261
-                            unsentInviteIDs.includes(item.id || item.user_id || item.number));
262
-
263
-                    if (this._multiselect) {
264
-                        this._multiselect.setSelectedItems(itemsToSelect);
265
-                    }
266
-                } else {
267
-                    this.props.dispatch(hideDialog());
268
-                }
269
-            });
270
-    }
271
-
272
-    _parseQueryResults: (?Array<Object>) => Array<Object>;
273
-
274
-    /**
275
-     * Returns the avatar component for a user.
276
-     *
277
-     * @param {Object} user - The user.
278
-     * @param {string} className - The CSS class for the avatar component.
279
-     * @private
280
-     * @returns {ReactElement}
281
-     */
282
-    _getAvatar(user, className = 'avatar-small') {
283
-        return (<Avatar
284
-            className = { className }
285
-            status = { user.status }
286
-            url = { user.avatar } />);
287
-    }
288
-
289
-    /**
290
-     * Processes results from requesting available numbers and people by munging
291
-     * each result into a format {@code MultiSelectAutocomplete} can use for
292
-     * display.
293
-     *
294
-     * @param {Array} response - The response object from the server for the
295
-     * query.
296
-     * @private
297
-     * @returns {Object[]} Configuration objects for items to display in the
298
-     * search autocomplete.
299
-     */
300
-    _parseQueryResults(response = []) {
301
-        const { t, _dialOutEnabled } = this.props;
302
-        const users = response.filter(item => item.type !== 'phone');
303
-        const userDisplayItems = [];
304
-
305
-        users.forEach(user => {
306
-            const { name, phone } = user;
307
-            const tagAvatar = this._getAvatar(user, 'avatar-xsmall');
308
-            const elemAvatar = this._getAvatar(user);
309
-
310
-            userDisplayItems.push({
311
-                content: name,
312
-                elemBefore: elemAvatar,
313
-                item: user,
314
-                tag: {
315
-                    elemBefore: tagAvatar
316
-                },
317
-                value: user.id || user.user_id
318
-            });
319
-
320
-            if (phone && _dialOutEnabled) {
321
-                userDisplayItems.push({
322
-                    filterValues: [ name, phone ],
323
-                    content: `${phone} (${name})`,
324
-                    elemBefore: elemAvatar,
325
-                    item: {
326
-                        type: 'phone',
327
-                        number: phone
328
-                    },
329
-                    tag: {
330
-                        elemBefore: tagAvatar
331
-                    },
332
-                    value: phone
333
-                });
334
-            }
335
-        });
336
-
337
-        const numbers = response.filter(item => item.type === 'phone');
338
-        const telephoneIcon = this._renderTelephoneIcon();
339
-
340
-        const numberDisplayItems = numbers.map(number => {
341
-            const numberNotAllowedMessage
342
-                = number.allowed ? '' : t('addPeople.countryNotSupported');
343
-            const countryCodeReminder = number.showCountryCodeReminder
344
-                ? t('addPeople.countryReminder') : '';
345
-            const description
346
-                = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
347
-
348
-            return {
349
-                filterValues: [
350
-                    number.originalEntry,
351
-                    number.number
352
-                ],
353
-                content: t('addPeople.telephone', { number: number.number }),
354
-                description,
355
-                isDisabled: !number.allowed,
356
-                elemBefore: telephoneIcon,
357
-                item: number,
358
-                tag: {
359
-                    elemBefore: telephoneIcon
360
-                },
361
-                value: number.number
362
-            };
363
-        });
364
-
365
-        return [
366
-            ...userDisplayItems,
367
-            ...numberDisplayItems
368
-        ];
369
-    }
370
-
371
-    _query: (string) => Promise<Array<Object>>;
372
-
373
-    /**
374
-     * Renders the error message if the add doesn't succeed.
375
-     *
376
-     * @private
377
-     * @returns {ReactElement|null}
378
-     */
379
-    _renderErrorMessage() {
380
-        if (!this.state.addToCallError) {
381
-            return null;
121
+    useEffect(() => {
122
+        if (!phoneNumber && _dialIn && _dialIn.numbers) {
123
+            setPhoneNumber(_getDefaultPhoneNumber(_dialIn.numbers));
382 124
         }
383
-
384
-        const { t } = this.props;
385
-        const supportString = t('inlineDialogFailure.supportMsg');
386
-        const supportLink = interfaceConfig.SUPPORT_URL;
387
-        const supportLinkContent
388
-            = (
389
-                <span>
390
-                    <span>
391
-                        { supportString.padEnd(supportString.length + 1) }
392
-                    </span>
393
-                    <span>
394
-                        <a
395
-                            href = { supportLink }
396
-                            rel = 'noopener noreferrer'
397
-                            target = '_blank'>
398
-                            { t('inlineDialogFailure.support') }
399
-                        </a>
400
-                    </span>
401
-                    <span>.</span>
402
-                </span>
403
-            );
404
-
405
-        return (
406
-            <div className = 'modal-dialog-form-error'>
407
-                <InlineMessage
408
-                    title = { t('addPeople.failedToAdd') }
409
-                    type = 'error'>
410
-                    { supportLinkContent }
411
-                </InlineMessage>
125
+    }, [ _dialIn ]);
126
+
127
+    const invite = getInviteText({
128
+        _conferenceName,
129
+        _localParticipantName,
130
+        _inviteUrl,
131
+        _locationUrl,
132
+        _dialIn,
133
+        _liveStreamViewURL,
134
+        phoneNumber,
135
+        t
136
+    });
137
+    const inviteSubject = t('addPeople.inviteMoreMailSubject', {
138
+        appName: interfaceConfig.APP_NAME
139
+    });
140
+
141
+    return (
142
+        <Dialog
143
+            cancelKey = { 'dialog.close' }
144
+            customHeader = { Header }
145
+            hideCancelButton = { true }
146
+            submitDisabled = { true }
147
+            titleKey = 'addPeople.inviteMorePrompt'
148
+            width = { 'small' }>
149
+            <div className = 'invite-more-dialog'>
150
+                { !_hideInviteContacts && <InviteContactsSection /> }
151
+                <CopyMeetingLinkSection url = { _inviteUrl } />
152
+                <InviteByEmailSection
153
+                    inviteSubject = { inviteSubject }
154
+                    inviteText = { invite } />
155
+                {
156
+                    _liveStreamViewURL
157
+                        && <LiveStreamSection liveStreamViewURL = { _liveStreamViewURL } />
158
+                }
159
+                {
160
+                    _dialIn.numbers
161
+                        && <DialInSection
162
+                            conferenceName = { _conferenceName }
163
+                            dialIn = { _dialIn }
164
+                            locationUrl = { _locationUrl }
165
+                            phoneNumber = { phoneNumber } />
166
+                }
412 167
             </div>
413
-        );
414
-    }
415
-
416
-    /**
417
-     * Renders a telephone icon.
418
-     *
419
-     * @private
420
-     * @returns {ReactElement}
421
-     */
422
-    _renderTelephoneIcon() {
423
-        return (
424
-            <span className = 'add-telephone-icon'>
425
-                <Icon src = { IconPhone } />
426
-            </span>
427
-        );
428
-    }
429
-
430
-    _setMultiSelectElement: (React$ElementRef<*> | null) => void;
431
-
432
-    /**
433
-     * Sets the instance variable for the multi select component
434
-     * element so it can be accessed directly.
435
-     *
436
-     * @param {Object} element - The DOM element for the component's dialog.
437
-     * @private
438
-     * @returns {void}
439
-     */
440
-    _setMultiSelectElement(element) {
441
-        this._multiselect = element;
442
-    }
168
+        </Dialog>
169
+    );
443 170
 }
444 171
 
445 172
 /**
446
- * Maps (parts of) the Redux state to the associated
447
- * {@code AddPeopleDialog}'s props.
173
+ * Maps (parts of) the Redux state to the associated props for the
174
+ * {@code AddPeopleDialog} component.
448 175
  *
449 176
  * @param {Object} state - The Redux state.
450 177
  * @private
451
- * @returns {{
452
- *     _dialOutAuthUrl: string,
453
- *     _jwt: string,
454
- *     _peopleSearchQueryTypes: Array<string>,
455
- *     _peopleSearchUrl: string
456
- * }}
178
+ * @returns {Props}
457 179
  */
458
-function _mapStateToProps(state) {
459
-    const {
460
-        enableFeaturesBasedOnToken
461
-    } = state['features/base/config'];
462
-    let footerTextEnabled = false;
463
-
464
-    if (enableFeaturesBasedOnToken) {
465
-        const { features = {} } = getLocalParticipant(state);
466
-
467
-        if (String(features['outbound-call']) !== 'true') {
468
-            footerTextEnabled = true;
469
-        }
470
-    }
180
+function mapStateToProps(state) {
181
+    const localParticipant = getLocalParticipant(state);
182
+    const currentLiveStreamingSession
183
+        = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
184
+    const { iAmRecorder } = state['features/base/config'];
185
+    const addPeopleEnabled = isAddPeopleEnabled(state);
186
+    const dialOutEnabled = isDialOutEnabled(state);
471 187
 
472 188
     return {
473
-        ..._abstractMapStateToProps(state),
474
-        _footerTextEnabled: footerTextEnabled
189
+        _conferenceName: getRoomName(state),
190
+        _dialIn: state['features/invite'],
191
+        _hideInviteContacts:
192
+            iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
193
+        _inviteUrl: getInviteURL(state),
194
+        _liveStreamViewURL:
195
+            currentLiveStreamingSession
196
+                && currentLiveStreamingSession.liveStreamViewURL,
197
+        _localParticipantName: localParticipant?.name,
198
+        _locationUrl: state['features/base/connection'].locationURL
475 199
     };
476 200
 }
477 201
 
478
-export default translate(connect(_mapStateToProps)(AddPeopleDialog));
202
+/**
203
+ * Maps dispatching of some action to React component props.
204
+ *
205
+ * @param {Function} dispatch - Redux action dispatcher.
206
+ * @returns {Props}
207
+ */
208
+const mapDispatchToProps = {
209
+    updateNumbers: () => updateDialInNumbers()
210
+};
211
+
212
+export default translate(
213
+    connect(mapStateToProps, mapDispatchToProps)(AddPeopleDialog)
214
+);

+ 111
- 0
react/features/invite/components/add-people-dialog/web/CopyMeetingLinkSection.js Ver arquivo

@@ -0,0 +1,111 @@
1
+// @flow
2
+
3
+import React, { useState } from 'react';
4
+
5
+import { translate } from '../../../../base/i18n';
6
+import { Icon, IconCheck, IconCopy } from '../../../../base/icons';
7
+
8
+import { copyText } from './utils';
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * Invoked to obtain translated strings.
14
+     */
15
+    t: Function,
16
+
17
+    /**
18
+     * The URL of the conference.
19
+     */
20
+    url: string
21
+};
22
+
23
+/**
24
+ * Component meant to enable users to copy the conference URL.
25
+ *
26
+ * @returns {React$Element<any>}
27
+ */
28
+function CopyMeetingLinkSection({ t, url }: Props) {
29
+    const [ isClicked, setIsClicked ] = useState(false);
30
+    const [ isHovered, setIsHovered ] = useState(false);
31
+
32
+    /**
33
+     * Click handler for the element.
34
+     *
35
+     * @returns {void}
36
+     */
37
+    function onClick() {
38
+        setIsHovered(false);
39
+        if (copyText(url)) {
40
+            setIsClicked(true);
41
+
42
+            setTimeout(() => {
43
+                setIsClicked(false);
44
+            }, 2500);
45
+        }
46
+    }
47
+
48
+    /**
49
+     * Hover handler for the element.
50
+     *
51
+     * @returns {void}
52
+     */
53
+    function onHoverIn() {
54
+        if (!isClicked) {
55
+            setIsHovered(true);
56
+        }
57
+    }
58
+
59
+    /**
60
+     * Hover handler for the element.
61
+     *
62
+     * @returns {void}
63
+     */
64
+    function onHoverOut() {
65
+        setIsHovered(false);
66
+    }
67
+
68
+    /**
69
+     * Renders the content of the link based on the state.
70
+     *
71
+     * @returns {React$Element<any>}
72
+     */
73
+    function renderLinkContent() {
74
+        if (isClicked) {
75
+            return (
76
+                <>
77
+                    <div className = 'invite-more-dialog copy-link-text selected'>
78
+                        {t('addPeople.linkCopied')}
79
+                    </div>
80
+                    <Icon src = { IconCheck } />
81
+                </>
82
+            );
83
+        }
84
+
85
+        const displayUrl = decodeURI(url.replace(/^https?:\/\//i, ''));
86
+
87
+        return (
88
+            <>
89
+                <div className = 'invite-more-dialog invite-more-dialog-conference-url copy-link-text'>
90
+                    {isHovered ? t('addPeople.copyLink') : displayUrl}
91
+                </div>
92
+                <Icon src = { IconCopy } />
93
+            </>
94
+        );
95
+    }
96
+
97
+    return (
98
+        <>
99
+            <span>{t('addPeople.shareLink')}</span>
100
+            <div
101
+                className = { `invite-more-dialog copy-link${isClicked ? ' clicked' : ''}` }
102
+                onClick = { onClick }
103
+                onMouseOut = { onHoverOut }
104
+                onMouseOver = { onHoverIn }>
105
+                { renderLinkContent() }
106
+            </div>
107
+        </>
108
+    );
109
+}
110
+
111
+export default translate(CopyMeetingLinkSection);

react/features/invite/components/info-dialog/web/DialInNumber.js → react/features/invite/components/add-people-dialog/web/DialInNumber.js Ver arquivo

@@ -3,9 +3,12 @@
3 3
 import React, { Component } from 'react';
4 4
 
5 5
 import { translate } from '../../../../base/i18n';
6
+import { Icon, IconCopy } from '../../../../base/icons';
6 7
 
7 8
 import { _formatConferenceIDPin } from '../../../_utils';
8 9
 
10
+import { copyText } from './utils';
11
+
9 12
 /**
10 13
  * The type of the React {@code Component} props of {@link DialInNumber}.
11 14
  */
@@ -36,6 +39,37 @@ type Props = {
36 39
  * @extends Component
37 40
  */
38 41
 class DialInNumber extends Component<Props> {
42
+
43
+    /**
44
+     * Initializes a new DialInNumber instance.
45
+     *
46
+     * @param {Object} props - The read-only properties with which the new
47
+     * instance is to be initialized.
48
+     */
49
+    constructor(props) {
50
+        super(props);
51
+
52
+        // Bind event handler so it is only bound once for every instance.
53
+        this._onCopyText = this._onCopyText.bind(this);
54
+    }
55
+
56
+    _onCopyText: () => void;
57
+
58
+    /**
59
+     * Copies the dial-in information to the clipboard.
60
+     *
61
+     * @returns {void}
62
+     */
63
+    _onCopyText() {
64
+        const { conferenceID, phoneNumber, t } = this.props;
65
+        const dialInLabel = t('info.dialInNumber');
66
+        const passcode = t('info.dialInConferenceID');
67
+        const conferenceIDPin = `${_formatConferenceIDPin(conferenceID)}#`;
68
+        const textToCopy = `${dialInLabel} ${phoneNumber} ${passcode} ${conferenceIDPin}`;
69
+
70
+        copyText(textToCopy);
71
+    }
72
+
39 73
     /**
40 74
      * Implements React's {@link Component#render()}.
41 75
      *
@@ -66,6 +100,11 @@ class DialInNumber extends Component<Props> {
66 100
                         { `${_formatConferenceIDPin(conferenceID)}#` }
67 101
                     </span>
68 102
                 </span>
103
+                <a
104
+                    className = 'dial-in-copy'
105
+                    onClick = { this._onCopyText }>
106
+                    <Icon src = { IconCopy } />
107
+                </a>
69 108
             </div>
70 109
         );
71 110
     }

+ 76
- 0
react/features/invite/components/add-people-dialog/web/DialInSection.js Ver arquivo

@@ -0,0 +1,76 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../../base/i18n';
6
+
7
+import { getDialInfoPageURL } from '../../../functions';
8
+
9
+import DialInNumber from './DialInNumber';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * The name of the current conference. Used as part of inviting users.
15
+     */
16
+    conferenceName: string,
17
+
18
+    /**
19
+     * The object representing the dialIn feature.
20
+     */
21
+    dialIn: Object,
22
+
23
+    /**
24
+     * The current location url of the conference.
25
+     */
26
+    locationUrl: Object,
27
+
28
+    /**
29
+     * The phone number to dial to begin the process of dialing into a
30
+     * conference.
31
+     */
32
+    phoneNumber: string,
33
+
34
+    /**
35
+     * Invoked to obtain translated strings.
36
+     */
37
+    t: Function
38
+
39
+};
40
+
41
+/**
42
+ * Returns a ReactElement for showing how to dial into the conference, if
43
+ * dialing in is available.
44
+ *
45
+ * @private
46
+ * @returns {null|ReactElement}
47
+ */
48
+function DialInSection({
49
+    conferenceName,
50
+    dialIn,
51
+    locationUrl,
52
+    phoneNumber,
53
+    t
54
+}: Props) {
55
+    return (
56
+        <div className = 'invite-more-dialog dial-in-display'>
57
+            <DialInNumber
58
+                conferenceID = { dialIn.conferenceID }
59
+                phoneNumber = { phoneNumber } />
60
+            <a
61
+                className = 'more-numbers'
62
+                href = {
63
+                    getDialInfoPageURL(
64
+                        conferenceName,
65
+                        locationUrl
66
+                    )
67
+                }
68
+                rel = 'noopener noreferrer'
69
+                target = '_blank'>
70
+                { t('info.moreNumbers') }
71
+            </a>
72
+        </div>
73
+    );
74
+}
75
+
76
+export default translate(DialInSection);

+ 38
- 0
react/features/invite/components/add-people-dialog/web/Header.js Ver arquivo

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../../base/i18n';
6
+import { Icon, IconClose } from '../../../../base/icons';
7
+
8
+type Props = {
9
+
10
+    /**
11
+     * The {@link ModalDialog} closing function.
12
+     */
13
+    onClose: Function,
14
+
15
+    /**
16
+     * Invoked to obtain translated strings.
17
+     */
18
+    t: Function
19
+};
20
+
21
+/**
22
+ * Custom header of the {@code AddPeopleDialog}.
23
+ *
24
+ * @returns {React$Element<any>}
25
+ */
26
+function Header({ onClose, t }: Props) {
27
+    return (
28
+        <div
29
+            className = 'invite-more-dialog header'>
30
+            { t('addPeople.inviteMorePrompt') }
31
+            <Icon
32
+                onClick = { onClose }
33
+                src = { IconClose } />
34
+        </div>
35
+    );
36
+}
37
+
38
+export default translate(Header);

+ 156
- 0
react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js Ver arquivo

@@ -0,0 +1,156 @@
1
+// @flow
2
+
3
+import React, { useState } from 'react';
4
+import Tooltip from '@atlaskit/tooltip';
5
+
6
+import { translate } from '../../../../base/i18n';
7
+import {
8
+    Icon,
9
+    IconArrowDownSmall,
10
+    IconCopy,
11
+    IconEmail,
12
+    IconGoogle,
13
+    IconOutlook,
14
+    IconYahoo
15
+} from '../../../../base/icons';
16
+import { openURLInBrowser } from '../../../../base/util';
17
+
18
+import { copyText } from './utils';
19
+
20
+type Props = {
21
+
22
+    /**
23
+     * The encoded invitation subject.
24
+     */
25
+    inviteSubject: string,
26
+
27
+    /**
28
+     * The encoded invitation text to be sent.
29
+     */
30
+    inviteText: string,
31
+
32
+    /**
33
+     * Invoked to obtain translated strings.
34
+     */
35
+    t: Function,
36
+};
37
+
38
+/**
39
+ * Component that renders email invite options.
40
+ *
41
+ * @returns {React$Element<any>}
42
+ */
43
+function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
44
+    const [ isActive, setIsActive ] = useState(false);
45
+    const encodedInviteSubject = encodeURIComponent(inviteSubject);
46
+    const encodedInviteText = encodeURIComponent(inviteText);
47
+
48
+    /**
49
+     * Copies the conference invitation to the clipboard.
50
+     *
51
+     * @returns {void}
52
+     */
53
+    function _onCopyText() {
54
+        copyText(inviteText);
55
+    }
56
+
57
+    /**
58
+     * Opens an email provider containing the conference invite.
59
+     *
60
+     * @param {string} url - The url to be opened.
61
+     * @returns {Function}
62
+     */
63
+    function _onSelectProvider(url) {
64
+        return function() {
65
+            openURLInBrowser(url, true);
66
+        };
67
+    }
68
+
69
+    /**
70
+     * Toggles the email invite drawer.
71
+     *
72
+     * @returns {void}
73
+     */
74
+    function _onToggleActiveState() {
75
+        setIsActive(!isActive);
76
+    }
77
+
78
+    /**
79
+     * Renders clickable elements that each open an email client
80
+     * containing a conference invite.
81
+     *
82
+     * @returns {React$Element<any>}
83
+     */
84
+    function renderEmailIcons() {
85
+        const PROVIDER_MAPPING = [
86
+            {
87
+                icon: IconEmail,
88
+                tooltipKey: 'addPeople.defaultEmail',
89
+                url: `mailto:?subject=${encodedInviteSubject}&body=${encodedInviteText}`
90
+            },
91
+            {
92
+                icon: IconGoogle,
93
+                tooltipKey: 'addPeople.googleEmail',
94
+                url: `https://mail.google.com/mail/?view=cm&fs=1&su=${encodedInviteSubject}&body=${encodedInviteText}`
95
+            },
96
+            {
97
+                icon: IconOutlook,
98
+                tooltipKey: 'addPeople.outlookEmail',
99
+                // eslint-disable-next-line max-len
100
+                url: `https://outlook.office.com/mail/deeplink/compose?subject=${encodedInviteSubject}&body=${encodedInviteText}`
101
+            },
102
+            {
103
+                icon: IconYahoo,
104
+                tooltipKey: 'addPeople.yahooEmail',
105
+                url: `https://compose.mail.yahoo.com/?To=&Subj=${encodedInviteSubject}&Body=${encodedInviteText}`
106
+            }
107
+        ];
108
+
109
+        return (
110
+            <>
111
+                {
112
+                    PROVIDER_MAPPING.map(({ icon, tooltipKey, url }, idx) => (
113
+                        <Tooltip
114
+                            content = { t(tooltipKey) }
115
+                            key = { idx }
116
+                            position = 'top'>
117
+                            <div
118
+                                onClick = { _onSelectProvider(url) }>
119
+                                <Icon src = { icon } />
120
+                            </div>
121
+                        </Tooltip>
122
+                    ))
123
+                }
124
+            </>
125
+        );
126
+
127
+    }
128
+
129
+    return (
130
+        <>
131
+            <div>
132
+                <div
133
+                    className = { `invite-more-dialog email-container${isActive ? ' active' : ''}` }
134
+                    onClick = { _onToggleActiveState }>
135
+                    <span>{t('addPeople.shareInvite')}</span>
136
+                    <Icon src = { IconArrowDownSmall } />
137
+                </div>
138
+                <div className = { `invite-more-dialog icon-container${isActive ? ' active' : ''}` }>
139
+                    <Tooltip
140
+                        content = { t('addPeople.copyInvite') }
141
+                        position = 'top'>
142
+                        <div
143
+                            className = 'copy-invite-icon'
144
+                            onClick = { _onCopyText }>
145
+                            <Icon src = { IconCopy } />
146
+                        </div>
147
+                    </Tooltip>
148
+                    {renderEmailIcons()}
149
+                </div>
150
+            </div>
151
+            <div className = 'invite-more-dialog separator' />
152
+        </>
153
+    );
154
+}
155
+
156
+export default translate(InviteByEmailSection);

+ 501
- 0
react/features/invite/components/add-people-dialog/web/InviteContactsForm.js Ver arquivo

@@ -0,0 +1,501 @@
1
+// @flow
2
+
3
+import InlineMessage from '@atlaskit/inline-message';
4
+import React from 'react';
5
+import type { Dispatch } from 'redux';
6
+
7
+import { Avatar } from '../../../../base/avatar';
8
+import { translate, translateToHTML } from '../../../../base/i18n';
9
+import { Icon, IconPhone } from '../../../../base/icons';
10
+import { getLocalParticipant } from '../../../../base/participants';
11
+import { MultiSelectAutocomplete } from '../../../../base/react';
12
+import { connect } from '../../../../base/redux';
13
+
14
+import AbstractAddPeopleDialog, {
15
+    type Props as AbstractProps,
16
+    type State,
17
+    _mapStateToProps as _abstractMapStateToProps
18
+} from '../AbstractAddPeopleDialog';
19
+
20
+declare var interfaceConfig: Object;
21
+
22
+type Props = AbstractProps & {
23
+
24
+    /**
25
+     * The {@link JitsiMeetConference} which will be used to invite "room" participants.
26
+     */
27
+    _conference: Object,
28
+
29
+    /**
30
+     * Whether to show a footer text after the search results as a last element.
31
+     */
32
+    _footerTextEnabled: boolean,
33
+
34
+    /**
35
+     * The redux {@code dispatch} function.
36
+     */
37
+    dispatch: Dispatch<any>,
38
+
39
+    /**
40
+     * Invoked to obtain translated strings.
41
+     */
42
+    t: Function,
43
+};
44
+
45
+/**
46
+ * Form that enables inviting others to the call.
47
+ */
48
+class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
49
+    _multiselect = null;
50
+
51
+    _resourceClient: Object;
52
+
53
+    state = {
54
+        addToCallError: false,
55
+        addToCallInProgress: false,
56
+        inviteItems: []
57
+    };
58
+
59
+    /**
60
+     * Initializes a new {@code AddPeopleDialog} instance.
61
+     *
62
+     * @param {Object} props - The read-only properties with which the new
63
+     * instance is to be initialized.
64
+     */
65
+    constructor(props: Props) {
66
+        super(props);
67
+
68
+        // Bind event handlers so they are only bound once per instance.
69
+        this._onClearItems = this._onClearItems.bind(this);
70
+        this._onItemSelected = this._onItemSelected.bind(this);
71
+        this._onSelectionChange = this._onSelectionChange.bind(this);
72
+        this._onSubmit = this._onSubmit.bind(this);
73
+        this._parseQueryResults = this._parseQueryResults.bind(this);
74
+        this._setMultiSelectElement = this._setMultiSelectElement.bind(this);
75
+        this._renderFooterText = this._renderFooterText.bind(this);
76
+
77
+        this._resourceClient = {
78
+            makeQuery: this._query,
79
+            parseResults: this._parseQueryResults
80
+        };
81
+    }
82
+
83
+    /**
84
+     * React Component method that executes once component is updated.
85
+     *
86
+     * @param {Object} prevProps - The state object before the update.
87
+     * @param {Object} prevState - The state object before the update.
88
+     * @returns {void}
89
+     */
90
+    componentDidUpdate(prevProps, prevState) {
91
+        /**
92
+         * Clears selected items from the multi select component on successful
93
+         * invite.
94
+         */
95
+        if (prevState.addToCallError
96
+                && !this.state.addToCallInProgress
97
+                && !this.state.addToCallError
98
+                && this._multiselect) {
99
+            this._multiselect.setSelectedItems([]);
100
+        }
101
+    }
102
+
103
+    /**
104
+     * Renders the content of this component.
105
+     *
106
+     * @returns {ReactElement}
107
+     */
108
+    render() {
109
+        const {
110
+            _addPeopleEnabled,
111
+            _dialOutEnabled,
112
+            t
113
+        } = this.props;
114
+        const footerText = this._renderFooterText();
115
+        let isMultiSelectDisabled = this.state.addToCallInProgress;
116
+        let placeholder;
117
+        let loadingMessage;
118
+        let noMatches;
119
+
120
+        if (_addPeopleEnabled && _dialOutEnabled) {
121
+            loadingMessage = 'addPeople.loading';
122
+            noMatches = 'addPeople.noResults';
123
+            placeholder = 'addPeople.searchPeopleAndNumbers';
124
+        } else if (_addPeopleEnabled) {
125
+            loadingMessage = 'addPeople.loadingPeople';
126
+            noMatches = 'addPeople.noResults';
127
+            placeholder = 'addPeople.searchPeople';
128
+        } else if (_dialOutEnabled) {
129
+            loadingMessage = 'addPeople.loadingNumber';
130
+            noMatches = 'addPeople.noValidNumbers';
131
+            placeholder = 'addPeople.searchNumbers';
132
+        } else {
133
+            isMultiSelectDisabled = true;
134
+            noMatches = 'addPeople.noResults';
135
+            placeholder = 'addPeople.disabled';
136
+        }
137
+
138
+        return (
139
+            <div className = 'add-people-form-wrap'>
140
+                { this._renderErrorMessage() }
141
+                <MultiSelectAutocomplete
142
+                    footer = { footerText }
143
+                    isDisabled = { isMultiSelectDisabled }
144
+                    loadingMessage = { t(loadingMessage) }
145
+                    noMatchesFound = { t(noMatches) }
146
+                    onItemSelected = { this._onItemSelected }
147
+                    onSelectionChange = { this._onSelectionChange }
148
+                    placeholder = { t(placeholder) }
149
+                    ref = { this._setMultiSelectElement }
150
+                    resourceClient = { this._resourceClient }
151
+                    shouldFitContainer = { true }
152
+                    shouldFocus = { true } />
153
+                { this._renderFormActions() }
154
+            </div>
155
+        );
156
+    }
157
+
158
+    _invite: Array<Object> => Promise<*>
159
+
160
+    _isAddDisabled: () => boolean;
161
+
162
+    _onItemSelected: (Object) => Object;
163
+
164
+    /**
165
+     * Callback invoked when a selection has been made but before it has been
166
+     * set as selected.
167
+     *
168
+     * @param {Object} item - The item that has just been selected.
169
+     * @private
170
+     * @returns {Object} The item to display as selected in the input.
171
+     */
172
+    _onItemSelected(item) {
173
+        if (item.item.type === 'phone') {
174
+            item.content = item.item.number;
175
+        }
176
+
177
+        return item;
178
+    }
179
+
180
+    _onSelectionChange: (Map<*, *>) => void;
181
+
182
+    /**
183
+     * Handles a selection change.
184
+     *
185
+     * @param {Array} selectedItems - The list of selected items.
186
+     * @private
187
+     * @returns {void}
188
+     */
189
+    _onSelectionChange(selectedItems) {
190
+        this.setState({
191
+            inviteItems: selectedItems
192
+        });
193
+    }
194
+
195
+    _onSubmit: () => void;
196
+
197
+    /**
198
+     * Submits the selection for inviting.
199
+     *
200
+     * @private
201
+     * @returns {void}
202
+     */
203
+    _onSubmit() {
204
+        const { inviteItems } = this.state;
205
+        const invitees = inviteItems.map(({ item }) => item);
206
+
207
+        this._invite(invitees)
208
+            .then(invitesLeftToSend => {
209
+                if (invitesLeftToSend.length) {
210
+                    const unsentInviteIDs
211
+                        = invitesLeftToSend.map(invitee =>
212
+                            invitee.id || invitee.user_id || invitee.number);
213
+                    const itemsToSelect
214
+                        = inviteItems.filter(({ item }) =>
215
+                            unsentInviteIDs.includes(item.id || item.user_id || item.number));
216
+
217
+                    if (this._multiselect) {
218
+                        this._multiselect.setSelectedItems(itemsToSelect);
219
+                    }
220
+                } else {
221
+                    // Do nothing.
222
+                }
223
+            });
224
+    }
225
+
226
+    _parseQueryResults: (?Array<Object>) => Array<Object>;
227
+
228
+    /**
229
+     * Returns the avatar component for a user.
230
+     *
231
+     * @param {Object} user - The user.
232
+     * @param {string} className - The CSS class for the avatar component.
233
+     * @private
234
+     * @returns {ReactElement}
235
+     */
236
+    _getAvatar(user, className = 'avatar-small') {
237
+        return (
238
+            <Avatar
239
+                className = { className }
240
+                status = { user.status }
241
+                url = { user.avatar } />
242
+        );
243
+    }
244
+
245
+    /**
246
+     * Processes results from requesting available numbers and people by munging
247
+     * each result into a format {@code MultiSelectAutocomplete} can use for
248
+     * display.
249
+     *
250
+     * @param {Array} response - The response object from the server for the
251
+     * query.
252
+     * @private
253
+     * @returns {Object[]} Configuration objects for items to display in the
254
+     * search autocomplete.
255
+     */
256
+    _parseQueryResults(response = []) {
257
+        const { t, _dialOutEnabled } = this.props;
258
+        const users = response.filter(item => item.type !== 'phone');
259
+        const userDisplayItems = [];
260
+
261
+        for (const user of users) {
262
+            const { name, phone } = user;
263
+            const tagAvatar = this._getAvatar(user, 'avatar-xsmall');
264
+            const elemAvatar = this._getAvatar(user);
265
+
266
+            userDisplayItems.push({
267
+                content: name,
268
+                elemBefore: elemAvatar,
269
+                item: user,
270
+                tag: {
271
+                    elemBefore: tagAvatar
272
+                },
273
+                value: user.id || user.user_id
274
+            });
275
+
276
+            if (phone && _dialOutEnabled) {
277
+                userDisplayItems.push({
278
+                    filterValues: [ name, phone ],
279
+                    content: `${phone} (${name})`,
280
+                    elemBefore: elemAvatar,
281
+                    item: {
282
+                        type: 'phone',
283
+                        number: phone
284
+                    },
285
+                    tag: {
286
+                        elemBefore: tagAvatar
287
+                    },
288
+                    value: phone
289
+                });
290
+            }
291
+        }
292
+
293
+        const numbers = response.filter(item => item.type === 'phone');
294
+        const telephoneIcon = this._renderTelephoneIcon();
295
+
296
+        const numberDisplayItems = numbers.map(number => {
297
+            const numberNotAllowedMessage
298
+                = number.allowed ? '' : t('addPeople.countryNotSupported');
299
+            const countryCodeReminder = number.showCountryCodeReminder
300
+                ? t('addPeople.countryReminder') : '';
301
+            const description
302
+                = `${numberNotAllowedMessage} ${countryCodeReminder}`.trim();
303
+
304
+            return {
305
+                filterValues: [
306
+                    number.originalEntry,
307
+                    number.number
308
+                ],
309
+                content: t('addPeople.telephone', { number: number.number }),
310
+                description,
311
+                isDisabled: !number.allowed,
312
+                elemBefore: telephoneIcon,
313
+                item: number,
314
+                tag: {
315
+                    elemBefore: telephoneIcon
316
+                },
317
+                value: number.number
318
+            };
319
+        });
320
+
321
+        return [
322
+            ...userDisplayItems,
323
+            ...numberDisplayItems
324
+        ];
325
+    }
326
+
327
+    _query: (string) => Promise<Array<Object>>;
328
+
329
+    _renderFooterText: () => Object;
330
+
331
+    /**
332
+     * Sets up the rendering of the footer text, if enabled.
333
+     *
334
+     * @returns {Object | undefined}
335
+     */
336
+    _renderFooterText() {
337
+        const { _footerTextEnabled, t } = this.props;
338
+        let footerText;
339
+
340
+        if (_footerTextEnabled) {
341
+            footerText = {
342
+                content: <div className = 'footer-text-wrap'>
343
+                    <div>
344
+                        <span className = 'footer-telephone-icon'>
345
+                            <Icon src = { IconPhone } />
346
+                        </span>
347
+                    </div>
348
+                    { translateToHTML(t, 'addPeople.footerText') }
349
+                </div>
350
+            };
351
+        }
352
+
353
+        return footerText;
354
+    }
355
+
356
+    _onClearItems: () => void;
357
+
358
+    /**
359
+     * Clears the selected items from state and form.
360
+     *
361
+     * @returns {void}
362
+     */
363
+    _onClearItems() {
364
+        if (this._multiselect) {
365
+            this._multiselect.setSelectedItems([]);
366
+        }
367
+        this.setState({ inviteItems: [] });
368
+    }
369
+
370
+    /**
371
+     * Renders the add/cancel actions for the form.
372
+     *
373
+     * @returns {ReactElement|null}
374
+     */
375
+    _renderFormActions() {
376
+        const { inviteItems } = this.state;
377
+        const { t } = this.props;
378
+
379
+        if (!inviteItems.length) {
380
+            return null;
381
+        }
382
+
383
+        return (
384
+            <div className = 'invite-more-dialog invite-buttons'>
385
+                <a
386
+                    className = 'invite-more-dialog invite-buttons-cancel'
387
+                    onClick = { this._onClearItems }>
388
+                    {t('dialog.Cancel')}
389
+                </a>
390
+                <a
391
+                    className = 'invite-more-dialog invite-buttons-add'
392
+                    onClick = { this._onSubmit }>
393
+                    {t('addPeople.add')}
394
+                </a>
395
+            </div>
396
+        );
397
+    }
398
+
399
+    /**
400
+     * Renders the error message if the add doesn't succeed.
401
+     *
402
+     * @private
403
+     * @returns {ReactElement|null}
404
+     */
405
+    _renderErrorMessage() {
406
+        if (!this.state.addToCallError) {
407
+            return null;
408
+        }
409
+
410
+        const { t } = this.props;
411
+        const supportString = t('inlineDialogFailure.supportMsg');
412
+        const supportLink = interfaceConfig.SUPPORT_URL;
413
+
414
+        if (!supportLink) {
415
+            return null;
416
+        }
417
+
418
+        const supportLinkContent = (
419
+            <span>
420
+                <span>
421
+                    { supportString.padEnd(supportString.length + 1) }
422
+                </span>
423
+                <span>
424
+                    <a
425
+                        href = { supportLink }
426
+                        rel = 'noopener noreferrer'
427
+                        target = '_blank'>
428
+                        { t('inlineDialogFailure.support') }
429
+                    </a>
430
+                </span>
431
+                <span>.</span>
432
+            </span>
433
+        );
434
+
435
+        return (
436
+            <div className = 'modal-dialog-form-error'>
437
+                <InlineMessage
438
+                    title = { t('addPeople.failedToAdd') }
439
+                    type = 'error'>
440
+                    { supportLinkContent }
441
+                </InlineMessage>
442
+            </div>
443
+        );
444
+    }
445
+
446
+    /**
447
+     * Renders a telephone icon.
448
+     *
449
+     * @private
450
+     * @returns {ReactElement}
451
+     */
452
+    _renderTelephoneIcon() {
453
+        return (
454
+            <span className = 'add-telephone-icon'>
455
+                <Icon src = { IconPhone } />
456
+            </span>
457
+        );
458
+    }
459
+
460
+    _setMultiSelectElement: (React$ElementRef<*> | null) => void;
461
+
462
+    /**
463
+     * Sets the instance variable for the multi select component
464
+     * element so it can be accessed directly.
465
+     *
466
+     * @param {Object} element - The DOM element for the component's dialog.
467
+     * @private
468
+     * @returns {void}
469
+     */
470
+    _setMultiSelectElement(element) {
471
+        this._multiselect = element;
472
+    }
473
+}
474
+
475
+/**
476
+ * Maps (parts of) the Redux state to the associated
477
+ * {@code AddPeopleDialog}'s props.
478
+ *
479
+ * @param {Object} state - The Redux state.
480
+ * @private
481
+ * @returns {Props}
482
+ */
483
+function _mapStateToProps(state) {
484
+    const { enableFeaturesBasedOnToken } = state['features/base/config'];
485
+    let footerTextEnabled = false;
486
+
487
+    if (enableFeaturesBasedOnToken) {
488
+        const { features = {} } = getLocalParticipant(state);
489
+
490
+        if (String(features['outbound-call']) !== 'true') {
491
+            footerTextEnabled = true;
492
+        }
493
+    }
494
+
495
+    return {
496
+        ..._abstractMapStateToProps(state),
497
+        _footerTextEnabled: footerTextEnabled
498
+    };
499
+}
500
+
501
+export default translate(connect(_mapStateToProps)(InviteContactsForm));

+ 32
- 0
react/features/invite/components/add-people-dialog/web/InviteContactsSection.js Ver arquivo

@@ -0,0 +1,32 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../../base/i18n';
6
+
7
+import InviteContactsForm from './InviteContactsForm';
8
+
9
+type Props = {
10
+
11
+    /**
12
+     * Invoked to obtain translated strings.
13
+     */
14
+    t: Function
15
+};
16
+
17
+/**
18
+ * Component that represents the invitation section of the {@code AddPeopleDialog}.
19
+ *
20
+ * @returns {ReactElement$<any>}
21
+ */
22
+function InviteContactsSection({ t }: Props) {
23
+    return (
24
+        <>
25
+            <span>{t('addPeople.addContacts')}</span>
26
+            <InviteContactsForm />
27
+            <div className = 'invite-more-dialog separator' />
28
+        </>
29
+    );
30
+}
31
+
32
+export default translate(InviteContactsSection);

+ 111
- 0
react/features/invite/components/add-people-dialog/web/LiveStreamSection.js Ver arquivo

@@ -0,0 +1,111 @@
1
+// @flow
2
+
3
+import React, { useState } from 'react';
4
+
5
+import { translate } from '../../../../base/i18n';
6
+import { Icon, IconCheck, IconCopy } from '../../../../base/icons';
7
+
8
+import { copyText } from './utils';
9
+
10
+type Props = {
11
+
12
+    /**
13
+     * The current known URL for a live stream in progress.
14
+     */
15
+    liveStreamViewURL: string,
16
+
17
+    /**
18
+     * Invoked to obtain translated strings.
19
+     */
20
+    t: Function
21
+}
22
+
23
+/**
24
+ * Section of the {@code AddPeopleDialog} that renders the
25
+ * live streaming url, allowing a copy action.
26
+ *
27
+ * @returns {React$Element<any>}
28
+ */
29
+function LiveStreamSection({ liveStreamViewURL, t }: Props) {
30
+    const [ isClicked, setIsClicked ] = useState(false);
31
+    const [ isHovered, setIsHovered ] = useState(false);
32
+
33
+    /**
34
+     * Click handler for the element.
35
+     *
36
+     * @returns {void}
37
+     */
38
+    function onClick() {
39
+        setIsHovered(false);
40
+        if (copyText(liveStreamViewURL)) {
41
+            setIsClicked(true);
42
+
43
+            setTimeout(() => {
44
+                setIsClicked(false);
45
+            }, 2500);
46
+        }
47
+    }
48
+
49
+    /**
50
+     * Hover handler for the element.
51
+     *
52
+     * @returns {void}
53
+     */
54
+    function onHoverIn() {
55
+        if (!isClicked) {
56
+            setIsHovered(true);
57
+        }
58
+    }
59
+
60
+    /**
61
+     * Hover handler for the element.
62
+     *
63
+     * @returns {void}
64
+     */
65
+    function onHoverOut() {
66
+        setIsHovered(false);
67
+    }
68
+
69
+    /**
70
+     * Renders the content of the link based on the state.
71
+     *
72
+     * @returns {React$Element<any>}
73
+     */
74
+    function renderLinkContent() {
75
+        if (isClicked) {
76
+            return (
77
+                <>
78
+                    <div className = 'invite-more-dialog stream-text selected'>
79
+                        {t('addPeople.linkCopied')}
80
+                    </div>
81
+                    <Icon src = { IconCheck } />
82
+                </>
83
+            );
84
+        }
85
+
86
+        return (
87
+            <>
88
+                <div className = 'invite-more-dialog stream-text'>
89
+                    {isHovered ? t('addPeople.copyStream') : liveStreamViewURL}
90
+                </div>
91
+                <Icon src = { IconCopy } />
92
+            </>
93
+        );
94
+    }
95
+
96
+    return (
97
+        <>
98
+            <span>{t('addPeople.shareStream')}</span>
99
+            <div
100
+                className = { `invite-more-dialog stream${isClicked ? ' clicked' : ''}` }
101
+                onClick = { onClick }
102
+                onMouseOut = { onHoverOut }
103
+                onMouseOver = { onHoverIn }>
104
+                { renderLinkContent() }
105
+            </div>
106
+            <div className = 'invite-more-dialog separator' />
107
+        </>
108
+    );
109
+}
110
+
111
+export default translate(LiveStreamSection);

+ 1
- 0
react/features/invite/components/add-people-dialog/web/index.js Ver arquivo

@@ -1,3 +1,4 @@
1 1
 // @flow
2 2
 
3 3
 export { default as AddPeopleDialog } from './AddPeopleDialog';
4
+export * from './utils';

+ 23
- 0
react/features/invite/components/add-people-dialog/web/utils.js Ver arquivo

@@ -0,0 +1,23 @@
1
+// @flow
2
+
3
+/**
4
+ * Tries to copy a given text to the clipboard.
5
+ *
6
+ * @param {string} textToCopy - Text to be copied.
7
+ * @returns {boolean}
8
+ */
9
+export function copyText(textToCopy: string) {
10
+    const fakeTextArea = document.createElement('textarea');
11
+
12
+    // $FlowFixMe
13
+    document.body.appendChild(fakeTextArea);
14
+    fakeTextArea.value = textToCopy;
15
+    fakeTextArea.select();
16
+
17
+    const result = document.execCommand('copy');
18
+
19
+    // $FlowFixMe
20
+    document.body.removeChild(fakeTextArea);
21
+
22
+    return result;
23
+}

+ 0
- 1
react/features/invite/components/index.js Ver arquivo

@@ -2,5 +2,4 @@
2 2
 
3 3
 export * from './add-people-dialog';
4 4
 export * from './dial-in-summary';
5
-export * from './info-dialog';
6 5
 export * from './callee-info';

+ 0
- 0
react/features/invite/components/info-dialog/index.native.js Ver arquivo


+ 0
- 3
react/features/invite/components/info-dialog/index.web.js Ver arquivo

@@ -1,3 +0,0 @@
1
-// @flow
2
-
3
-export * from './web';

+ 0
- 644
react/features/invite/components/info-dialog/web/InfoDialog.js Ver arquivo

@@ -1,644 +0,0 @@
1
-// @flow
2
-
3
-import React, { Component } from 'react';
4
-import type { Dispatch } from 'redux';
5
-
6
-import { setPassword } from '../../../../base/conference';
7
-import { getInviteURL } from '../../../../base/connection';
8
-import { Dialog } from '../../../../base/dialog';
9
-import { translate } from '../../../../base/i18n';
10
-import { Icon, IconInfo, IconCopy } from '../../../../base/icons';
11
-import { connect } from '../../../../base/redux';
12
-import {
13
-    isLocalParticipantModerator,
14
-    getLocalParticipant
15
-} from '../../../../base/participants';
16
-
17
-import {
18
-    _decodeRoomURI,
19
-    _getDefaultPhoneNumber,
20
-    getDialInfoPageURL,
21
-    shouldDisplayDialIn
22
-} from '../../../functions';
23
-import logger from '../../../logger';
24
-import DialInNumber from './DialInNumber';
25
-import PasswordForm from './PasswordForm';
26
-
27
-
28
-/**
29
- * The type of the React {@code Component} props of {@link InfoDialog}.
30
- */
31
-type Props = {
32
-
33
-    /**
34
-     * Whether or not the current user can modify the current password.
35
-     */
36
-    _canEditPassword: boolean,
37
-
38
-    /**
39
-     * The JitsiConference for which to display a lock state and change the
40
-     * password.
41
-     */
42
-    _conference: Object,
43
-
44
-    /**
45
-     * The name of the current conference. Used as part of inviting users.
46
-     */
47
-    _conferenceName: string,
48
-
49
-    /**
50
-     * The number of digits to be used in the password.
51
-     */
52
-    _passwordNumberOfDigits: ?number,
53
-
54
-    /**
55
-     * The current url of the conference to be copied onto the clipboard.
56
-     */
57
-    _inviteURL: string,
58
-
59
-    /**
60
-     * The redux representation of the local participant.
61
-     */
62
-    _localParticipantName: ?string,
63
-
64
-    /**
65
-     * The current location url of the conference.
66
-     */
67
-    _locationURL: Object,
68
-
69
-    /**
70
-     * The value for how the conference is locked (or undefined if not locked)
71
-     * as defined by room-lock constants.
72
-     */
73
-    _locked: string,
74
-
75
-    /**
76
-     * The current known password for the JitsiConference.
77
-     */
78
-    _password: string,
79
-
80
-    /**
81
-     * The object representing the dialIn feature.
82
-     */
83
-    dialIn: Object,
84
-
85
-    /**
86
-     * Invoked to open a dialog for adding participants to the conference.
87
-     */
88
-    dispatch: Dispatch<any>,
89
-
90
-    /**
91
-     * Whether is Atlaskit InlineDialog or a normal dialog.
92
-     */
93
-    isInlineDialog: boolean,
94
-
95
-    /**
96
-     * The current known URL for a live stream in progress.
97
-     */
98
-    liveStreamViewURL: string,
99
-
100
-    /**
101
-     * Callback invoked when the dialog should be closed.
102
-     */
103
-    onClose: Function,
104
-
105
-    /**
106
-     * Callback invoked when a mouse-related event has been detected.
107
-     */
108
-    onMouseOver: Function,
109
-
110
-    /**
111
-     * Invoked to obtain translated strings.
112
-     */
113
-    t: Function
114
-};
115
-
116
-/**
117
- * The type of the React {@code Component} state of {@link InfoDialog}.
118
- */
119
-type State = {
120
-
121
-    /**
122
-     * Whether or not to show the password in editing mode.
123
-     */
124
-    passwordEditEnabled: boolean,
125
-
126
-    /**
127
-     * The conference dial-in number to display.
128
-     */
129
-    phoneNumber: ?string
130
-};
131
-
132
-/**
133
- * A React Component with the contents for a dialog that shows information about
134
- * the current conference.
135
- *
136
- * @extends Component
137
- */
138
-class InfoDialog extends Component<Props, State> {
139
-    _copyElement: ?Object;
140
-    _copyUrlElement: ?Object;
141
-
142
-    /**
143
-     * Implements React's {@link Component#getDerivedStateFromProps()}.
144
-     *
145
-     * @inheritdoc
146
-     */
147
-    static getDerivedStateFromProps(props, state) {
148
-        let phoneNumber = state.phoneNumber;
149
-
150
-        if (!state.phoneNumber && props.dialIn.numbers) {
151
-            phoneNumber = _getDefaultPhoneNumber(props.dialIn.numbers);
152
-        }
153
-
154
-        return {
155
-            // Exit edit mode when a password is set locally or remotely.
156
-            passwordEditEnabled: state.passwordEditEnabled && props._password
157
-                ? false : state.passwordEditEnabled,
158
-            phoneNumber
159
-        };
160
-    }
161
-
162
-    /**
163
-     * {@code InfoDialog} component's local state.
164
-     *
165
-     * @type {Object}
166
-     * @property {boolean} passwordEditEnabled - Whether or not to show the
167
-     * {@code PasswordForm} in its editing state.
168
-     * @property {string} phoneNumber - The number to display for dialing into
169
-     * the conference.
170
-     */
171
-    state = {
172
-        passwordEditEnabled: false,
173
-        phoneNumber: undefined
174
-    };
175
-
176
-    /**
177
-     * Initializes new {@code InfoDialog} instance.
178
-     *
179
-     * @param {Object} props - The read-only properties with which the new
180
-     * instance is to be initialized.
181
-     */
182
-    constructor(props: Props) {
183
-        super(props);
184
-
185
-        if (props.dialIn && props.dialIn.numbers) {
186
-            this.state.phoneNumber
187
-                = _getDefaultPhoneNumber(props.dialIn.numbers);
188
-        }
189
-
190
-        /**
191
-         * The internal reference to the DOM/HTML element backing the React
192
-         * {@code Component} text area. It is necessary for the implementation
193
-         * of copying to the clipboard.
194
-         *
195
-         * @private
196
-         * @type {HTMLTextAreaElement}
197
-         */
198
-        this._copyElement = null;
199
-
200
-        // Bind event handlers so they are only bound once for every instance.
201
-        this._onClickURLText = this._onClickURLText.bind(this);
202
-        this._onCopyInviteInfo = this._onCopyInviteInfo.bind(this);
203
-        this._onCopyInviteUrl = this._onCopyInviteUrl.bind(this);
204
-        this._onPasswordRemove = this._onPasswordRemove.bind(this);
205
-        this._onPasswordSubmit = this._onPasswordSubmit.bind(this);
206
-        this._onTogglePasswordEditState
207
-            = this._onTogglePasswordEditState.bind(this);
208
-        this._setCopyElement = this._setCopyElement.bind(this);
209
-        this._setCopyUrlElement = this._setCopyUrlElement.bind(this);
210
-    }
211
-
212
-    /**
213
-     * Implements React's {@link Component#render()}.
214
-     *
215
-     * @inheritdoc
216
-     * @returns {ReactElement}
217
-     */
218
-    render() {
219
-        const {
220
-            isInlineDialog,
221
-            liveStreamViewURL,
222
-            onMouseOver,
223
-            t
224
-        } = this.props;
225
-
226
-        const inlineDialog = (
227
-            <div
228
-                className = 'info-dialog'
229
-                onMouseOver = { onMouseOver } >
230
-                <div className = 'info-dialog-column'>
231
-                    <h4 className = 'info-dialog-icon'>
232
-                        <Icon src = { IconInfo } />
233
-                    </h4>
234
-                </div>
235
-                <div className = 'info-dialog-column'>
236
-                    <div className = 'info-dialog-title'>
237
-                        { t('info.title') }
238
-                    </div>
239
-                    <div className = 'info-dialog-conference-url'>
240
-                        <span className = 'info-label'>
241
-                            { t('info.conferenceURL') }
242
-                        </span>
243
-                        <span className = 'spacer'>&nbsp;</span>
244
-                        <span className = 'info-value'>
245
-                            <a
246
-                                className = 'info-dialog-url-text info-dialog-url-text-unselectable'
247
-                                href = { this.props._inviteURL }
248
-                                onClick = { this._onClickURLText } >
249
-                                { decodeURI(this._getURLToDisplay()) }
250
-                            </a>
251
-                        </span>
252
-                        <span className = 'info-dialog-url-icon'>
253
-                            <Icon
254
-                                onClick = { this._onCopyInviteUrl }
255
-                                size = { 18 }
256
-                                src = { IconCopy } />
257
-                        </span>
258
-                    </div>
259
-                    <div className = 'info-dialog-dial-in'>
260
-                        { this._renderDialInDisplay() }
261
-                    </div>
262
-                    { liveStreamViewURL && this._renderLiveStreamURL() }
263
-                    <div className = 'info-dialog-password'>
264
-                        <PasswordForm
265
-                            editEnabled = { this.state.passwordEditEnabled }
266
-                            locked = { this.props._locked }
267
-                            onSubmit = { this._onPasswordSubmit }
268
-                            password = { this.props._password }
269
-                            passwordNumberOfDigits = { this.props._passwordNumberOfDigits } />
270
-                    </div>
271
-                    <div className = 'info-dialog-action-links'>
272
-                        <div className = 'info-dialog-action-link'>
273
-                            <a
274
-                                className = 'info-copy'
275
-                                onClick = { this._onCopyInviteInfo }>
276
-                                { t('dialog.copy') }
277
-                            </a>
278
-                        </div>
279
-                        { this._renderPasswordAction() }
280
-                    </div>
281
-                </div>
282
-                <textarea
283
-                    className = 'info-dialog-copy-element'
284
-                    readOnly = { true }
285
-                    ref = { this._setCopyElement }
286
-                    tabIndex = '-1'
287
-                    value = { this._getTextToCopy() } />
288
-                <textarea
289
-                    className = 'info-dialog-copy-element'
290
-                    readOnly = { true }
291
-                    ref = { this._setCopyUrlElement }
292
-                    tabIndex = '-1'
293
-                    value = { this.props._inviteURL } />
294
-            </div>
295
-        );
296
-
297
-        if (isInlineDialog) {
298
-            return inlineDialog;
299
-        }
300
-
301
-        return (
302
-            <Dialog
303
-                cancelTitleKey = 'dialog.close'
304
-                submitDisabled = { true }
305
-                titleKey = 'info.label'
306
-                width = 'small'>
307
-                { inlineDialog }
308
-            </Dialog>
309
-        );
310
-    }
311
-
312
-    /**
313
-     * Creates a message describing how to dial in to the conference.
314
-     *
315
-     * @private
316
-     * @returns {string}
317
-     */
318
-    _getTextToCopy() {
319
-        const { _localParticipantName, liveStreamViewURL, t } = this.props;
320
-        const _inviteURL = _decodeRoomURI(this.props._inviteURL);
321
-
322
-        let invite = _localParticipantName
323
-            ? t('info.inviteURLFirstPartPersonal', { name: _localParticipantName })
324
-            : t('info.inviteURLFirstPartGeneral');
325
-
326
-        invite += t('info.inviteURLSecondPart', {
327
-            url: _inviteURL
328
-        });
329
-
330
-        if (liveStreamViewURL) {
331
-            const liveStream = t('info.inviteLiveStream', {
332
-                url: liveStreamViewURL
333
-            });
334
-
335
-            invite = `${invite}\n${liveStream}`;
336
-        }
337
-
338
-        if (shouldDisplayDialIn(this.props.dialIn)) {
339
-            const dial = t('info.invitePhone', {
340
-                number: this.state.phoneNumber,
341
-                conferenceID: this.props.dialIn.conferenceID
342
-            });
343
-            const moreNumbers = t('info.invitePhoneAlternatives', {
344
-                url: getDialInfoPageURL(
345
-                    this.props._conferenceName,
346
-                    this.props._locationURL
347
-                ),
348
-                silentUrl: `${_inviteURL}#config.startSilent=true`
349
-            });
350
-
351
-            invite = `${invite}\n${dial}\n${moreNumbers}`;
352
-        }
353
-
354
-        return invite;
355
-    }
356
-
357
-    /**
358
-     * Modifies the inviteURL for display in the modal.
359
-     *
360
-     * @private
361
-     * @returns {string}
362
-     */
363
-    _getURLToDisplay() {
364
-        return this.props._inviteURL.replace(/^https?:\/\//i, '');
365
-    }
366
-
367
-    _onClickURLText: (Object) => void;
368
-
369
-    /**
370
-     * Callback invoked when a displayed URL link is clicked to prevent actual
371
-     * navigation from happening. The URL links have an href to display the
372
-     * action "Copy Link Address" in the context menu but otherwise it should
373
-     * not behave like links.
374
-     *
375
-     * @param {Object} event - The click event from clicking on the link.
376
-     * @private
377
-     * @returns {void}
378
-     */
379
-    _onClickURLText(event) {
380
-        event.preventDefault();
381
-    }
382
-
383
-    _onCopyInviteInfo: () => void;
384
-
385
-    /**
386
-     * Callback invoked to copy the contents of {@code this._copyElement} to the
387
-     * clipboard.
388
-     *
389
-     * @private
390
-     * @returns {void}
391
-     */
392
-    _onCopyInviteInfo() {
393
-        try {
394
-            if (!this._copyElement) {
395
-                throw new Error('No element to copy from.');
396
-            }
397
-
398
-            this._copyElement && this._copyElement.select();
399
-            document.execCommand('copy');
400
-            this._copyElement && this._copyElement.blur();
401
-        } catch (err) {
402
-            logger.error('error when copying the text', err);
403
-        }
404
-    }
405
-
406
-    _onCopyInviteUrl: () => void;
407
-
408
-    /**
409
-     * Callback invoked to copy the contents of {@code this._copyUrlElement} to the clipboard.
410
-     *
411
-     * @private
412
-     * @returns {void}
413
-     */
414
-    _onCopyInviteUrl() {
415
-        try {
416
-            if (!this._copyUrlElement) {
417
-                throw new Error('No element to copy from.');
418
-            }
419
-
420
-            this._copyUrlElement && this._copyUrlElement.select();
421
-            document.execCommand('copy');
422
-            this._copyUrlElement && this._copyUrlElement.blur();
423
-        } catch (err) {
424
-            logger.error('error when copying the text', err);
425
-        }
426
-    }
427
-
428
-    _onPasswordRemove: () => void;
429
-
430
-    /**
431
-     * Callback invoked to unlock the current JitsiConference.
432
-     *
433
-     * @private
434
-     * @returns {void}
435
-     */
436
-    _onPasswordRemove() {
437
-        this._onPasswordSubmit('');
438
-    }
439
-
440
-    _onPasswordSubmit: (string) => void;
441
-
442
-    /**
443
-     * Callback invoked to set a password on the current JitsiConference.
444
-     *
445
-     * @param {string} enteredPassword - The new password to be used to lock the
446
-     * current JitsiConference.
447
-     * @private
448
-     * @returns {void}
449
-     */
450
-    _onPasswordSubmit(enteredPassword) {
451
-        const { _conference } = this.props;
452
-
453
-        this.props.dispatch(setPassword(
454
-            _conference,
455
-            _conference.lock,
456
-            enteredPassword
457
-        ));
458
-    }
459
-
460
-    _onTogglePasswordEditState: () => void;
461
-
462
-    /**
463
-     * Toggles whether or not the password should currently be shown as being
464
-     * edited locally.
465
-     *
466
-     * @private
467
-     * @returns {void}
468
-     */
469
-    _onTogglePasswordEditState() {
470
-        this.setState({
471
-            passwordEditEnabled: !this.state.passwordEditEnabled
472
-        });
473
-    }
474
-
475
-    /**
476
-     * Returns a ReactElement for showing how to dial into the conference, if
477
-     * dialing in is available.
478
-     *
479
-     * @private
480
-     * @returns {null|ReactElement}
481
-     */
482
-    _renderDialInDisplay() {
483
-        if (!shouldDisplayDialIn(this.props.dialIn)) {
484
-            return null;
485
-        }
486
-
487
-        return (
488
-            <div>
489
-                <DialInNumber
490
-                    conferenceID = { this.props.dialIn.conferenceID }
491
-                    phoneNumber = { this.state.phoneNumber } />
492
-                <a
493
-                    className = 'more-numbers'
494
-                    href = {
495
-                        getDialInfoPageURL(
496
-                            this.props._conferenceName,
497
-                            this.props._locationURL
498
-                        )
499
-                    }
500
-                    rel = 'noopener noreferrer'
501
-                    target = '_blank'>
502
-                    { this.props.t('info.moreNumbers') }
503
-                </a>
504
-            </div>
505
-        );
506
-    }
507
-
508
-    /**
509
-     * Returns a ReactElement for interacting with the password field.
510
-     *
511
-     * @private
512
-     * @returns {null|ReactElement}
513
-     */
514
-    _renderPasswordAction() {
515
-        const { t } = this.props;
516
-        let className, onClick, textKey;
517
-
518
-
519
-        if (!this.props._canEditPassword) {
520
-            // intentionally left blank to prevent rendering anything
521
-        } else if (this.state.passwordEditEnabled) {
522
-            className = 'cancel-password';
523
-            onClick = this._onTogglePasswordEditState;
524
-            textKey = 'info.cancelPassword';
525
-        } else if (this.props._locked) {
526
-            className = 'remove-password';
527
-            onClick = this._onPasswordRemove;
528
-            textKey = 'dialog.removePassword';
529
-        } else {
530
-            className = 'add-password';
531
-            onClick = this._onTogglePasswordEditState;
532
-            textKey = 'info.addPassword';
533
-        }
534
-
535
-        return className && onClick && textKey
536
-            ? <div className = 'info-dialog-action-link'>
537
-                <a
538
-                    className = { className }
539
-                    onClick = { onClick }>
540
-                    { t(textKey) }
541
-                </a>
542
-            </div>
543
-            : null;
544
-    }
545
-
546
-    /**
547
-     * Returns a ReactElement for display a link to the current url of a
548
-     * live stream in progress.
549
-     *
550
-     * @private
551
-     * @returns {null|ReactElement}
552
-     */
553
-    _renderLiveStreamURL() {
554
-        const { liveStreamViewURL, t } = this.props;
555
-
556
-        return (
557
-            <div className = 'info-dialog-live-stream-url'>
558
-                <span className = 'info-label'>
559
-                    { t('info.liveStreamURL') }
560
-                </span>
561
-                <span className = 'spacer'>&nbsp;</span>
562
-                <span className = 'info-value'>
563
-                    <a
564
-                        className = 'info-dialog-url-text'
565
-                        href = { liveStreamViewURL }
566
-                        onClick = { this._onClickURLText } >
567
-                        { liveStreamViewURL }
568
-                    </a>
569
-                </span>
570
-            </div>
571
-        );
572
-    }
573
-
574
-    _setCopyElement: () => void;
575
-
576
-    /**
577
-     * Sets the internal reference to the DOM/HTML element backing the React
578
-     * {@code Component} input.
579
-     *
580
-     * @param {HTMLInputElement} element - The DOM/HTML element for this
581
-     * {@code Component}'s input.
582
-     * @private
583
-     * @returns {void}
584
-     */
585
-    _setCopyElement(element: Object) {
586
-        this._copyElement = element;
587
-    }
588
-
589
-    _setCopyUrlElement: () => void;
590
-
591
-    /**
592
-     * Sets the internal reference to the DOM/HTML element backing the React
593
-     * {@code Component} input.
594
-     *
595
-     * @param {HTMLInputElement} element - The DOM/HTML element for this
596
-     * {@code Component}'s input.
597
-     * @private
598
-     * @returns {void}
599
-     */
600
-    _setCopyUrlElement(element: Object) {
601
-        this._copyUrlElement = element;
602
-    }
603
-}
604
-
605
-/**
606
- * Maps (parts of) the Redux state to the associated props for the
607
- * {@code InfoDialog} component.
608
- *
609
- * @param {Object} state - The Redux state.
610
- * @private
611
- * @returns {{
612
- *     _canEditPassword: boolean,
613
- *     _conference: Object,
614
- *     _conferenceName: string,
615
- *     _inviteURL: string,
616
- *     _localParticipantName: ?string,
617
- *     _locationURL: string,
618
- *     _locked: string,
619
- *     _password: string
620
- * }}
621
- */
622
-function _mapStateToProps(state) {
623
-    const {
624
-        conference,
625
-        locked,
626
-        password,
627
-        room
628
-    } = state['features/base/conference'];
629
-    const localParticipant = getLocalParticipant(state);
630
-
631
-    return {
632
-        _canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled),
633
-        _conference: conference,
634
-        _conferenceName: room,
635
-        _passwordNumberOfDigits: state['features/base/config'].roomPasswordNumberOfDigits,
636
-        _inviteURL: getInviteURL(state),
637
-        _localParticipantName: localParticipant?.name,
638
-        _locationURL: state['features/base/connection'].locationURL,
639
-        _locked: locked,
640
-        _password: password
641
-    };
642
-}
643
-
644
-export default translate(connect(_mapStateToProps)(InfoDialog));

+ 0
- 268
react/features/invite/components/info-dialog/web/InfoDialogButton.js Ver arquivo

@@ -1,268 +0,0 @@
1
-// @flow
2
-
3
-import InlineDialog from '@atlaskit/inline-dialog';
4
-import React, { Component } from 'react';
5
-import type { Dispatch } from 'redux';
6
-
7
-import { createToolbarEvent, sendAnalytics } from '../../../../analytics';
8
-import { openDialog } from '../../../../base/dialog';
9
-import { translate } from '../../../../base/i18n';
10
-import { IconInfo } from '../../../../base/icons';
11
-import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
12
-import { getParticipantCount } from '../../../../base/participants';
13
-import { OverflowMenuItem } from '../../../../base/toolbox';
14
-import { connect } from '../../../../base/redux';
15
-import { getActiveSession } from '../../../../recording';
16
-import { ToolbarButton } from '../../../../toolbox';
17
-import { updateDialInNumbers } from '../../../actions';
18
-
19
-import InfoDialog from './InfoDialog';
20
-
21
-/**
22
- * The type of the React {@code Component} props of {@link InfoDialogButton}.
23
- */
24
-type Props = {
25
-
26
-    /**
27
-     * The redux state representing the dial-in numbers feature.
28
-     */
29
-    _dialIn: Object,
30
-
31
-    /**
32
-     * Whether or not the {@code InfoDialog} should display automatically when
33
-     * in a lonely call.
34
-     */
35
-    _disableAutoShow: boolean,
36
-
37
-    /**
38
-     * Whether or not the local participant has joined a
39
-     * {@code JitsiConference}. Used to trigger auto showing of the
40
-     * {@code InfoDialog}.
41
-     */
42
-    _isConferenceJoined: Boolean,
43
-
44
-    /**
45
-     * The URL for a currently active live broadcast
46
-     */
47
-    _liveStreamViewURL: ?string,
48
-
49
-    /**
50
-     * True if the number of real participants in the call is less than 2. If in a lonely call, the
51
-     * {@code InfoDialog} will be automatically shown.
52
-     */
53
-    _isLonelyCall: boolean,
54
-
55
-    /**
56
-     * Whether or not the toolbox, in which this component exists, is visible.
57
-     */
58
-    _toolboxVisible: boolean,
59
-
60
-    /**
61
-     * Invoked to toggle display of the info dialog.
62
-     */
63
-    dispatch: Dispatch<any>,
64
-
65
-    /**
66
-     * Whether to show the label or not.
67
-     */
68
-    showLabel: boolean,
69
-
70
-    /**
71
-     * Invoked to obtain translated strings.
72
-     */
73
-    t: Function
74
-};
75
-
76
-/**
77
- * The type of the React {@code Component} state of {@link InfoDialogButton}.
78
- */
79
-type State = {
80
-
81
-    /**
82
-     * Cache the conference connection state to derive when transitioning from
83
-     * not joined to join, in order to auto-show the InfoDialog.
84
-     */
85
-    hasConnectedToConference: boolean,
86
-
87
-    /**
88
-     * Whether or not {@code InfoDialog} should be visible.
89
-     */
90
-    showDialog: boolean
91
-};
92
-
93
-/**
94
- * A React Component for displaying a button which opens a dialog with
95
- * information about the conference and with ways to invite people.
96
- *
97
- * @extends Component
98
- */
99
-class InfoDialogButton extends Component<Props, State> {
100
-    /**
101
-     * Implements React's {@link Component#getDerivedStateFromProps()}.
102
-     *
103
-     * @inheritdoc
104
-     */
105
-    static getDerivedStateFromProps(props, state) {
106
-        return {
107
-            hasConnectedToConference: props._isConferenceJoined,
108
-            showDialog: (props._toolboxVisible && state.showDialog)
109
-                || (!state.hasConnectedToConference
110
-                    && props._isConferenceJoined
111
-                    && props._isLonelyCall
112
-                    && props._toolboxVisible
113
-                    && !props._disableAutoShow)
114
-        };
115
-    }
116
-
117
-    /**
118
-     * Initializes new {@code InfoDialogButton} instance.
119
-     *
120
-     * @inheritdoc
121
-     */
122
-    constructor(props) {
123
-        super(props);
124
-
125
-        this.state = {
126
-            hasConnectedToConference: props._isConferenceJoined,
127
-            showDialog: false
128
-        };
129
-
130
-        // Bind event handlers so they are only bound once for every instance.
131
-        this._onDialogClose = this._onDialogClose.bind(this);
132
-        this._onDialogToggle = this._onDialogToggle.bind(this);
133
-        this._onClickOverflowMenuButton
134
-            = this._onClickOverflowMenuButton.bind(this);
135
-    }
136
-
137
-    /**
138
-     * Update dial-in numbers {@code InfoDialog}.
139
-     *
140
-     * @inheritdoc
141
-     */
142
-    componentDidMount() {
143
-        if (!this.props._dialIn.numbers) {
144
-            this.props.dispatch(updateDialInNumbers());
145
-        }
146
-    }
147
-
148
-    /**
149
-     * Implements React's {@link Component#render()}.
150
-     *
151
-     * @inheritdoc
152
-     * @returns {ReactElement}
153
-     */
154
-    render() {
155
-        const { _dialIn, _liveStreamViewURL, showLabel, t } = this.props;
156
-        const { showDialog } = this.state;
157
-
158
-        if (showLabel) {
159
-            return (
160
-                <OverflowMenuItem
161
-                    accessibilityLabel = { t('info.accessibilityLabel') }
162
-                    icon = 'icon-info'
163
-                    key = 'info-button'
164
-                    onClick = { this._onClickOverflowMenuButton }
165
-                    text = { t('info.label') } />
166
-            );
167
-        }
168
-
169
-        return (
170
-            <div className = 'toolbox-button-wth-dialog'>
171
-                <InlineDialog
172
-                    content = {
173
-                        <InfoDialog
174
-                            dialIn = { _dialIn }
175
-                            isInlineDialog = { true }
176
-                            liveStreamViewURL = { _liveStreamViewURL }
177
-                            onClose = { this._onDialogClose } /> }
178
-                    isOpen = { showDialog }
179
-                    onClose = { this._onDialogClose }
180
-                    position = { 'top right' }>
181
-                    <ToolbarButton
182
-                        accessibilityLabel = { t('info.accessibilityLabel') }
183
-                        icon = { IconInfo }
184
-                        onClick = { this._onDialogToggle }
185
-                        tooltip = { t('info.tooltip') } />
186
-                </InlineDialog>
187
-            </div>
188
-        );
189
-    }
190
-
191
-    _onDialogClose: () => void;
192
-
193
-    /**
194
-     * Hides {@code InfoDialog}.
195
-     *
196
-     * @private
197
-     * @returns {void}
198
-     */
199
-    _onDialogClose() {
200
-        this.setState({ showDialog: false });
201
-    }
202
-
203
-    _onClickOverflowMenuButton: () => void;
204
-
205
-    /**
206
-     * Opens the Info dialog.
207
-     *
208
-     * @returns {void}
209
-     */
210
-    _onClickOverflowMenuButton() {
211
-        const { _dialIn, _liveStreamViewURL } = this.props;
212
-
213
-        this.props.dispatch(openDialog(InfoDialog, {
214
-            dialIn: _dialIn,
215
-            liveStreamViewURL: _liveStreamViewURL,
216
-            isInlineDialog: false
217
-        }));
218
-    }
219
-
220
-    _onDialogToggle: () => void;
221
-
222
-    /**
223
-     * Toggles the display of {@code InfoDialog}.
224
-     *
225
-     * @private
226
-     * @returns {void}
227
-     */
228
-    _onDialogToggle() {
229
-        sendAnalytics(createToolbarEvent('info'));
230
-
231
-        this.setState({ showDialog: !this.state.showDialog });
232
-    }
233
-}
234
-
235
-/**
236
- * Maps (parts of) the Redux state to the associated {@code InfoDialogButton}
237
- * component's props.
238
- *
239
- * @param {Object} state - The Redux state.
240
- * @private
241
- * @returns {{
242
- *     _dialIn: Object,
243
- *     _disableAutoShow: boolean,
244
- *     _isConferenceIsJoined: boolean,
245
- *     _liveStreamViewURL: string,
246
- *     _isLonelyCall: boolean,
247
- *     _toolboxVisible: boolean
248
- * }}
249
- */
250
-function _mapStateToProps(state) {
251
-    const currentLiveStreamingSession
252
-        = getActiveSession(state, JitsiRecordingConstants.mode.STREAM);
253
-    const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
254
-
255
-    return {
256
-        _dialIn: state['features/invite'],
257
-        _disableAutoShow: iAmRecorder || iAmSipGateway,
258
-        _isConferenceJoined:
259
-            Boolean(state['features/base/conference'].conference),
260
-        _liveStreamViewURL:
261
-            currentLiveStreamingSession
262
-                && currentLiveStreamingSession.liveStreamViewURL,
263
-        _isLonelyCall: getParticipantCount(state) < 2,
264
-        _toolboxVisible: state['features/toolbox'].visible
265
-    };
266
-}
267
-
268
-export default translate(connect(_mapStateToProps)(InfoDialogButton));

+ 0
- 4
react/features/invite/components/info-dialog/web/index.js Ver arquivo

@@ -1,4 +0,0 @@
1
-// @flow
2
-
3
-export { default as InfoDialog } from './InfoDialog';
4
-export { default as InfoDialogButton } from './InfoDialogButton';

+ 52
- 0
react/features/invite/functions.js Ver arquivo

@@ -232,6 +232,58 @@ export function getInviteResultsForQuery(
232 232
         });
233 233
 }
234 234
 
235
+/**
236
+ * Creates a message describing how to dial in to the conference.
237
+ *
238
+ * @returns {string}
239
+ */
240
+export function getInviteText({
241
+    _conferenceName,
242
+    _localParticipantName,
243
+    _inviteUrl,
244
+    _locationUrl,
245
+    _dialIn,
246
+    _liveStreamViewURL,
247
+    phoneNumber,
248
+    t
249
+}: Object) {
250
+    const inviteURL = _decodeRoomURI(_inviteUrl);
251
+
252
+    let invite = _localParticipantName
253
+        ? t('info.inviteURLFirstPartPersonal', { name: _localParticipantName })
254
+        : t('info.inviteURLFirstPartGeneral');
255
+
256
+    invite += t('info.inviteURLSecondPart', {
257
+        url: inviteURL
258
+    });
259
+
260
+    if (_liveStreamViewURL) {
261
+        const liveStream = t('info.inviteLiveStream', {
262
+            url: _liveStreamViewURL
263
+        });
264
+
265
+        invite = `${invite}\n${liveStream}`;
266
+    }
267
+
268
+    if (shouldDisplayDialIn(_dialIn)) {
269
+        const dial = t('info.invitePhone', {
270
+            number: phoneNumber,
271
+            conferenceID: _dialIn.conferenceID
272
+        });
273
+        const moreNumbers = t('info.invitePhoneAlternatives', {
274
+            url: getDialInfoPageURL(
275
+                _conferenceName,
276
+                _locationUrl
277
+            ),
278
+            silentUrl: `${inviteURL}#config.startSilent=true`
279
+        });
280
+
281
+        invite = `${invite}\n${dial}\n${moreNumbers}`;
282
+    }
283
+
284
+    return invite;
285
+}
286
+
235 287
 /**
236 288
  * Helper for determining how many of each type of user is being invited. Used
237 289
  * for logging and sending analytics related to invites.

+ 15
- 0
react/features/security/actions.js Ver arquivo

@@ -0,0 +1,15 @@
1
+// @flow
2
+
3
+import { openDialog } from '../base/dialog';
4
+import { SecurityDialog } from './components/security-dialog';
5
+
6
+/**
7
+ * Action that triggers opening the security options dialog.
8
+ *
9
+ * @returns {Function}
10
+ */
11
+export function openSecurityDialog() {
12
+    return function(dispatch: (Object) => Object) {
13
+        dispatch(openDialog(SecurityDialog));
14
+    };
15
+}

+ 3
- 0
react/features/security/components/index.js Ver arquivo

@@ -0,0 +1,3 @@
1
+// @flow
2
+
3
+export * from './security-dialog';

+ 38
- 0
react/features/security/components/security-dialog/Header.js Ver arquivo

@@ -0,0 +1,38 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { Icon, IconClose } from '../../../base/icons';
7
+
8
+type Props = {
9
+
10
+    /**
11
+     * The {@link ModalDialog} closing function.
12
+     */
13
+    onClose: Function,
14
+
15
+    /**
16
+     * Invoked to obtain translated strings.
17
+     */
18
+    t: Function
19
+};
20
+
21
+/**
22
+ * Custom header of the {@code SecurityDialog}.
23
+ *
24
+ * @returns {React$Element<any>}
25
+ */
26
+function Header({ onClose, t }: Props) {
27
+    return (
28
+        <div
29
+            className = 'invite-more-dialog header'>
30
+            { t('security.securityOptions') }
31
+            <Icon
32
+                onClick = { onClose }
33
+                src = { IconClose } />
34
+        </div>
35
+    );
36
+}
37
+
38
+export default translate(Header);

react/features/invite/components/info-dialog/web/PasswordForm.js → react/features/security/components/security-dialog/PasswordForm.js Ver arquivo

@@ -2,8 +2,8 @@
2 2
 
3 3
 import React, { Component } from 'react';
4 4
 
5
-import { translate } from '../../../../base/i18n';
6
-import { LOCKED_LOCALLY } from '../../../../room-lock';
5
+import { translate } from '../../../base/i18n';
6
+import { LOCKED_LOCALLY } from '../../../room-lock';
7 7
 
8 8
 /**
9 9
  * The type of the React {@code Component} props of {@link PasswordForm}.

+ 190
- 0
react/features/security/components/security-dialog/PasswordSection.js Ver arquivo

@@ -0,0 +1,190 @@
1
+/* eslint-disable react/no-multi-comp */
2
+// @flow
3
+
4
+import React, { useRef } from 'react';
5
+
6
+import { translate } from '../../../base/i18n';
7
+import { copyText } from '../../../invite';
8
+
9
+import PasswordForm from './PasswordForm';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * Whether or not the current user can modify the current password.
15
+     */
16
+    canEditPassword: boolean,
17
+
18
+    /**
19
+     * The JitsiConference for which to display a lock state and change the
20
+     * password.
21
+     */
22
+    conference: Object,
23
+
24
+    /**
25
+     * The value for how the conference is locked (or undefined if not locked)
26
+     * as defined by room-lock constants.
27
+     */
28
+    locked: string,
29
+
30
+    /**
31
+     * The current known password for the JitsiConference.
32
+     */
33
+    password: string,
34
+
35
+    /**
36
+     * Whether or not to show the password in editing mode.
37
+     */
38
+    passwordEditEnabled: boolean,
39
+
40
+    /**
41
+     * The number of digits to be used in the password.
42
+     */
43
+    passwordNumberOfDigits: ?number,
44
+
45
+    /**
46
+     * Action that sets the conference password.
47
+     */
48
+    setPassword: Function,
49
+
50
+    /**
51
+     * Method that sets whether the password editing is enabled or not.
52
+     */
53
+    setPasswordEditEnabled: Function,
54
+
55
+    /**
56
+     * Invoked to obtain translated strings.
57
+     */
58
+    t: Function
59
+};
60
+
61
+/**
62
+ * Component that handles the password manipulation from the invite dialog.
63
+ *
64
+ * @returns {React$Element<any>}
65
+ */
66
+function PasswordSection({
67
+    canEditPassword,
68
+    conference,
69
+    locked,
70
+    password,
71
+    passwordEditEnabled,
72
+    passwordNumberOfDigits,
73
+    setPassword,
74
+    setPasswordEditEnabled,
75
+    t }: Props) {
76
+
77
+    const formRef: Object = useRef(null);
78
+
79
+    /**
80
+     * Callback invoked to set a password on the current JitsiConference.
81
+     *
82
+     * @param {string} enteredPassword - The new password to be used to lock the
83
+     * current JitsiConference.
84
+     * @private
85
+     * @returns {void}
86
+     */
87
+    function onPasswordSubmit(enteredPassword) {
88
+        setPassword(conference, conference.lock, enteredPassword);
89
+    }
90
+
91
+    /**
92
+     * Toggles whether or not the password should currently be shown as being
93
+     * edited locally.
94
+     *
95
+     * @private
96
+     * @returns {void}
97
+     */
98
+    function onTogglePasswordEditState() {
99
+        setPasswordEditEnabled(!passwordEditEnabled);
100
+    }
101
+
102
+    /**
103
+     * Method to remotely submit the password from outside of the password form.
104
+     *
105
+     * @returns {void}
106
+     */
107
+    function onPasswordSave() {
108
+        if (formRef.current) {
109
+            formRef.current.querySelector('form').requestSubmit();
110
+        }
111
+    }
112
+
113
+    /**
114
+     * Callback invoked to unlock the current JitsiConference.
115
+     *
116
+     * @returns {void}
117
+     */
118
+    function onPasswordRemove() {
119
+        onPasswordSubmit('');
120
+    }
121
+
122
+    /**
123
+     * Copies the password to the clipboard.
124
+     *
125
+     * @returns {void}
126
+     */
127
+    function onPasswordCopy() {
128
+        copyText(password);
129
+    }
130
+
131
+    /**
132
+     * Method that renders the password action(s) based on the current
133
+     * locked-status of the conference.
134
+     *
135
+     * @returns {React$Element<any>}
136
+     */
137
+    function renderPasswordActions() {
138
+        if (!canEditPassword) {
139
+            return null;
140
+        }
141
+
142
+        if (passwordEditEnabled) {
143
+            return (
144
+                <>
145
+                    <a onClick = { onTogglePasswordEditState }>{ t('dialog.Cancel') }</a>
146
+                    <a onClick = { onPasswordSave }>{ t('dialog.add') }</a>
147
+                </>
148
+            );
149
+        }
150
+
151
+        if (locked) {
152
+            return (
153
+                <>
154
+                    <a
155
+                        className = 'remove-password'
156
+                        onClick = { onPasswordRemove }>{ t('dialog.Remove') }</a>
157
+                    <a
158
+                        className = 'copy-password'
159
+                        onClick = { onPasswordCopy }>{ t('dialog.copy') }</a>
160
+                </>
161
+            );
162
+        }
163
+
164
+        return (
165
+            <a
166
+                className = 'add-password'
167
+                onClick = { onTogglePasswordEditState }>{ t('info.addPassword') }</a>
168
+        );
169
+    }
170
+
171
+    return (
172
+        <div className = 'security-dialog password'>
173
+            <div
174
+                className = 'info-dialog info-dialog-column info-dialog-password'
175
+                ref = { formRef }>
176
+                <PasswordForm
177
+                    editEnabled = { passwordEditEnabled }
178
+                    locked = { locked }
179
+                    onSubmit = { onPasswordSubmit }
180
+                    password = { password }
181
+                    passwordNumberOfDigits = { passwordNumberOfDigits } />
182
+            </div>
183
+            <div className = 'security-dialog password-actions'>
184
+                { renderPasswordActions() }
185
+            </div>
186
+        </div>
187
+    );
188
+}
189
+
190
+export default translate(PasswordSection);

+ 126
- 0
react/features/security/components/security-dialog/SecurityDialog.js Ver arquivo

@@ -0,0 +1,126 @@
1
+// @flow
2
+
3
+import React, { useState, useEffect } from 'react';
4
+
5
+import { setPassword as setPass } from '../../../base/conference';
6
+import { Dialog } from '../../../base/dialog';
7
+import { translate } from '../../../base/i18n';
8
+import { isLocalParticipantModerator } from '../../../base/participants';
9
+import { connect } from '../../../base/redux';
10
+
11
+import Header from './Header';
12
+import PasswordSection from './PasswordSection';
13
+
14
+type Props = {
15
+
16
+    /**
17
+     * Whether or not the current user can modify the current password.
18
+     */
19
+    _canEditPassword: boolean,
20
+
21
+    /**
22
+     * The JitsiConference for which to display a lock state and change the
23
+     * password.
24
+     */
25
+    _conference: Object,
26
+
27
+    /**
28
+     * The value for how the conference is locked (or undefined if not locked)
29
+     * as defined by room-lock constants.
30
+     */
31
+    _locked: string,
32
+
33
+    /**
34
+     * The current known password for the JitsiConference.
35
+     */
36
+    _password: string,
37
+
38
+    /**
39
+     * The number of digits to be used in the password.
40
+     */
41
+    _passwordNumberOfDigits: ?number,
42
+
43
+    /**
44
+     * Action that sets the conference password.
45
+     */
46
+    setPassword: Function,
47
+
48
+    /**
49
+     * Invoked to obtain translated strings.
50
+     */
51
+    t: Function
52
+};
53
+
54
+/**
55
+ * Component that renders the security options dialog.
56
+ *
57
+ * @returns {React$Element<any>}
58
+ */
59
+function SecurityDialog({
60
+    _canEditPassword,
61
+    _conference,
62
+    _locked,
63
+    _password,
64
+    _passwordNumberOfDigits,
65
+    setPassword,
66
+    t
67
+}: Props) {
68
+    const [ passwordEditEnabled, setPasswordEditEnabled ] = useState(false);
69
+
70
+    useEffect(() => {
71
+        if (passwordEditEnabled && _password) {
72
+            setPasswordEditEnabled(false);
73
+        }
74
+    }, [ _password ]);
75
+
76
+    return (
77
+        <Dialog
78
+            customHeader = { Header }
79
+            hideCancelButton = { true }
80
+            submitDisabled = { true }
81
+            titleKey = 'security.securityOptions'
82
+            width = { 'small' }>
83
+            <div className = 'security-dialog'>
84
+                { t('security.about') }
85
+                <div className = 'invite-more-dialog separator' />
86
+                <PasswordSection
87
+                    canEditPassword = { _canEditPassword }
88
+                    conference = { _conference }
89
+                    locked = { _locked }
90
+                    password = { _password }
91
+                    passwordEditEnabled = { passwordEditEnabled }
92
+                    passwordNumberOfDigits = { _passwordNumberOfDigits }
93
+                    setPassword = { setPassword }
94
+                    setPasswordEditEnabled = { setPasswordEditEnabled } />
95
+            </div>
96
+        </Dialog>
97
+    );
98
+}
99
+
100
+/**
101
+ * Maps (parts of) the Redux state to the associated props for the
102
+ * {@code SecurityDialog} component.
103
+ *
104
+ * @param {Object} state - The Redux state.
105
+ * @private
106
+ * @returns {Props}
107
+ */
108
+function mapStateToProps(state) {
109
+    const {
110
+        conference,
111
+        locked,
112
+        password
113
+    } = state['features/base/conference'];
114
+
115
+    return {
116
+        _canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled),
117
+        _conference: conference,
118
+        _dialIn: state['features/invite'],
119
+        _locked: locked,
120
+        _password: password
121
+    };
122
+}
123
+
124
+const mapDispatchToProps = { setPassword: setPass };
125
+
126
+export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialog));

+ 83
- 0
react/features/security/components/security-dialog/SecurityDialogButton.js Ver arquivo

@@ -0,0 +1,83 @@
1
+// @flow
2
+
3
+import { createToolbarEvent, sendAnalytics } from '../../../analytics';
4
+import { translate } from '../../../base/i18n';
5
+import { IconLockPassword, IconUnlockPassword } from '../../../base/icons';
6
+import { connect } from '../../../base/redux';
7
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox';
8
+
9
+import { openSecurityDialog } from '../../actions';
10
+
11
+
12
+type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * Whether the shared document is being edited or not.
16
+     */
17
+    _locked: boolean,
18
+
19
+    /**
20
+     * On click handler that opens the security dialog.
21
+     */
22
+    onClick: Function
23
+
24
+};
25
+
26
+
27
+/**
28
+ * Implements an {@link AbstractButton} to open the security dialog.
29
+ */
30
+class SecurityDialogButton extends AbstractButton<Props, *> {
31
+    accessibilityLabel = 'toolbar.accessibilityLabel.security';
32
+    icon = IconUnlockPassword;
33
+    label = 'toolbar.security';
34
+    toggledIcon = IconLockPassword;
35
+    tooltip = 'toolbar.security';
36
+
37
+    /**
38
+     * Handles clicking / pressing the button, and opens / closes the appropriate dialog.
39
+     *
40
+     * @private
41
+     * @returns {void}
42
+     */
43
+    _handleClick() {
44
+        sendAnalytics(createToolbarEvent('toggle.security', { enable: !this.props._locked }));
45
+        this.props.onClick();
46
+    }
47
+
48
+    /**
49
+     * Indicates whether this button is in toggled state or not.
50
+     *
51
+     * @override
52
+     * @returns {boolean}
53
+     */
54
+    _isToggled() {
55
+        return this.props._locked;
56
+    }
57
+}
58
+
59
+/**
60
+ * Maps part of the redux state to the component's props.
61
+ *
62
+ * @param {Object} state - The redux store/state.
63
+ * @returns {Props}
64
+ */
65
+function mapStateToProps(state: Object) {
66
+    const { locked } = state['features/base/conference'];
67
+
68
+    return {
69
+        _locked: locked
70
+    };
71
+}
72
+
73
+/**
74
+ * Maps dispatching of some action to React component props.
75
+ *
76
+ * @param {Function} dispatch - Redux action dispatcher.
77
+ * @returns {Props}
78
+ */
79
+const mapDispatchToProps = {
80
+    onClick: () => openSecurityDialog()
81
+};
82
+
83
+export default translate(connect(mapStateToProps, mapDispatchToProps)(SecurityDialogButton));

+ 4
- 0
react/features/security/components/security-dialog/index.js Ver arquivo

@@ -0,0 +1,4 @@
1
+// @flow
2
+
3
+export { default as SecurityDialog } from './SecurityDialog';
4
+export { default as SecurityDialogButton } from './SecurityDialogButton';

+ 4
- 0
react/features/security/index.js Ver arquivo

@@ -0,0 +1,4 @@
1
+// @flow
2
+
3
+export * from './actions';
4
+export * from './components';

+ 27
- 33
react/features/toolbox/components/web/Toolbox.js Ver arquivo

@@ -15,7 +15,7 @@ import {
15 15
     IconExitFullScreen,
16 16
     IconFeedback,
17 17
     IconFullScreen,
18
-    IconInvite,
18
+    IconInviteMore,
19 19
     IconOpenInNew,
20 20
     IconPresentation,
21 21
     IconRaisedHand,
@@ -36,12 +36,7 @@ import { ChatCounter, toggleChat } from '../../../chat';
36 36
 import { E2EEButton } from '../../../e2ee';
37 37
 import { SharedDocumentButton } from '../../../etherpad';
38 38
 import { openFeedbackDialog } from '../../../feedback';
39
-import {
40
-    beginAddPeople,
41
-    InfoDialogButton,
42
-    isAddPeopleEnabled,
43
-    isDialOutEnabled
44
-} from '../../../invite';
39
+import { beginAddPeople } from '../../../invite';
45 40
 import { openKeyboardShortcutsDialog } from '../../../keyboard-shortcuts';
46 41
 import {
47 42
     LocalRecordingButton,
@@ -51,6 +46,7 @@ import {
51 46
     LiveStreamButton,
52 47
     RecordButton
53 48
 } from '../../../recording';
49
+import { SecurityDialogButton } from '../../../security';
54 50
 import {
55 51
     SETTINGS_TABS,
56 52
     SettingsButton,
@@ -132,12 +128,6 @@ type Props = {
132 128
      */
133 129
     _tileViewEnabled: boolean,
134 130
 
135
-    /**
136
-     * Whether or not invite should be hidden, regardless of feature
137
-     * availability.
138
-     */
139
-    _hideInviteButton: boolean,
140
-
141 131
     /**
142 132
      * Whether or not the current user is logged in through a JWT.
143 133
      */
@@ -153,6 +143,12 @@ type Props = {
153 143
      */
154 144
     _localRecState: Object,
155 145
 
146
+    /**
147
+     * The value for how the conference is locked (or undefined if not locked)
148
+     * as defined by room-lock constants.
149
+     */
150
+    _locked: boolean,
151
+
156 152
     /**
157 153
      * Whether or not the overflow menu is visible.
158 154
      */
@@ -1093,13 +1089,17 @@ class Toolbox extends Component<Props, State> {
1093 1089
                 );
1094 1090
             case 'closedcaptions':
1095 1091
                 return <ClosedCaptionButton showLabel = { true } />;
1096
-            case 'info':
1097
-                return <InfoDialogButton showLabel = { true } />;
1092
+            case 'security':
1093
+                return (
1094
+                    <SecurityDialogButton
1095
+                        key = 'security'
1096
+                        showLabel = { true } />
1097
+                );
1098 1098
             case 'invite':
1099 1099
                 return (
1100 1100
                     <OverflowMenuItem
1101 1101
                         accessibilityLabel = { t('toolbar.accessibilityLabel.invite') }
1102
-                        icon = { IconInvite }
1102
+                        icon = { IconInviteMore }
1103 1103
                         key = 'invite'
1104 1104
                         onClick = { this._onToolbarOpenInvite }
1105 1105
                         text = { t('toolbar.invite') } />
@@ -1155,7 +1155,6 @@ class Toolbox extends Component<Props, State> {
1155 1155
     _renderToolboxContent() {
1156 1156
         const {
1157 1157
             _chatOpen,
1158
-            _hideInviteButton,
1159 1158
             _overflowMenuVisible,
1160 1159
             _raisedHand,
1161 1160
             t
@@ -1192,12 +1191,13 @@ class Toolbox extends Component<Props, State> {
1192 1191
         if (overflowHasItems) {
1193 1192
             buttonsRight.push('overflowmenu');
1194 1193
         }
1195
-        if (this._shouldShowButton('info')) {
1196
-            buttonsRight.push('info');
1197
-        }
1198
-        if (this._shouldShowButton('invite') && !_hideInviteButton) {
1194
+        if (this._shouldShowButton('invite')) {
1199 1195
             buttonsRight.push('invite');
1200 1196
         }
1197
+        if (this._shouldShowButton('security') || this._shouldShowButton('info')) {
1198
+            buttonsRight.push('security');
1199
+        }
1200
+
1201 1201
         if (this._shouldShowButton('tileview')) {
1202 1202
             buttonsRight.push('tileview');
1203 1203
         }
@@ -1283,13 +1283,11 @@ class Toolbox extends Component<Props, State> {
1283 1283
                         && <ToolbarButton
1284 1284
                             accessibilityLabel =
1285 1285
                                 { t('toolbar.accessibilityLabel.invite') }
1286
-                            icon = { IconInvite }
1286
+                            icon = { IconInviteMore }
1287 1287
                             onClick = { this._onToolbarOpenInvite }
1288 1288
                             tooltip = { t('toolbar.invite') } /> }
1289
-                    {
1290
-                        buttonsRight.indexOf('info') !== -1
1291
-                            && <InfoDialogButton />
1292
-                    }
1289
+                    { buttonsRight.indexOf('security') !== -1
1290
+                        && <SecurityDialogButton customClass = 'security-toolbar-button' /> }
1293 1291
                     { buttonsRight.indexOf('overflowmenu') !== -1
1294 1292
                         && <OverflowMenuButton
1295 1293
                             isOpen = { _overflowMenuVisible }
@@ -1328,12 +1326,11 @@ class Toolbox extends Component<Props, State> {
1328 1326
  * @returns {{}}
1329 1327
  */
1330 1328
 function _mapStateToProps(state) {
1331
-    const { conference } = state['features/base/conference'];
1329
+    const { conference, locked } = state['features/base/conference'];
1332 1330
     let { desktopSharingEnabled } = state['features/base/conference'];
1333 1331
     const {
1334 1332
         callStatsID,
1335
-        enableFeaturesBasedOnToken,
1336
-        iAmRecorder
1333
+        enableFeaturesBasedOnToken
1337 1334
     } = state['features/base/config'];
1338 1335
     const sharedVideoStatus = state['features/shared-video'].status;
1339 1336
     const {
@@ -1343,8 +1340,6 @@ function _mapStateToProps(state) {
1343 1340
     const localParticipant = getLocalParticipant(state);
1344 1341
     const localRecordingStates = state['features/local-recording'];
1345 1342
     const localVideo = getLocalVideoTrack(state['features/base/tracks']);
1346
-    const addPeopleEnabled = isAddPeopleEnabled(state);
1347
-    const dialOutEnabled = isDialOutEnabled(state);
1348 1343
 
1349 1344
     let desktopSharingDisabledTooltipKey;
1350 1345
 
@@ -1376,13 +1371,12 @@ function _mapStateToProps(state) {
1376 1371
         _desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey,
1377 1372
         _dialog: Boolean(state['features/base/dialog'].component),
1378 1373
         _feedbackConfigured: Boolean(callStatsID),
1379
-        _hideInviteButton:
1380
-            iAmRecorder || (!addPeopleEnabled && !dialOutEnabled),
1381 1374
         _isGuest: state['features/base/jwt'].isGuest,
1382 1375
         _fullScreen: fullScreen,
1383 1376
         _tileViewEnabled: state['features/video-layout'].tileViewEnabled,
1384 1377
         _localParticipantID: localParticipant.id,
1385 1378
         _localRecState: localRecordingStates,
1379
+        _locked: locked,
1386 1380
         _overflowMenuVisible: overflowMenuVisible,
1387 1381
         _raisedHand: localParticipant.raisedHand,
1388 1382
         _screensharing: localVideo && localVideo.videoType === 'desktop',

Carregando…
Cancelar
Salvar