浏览代码

feat(embed) implement embed meeting feature

master
Tudor-Ovidiu Avram 4 年前
父节点
当前提交
b1e12d33ab

+ 1
- 0
css/buttons/copy.scss 查看文件

21
         text-overflow: ellipsis;
21
         text-overflow: ellipsis;
22
         white-space: nowrap;
22
         white-space: nowrap;
23
         max-width: 292px;
23
         max-width: 292px;
24
+        margin-right: 16px;
24
 
25
 
25
         &.selected {
26
         &.selected {
26
             font-weight: 600;
27
             font-weight: 600;

+ 1
- 0
css/main.scss 查看文件

37
 @import 'modals/desktop-picker/desktop-picker';
37
 @import 'modals/desktop-picker/desktop-picker';
38
 @import 'modals/device-selection/device-selection';
38
 @import 'modals/device-selection/device-selection';
39
 @import 'modals/dialog';
39
 @import 'modals/dialog';
40
+@import 'modals/embed-meeting/embed-meeting';
40
 @import 'modals/feedback/feedback';
41
 @import 'modals/feedback/feedback';
41
 @import 'modals/invite/info';
42
 @import 'modals/invite/info';
42
 @import 'modals/settings/settings';
43
 @import 'modals/settings/settings';

+ 59
- 0
css/modals/embed-meeting/_embed-meeting.scss 查看文件

1
+.embed-meeting {
2
+    &-dialog {
3
+        display: flex;
4
+        flex-direction: column;
5
+
6
+        &-header {
7
+            display: flex;
8
+            justify-content: space-between;
9
+            margin: 16px 16px 24px;
10
+            width: calc(100% - 32px);
11
+            color: #fff;
12
+            font-weight: 600;
13
+            font-size: 24px;
14
+            line-height: 32px;
15
+
16
+            & > div > svg {
17
+                cursor: pointer;
18
+                fill: #A4B8D1;
19
+            }
20
+        }
21
+    }
22
+
23
+    &-copy {
24
+        color: white;
25
+        font-size: 15px;
26
+        margin-left: auto;
27
+        margin-top: 16px;
28
+        width: auto;
29
+    }
30
+
31
+    &-code {
32
+        background: transparent;
33
+        border: 1px solid #A4B8D1;
34
+        color: white;
35
+        font-size: 15px;
36
+        height: 165px;
37
+        line-height: 24px;
38
+        padding: 8px;
39
+        width: 100%;
40
+        resize: vertical;
41
+    }
42
+
43
+    &-trigger {
44
+        display: flex;
45
+        align-items: center;
46
+        padding: 8px 8px 8px 16px;
47
+        margin-top: 24px;
48
+        width: calc(100% - 24px);
49
+        height: 24px;
50
+        background: #2A3A4B;
51
+        border: 1px solid #5E6D7A;
52
+        border-radius: 4px;
53
+        cursor: pointer;
54
+
55
+        .jitsi-icon {
56
+            margin-right: 20px;
57
+        }
58
+    }
59
+}

+ 0
- 4
css/modals/invite/_invite_more.scss 查看文件

47
         font-size: 15px;
47
         font-size: 15px;
48
         line-height: 24px;
48
         line-height: 24px;
49
 
49
 
50
-        & > span {
51
-            font-weight: 600;
52
-        }
53
-
54
         &.header {
50
         &.header {
55
             display: flex;
51
             display: flex;
56
             justify-content: space-between;
52
             justify-content: space-between;

+ 1
- 1
interface_config.js 查看文件

185
      * - 'desktop' controls the "Share your screen" button
185
      * - 'desktop' controls the "Share your screen" button
186
      */
186
      */
187
     TOOLBAR_BUTTONS: [
187
     TOOLBAR_BUTTONS: [
188
-        'microphone', 'camera', 'closedcaptions', 'desktop', 'fullscreen',
188
+        'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
189
         'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
189
         'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
190
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
190
         'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
191
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
191
         'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',

+ 6
- 0
lang/main.json 查看文件

190
         "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: {{msg}}",
190
         "connectErrorWithMsg": "Oops! Something went wrong and we couldn't connect to the conference: {{msg}}",
191
         "connecting": "Connecting",
191
         "connecting": "Connecting",
192
         "contactSupport": "Contact support",
192
         "contactSupport": "Contact support",
193
+        "copied": "Copied",
193
         "copy": "Copy",
194
         "copy": "Copy",
194
         "dismiss": "Dismiss",
195
         "dismiss": "Dismiss",
195
         "displayNameRequired": "Hi! What’s your name?",
196
         "displayNameRequired": "Hi! What’s your name?",
316
     "e2ee": {
317
     "e2ee": {
317
         "labelToolTip": "Audio and Video Communication on this call is end-to-end encrypted"
318
         "labelToolTip": "Audio and Video Communication on this call is end-to-end encrypted"
318
     },
319
     },
320
+    "embedMeeting": {
321
+        "title": "Embed this meeting"
322
+    },
319
     "feedback": {
323
     "feedback": {
320
         "average": "Average",
324
         "average": "Average",
321
         "bad": "Bad",
325
         "bad": "Bad",
670
             "chat": "Toggle chat window",
674
             "chat": "Toggle chat window",
671
             "document": "Toggle shared document",
675
             "document": "Toggle shared document",
672
             "download": "Download our apps",
676
             "download": "Download our apps",
677
+            "embedMeeting": "Embed meeting",
673
             "e2ee": "End-to-End Encryption",
678
             "e2ee": "End-to-End Encryption",
674
             "feedback": "Leave feedback",
679
             "feedback": "Leave feedback",
675
             "fullScreen": "Toggle full screen",
680
             "fullScreen": "Toggle full screen",
718
         "documentOpen": "Open shared document",
723
         "documentOpen": "Open shared document",
719
         "download": "Download our apps",
724
         "download": "Download our apps",
720
         "e2ee": "End-to-End Encryption",
725
         "e2ee": "End-to-End Encryption",
726
+        "embedMeeting": "Embed meeting",
721
         "enterFullScreen": "View full screen",
727
         "enterFullScreen": "View full screen",
722
         "enterTileView": "Enter tile view",
728
         "enterTileView": "Enter tile view",
723
         "exitFullScreen": "Exit full screen",
729
         "exitFullScreen": "Exit full screen",

+ 4
- 0
react/features/base/icons/svg/code-block.svg 查看文件

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="M15.8716 7.70278C15.4611 7.33332 15.4278 6.70103 15.7972 6.29052C16.1667 5.88001 16.799 5.84673 17.2095 6.21619L22.4652 10.9212C22.9066 11.3184 22.9066 12.0105 22.4652 12.4077L17.2095 17.0394C16.799 17.4089 16.1667 17.3756 15.7972 16.9651C15.4278 16.5546 15.4611 15.9223 15.8716 15.5528L20.3014 11.6645L15.8716 7.70278Z" fill="#A4B8D1"/>
3
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.4651 15.5531C8.87561 15.9225 8.90889 16.5548 8.53943 16.9653C8.16997 17.3759 7.53768 17.4091 7.12717 17.0397L1.87145 12.3347C1.43007 11.9375 1.43007 11.2454 1.87145 10.8481L7.12717 6.21644C7.53768 5.84698 8.16997 5.88026 8.53943 6.29077C8.90889 6.70128 8.87561 7.33357 8.4651 7.70302L4.03526 11.5914L8.4651 15.5531Z" fill="#A4B8D1"/>
4
+</svg>

+ 1
- 0
react/features/base/icons/svg/index.js 查看文件

20
 export { default as IconClose } from './close.svg';
20
 export { default as IconClose } from './close.svg';
21
 export { default as IconCloseX } from './close-x.svg';
21
 export { default as IconCloseX } from './close-x.svg';
22
 export { default as IconClosedCaption } from './closed_caption.svg';
22
 export { default as IconClosedCaption } from './closed_caption.svg';
23
+export { default as IconCodeBlock } from './code-block.svg';
23
 export { default as IconConnectionActive } from './gsm-bars.svg';
24
 export { default as IconConnectionActive } from './gsm-bars.svg';
24
 export { default as IconConnectionInactive } from './ninja.svg';
25
 export { default as IconConnectionInactive } from './ninja.svg';
25
 export { default as IconCopy } from './copy.svg';
26
 export { default as IconCopy } from './copy.svg';

+ 69
- 0
react/features/embed-meeting/components/EmbedMeetingDialog.js 查看文件

1
+// @flow
2
+
3
+import React from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import CopyButton from '../../base/buttons/CopyButton';
7
+import { getInviteURL } from '../../base/connection';
8
+import { Dialog } from '../../base/dialog';
9
+import { translate } from '../../base/i18n';
10
+
11
+import Header from './Header';
12
+
13
+type Props = {
14
+
15
+    /**
16
+     * Invoked to obtain translated strings.
17
+     */
18
+    t: Function,
19
+
20
+    /**
21
+     * The URL of the conference.
22
+     */
23
+    url: string
24
+};
25
+
26
+/**
27
+ * Allow users to embed a jitsi meeting in an iframe.
28
+ *
29
+ * @returns {React$Element<any>}
30
+ */
31
+function EmbedMeeting({ t, url }: Props) {
32
+    /**
33
+     * Get the embed code for a jitsi meeting.
34
+     *
35
+     * @returns {string} The iframe embed code.
36
+     */
37
+    const getEmbedCode = () =>
38
+        `<iframe allow="camera; microphone; display-capture" src="${url}`
39
+        + 'allowfullscreen="true" style="height: 100%; width: 100%; border: 0px;"></iframe>';
40
+
41
+    return (
42
+        <Dialog
43
+            customHeader = { Header }
44
+            hideCancelButton = { true }
45
+            submitDisabled = { true }
46
+            width = 'small'>
47
+            <div className = 'embed-meeting-dialog'>
48
+                <textarea
49
+                    className = 'embed-meeting-code'
50
+                    readOnly = { true }
51
+                    value = { getEmbedCode() } />
52
+                <CopyButton
53
+                    className = 'embed-meeting-copy'
54
+                    displayedText = { t('dialog.copy') }
55
+                    textOnCopySuccess = { t('dialog.copied') }
56
+                    textOnHover = { t('dialog.copy') }
57
+                    textToCopy = { getEmbedCode() } />
58
+            </div>
59
+        </Dialog>
60
+    );
61
+}
62
+
63
+const mapStateToProps = state => {
64
+    return {
65
+        url: getInviteURL(state)
66
+    };
67
+};
68
+
69
+export default translate(connect(mapStateToProps)(EmbedMeeting));

+ 50
- 0
react/features/embed-meeting/components/EmbedMeetingTrigger.js 查看文件

1
+// @flow
2
+
3
+import React from 'react';
4
+import { connect } from 'react-redux';
5
+
6
+import { openDialog } from '../../base/dialog';
7
+import { translate } from '../../base/i18n';
8
+
9
+import EmbedMeetingDialog from './EmbedMeetingDialog';
10
+
11
+type Props = {
12
+
13
+    /**
14
+     * Open the embed meeting dialog
15
+     */
16
+    openEmbedDialog: Function,
17
+
18
+    /**
19
+     * Invoked to obtain translated strings.
20
+     */
21
+    t: Function,
22
+};
23
+
24
+/**
25
+ * Component meant to trigger showing the EmbedMeetingDialog.
26
+ *
27
+ * @returns {React$Element<any>}
28
+ */
29
+function EmbedMeetingTrigger({ t, openEmbedDialog }: Props) {
30
+    /**
31
+     * Handles opeming the embed dialog.
32
+     *
33
+     * @returns {void}
34
+     */
35
+    function onClick() {
36
+        openEmbedDialog(EmbedMeetingDialog);
37
+    }
38
+
39
+    return (
40
+        <div
41
+            className = 'embed-meeting-trigger'
42
+            onClick = { onClick }>
43
+            {t('embedMeeting.title')}
44
+        </div>
45
+    );
46
+}
47
+
48
+const mapDispatchToProps = { openEmbedDialog: openDialog };
49
+
50
+export default translate(connect(null, mapDispatchToProps)(EmbedMeetingTrigger));

+ 38
- 0
react/features/embed-meeting/components/Header.js 查看文件

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 EmbedMeetingDialog}.
23
+ *
24
+ * @returns {React$Element<any>}
25
+ */
26
+function Header({ onClose, t }: Props) {
27
+    return (
28
+        <div
29
+            className = 'embed-meeting-dialog-header'>
30
+            { t('embedMeeting.title') }
31
+            <Icon
32
+                onClick = { onClose }
33
+                src = { IconClose } />
34
+        </div>
35
+    );
36
+}
37
+
38
+export default translate(Header);

+ 1
- 0
react/features/embed-meeting/components/index.js 查看文件

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

+ 1
- 0
react/features/embed-meeting/index.js 查看文件

1
+export * from './components';

+ 3
- 0
react/features/invite/components/add-people-dialog/web/AddPeopleDialog.js 查看文件

10
 import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
10
 import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
11
 import { getLocalParticipant } from '../../../../base/participants';
11
 import { getLocalParticipant } from '../../../../base/participants';
12
 import { connect } from '../../../../base/redux';
12
 import { connect } from '../../../../base/redux';
13
+import EmbedMeetingTrigger from '../../../../embed-meeting/components/EmbedMeetingTrigger';
13
 import { getActiveSession } from '../../../../recording';
14
 import { getActiveSession } from '../../../../recording';
14
 import { updateDialInNumbers } from '../../../actions';
15
 import { updateDialInNumbers } from '../../../actions';
15
 import { _getDefaultPhoneNumber, getInviteText, isAddPeopleEnabled, isDialOutEnabled } from '../../../functions';
16
 import { _getDefaultPhoneNumber, getInviteText, isAddPeopleEnabled, isDialOutEnabled } from '../../../functions';
151
                 <InviteByEmailSection
152
                 <InviteByEmailSection
152
                     inviteSubject = { inviteSubject }
153
                     inviteSubject = { inviteSubject }
153
                     inviteText = { invite } />
154
                     inviteText = { invite } />
155
+                <EmbedMeetingTrigger />
156
+                <div className = 'invite-more-dialog separator' />
154
                 {
157
                 {
155
                     _liveStreamViewURL
158
                     _liveStreamViewURL
156
                         && <LiveStreamSection liveStreamViewURL = { _liveStreamViewURL } />
159
                         && <LiveStreamSection liveStreamViewURL = { _liveStreamViewURL } />

+ 0
- 1
react/features/invite/components/add-people-dialog/web/InviteByEmailSection.js 查看文件

146
                     {renderEmailIcons()}
146
                     {renderEmailIcons()}
147
                 </div>
147
                 </div>
148
             </div>
148
             </div>
149
-            <div className = 'invite-more-dialog separator' />
150
         </>
149
         </>
151
     );
150
     );
152
 }
151
 }

+ 35
- 0
react/features/toolbox/components/web/Toolbox.js 查看文件

12
 import { translate } from '../../../base/i18n';
12
 import { translate } from '../../../base/i18n';
13
 import {
13
 import {
14
     IconChat,
14
     IconChat,
15
+    IconCodeBlock,
15
     IconExitFullScreen,
16
     IconExitFullScreen,
16
     IconFeedback,
17
     IconFeedback,
17
     IconFullScreen,
18
     IconFullScreen,
33
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
34
 import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
34
 import { VideoBlurButton } from '../../../blur';
35
 import { VideoBlurButton } from '../../../blur';
35
 import { CHAT_SIZE, ChatCounter, toggleChat } from '../../../chat';
36
 import { CHAT_SIZE, ChatCounter, toggleChat } from '../../../chat';
37
+import { EmbedMeetingDialog } from '../../../embed-meeting';
36
 import { SharedDocumentButton } from '../../../etherpad';
38
 import { SharedDocumentButton } from '../../../etherpad';
37
 import { openFeedbackDialog } from '../../../feedback';
39
 import { openFeedbackDialog } from '../../../feedback';
38
 import { beginAddPeople } from '../../../invite';
40
 import { beginAddPeople } from '../../../invite';
237
         this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
239
         this._onToolbarOpenInvite = this._onToolbarOpenInvite.bind(this);
238
         this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
240
         this._onToolbarOpenKeyboardShortcuts = this._onToolbarOpenKeyboardShortcuts.bind(this);
239
         this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
241
         this._onToolbarOpenSpeakerStats = this._onToolbarOpenSpeakerStats.bind(this);
242
+        this._onToolbarOpenEmbedMeeting = this._onToolbarOpenEmbedMeeting.bind(this);
240
         this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
243
         this._onToolbarOpenVideoQuality = this._onToolbarOpenVideoQuality.bind(this);
241
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
244
         this._onToolbarToggleChat = this._onToolbarToggleChat.bind(this);
242
         this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
245
         this._onToolbarToggleFullScreen = this._onToolbarToggleFullScreen.bind(this);
376
         this.props.dispatch(openFeedbackDialog(_conference));
379
         this.props.dispatch(openFeedbackDialog(_conference));
377
     }
380
     }
378
 
381
 
382
+    /**
383
+     * Callback invoked to display {@code FeedbackDialog}.
384
+     *
385
+     * @private
386
+     * @returns {void}
387
+     */
388
+    _doOpenEmbedMeeting() {
389
+        this.props.dispatch(openDialog(EmbedMeetingDialog));
390
+    }
391
+
379
     /**
392
     /**
380
      * Dispatches an action to display {@code KeyboardShortcuts}.
393
      * Dispatches an action to display {@code KeyboardShortcuts}.
381
      *
394
      *
717
         this._doOpenKeyboardShorcuts();
730
         this._doOpenKeyboardShorcuts();
718
     }
731
     }
719
 
732
 
733
+    _onToolbarOpenEmbedMeeting: () => void;
734
+
735
+    /**
736
+     * Creates an analytics toolbar event and dispatches an action for opening
737
+     * the embed meeting modal.
738
+     *
739
+     * @private
740
+     * @returns {void}
741
+     */
742
+    _onToolbarOpenEmbedMeeting() {
743
+        sendAnalytics(createToolbarEvent('embed.meeting'));
744
+
745
+        this._doOpenEmbedMeeting();
746
+    }
747
+
720
     _onToolbarOpenSpeakerStats: () => void;
748
     _onToolbarOpenSpeakerStats: () => void;
721
 
749
 
722
     /**
750
     /**
1017
                     key = 'stats'
1045
                     key = 'stats'
1018
                     onClick = { this._onToolbarOpenSpeakerStats }
1046
                     onClick = { this._onToolbarOpenSpeakerStats }
1019
                     text = { t('toolbar.speakerStats') } />,
1047
                     text = { t('toolbar.speakerStats') } />,
1048
+            this._shouldShowButton('embedmeeting')
1049
+                && <OverflowMenuItem
1050
+                    accessibilityLabel = { t('toolbar.accessibilityLabel.embedMeeting') }
1051
+                    icon = { IconCodeBlock }
1052
+                    key = 'embed'
1053
+                    onClick = { this._onToolbarOpenEmbedMeeting }
1054
+                    text = { t('toolbar.embedMeeting') } />,
1020
             this._shouldShowButton('feedback')
1055
             this._shouldShowButton('feedback')
1021
                 && _feedbackConfigured
1056
                 && _feedbackConfigured
1022
                 && <OverflowMenuItem
1057
                 && <OverflowMenuItem

正在加载...
取消
保存