Browse Source

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

j8
Mihai Uscat 5 years ago
parent
commit
75c836c70c
52 changed files with 2251 additions and 1481 deletions
  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 View File

94
 @import 'prejoin';
94
 @import 'prejoin';
95
 @import 'prejoin-dialog';
95
 @import 'prejoin-dialog';
96
 @import 'country-picker';
96
 @import 'country-picker';
97
+@import 'modals/invite/invite_more';
98
+@import 'modals/security/security';
97
 
99
 
98
 /* Modules END */
100
 /* Modules END */

+ 1
- 0
css/modals/invite/_add-people.scss View File

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

+ 0
- 93
css/modals/invite/_info.scss View File

3
     display: flex;
3
     display: flex;
4
     font-size: 14px;
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
     .info-dialog-column {
6
     .info-dialog-column {
48
         margin-right: 10px;
7
         margin-right: 10px;
49
         overflow: hidden;
8
         overflow: hidden;
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
     .info-dialog-password,
18
     .info-dialog-password,
106
     .info-password,
19
     .info-password,
107
     .info-password-form {
20
     .info-password-form {
223
         -moz-user-select: text;
136
         -moz-user-select: text;
224
         -webkit-user-select: text;
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 View File

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 View File

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 View File

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

+ 22
- 2
lang/main.json View File

1
 {
1
 {
2
     "addPeople": {
2
     "addPeople": {
3
         "add": "Invite",
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
         "countryNotSupported": "We do not support this destination yet.",
8
         "countryNotSupported": "We do not support this destination yet.",
5
         "countryReminder": "Calling outside the US? Please make sure you start with the country code!",
9
         "countryReminder": "Calling outside the US? Please make sure you start with the country code!",
10
+        "defaultEmail": "Your Default Email",
6
         "disabled": "You can't invite people.",
11
         "disabled": "You can't invite people.",
7
         "failedToAdd": "Failed to add participants",
12
         "failedToAdd": "Failed to add participants",
8
         "footerText": "Dialing out is disabled.",
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
         "loading": "Searching for people and phone numbers",
19
         "loading": "Searching for people and phone numbers",
10
         "loadingNumber": "Validating phone number",
20
         "loadingNumber": "Validating phone number",
11
         "loadingPeople": "Searching for people to invite",
21
         "loadingPeople": "Searching for people to invite",
12
         "noResults": "No matching search results",
22
         "noResults": "No matching search results",
13
         "noValidNumbers": "Please enter a phone number",
23
         "noValidNumbers": "Please enter a phone number",
24
+        "outlookEmail": "Outlook Email",
14
         "searchNumbers": "Add phone numbers",
25
         "searchNumbers": "Add phone numbers",
15
         "searchPeople": "Search for people",
26
         "searchPeople": "Search for people",
16
         "searchPeopleAndNumbers": "Search for people or add their phone numbers",
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
         "telephone": "Telephone: {{number}}",
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
     "audioDevices": {
35
     "audioDevices": {
21
         "bluetooth": "Bluetooth",
36
         "bluetooth": "Bluetooth",
146
         "accessibilityLabel": {
161
         "accessibilityLabel": {
147
             "liveStreaming": "Live Stream"
162
             "liveStreaming": "Live Stream"
148
         },
163
         },
164
+        "add": "Add",
149
         "allow": "Allow",
165
         "allow": "Allow",
150
         "alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
166
         "alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
151
         "alreadySharedVideoTitle": "Only one shared video is allowed at a time",
167
         "alreadySharedVideoTitle": "Only one shared video is allowed at a time",
562
         "pullToRefresh": "Pull to refresh"
578
         "pullToRefresh": "Pull to refresh"
563
     },
579
     },
564
     "security": {
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
     "settings": {
585
     "settings": {
568
         "calendar": {
586
         "calendar": {
662
             "raiseHand": "Toggle raise hand",
680
             "raiseHand": "Toggle raise hand",
663
             "recording": "Toggle recording",
681
             "recording": "Toggle recording",
664
             "remoteMute": "Mute participant",
682
             "remoteMute": "Mute participant",
683
+            "security": "Security options",
665
             "Settings": "Toggle settings",
684
             "Settings": "Toggle settings",
666
             "sharedvideo": "Toggle Youtube video sharing",
685
             "sharedvideo": "Toggle Youtube video sharing",
667
             "shareRoom": "Invite someone",
686
             "shareRoom": "Invite someone",
715
         "profile": "Edit your profile",
734
         "profile": "Edit your profile",
716
         "raiseHand": "Raise / Lower your hand",
735
         "raiseHand": "Raise / Lower your hand",
717
         "raiseYourHand": "Raise your hand",
736
         "raiseYourHand": "Raise your hand",
737
+        "security": "Security options",
718
         "Settings": "Settings",
738
         "Settings": "Settings",
719
         "sharedvideo": "Share a YouTube video",
739
         "sharedvideo": "Share a YouTube video",
720
         "shareRoom": "Invite someone",
740
         "shareRoom": "Invite someone",

+ 11
- 2
react/features/base/dialog/components/web/StatelessDialog.js View File

29
 type Props = {
29
 type Props = {
30
     ...DialogProps,
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
      * Disables dismissing the dialog when the blanket is clicked. Enabled
38
      * Disables dismissing the dialog when the blanket is clicked. Enabled
43
      */
46
      */
44
     hideCancelButton: boolean,
47
     hideCancelButton: boolean,
45
 
48
 
49
+    i18n: Object,
50
+
46
     /**
51
     /**
47
      * Whether the dialog is modal. This means clicking on the blanket will
52
      * Whether the dialog is modal. This means clicking on the blanket will
48
      * leave the dialog open. No cancel button.
53
      * leave the dialog open. No cancel button.
106
      */
111
      */
107
     render() {
112
     render() {
108
         const {
113
         const {
114
+            customHeader,
109
             children,
115
             children,
110
             t /* The following fixes a flow error: */ = _.identity,
116
             t /* The following fixes a flow error: */ = _.identity,
111
             titleString,
117
             titleString,
116
         return (
122
         return (
117
             <Modal
123
             <Modal
118
                 autoFocus = { true }
124
                 autoFocus = { true }
125
+                components = {{
126
+                    Header: customHeader
127
+                }}
119
                 footer = { this._renderFooter }
128
                 footer = { this._renderFooter }
120
-                heading = { titleString || t(titleKey) }
129
+                heading = { customHeader ? undefined : titleString || t(titleKey) }
121
                 i18n = { this.props.i18n }
130
                 i18n = { this.props.i18n }
122
                 onClose = { this._onDialogDismissed }
131
                 onClose = { this._onDialogDismissed }
123
                 onDialogDismissed = { this._onDialogDismissed }
132
                 onDialogDismissed = { this._onDialogDismissed }

+ 3
- 0
react/features/base/icons/svg/arrow-down-small.svg View File

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 View File

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 View File

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 View File

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

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 View File

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 View File

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 View File

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 View File

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 View File

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 View File

35
      */
35
      */
36
     accessibilityLabel: string,
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
      * Whether this item is disabled or not. When disabled, clicking an the item
45
      * Whether this item is disabled or not. When disabled, clicking an the item
40
      * has no effect, and it may reflect on its style.
46
      * has no effect, and it may reflect on its style.

+ 2
- 2
react/features/base/toolbox/components/ToolboxItem.web.js View File

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

+ 6
- 3
react/features/conference/components/web/Conference.js View File

25
 
25
 
26
 import { maybeShowSuboptimalExperienceNotification } from '../../functions';
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
 import {
28
 import {
32
     AbstractConference,
29
     AbstractConference,
33
     abstractMapStateToProps
30
     abstractMapStateToProps
34
 } from '../AbstractConference';
31
 } from '../AbstractConference';
35
 import type { AbstractProps } from '../AbstractConference';
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
 declare var APP: Object;
39
 declare var APP: Object;
38
 declare var config: Object;
40
 declare var config: Object;
39
 declare var interfaceConfig: Object;
41
 declare var interfaceConfig: Object;
202
 
204
 
203
                 <Notice />
205
                 <Notice />
204
                 <Subject />
206
                 <Subject />
207
+                <InviteMore />
205
                 <div id = 'videospace'>
208
                 <div id = 'videospace'>
206
                     <LargeVideo />
209
                     <LargeVideo />
207
                     { hideVideoQualityLabel
210
                     { hideVideoQualityLabel

+ 94
- 0
react/features/conference/components/web/InviteMore.js View File

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 View File

75
     return {
75
     return {
76
         _showParticipantCount: participantCount > 2,
76
         _showParticipantCount: participantCount > 2,
77
         _subject: getConferenceName(state),
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 View File

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

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

1
 // @flow
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
 import { createInviteDialogEvent, sendAnalytics } from '../../../../analytics';
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
 import { getLocalParticipant } from '../../../../base/participants';
11
 import { getLocalParticipant } from '../../../../base/participants';
13
-import { MultiSelectAutocomplete } from '../../../../base/react';
14
 import { connect } from '../../../../base/redux';
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
 declare var interfaceConfig: Object;
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
      * @returns {void}
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
      * @returns {void}
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
             </div>
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
  * @param {Object} state - The Redux state.
176
  * @param {Object} state - The Redux state.
450
  * @private
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
     return {
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 View File

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 View File

3
 import React, { Component } from 'react';
3
 import React, { Component } from 'react';
4
 
4
 
5
 import { translate } from '../../../../base/i18n';
5
 import { translate } from '../../../../base/i18n';
6
+import { Icon, IconCopy } from '../../../../base/icons';
6
 
7
 
7
 import { _formatConferenceIDPin } from '../../../_utils';
8
 import { _formatConferenceIDPin } from '../../../_utils';
8
 
9
 
10
+import { copyText } from './utils';
11
+
9
 /**
12
 /**
10
  * The type of the React {@code Component} props of {@link DialInNumber}.
13
  * The type of the React {@code Component} props of {@link DialInNumber}.
11
  */
14
  */
36
  * @extends Component
39
  * @extends Component
37
  */
40
  */
38
 class DialInNumber extends Component<Props> {
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
      * Implements React's {@link Component#render()}.
74
      * Implements React's {@link Component#render()}.
41
      *
75
      *
66
                         { `${_formatConferenceIDPin(conferenceID)}#` }
100
                         { `${_formatConferenceIDPin(conferenceID)}#` }
67
                     </span>
101
                     </span>
68
                 </span>
102
                 </span>
103
+                <a
104
+                    className = 'dial-in-copy'
105
+                    onClick = { this._onCopyText }>
106
+                    <Icon src = { IconCopy } />
107
+                </a>
69
             </div>
108
             </div>
70
         );
109
         );
71
     }
110
     }

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

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 View File

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 View File

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 View File

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 View File

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 View File

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 View File

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

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

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 View File

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

+ 0
- 0
react/features/invite/components/info-dialog/index.native.js View File


+ 0
- 3
react/features/invite/components/info-dialog/index.web.js View File

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

+ 0
- 644
react/features/invite/components/info-dialog/web/InfoDialog.js View File

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 View File

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 View File

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

+ 52
- 0
react/features/invite/functions.js View File

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
  * Helper for determining how many of each type of user is being invited. Used
288
  * Helper for determining how many of each type of user is being invited. Used
237
  * for logging and sending analytics related to invites.
289
  * for logging and sending analytics related to invites.

+ 15
- 0
react/features/security/actions.js View File

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 View File

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

+ 38
- 0
react/features/security/components/security-dialog/Header.js View File

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 View File

2
 
2
 
3
 import React, { Component } from 'react';
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
  * The type of the React {@code Component} props of {@link PasswordForm}.
9
  * The type of the React {@code Component} props of {@link PasswordForm}.

+ 190
- 0
react/features/security/components/security-dialog/PasswordSection.js View File

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 View File

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 View File

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 View File

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

+ 4
- 0
react/features/security/index.js View File

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

+ 27
- 33
react/features/toolbox/components/web/Toolbox.js View File

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

Loading…
Cancel
Save