Browse Source

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
master
vp8x8 3 years ago
parent
commit
3d83847e4b
No account linked to committer's email address

BIN
images/icon-cloud.png View File


+ 5
- 1
lang/main.json View File

@@ -655,12 +655,15 @@
655 655
         "beta": "BETA",
656 656
         "busy": "We're working on freeing recording resources. Please try again in a few minutes.",
657 657
         "busyTitle": "All recorders are currently busy",
658
+        "copyLink": "Copy Link",
658 659
         "error": "Recording failed. Please try again.",
660
+        "errorFetchingLink": "Error fetching recording link.",
659 661
         "expandedOff": "Recording has stopped",
660 662
         "expandedOn": "The meeting is currently being recorded.",
661 663
         "expandedPending": "Recording is being started...",
662 664
         "failedToStart": "Recording failed to start",
663 665
         "fileSharingdescription": "Share recording with meeting participants",
666
+        "linkGenerated": "We have generated a link to your recording.",
664 667
         "live": "LIVE",
665 668
         "loggedIn": "Logged in as {{userName}}",
666 669
         "off": "Recording stopped",
@@ -675,7 +678,8 @@
675 678
         "signIn": "Sign in",
676 679
         "signOut": "Sign out",
677 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 684
     "sectionList": {
681 685
         "pullToRefresh": "Pull to refresh"

+ 20
- 0
react/features/base/config/functions.any.js View File

@@ -40,6 +40,26 @@ export function createFakeConfig(baseURL: string) {
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 63
 /* eslint-disable max-params, no-shadow */
44 64
 
45 65
 /**

+ 10
- 0
react/features/billing-counter/functions.js View File

@@ -22,6 +22,16 @@ export function extractVpaasTenantFromPath(path: string) {
22 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 36
  * Returns true if the current meeting is a vpaas one.
27 37
  *

+ 66
- 18
react/features/recording/actions.any.js View File

@@ -1,6 +1,10 @@
1 1
 // @flow
2 2
 
3
+import { getMeetingRegion, getRecordingSharingUrl } from '../base/config';
3 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 8
 import {
5 9
     NOTIFICATION_TIMEOUT,
6 10
     hideNotification,
@@ -14,6 +18,8 @@ import {
14 18
     SET_PENDING_RECORDING_NOTIFICATION_UID,
15 19
     SET_STREAM_KEY
16 20
 } from './actionTypes';
21
+import { getRecordingLink, getResourceId } from './functions';
22
+import logger from './logger';
17 23
 
18 24
 /**
19 25
  * Clears the data of every recording sessions.
@@ -136,26 +142,68 @@ export function showStoppedRecordingNotification(streamType: string, participant
136 142
  * Signals that a started recording notification should be shown on the
137 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 View File

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

+ 1
- 1
react/features/recording/components/Recording/styles.native.js View File

@@ -4,7 +4,7 @@ import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
4 4
 import { BoxModel, ColorPalette } from '../../../base/styles';
5 5
 
6 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 8
 export const JITSI_LOGO = require('../../../../../images/jitsiLogo_square.png');
9 9
 
10 10
 // XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in

+ 1
- 1
react/features/recording/components/Recording/styles.web.js View File

@@ -4,6 +4,6 @@ export default {};
4 4
 
5 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 9
 export const JITSI_LOGO = 'images/jitsiLogo_square.png';

+ 36
- 0
react/features/recording/functions.js View File

@@ -46,6 +46,27 @@ export function getSessionById(state: Object, id: string) {
46 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 71
  * Returns the recording session status that is to be shown in a label. E.g. If
51 72
  * there is a session with the status OFF and one with PENDING, then the PENDING
@@ -72,3 +93,18 @@ export function getSessionStatusToShow(state: Object, mode: string): ?string {
72 93
 
73 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 View File

@@ -37,7 +37,7 @@ import {
37 37
     RECORDING_OFF_SOUND_ID,
38 38
     RECORDING_ON_SOUND_ID
39 39
 } from './constants';
40
-import { getSessionById } from './functions';
40
+import { getSessionById, getResourceId } from './functions';
41 41
 import {
42 42
     LIVE_STREAMING_OFF_SOUND_FILE,
43 43
     LIVE_STREAMING_ON_SOUND_FILE,
@@ -160,11 +160,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
160 160
                     // Show notification with additional information to the initiator.
161 161
                     dispatch(showRecordingLimitNotification(mode));
162 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 166
                 sendAnalytics(createRecordingEvent('start', mode));
169 167
 
170 168
                 if (disableRecordAudioNotification) {
@@ -188,8 +186,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
188 186
                 }
189 187
             } else if (updatedSessionData.status === OFF
190 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 195
                 let duration = 0, soundOff, soundOn;
194 196
 
195 197
                 if (oldSessionData && oldSessionData.timestamp) {

Loading…
Cancel
Save