浏览代码

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

master
Avram Tudor 3 年前
父节点
当前提交
d651ecb166
没有帐户链接到提交者的电子邮件

+ 3
- 1
config.js 查看文件

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

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

865
         "expandedPending": "Recording is being started...",
865
         "expandedPending": "Recording is being started...",
866
         "failedToStart": "Recording failed to start",
866
         "failedToStart": "Recording failed to start",
867
         "fileSharingdescription": "Share the recording link with the meeting participants",
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
         "inProgress": "Recording or live streaming in progress",
872
         "inProgress": "Recording or live streaming in progress",
869
         "limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
873
         "limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
870
         "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>.",
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
     'fullscreen',
26
     'fullscreen',
27
     'hangup',
27
     'hangup',
28
     'help',
28
     'help',
29
+    'highlight',
29
     'invite',
30
     'invite',
30
     'linktosalesforce',
31
     'linktosalesforce',
31
     'livestreaming',
32
     'livestreaming',

+ 3
- 0
react/features/base/icons/svg/highlight.svg 查看文件

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
 export { default as IconGoogle } from './google.svg';
60
 export { default as IconGoogle } from './google.svg';
61
 export { default as IconHangup } from './hangup.svg';
61
 export { default as IconHangup } from './hangup.svg';
62
 export { default as IconHelp } from './help.svg';
62
 export { default as IconHelp } from './help.svg';
63
+export { default as IconHighlight } from './highlight.svg';
63
 export { default as IconHome } from './home.svg';
64
 export { default as IconHome } from './home.svg';
64
 export { default as IconHorizontalPoints } from './horizontal-points.svg';
65
 export { default as IconHorizontalPoints } from './horizontal-points.svg';
65
 export { default as IconInfo } from './info.svg';
66
 export { default as IconInfo } from './info.svg';

+ 1
- 0
react/features/conference/components/constants.js 查看文件

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

+ 5
- 0
react/features/conference/components/web/ConferenceInfo.js 查看文件

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

+ 10
- 0
react/features/recording/actionTypes.js 查看文件

56
  * }
56
  * }
57
  */
57
  */
58
 export const SET_STREAM_KEY = 'SET_STREAM_KEY';
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
 import {
16
 import {
17
     CLEAR_RECORDING_SESSIONS,
17
     CLEAR_RECORDING_SESSIONS,
18
     RECORDING_SESSION_UPDATED,
18
     RECORDING_SESSION_UPDATED,
19
+    SET_MEETING_HIGHLIGHT_BUTTON_STATE,
19
     SET_PENDING_RECORDING_NOTIFICATION_UID,
20
     SET_PENDING_RECORDING_NOTIFICATION_UID,
20
     SET_SELECTED_RECORDING_SERVICE,
21
     SET_SELECTED_RECORDING_SERVICE,
21
     SET_STREAM_KEY
22
     SET_STREAM_KEY
22
 } from './actionTypes';
23
 } from './actionTypes';
23
-import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox } from './functions';
24
+import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox, sendMeetingHighlight } from './functions';
24
 import logger from './logger';
25
 import logger from './logger';
25
 
26
 
26
 declare var APP: Object;
27
 declare var APP: Object;
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
  * Signals that the pending recording notification should be removed from the
58
  * Signals that the pending recording notification should be removed from the
43
  * screen.
59
  * screen.
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
  * Signals that the recording error notification should be shown.
150
  * Signals that the recording error notification should be shown.
110
  *
151
  *

+ 71
- 0
react/features/recording/components/Recording/AbstractHighlightButton.js 查看文件

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 查看文件

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
 // @flow
1
 // @flow
2
 
2
 
3
+export { default as HighlightButton } from './HighlightButton';
3
 export { default as RecordButton } from './RecordButton';
4
 export { default as RecordButton } from './RecordButton';
4
 export { default as StartRecordingDialog } from './StartRecordingDialog';
5
 export { default as StartRecordingDialog } from './StartRecordingDialog';
5
 export { default as StopRecordingDialog } from './StopRecordingDialog';
6
 export { default as StopRecordingDialog } from './StopRecordingDialog';

+ 61
- 0
react/features/recording/functions.js 查看文件

1
 // @flow
1
 // @flow
2
 
2
 
3
 import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
3
 import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
4
+import { getLocalParticipant } from '../base/participants';
4
 import { isEnabled as isDropboxEnabled } from '../dropbox';
5
 import { isEnabled as isDropboxEnabled } from '../dropbox';
6
+import { extractFqnFromPath } from '../dynamic-branding';
5
 
7
 
6
 import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
8
 import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
9
+import logger from './logger';
7
 
10
 
8
 /**
11
 /**
9
  * Searches in the passed in redux state for an active recording session of the
12
  * Searches in the passed in redux state for an active recording session of the
79
         && state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
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
  * Returns the recording session status that is to be shown in a label. E.g. If
96
  * Returns the recording session status that is to be shown in a label. E.g. If
84
  * there is a session with the status OFF and one with PENDING, then the PENDING
97
  * there is a session with the status OFF and one with PENDING, then the PENDING
120
             : recorder.getId();
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
 import {
3
 import {
4
     CLEAR_RECORDING_SESSIONS,
4
     CLEAR_RECORDING_SESSIONS,
5
     RECORDING_SESSION_UPDATED,
5
     RECORDING_SESSION_UPDATED,
6
+    SET_MEETING_HIGHLIGHT_BUTTON_STATE,
6
     SET_PENDING_RECORDING_NOTIFICATION_UID,
7
     SET_PENDING_RECORDING_NOTIFICATION_UID,
7
     SET_SELECTED_RECORDING_SERVICE,
8
     SET_SELECTED_RECORDING_SERVICE,
8
     SET_STREAM_KEY
9
     SET_STREAM_KEY
9
 } from './actionTypes';
10
 } from './actionTypes';
10
 
11
 
11
 const DEFAULT_STATE = {
12
 const DEFAULT_STATE = {
13
+    disableHighlightMeetingMoment: false,
12
     pendingNotificationUids: {},
14
     pendingNotificationUids: {},
13
     selectedRecordingService: '',
15
     selectedRecordingService: '',
14
     sessionDatas: []
16
     sessionDatas: []
65
                 streamKey: action.streamKey
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
         default:
76
         default:
69
             return state;
77
             return state;
70
         }
78
         }

正在加载...
取消
保存