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
This commit is contained in:
vp8x8 2021-06-21 11:36:18 +03:00 committed by GitHub
parent 993b6ba4f2
commit 3d83847e4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 47 deletions

BIN
images/icon-cloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

@ -655,12 +655,15 @@
"beta": "BETA",
"busy": "We're working on freeing recording resources. Please try again in a few minutes.",
"busyTitle": "All recorders are currently busy",
"copyLink": "Copy Link",
"error": "Recording failed. Please try again.",
"errorFetchingLink": "Error fetching recording link.",
"expandedOff": "Recording has stopped",
"expandedOn": "The meeting is currently being recorded.",
"expandedPending": "Recording is being started...",
"failedToStart": "Recording failed to start",
"fileSharingdescription": "Share recording with meeting participants",
"linkGenerated": "We have generated a link to your recording.",
"live": "LIVE",
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
@ -675,7 +678,8 @@
"signIn": "Sign in",
"signOut": "Sign out",
"unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
"unavailableTitle": "Recording unavailable"
"unavailableTitle": "Recording unavailable",
"uploadToCloud": "Upload to the cloud"
},
"sectionList": {
"pullToRefresh": "Pull to refresh"

View File

@ -40,6 +40,26 @@ export function createFakeConfig(baseURL: string) {
};
}
/**
* Selector used to get the meeting region.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getMeetingRegion(state: Object) {
return state['features/base/config']?.deploymentInfo?.region || '';
}
/**
* Selector used to get the endpoint used for fetching the recording.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getRecordingSharingUrl(state: Object) {
return state['features/base/config'].recordingSharingUrl;
}
/* eslint-disable max-params, no-shadow */
/**

View File

@ -22,6 +22,16 @@ export function extractVpaasTenantFromPath(path: string) {
return '';
}
/**
* Returns the vpaas tenant.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getVpaasTenant(state: Object) {
return extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
}
/**
* Returns true if the current meeting is a vpaas one.
*

View File

@ -1,6 +1,10 @@
// @flow
import { getMeetingRegion, getRecordingSharingUrl } from '../base/config';
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants';
import { copyText } from '../base/util/helpers';
import { getVpaasTenant, isVpaasMeeting } from '../billing-counter/functions';
import {
NOTIFICATION_TIMEOUT,
hideNotification,
@ -14,6 +18,8 @@ import {
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_STREAM_KEY
} from './actionTypes';
import { getRecordingLink, getResourceId } from './functions';
import logger from './logger';
/**
* Clears the data of every recording sessions.
@ -136,26 +142,68 @@ export function showStoppedRecordingNotification(streamType: string, participant
* Signals that a started recording notification should be shown on the
* screen for a given period.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @param {string} participantName - The participant name that started the recording.
* @returns {showNotification}
* @param {string} mode - The type of the recording: Stream of File.
* @param {string | Object } initiator - The participant who started recording.
* @param {string} sessionId - The recording session id.
* @returns {Function}
*/
export function showStartedRecordingNotification(streamType: string, participantName: string) {
const isLiveStreaming
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
const descriptionArguments = { name: participantName };
const dialogProps = isLiveStreaming ? {
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
descriptionArguments,
titleKey: 'dialog.liveStreaming'
} : {
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
descriptionArguments,
titleKey: 'dialog.recording'
};
export function showStartedRecordingNotification(
mode: string,
initiator: Object | string,
sessionId: string) {
return async (dispatch: Function, getState: Function) => {
const state = getState();
const initiatorId = getResourceId(initiator);
const participantName = getParticipantDisplayName(state, initiatorId);
let dialogProps = {
customActionNameKey: undefined,
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
descriptionArguments: { name: participantName },
isDismissAllowed: true,
titleKey: 'dialog.liveStreaming'
};
return showNotification(dialogProps, NOTIFICATION_TIMEOUT);
if (mode !== JitsiMeetJS.constants.recording.mode.STREAM) {
const recordingSharingUrl = getRecordingSharingUrl(state);
const iAmRecordingInitiator = getLocalParticipant(state).id === initiatorId;
dialogProps = {
customActionHandler: undefined,
customActionNameKey: undefined,
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
descriptionArguments: { name: participantName },
isDismissAllowed: true,
titleKey: 'dialog.recording'
};
// fetch the recording link from the server for recording initiators in jaas meetings
if (recordingSharingUrl
&& isVpaasMeeting(state)
&& iAmRecordingInitiator) {
const region = getMeetingRegion(state);
const tenant = getVpaasTenant(state);
try {
const link = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
// add the option to copy recording link
dialogProps.customActionNameKey = 'recording.copyLink';
dialogProps.customActionHandler = () => copyText(link);
dialogProps.titleKey = 'recording.on';
dialogProps.descriptionKey = 'recording.linkGenerated';
dialogProps.isDismissAllowed = false;
} catch (err) {
dispatch(showErrorNotification({
titleKey: 'recording.errorFetchingLink'
}));
return logger.error('Could not fetch recording link', err);
}
}
}
dispatch(showNotification(dialogProps));
};
}
/**

View File

@ -26,8 +26,7 @@ import { authorizeDropbox, updateDropboxToken } from '../../../dropbox';
import { RECORDING_TYPES } from '../../constants';
import { getRecordingDurationEstimation } from '../../functions';
import { DROPBOX_LOGO, ICON_SHARE, JITSI_LOGO } from './styles';
import { DROPBOX_LOGO, ICON_CLOUD, JITSI_LOGO } from './styles';
type Props = {
@ -162,9 +161,11 @@ class StartRecordingDialogContent extends Component<Props> {
* @returns {React$Component}
*/
_renderFileSharingContent() {
const { fileRecordingsServiceSharingEnabled, isVpaas } = this.props;
const { fileRecordingsServiceSharingEnabled, isVpaas, selectedRecordingService } = this.props;
if (!fileRecordingsServiceSharingEnabled || isVpaas) {
if (!fileRecordingsServiceSharingEnabled
|| isVpaas
|| selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE) {
return null;
}
@ -173,31 +174,22 @@ class StartRecordingDialogContent extends Component<Props> {
_styles: styles,
isValidating,
onSharingSettingChanged,
selectedRecordingService,
sharingSetting,
t
} = this.props;
const controlDisabled = selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE;
let mainContainerClasses = 'recording-header recording-header-line';
if (controlDisabled) {
mainContainerClasses += ' recording-switch-disabled';
}
return (
<Container
className = { mainContainerClasses }
className = 'recording-header'
key = 'fileSharingSetting'
style = { [
styles.header,
_dialogStyles.topBorderContainer,
controlDisabled ? styles.controlDisabled : null
_dialogStyles.topBorderContainer
] }>
<Container className = 'recording-icon-container'>
<Image
className = 'recording-icon'
src = { ICON_SHARE }
src = { ICON_CLOUD }
style = { styles.recordingIcon } />
</Container>
<Text
@ -210,12 +202,12 @@ class StartRecordingDialogContent extends Component<Props> {
</Text>
<Switch
className = 'recording-switch'
disabled = { controlDisabled || isValidating }
disabled = { isValidating }
onValueChange
= { onSharingSettingChanged }
style = { styles.switch }
trackColor = {{ false: ColorPalette.lightGrey }}
value = { !controlDisabled && sharingSetting } />
value = { sharingSetting } />
</Container>
);
}
@ -248,7 +240,7 @@ class StartRecordingDialogContent extends Component<Props> {
value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
) : null;
const icon = isVpaas ? ICON_SHARE : JITSI_LOGO;
const icon = isVpaas ? ICON_CLOUD : JITSI_LOGO;
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
return (
@ -334,7 +326,7 @@ class StartRecordingDialogContent extends Component<Props> {
return (
<Container>
<Container
className = 'recording-header'
className = 'recording-header recording-header-line'
style = { styles.header }>
<Container
className = 'recording-icon-container'>

View File

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

View File

@ -4,6 +4,6 @@ export default {};
export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
export const ICON_SHARE = 'images/icon-users.png';
export const ICON_CLOUD = 'images/icon-cloud.png';
export const JITSI_LOGO = 'images/jitsiLogo_square.png';

View File

@ -46,6 +46,27 @@ export function getSessionById(state: Object, id: string) {
sessionData => sessionData.id === id);
}
/**
* Fetches the recording link from the server.
*
* @param {string} url - The base url.
* @param {string} recordingSessionId - The ID of the recording session to find.
* @param {string} region - The meeting region.
* @param {string} tenant - The meeting tenant.
* @returns {Promise<any>}
*/
export async function getRecordingLink(url: string, recordingSessionId: string, region: string, tenant: string) {
const fullUrl = `${url}?recordingSessionId=${recordingSessionId}&region=${region}&tenant=${tenant}`;
const res = await fetch(fullUrl, {
headers: {
'Content-Type': 'application/json'
}
});
const json = await res.json();
return res.ok ? json.url : Promise.reject(json);
}
/**
* Returns the recording session status that is to be shown in a label. E.g. If
* 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 {
return status;
}
/**
* Returns the resource id.
*
* @param {Object | string} recorder - A participant or it's resource.
* @returns {string|undefined}
*/
export function getResourceId(recorder: string | Object) {
if (recorder) {
return typeof recorder === 'string'
? recorder
: recorder.getId();
}
}

View File

@ -37,7 +37,7 @@ import {
RECORDING_OFF_SOUND_ID,
RECORDING_ON_SOUND_ID
} from './constants';
import { getSessionById } from './functions';
import { getSessionById, getResourceId } from './functions';
import {
LIVE_STREAMING_OFF_SOUND_FILE,
LIVE_STREAMING_ON_SOUND_FILE,
@ -160,11 +160,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
} else {
dispatch(showStartedRecordingNotification(
mode, initiator && getParticipantDisplayName(getState, initiator.getId())));
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
}
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {
@ -188,8 +186,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
} else if (updatedSessionData.status === OFF
&& (!oldSessionData || oldSessionData.status !== OFF)) {
dispatch(showStoppedRecordingNotification(
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
if (terminator) {
dispatch(
showStoppedRecordingNotification(
mode, getParticipantDisplayName(getState, getResourceId(terminator))));
}
let duration = 0, soundOff, soundOn;
if (oldSessionData && oldSessionData.timestamp) {