ソースを参照

feat(recording) allow highlighting meeting recording moments (#10981)

master
Avram Tudor 3年前
コミット
d651ecb166
コミッターのメールアドレスに関連付けられたアカウントが存在しません

+ 3
- 1
config.js ファイルの表示

@@ -603,6 +603,7 @@ var config = {
603 603
     //    'fullscreen',
604 604
     //    'hangup',
605 605
     //    'help',
606
+    //    'highlight',
606 607
     //    'invite',
607 608
     //    'livestreaming',
608 609
     //    'microphone',
@@ -1118,7 +1119,8 @@ var config = {
1118 1119
     //         'e2ee',
1119 1120
     //         'transcribing',
1120 1121
     //         'video-quality',
1121
-    //         'insecure-room'
1122
+    //         'insecure-room',
1123
+    //         'highlight-moment'
1122 1124
     //     ]
1123 1125
     // },
1124 1126
 

+ 4
- 0
lang/main.json ファイルの表示

@@ -865,6 +865,10 @@
865 865
         "expandedPending": "Recording is being started...",
866 866
         "failedToStart": "Recording failed to start",
867 867
         "fileSharingdescription": "Share the recording link with the meeting participants",
868
+        "highlightMoment": "Highlight moment",
869
+        "highlightMomentDisabled": "You can highlight moments when the recording starts",
870
+        "highlightMomentSuccess": "Moment highlighted",
871
+        "highlightMomentSucessDescription": "Your highlighted moment will be added to the meeting summary.",
868 872
         "inProgress": "Recording or live streaming in progress",
869 873
         "limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
870 874
         "limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",

+ 1
- 0
react/features/base/config/constants.js ファイルの表示

@@ -26,6 +26,7 @@ export const TOOLBAR_BUTTONS = [
26 26
     'fullscreen',
27 27
     'hangup',
28 28
     'help',
29
+    'highlight',
29 30
     'invite',
30 31
     'linktosalesforce',
31 32
     'livestreaming',

+ 3
- 0
react/features/base/icons/svg/highlight.svg ファイルの表示

@@ -0,0 +1,3 @@
1
+<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1966 6.05507L16.0207 5.25708L17.669 6.85306L16.8448 7.65106L15.1966 6.05507ZM11.9 6.05507L13.5483 4.45908L14.3724 3.66109C15.2827 2.77966 16.7586 2.77961 17.669 3.66109L19.3173 5.25708C20.2276 6.13855 20.2276 7.56762 19.3173 8.44905L18.4931 9.24705L16.8448 10.843L12.7242 14.833L11.0759 16.429L10.882 16.6167L7.76993 17.7444C6.91521 18.0542 5.95229 17.8519 5.30692 17.227C4.66155 16.6021 4.45265 15.6697 4.77257 14.8421L5.93715 11.8288L6.13106 11.641L7.77933 10.045L11.9 6.05507ZM15.1966 9.24705L11.0759 13.237L9.42761 11.641L13.5483 7.65106L15.1966 9.24705ZM6.95731 15.629L7.85388 13.3092L9.35306 14.7608L6.95731 15.629ZM5.16551 18.7429C4.52181 18.7429 4 19.2482 4 19.8715C4 20.4947 4.52181 21 5.16551 21H17.9861C18.6298 21 19.1516 20.4947 19.1516 19.8715C19.1516 19.2482 18.6298 18.7429 17.9861 18.7429H5.16551Z" />
3
+</svg>

+ 1
- 0
react/features/base/icons/svg/index.js ファイルの表示

@@ -60,6 +60,7 @@ export { default as IconFullScreen } from './full-screen.svg';
60 60
 export { default as IconGoogle } from './google.svg';
61 61
 export { default as IconHangup } from './hangup.svg';
62 62
 export { default as IconHelp } from './help.svg';
63
+export { default as IconHighlight } from './highlight.svg';
63 64
 export { default as IconHome } from './home.svg';
64 65
 export { default as IconHorizontalPoints } from './horizontal-points.svg';
65 66
 export { default as IconInfo } from './info.svg';

+ 1
- 0
react/features/conference/components/constants.js ファイルの表示

@@ -1,6 +1,7 @@
1 1
 export const CONFERENCE_INFO = {
2 2
     alwaysVisible: [ 'recording', 'local-recording', 'raised-hands-count' ],
3 3
     autoHide: [
4
+        'highlight-moment',
4 5
         'subject',
5 6
         'conference-timer',
6 7
         'participants-count',

+ 5
- 0
react/features/conference/components/web/ConferenceInfo.js ファイルの表示

@@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
9 9
 import { E2EELabel } from '../../../e2ee';
10 10
 import { LocalRecordingLabel } from '../../../local-recording';
11 11
 import { RecordingLabel } from '../../../recording';
12
+import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
12 13
 import { isToolboxVisible } from '../../../toolbox/functions.web';
13 14
 import { TranscribingLabel } from '../../../transcribing';
14 15
 import { VideoQualityLabel } from '../../../video-quality';
@@ -38,6 +39,10 @@ type Props = {
38 39
 };
39 40
 
40 41
 const COMPONENTS = [
42
+    {
43
+        Component: HighlightButton,
44
+        id: 'highlight-moment'
45
+    },
41 46
     {
42 47
         Component: SubjectText,
43 48
         id: 'subject'

+ 10
- 0
react/features/recording/actionTypes.js ファイルの表示

@@ -56,3 +56,13 @@ export const SET_SELECTED_RECORDING_SERVICE = 'SET_SELECTED_RECORDING_SERVICE';
56 56
  * }
57 57
  */
58 58
 export const SET_STREAM_KEY = 'SET_STREAM_KEY';
59
+
60
+/**
61
+ * Sets the enable state of the meeting highlight button.
62
+ *
63
+ * {
64
+ *     type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
65
+ *     disabled: boolean
66
+ * }
67
+ */
68
+export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_STATE';

+ 42
- 1
react/features/recording/actions.any.js ファイルの表示

@@ -16,11 +16,12 @@ import {
16 16
 import {
17 17
     CLEAR_RECORDING_SESSIONS,
18 18
     RECORDING_SESSION_UPDATED,
19
+    SET_MEETING_HIGHLIGHT_BUTTON_STATE,
19 20
     SET_PENDING_RECORDING_NOTIFICATION_UID,
20 21
     SET_SELECTED_RECORDING_SERVICE,
21 22
     SET_STREAM_KEY
22 23
 } from './actionTypes';
23
-import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox } from './functions';
24
+import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox, sendMeetingHighlight } from './functions';
24 25
 import logger from './logger';
25 26
 
26 27
 declare var APP: Object;
@@ -38,6 +39,21 @@ export function clearRecordingSessions() {
38 39
     };
39 40
 }
40 41
 
42
+/**
43
+ * Sets the meeting highlight button disable state.
44
+ *
45
+ * @param {boolean} disabled - The disabled state value.
46
+ * @returns {{
47
+ *     type: CLEAR_RECORDING_SESSIONS
48
+ * }}
49
+ */
50
+export function setHighlightMomentButtonState(disabled: boolean) {
51
+    return {
52
+        type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
53
+        disabled
54
+    };
55
+}
56
+
41 57
 /**
42 58
  * Signals that the pending recording notification should be removed from the
43 59
  * screen.
@@ -105,6 +121,31 @@ export function showPendingRecordingNotification(streamType: string) {
105 121
     };
106 122
 }
107 123
 
124
+/**
125
+ * Highlights a meeting moment.
126
+ *
127
+ * {@code stream}).
128
+ *
129
+ * @returns {Function}
130
+ */
131
+export function highlightMeetingMoment() {
132
+    return async (dispatch: Function, getState: Function) => {
133
+        dispatch(setHighlightMomentButtonState(true));
134
+
135
+        try {
136
+            await sendMeetingHighlight(getState());
137
+            dispatch(showNotification({
138
+                descriptionKey: 'recording.highlightMomentSucessDescription',
139
+                titleKey: 'recording.highlightMomentSuccess'
140
+            }));
141
+        } catch (err) {
142
+            logger.error('Could not highlight meeting moment', err);
143
+        }
144
+
145
+        dispatch(setHighlightMomentButtonState(false));
146
+    };
147
+}
148
+
108 149
 /**
109 150
  * Signals that the recording error notification should be shown.
110 151
  *

+ 71
- 0
react/features/recording/components/Recording/AbstractHighlightButton.js ファイルの表示

@@ -0,0 +1,71 @@
1
+// @flow
2
+
3
+import { Component } from 'react';
4
+
5
+import { getActiveSession, isHighlightMeetingMomentDisabled } from '../..';
6
+import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
7
+import { highlightMeetingMoment } from '../../actions.any';
8
+
9
+export type Props = {
10
+
11
+    /**
12
+     * Whether or not the conference is in audio only mode.
13
+     */
14
+    _audioOnly: boolean,
15
+
16
+    /**
17
+     * Invoked to obtain translated strings.
18
+     */
19
+    t: Function
20
+};
21
+
22
+/**
23
+ * Abstract class for the {@code AbstractHighlightButton} component.
24
+ */
25
+export default class AbstractHighlightButton<P: Props> extends Component<P> {
26
+    /**
27
+     * Initializes a new AbstractVideoTrack instance.
28
+     *
29
+     * @param {Object} props - The read-only properties with which the new
30
+     * instance is to be initialized.
31
+     */
32
+    constructor(props: Props) {
33
+        super(props);
34
+
35
+        this._onClick = this._onClick.bind(this);
36
+    }
37
+
38
+    /**
39
+   * Handles clicking / pressing the button.
40
+   *
41
+   * @override
42
+   * @protected
43
+   * @returns {void}
44
+   */
45
+    _onClick() {
46
+        const { dispatch } = this.props;
47
+
48
+        dispatch(highlightMeetingMoment());
49
+    }
50
+}
51
+
52
+/**
53
+ * Maps (parts of) the Redux state to the associated
54
+ * {@code AbstractVideoQualityLabel}'s props.
55
+ *
56
+ * @param {Object} state - The Redux state.
57
+ * @private
58
+ * @returns {{
59
+ *     _audioOnly: boolean
60
+ * }}
61
+ */
62
+export function _abstractMapStateToProps(state: Object) {
63
+    const isRecordingRunning = getActiveSession(state, JitsiRecordingConstants.mode.FILE);
64
+    const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
65
+    const { webhookProxyUrl } = state['features/base/config'];
66
+
67
+    return {
68
+        _disabled: !isRecordingRunning || isButtonDisabled,
69
+        _visible: Boolean(webhookProxyUrl)
70
+    };
71
+}

+ 94
- 0
react/features/recording/components/Recording/web/HighlightButton.js ファイルの表示

@@ -0,0 +1,94 @@
1
+// @flow
2
+
3
+import { withStyles } from '@material-ui/core';
4
+import React from 'react';
5
+
6
+import { translate } from '../../../../base/i18n';
7
+import { IconHighlight } from '../../../../base/icons';
8
+import { Label } from '../../../../base/label';
9
+import { connect } from '../../../../base/redux';
10
+import { Tooltip } from '../../../../base/tooltip';
11
+import BaseTheme from '../../../../base/ui/components/BaseTheme';
12
+import AbstractHighlightButton, {
13
+    _abstractMapStateToProps,
14
+    type Props as AbstractProps
15
+} from '../AbstractHighlightButton';
16
+
17
+type Props = AbstractProps & {
18
+    _disabled: boolean,
19
+
20
+    /**
21
+     * The message to show within the label's tooltip.
22
+     */
23
+    _tooltipKey: string,
24
+
25
+    /**
26
+     * Flag controlling visibility of the component.
27
+     */
28
+    _visible: boolean,
29
+};
30
+
31
+/**
32
+ * Creates the styles for the component.
33
+ *
34
+ * @param {Object} theme - The current UI theme.
35
+ *
36
+ * @returns {Object}
37
+ */
38
+const styles = theme => {
39
+    return {
40
+        regular: {
41
+            background: theme.palette.field02,
42
+            margin: '0 4px 4px 4px'
43
+        },
44
+        disabled: {
45
+            background: theme.palette.text02,
46
+            margin: '0 4px 4px 4px'
47
+        }
48
+    };
49
+};
50
+
51
+/**
52
+ * React {@code Component} responsible for displaying an action that
53
+ * allows users to highlight a meeting moment.
54
+ */
55
+export class HighlightButton extends AbstractHighlightButton<Props> {
56
+
57
+    /**
58
+     * Implements React's {@link Component#render()}.
59
+     *
60
+     * @inheritdoc
61
+     * @returns {ReactElement}
62
+     */
63
+    render() {
64
+        const {
65
+            _disabled,
66
+            _visible,
67
+            classes,
68
+            t
69
+        } = this.props;
70
+
71
+
72
+        if (!_visible) {
73
+            return null;
74
+        }
75
+
76
+        const className = _disabled ? classes.disabled : classes.regular;
77
+        const tooltipKey = _disabled ? 'recording.highlightMomentDisabled' : 'recording.highlightMoment';
78
+
79
+        return (
80
+            <Tooltip
81
+                content = { t(tooltipKey) }
82
+                position = { 'bottom' }>
83
+                <Label
84
+                    className = { className }
85
+                    icon = { IconHighlight }
86
+                    iconColor = { _disabled ? BaseTheme.palette.text03 : BaseTheme.palette.field01 }
87
+                    id = 'highlightMeetingLabel'
88
+                    onClick = { this._onClick } />
89
+            </Tooltip>
90
+        );
91
+    }
92
+}
93
+
94
+export default withStyles(styles)(translate(connect(_abstractMapStateToProps)(HighlightButton)));

+ 1
- 0
react/features/recording/components/Recording/web/index.js ファイルの表示

@@ -1,5 +1,6 @@
1 1
 // @flow
2 2
 
3
+export { default as HighlightButton } from './HighlightButton';
3 4
 export { default as RecordButton } from './RecordButton';
4 5
 export { default as StartRecordingDialog } from './StartRecordingDialog';
5 6
 export { default as StopRecordingDialog } from './StopRecordingDialog';

+ 61
- 0
react/features/recording/functions.js ファイルの表示

@@ -1,9 +1,12 @@
1 1
 // @flow
2 2
 
3 3
 import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
4
+import { getLocalParticipant } from '../base/participants';
4 5
 import { isEnabled as isDropboxEnabled } from '../dropbox';
6
+import { extractFqnFromPath } from '../dynamic-branding';
5 7
 
6 8
 import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
9
+import logger from './logger';
7 10
 
8 11
 /**
9 12
  * Searches in the passed in redux state for an active recording session of the
@@ -79,6 +82,16 @@ export function isSavingRecordingOnDropbox(state: Object) {
79 82
         && state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
80 83
 }
81 84
 
85
+/**
86
+ * Selector used for determining disable state for the meeting highlight button.
87
+ *
88
+ * @param {Object} state - The redux state to search in.
89
+ * @returns {string}
90
+ */
91
+export function isHighlightMeetingMomentDisabled(state: Object) {
92
+    return state['features/recording'].disableHighlightMeetingMoment;
93
+}
94
+
82 95
 /**
83 96
  * Returns the recording session status that is to be shown in a label. E.g. If
84 97
  * there is a session with the status OFF and one with PENDING, then the PENDING
@@ -120,3 +133,51 @@ export function getResourceId(recorder: string | Object) {
120 133
             : recorder.getId();
121 134
     }
122 135
 }
136
+
137
+/**
138
+ * Sends a meeting highlight to backend.
139
+ *
140
+ * @param  {Object} state - Redux state.
141
+ * @returns {boolean} - True if sent, false otherwise.
142
+ */
143
+export async function sendMeetingHighlight(state: Object) {
144
+    const { webhookProxyUrl: url } = state['features/base/config'];
145
+    const { conference } = state['features/base/conference'];
146
+    const { jwt } = state['features/base/jwt'];
147
+    const { connection } = state['features/base/connection'];
148
+    const jid = connection.getJid();
149
+    const localParticipant = getLocalParticipant(state);
150
+
151
+    const headers = {
152
+        ...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
153
+        'Content-Type': 'application/json'
154
+    };
155
+
156
+    const reqBody = {
157
+        meetingFqn: extractFqnFromPath(),
158
+        sessionId: conference.sessionId,
159
+        submitted: Date.now(),
160
+        participantId: localParticipant.jwtId,
161
+        participantName: localParticipant.name,
162
+        participantJid: jid
163
+    };
164
+
165
+    if (url) {
166
+        try {
167
+            const res = await fetch(`${url}/v2/highlights`, {
168
+                method: 'POST',
169
+                headers,
170
+                body: JSON.stringify(reqBody)
171
+            });
172
+
173
+            if (res.ok) {
174
+                return true;
175
+            }
176
+            logger.error('Status error:', res.status);
177
+        } catch (err) {
178
+            logger.error('Could not send request', err);
179
+        }
180
+    }
181
+
182
+    return false;
183
+}

+ 8
- 0
react/features/recording/reducer.js ファイルの表示

@@ -3,12 +3,14 @@ import { ReducerRegistry } from '../base/redux';
3 3
 import {
4 4
     CLEAR_RECORDING_SESSIONS,
5 5
     RECORDING_SESSION_UPDATED,
6
+    SET_MEETING_HIGHLIGHT_BUTTON_STATE,
6 7
     SET_PENDING_RECORDING_NOTIFICATION_UID,
7 8
     SET_SELECTED_RECORDING_SERVICE,
8 9
     SET_STREAM_KEY
9 10
 } from './actionTypes';
10 11
 
11 12
 const DEFAULT_STATE = {
13
+    disableHighlightMeetingMoment: false,
12 14
     pendingNotificationUids: {},
13 15
     selectedRecordingService: '',
14 16
     sessionDatas: []
@@ -65,6 +67,12 @@ ReducerRegistry.register(STORE_NAME,
65 67
                 streamKey: action.streamKey
66 68
             };
67 69
 
70
+        case SET_MEETING_HIGHLIGHT_BUTTON_STATE:
71
+            return {
72
+                ...state,
73
+                disableHighlightMeetingMoment: action.disabled
74
+            };
75
+
68 76
         default:
69 77
             return state;
70 78
         }

読み込み中…
キャンセル
保存