Преглед изворни кода

feat(vpaas, recording): Show recording link to recording initiator (#9362)

* feat(vpaas, recording): Show recording link to recording initiator

This applies only for jaas users for now but is easily extensible.
Changed the recording sharing icon according to ui design.

* fix(vpaas, recording): Guard for deployment info
j8
vp8x8 пре 4 година
родитељ
комит
3d83847e4b
No account linked to committer's email address

BIN
images/icon-cloud.png Прегледај датотеку


+ 5
- 1
lang/main.json Прегледај датотеку

655
         "beta": "BETA",
655
         "beta": "BETA",
656
         "busy": "We're working on freeing recording resources. Please try again in a few minutes.",
656
         "busy": "We're working on freeing recording resources. Please try again in a few minutes.",
657
         "busyTitle": "All recorders are currently busy",
657
         "busyTitle": "All recorders are currently busy",
658
+        "copyLink": "Copy Link",
658
         "error": "Recording failed. Please try again.",
659
         "error": "Recording failed. Please try again.",
660
+        "errorFetchingLink": "Error fetching recording link.",
659
         "expandedOff": "Recording has stopped",
661
         "expandedOff": "Recording has stopped",
660
         "expandedOn": "The meeting is currently being recorded.",
662
         "expandedOn": "The meeting is currently being recorded.",
661
         "expandedPending": "Recording is being started...",
663
         "expandedPending": "Recording is being started...",
662
         "failedToStart": "Recording failed to start",
664
         "failedToStart": "Recording failed to start",
663
         "fileSharingdescription": "Share recording with meeting participants",
665
         "fileSharingdescription": "Share recording with meeting participants",
666
+        "linkGenerated": "We have generated a link to your recording.",
664
         "live": "LIVE",
667
         "live": "LIVE",
665
         "loggedIn": "Logged in as {{userName}}",
668
         "loggedIn": "Logged in as {{userName}}",
666
         "off": "Recording stopped",
669
         "off": "Recording stopped",
675
         "signIn": "Sign in",
678
         "signIn": "Sign in",
676
         "signOut": "Sign out",
679
         "signOut": "Sign out",
677
         "unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
680
         "unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
678
-        "unavailableTitle": "Recording unavailable"
681
+        "unavailableTitle": "Recording unavailable",
682
+        "uploadToCloud": "Upload to the cloud"
679
     },
683
     },
680
     "sectionList": {
684
     "sectionList": {
681
         "pullToRefresh": "Pull to refresh"
685
         "pullToRefresh": "Pull to refresh"

+ 20
- 0
react/features/base/config/functions.any.js Прегледај датотеку

40
     };
40
     };
41
 }
41
 }
42
 
42
 
43
+/**
44
+ * Selector used to get the meeting region.
45
+ *
46
+ * @param {Object} state - The global state.
47
+ * @returns {string}
48
+ */
49
+export function getMeetingRegion(state: Object) {
50
+    return state['features/base/config']?.deploymentInfo?.region || '';
51
+}
52
+
53
+/**
54
+ * Selector used to get the endpoint used for fetching the recording.
55
+ *
56
+ * @param {Object} state - The global state.
57
+ * @returns {string}
58
+ */
59
+export function getRecordingSharingUrl(state: Object) {
60
+    return state['features/base/config'].recordingSharingUrl;
61
+}
62
+
43
 /* eslint-disable max-params, no-shadow */
63
 /* eslint-disable max-params, no-shadow */
44
 
64
 
45
 /**
65
 /**

+ 10
- 0
react/features/billing-counter/functions.js Прегледај датотеку

22
     return '';
22
     return '';
23
 }
23
 }
24
 
24
 
25
+/**
26
+ * Returns the vpaas tenant.
27
+ *
28
+ * @param {Object} state - The global state.
29
+ * @returns {string}
30
+ */
31
+export function getVpaasTenant(state: Object) {
32
+    return extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
33
+}
34
+
25
 /**
35
 /**
26
  * Returns true if the current meeting is a vpaas one.
36
  * Returns true if the current meeting is a vpaas one.
27
  *
37
  *

+ 66
- 18
react/features/recording/actions.any.js Прегледај датотеку

1
 // @flow
1
 // @flow
2
 
2
 
3
+import { getMeetingRegion, getRecordingSharingUrl } from '../base/config';
3
 import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
4
 import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
5
+import { getLocalParticipant, getParticipantDisplayName } from '../base/participants';
6
+import { copyText } from '../base/util/helpers';
7
+import { getVpaasTenant, isVpaasMeeting } from '../billing-counter/functions';
4
 import {
8
 import {
5
     NOTIFICATION_TIMEOUT,
9
     NOTIFICATION_TIMEOUT,
6
     hideNotification,
10
     hideNotification,
14
     SET_PENDING_RECORDING_NOTIFICATION_UID,
18
     SET_PENDING_RECORDING_NOTIFICATION_UID,
15
     SET_STREAM_KEY
19
     SET_STREAM_KEY
16
 } from './actionTypes';
20
 } from './actionTypes';
21
+import { getRecordingLink, getResourceId } from './functions';
22
+import logger from './logger';
17
 
23
 
18
 /**
24
 /**
19
  * Clears the data of every recording sessions.
25
  * Clears the data of every recording sessions.
136
  * Signals that a started recording notification should be shown on the
142
  * Signals that a started recording notification should be shown on the
137
  * screen for a given period.
143
  * screen for a given period.
138
  *
144
  *
139
- * @param {string} streamType - The type of the stream ({@code file} or
140
- * {@code stream}).
141
- * @param {string} participantName - The participant name that started the recording.
142
- * @returns {showNotification}
145
+ * @param {string} mode - The type of the recording: Stream of File.
146
+ * @param {string | Object } initiator - The participant who started recording.
147
+ * @param {string} sessionId - The recording session id.
148
+ * @returns {Function}
143
  */
149
  */
144
-export function showStartedRecordingNotification(streamType: string, participantName: string) {
145
-    const isLiveStreaming
146
-        = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
147
-    const descriptionArguments = { name: participantName };
148
-    const dialogProps = isLiveStreaming ? {
149
-        descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
150
-        descriptionArguments,
151
-        titleKey: 'dialog.liveStreaming'
152
-    } : {
153
-        descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
154
-        descriptionArguments,
155
-        titleKey: 'dialog.recording'
156
-    };
150
+export function showStartedRecordingNotification(
151
+        mode: string,
152
+        initiator: Object | string,
153
+        sessionId: string) {
154
+    return async (dispatch: Function, getState: Function) => {
155
+        const state = getState();
156
+        const initiatorId = getResourceId(initiator);
157
+        const participantName = getParticipantDisplayName(state, initiatorId);
158
+        let dialogProps = {
159
+            customActionNameKey: undefined,
160
+            descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
161
+            descriptionArguments: { name: participantName },
162
+            isDismissAllowed: true,
163
+            titleKey: 'dialog.liveStreaming'
164
+        };
157
 
165
 
158
-    return showNotification(dialogProps, NOTIFICATION_TIMEOUT);
166
+        if (mode !== JitsiMeetJS.constants.recording.mode.STREAM) {
167
+            const recordingSharingUrl = getRecordingSharingUrl(state);
168
+            const iAmRecordingInitiator = getLocalParticipant(state).id === initiatorId;
169
+
170
+            dialogProps = {
171
+                customActionHandler: undefined,
172
+                customActionNameKey: undefined,
173
+                descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
174
+                descriptionArguments: { name: participantName },
175
+                isDismissAllowed: true,
176
+                titleKey: 'dialog.recording'
177
+            };
178
+
179
+            // fetch the recording link from the server for recording initiators in jaas meetings
180
+            if (recordingSharingUrl
181
+                && isVpaasMeeting(state)
182
+                && iAmRecordingInitiator) {
183
+                const region = getMeetingRegion(state);
184
+                const tenant = getVpaasTenant(state);
185
+
186
+                try {
187
+                    const link = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
188
+
189
+                    // add the option to copy recording link
190
+                    dialogProps.customActionNameKey = 'recording.copyLink';
191
+                    dialogProps.customActionHandler = () => copyText(link);
192
+                    dialogProps.titleKey = 'recording.on';
193
+                    dialogProps.descriptionKey = 'recording.linkGenerated';
194
+                    dialogProps.isDismissAllowed = false;
195
+                } catch (err) {
196
+                    dispatch(showErrorNotification({
197
+                        titleKey: 'recording.errorFetchingLink'
198
+                    }));
199
+
200
+                    return logger.error('Could not fetch recording link', err);
201
+                }
202
+            }
203
+        }
204
+
205
+        dispatch(showNotification(dialogProps));
206
+    };
159
 }
207
 }
160
 
208
 
161
 /**
209
 /**

+ 12
- 20
react/features/recording/components/Recording/StartRecordingDialogContent.js Прегледај датотеку

26
 import { RECORDING_TYPES } from '../../constants';
26
 import { RECORDING_TYPES } from '../../constants';
27
 import { getRecordingDurationEstimation } from '../../functions';
27
 import { getRecordingDurationEstimation } from '../../functions';
28
 
28
 
29
-import { DROPBOX_LOGO, ICON_SHARE, JITSI_LOGO } from './styles';
30
-
29
+import { DROPBOX_LOGO, ICON_CLOUD, JITSI_LOGO } from './styles';
31
 
30
 
32
 type Props = {
31
 type Props = {
33
 
32
 
162
      * @returns {React$Component}
161
      * @returns {React$Component}
163
      */
162
      */
164
     _renderFileSharingContent() {
163
     _renderFileSharingContent() {
165
-        const { fileRecordingsServiceSharingEnabled, isVpaas } = this.props;
164
+        const { fileRecordingsServiceSharingEnabled, isVpaas, selectedRecordingService } = this.props;
166
 
165
 
167
-        if (!fileRecordingsServiceSharingEnabled || isVpaas) {
166
+        if (!fileRecordingsServiceSharingEnabled
167
+            || isVpaas
168
+            || selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
168
             return null;
169
             return null;
169
         }
170
         }
170
 
171
 
173
             _styles: styles,
174
             _styles: styles,
174
             isValidating,
175
             isValidating,
175
             onSharingSettingChanged,
176
             onSharingSettingChanged,
176
-            selectedRecordingService,
177
             sharingSetting,
177
             sharingSetting,
178
             t
178
             t
179
         } = this.props;
179
         } = this.props;
180
 
180
 
181
-        const controlDisabled = selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE;
182
-        let mainContainerClasses = 'recording-header recording-header-line';
183
-
184
-        if (controlDisabled) {
185
-            mainContainerClasses += ' recording-switch-disabled';
186
-        }
187
-
188
         return (
181
         return (
189
             <Container
182
             <Container
190
-                className = { mainContainerClasses }
183
+                className = 'recording-header'
191
                 key = 'fileSharingSetting'
184
                 key = 'fileSharingSetting'
192
                 style = { [
185
                 style = { [
193
                     styles.header,
186
                     styles.header,
194
-                    _dialogStyles.topBorderContainer,
195
-                    controlDisabled ? styles.controlDisabled : null
187
+                    _dialogStyles.topBorderContainer
196
                 ] }>
188
                 ] }>
197
                 <Container className = 'recording-icon-container'>
189
                 <Container className = 'recording-icon-container'>
198
                     <Image
190
                     <Image
199
                         className = 'recording-icon'
191
                         className = 'recording-icon'
200
-                        src = { ICON_SHARE }
192
+                        src = { ICON_CLOUD }
201
                         style = { styles.recordingIcon } />
193
                         style = { styles.recordingIcon } />
202
                 </Container>
194
                 </Container>
203
                 <Text
195
                 <Text
210
                 </Text>
202
                 </Text>
211
                 <Switch
203
                 <Switch
212
                     className = 'recording-switch'
204
                     className = 'recording-switch'
213
-                    disabled = { controlDisabled || isValidating }
205
+                    disabled = { isValidating }
214
                     onValueChange
206
                     onValueChange
215
                         = { onSharingSettingChanged }
207
                         = { onSharingSettingChanged }
216
                     style = { styles.switch }
208
                     style = { styles.switch }
217
                     trackColor = {{ false: ColorPalette.lightGrey }}
209
                     trackColor = {{ false: ColorPalette.lightGrey }}
218
-                    value = { !controlDisabled && sharingSetting } />
210
+                    value = { sharingSetting } />
219
             </Container>
211
             </Container>
220
         );
212
         );
221
     }
213
     }
248
                         value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
240
                         value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
249
                 ) : null;
241
                 ) : null;
250
 
242
 
251
-        const icon = isVpaas ? ICON_SHARE : JITSI_LOGO;
243
+        const icon = isVpaas ? ICON_CLOUD : JITSI_LOGO;
252
         const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
244
         const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
253
 
245
 
254
         return (
246
         return (
334
         return (
326
         return (
335
             <Container>
327
             <Container>
336
                 <Container
328
                 <Container
337
-                    className = 'recording-header'
329
+                    className = 'recording-header recording-header-line'
338
                     style = { styles.header }>
330
                     style = { styles.header }>
339
                     <Container
331
                     <Container
340
                         className = 'recording-icon-container'>
332
                         className = 'recording-icon-container'>

+ 1
- 1
react/features/recording/components/Recording/styles.native.js Прегледај датотеку

4
 import { BoxModel, ColorPalette } from '../../../base/styles';
4
 import { BoxModel, ColorPalette } from '../../../base/styles';
5
 
5
 
6
 export const DROPBOX_LOGO = require('../../../../../images/dropboxLogo_square.png');
6
 export const DROPBOX_LOGO = require('../../../../../images/dropboxLogo_square.png');
7
-export const ICON_SHARE = require('../../../../../images/icon-users.png');
7
+export const ICON_CLOUD = require('../../../../../images/icon-cloud.png');
8
 export const JITSI_LOGO = require('../../../../../images/jitsiLogo_square.png');
8
 export const JITSI_LOGO = require('../../../../../images/jitsiLogo_square.png');
9
 
9
 
10
 // XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in
10
 // XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in

+ 1
- 1
react/features/recording/components/Recording/styles.web.js Прегледај датотеку

4
 
4
 
5
 export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
5
 export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
6
 
6
 
7
-export const ICON_SHARE = 'images/icon-users.png';
7
+export const ICON_CLOUD = 'images/icon-cloud.png';
8
 
8
 
9
 export const JITSI_LOGO = 'images/jitsiLogo_square.png';
9
 export const JITSI_LOGO = 'images/jitsiLogo_square.png';

+ 36
- 0
react/features/recording/functions.js Прегледај датотеку

46
         sessionData => sessionData.id === id);
46
         sessionData => sessionData.id === id);
47
 }
47
 }
48
 
48
 
49
+/**
50
+ * Fetches the recording link from the server.
51
+ *
52
+ * @param {string} url - The base url.
53
+ * @param {string} recordingSessionId - The ID of the recording session to find.
54
+ * @param {string} region - The meeting region.
55
+ * @param {string} tenant - The meeting tenant.
56
+ * @returns {Promise<any>}
57
+ */
58
+export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
59
+    const fullUrl = `${url}?recordingSessionId=${recordingSessionId}&region=${region}&tenant=${tenant}`;
60
+    const res = await fetch(fullUrl, {
61
+        headers: {
62
+            'Content-Type': 'application/json'
63
+        }
64
+    });
65
+    const json = await res.json();
66
+
67
+    return res.ok ? json.url : Promise.reject(json);
68
+}
69
+
49
 /**
70
 /**
50
  * Returns the recording session status that is to be shown in a label. E.g. If
71
  * Returns the recording session status that is to be shown in a label. E.g. If
51
  * there is a session with the status OFF and one with PENDING, then the PENDING
72
  * there is a session with the status OFF and one with PENDING, then the PENDING
72
 
93
 
73
     return status;
94
     return status;
74
 }
95
 }
96
+
97
+
98
+/**
99
+ * Returns the resource id.
100
+ *
101
+ * @param {Object | string} recorder - A participant or it's resource.
102
+ * @returns {string|undefined}
103
+ */
104
+export function getResourceId(recorder: string | Object) {
105
+    if (recorder) {
106
+        return typeof recorder === 'string'
107
+            ? recorder
108
+            : recorder.getId();
109
+    }
110
+}

+ 8
- 6
react/features/recording/middleware.js Прегледај датотеку

37
     RECORDING_OFF_SOUND_ID,
37
     RECORDING_OFF_SOUND_ID,
38
     RECORDING_ON_SOUND_ID
38
     RECORDING_ON_SOUND_ID
39
 } from './constants';
39
 } from './constants';
40
-import { getSessionById } from './functions';
40
+import { getSessionById, getResourceId } from './functions';
41
 import {
41
 import {
42
     LIVE_STREAMING_OFF_SOUND_FILE,
42
     LIVE_STREAMING_OFF_SOUND_FILE,
43
     LIVE_STREAMING_ON_SOUND_FILE,
43
     LIVE_STREAMING_ON_SOUND_FILE,
160
                     // Show notification with additional information to the initiator.
160
                     // Show notification with additional information to the initiator.
161
                     dispatch(showRecordingLimitNotification(mode));
161
                     dispatch(showRecordingLimitNotification(mode));
162
                 } else {
162
                 } else {
163
-                    dispatch(showStartedRecordingNotification(
164
-                        mode, initiator && getParticipantDisplayName(getState, initiator.getId())));
163
+                    dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
165
                 }
164
                 }
166
 
165
 
167
-
168
                 sendAnalytics(createRecordingEvent('start', mode));
166
                 sendAnalytics(createRecordingEvent('start', mode));
169
 
167
 
170
                 if (disableRecordAudioNotification) {
168
                 if (disableRecordAudioNotification) {
188
                 }
186
                 }
189
             } else if (updatedSessionData.status === OFF
187
             } else if (updatedSessionData.status === OFF
190
                 && (!oldSessionData || oldSessionData.status !== OFF)) {
188
                 && (!oldSessionData || oldSessionData.status !== OFF)) {
191
-                dispatch(showStoppedRecordingNotification(
192
-                    mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
189
+                if (terminator) {
190
+                    dispatch(
191
+                        showStoppedRecordingNotification(
192
+                            mode, getParticipantDisplayName(getState, getResourceId(terminator))));
193
+                }
194
+
193
                 let duration = 0, soundOff, soundOn;
195
                 let duration = 0, soundOff, soundOn;
194
 
196
 
195
                 if (oldSessionData && oldSessionData.timestamp) {
197
                 if (oldSessionData && oldSessionData.timestamp) {

Loading…
Откажи
Сачувај