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:
parent
993b6ba4f2
commit
3d83847e4b
Binary file not shown.
After Width: | Height: | Size: 349 B |
|
@ -655,12 +655,15 @@
|
||||||
"beta": "BETA",
|
"beta": "BETA",
|
||||||
"busy": "We're working on freeing recording resources. Please try again in a few minutes.",
|
"busy": "We're working on freeing recording resources. Please try again in a few minutes.",
|
||||||
"busyTitle": "All recorders are currently busy",
|
"busyTitle": "All recorders are currently busy",
|
||||||
|
"copyLink": "Copy Link",
|
||||||
"error": "Recording failed. Please try again.",
|
"error": "Recording failed. Please try again.",
|
||||||
|
"errorFetchingLink": "Error fetching recording link.",
|
||||||
"expandedOff": "Recording has stopped",
|
"expandedOff": "Recording has stopped",
|
||||||
"expandedOn": "The meeting is currently being recorded.",
|
"expandedOn": "The meeting is currently being recorded.",
|
||||||
"expandedPending": "Recording is being started...",
|
"expandedPending": "Recording is being started...",
|
||||||
"failedToStart": "Recording failed to start",
|
"failedToStart": "Recording failed to start",
|
||||||
"fileSharingdescription": "Share recording with meeting participants",
|
"fileSharingdescription": "Share recording with meeting participants",
|
||||||
|
"linkGenerated": "We have generated a link to your recording.",
|
||||||
"live": "LIVE",
|
"live": "LIVE",
|
||||||
"loggedIn": "Logged in as {{userName}}",
|
"loggedIn": "Logged in as {{userName}}",
|
||||||
"off": "Recording stopped",
|
"off": "Recording stopped",
|
||||||
|
@ -675,7 +678,8 @@
|
||||||
"signIn": "Sign in",
|
"signIn": "Sign in",
|
||||||
"signOut": "Sign out",
|
"signOut": "Sign out",
|
||||||
"unavailable": "Oops! The {{serviceName}} is currently unavailable. We're working on resolving the issue. Please try again later.",
|
"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": {
|
"sectionList": {
|
||||||
"pullToRefresh": "Pull to refresh"
|
"pullToRefresh": "Pull to refresh"
|
||||||
|
|
|
@ -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 */
|
/* eslint-disable max-params, no-shadow */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,6 +22,16 @@ export function extractVpaasTenantFromPath(path: string) {
|
||||||
return '';
|
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.
|
* Returns true if the current meeting is a vpaas one.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
|
import { getMeetingRegion, getRecordingSharingUrl } from '../base/config';
|
||||||
import JitsiMeetJS, { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
|
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 {
|
import {
|
||||||
NOTIFICATION_TIMEOUT,
|
NOTIFICATION_TIMEOUT,
|
||||||
hideNotification,
|
hideNotification,
|
||||||
|
@ -14,6 +18,8 @@ import {
|
||||||
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
SET_PENDING_RECORDING_NOTIFICATION_UID,
|
||||||
SET_STREAM_KEY
|
SET_STREAM_KEY
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
import { getRecordingLink, getResourceId } from './functions';
|
||||||
|
import logger from './logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the data of every recording sessions.
|
* 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
|
* Signals that a started recording notification should be shown on the
|
||||||
* screen for a given period.
|
* screen for a given period.
|
||||||
*
|
*
|
||||||
* @param {string} streamType - The type of the stream ({@code file} or
|
* @param {string} mode - The type of the recording: Stream of File.
|
||||||
* {@code stream}).
|
* @param {string | Object } initiator - The participant who started recording.
|
||||||
* @param {string} participantName - The participant name that started the recording.
|
* @param {string} sessionId - The recording session id.
|
||||||
* @returns {showNotification}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
export function showStartedRecordingNotification(streamType: string, participantName: string) {
|
export function showStartedRecordingNotification(
|
||||||
const isLiveStreaming
|
mode: string,
|
||||||
= streamType === JitsiMeetJS.constants.recording.mode.STREAM;
|
initiator: Object | string,
|
||||||
const descriptionArguments = { name: participantName };
|
sessionId: string) {
|
||||||
const dialogProps = isLiveStreaming ? {
|
return async (dispatch: Function, getState: Function) => {
|
||||||
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
|
const state = getState();
|
||||||
descriptionArguments,
|
const initiatorId = getResourceId(initiator);
|
||||||
titleKey: 'dialog.liveStreaming'
|
const participantName = getParticipantDisplayName(state, initiatorId);
|
||||||
} : {
|
let dialogProps = {
|
||||||
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
|
customActionNameKey: undefined,
|
||||||
descriptionArguments,
|
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
|
||||||
titleKey: 'dialog.recording'
|
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));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -26,8 +26,7 @@ import { authorizeDropbox, updateDropboxToken } from '../../../dropbox';
|
||||||
import { RECORDING_TYPES } from '../../constants';
|
import { RECORDING_TYPES } from '../../constants';
|
||||||
import { getRecordingDurationEstimation } from '../../functions';
|
import { getRecordingDurationEstimation } from '../../functions';
|
||||||
|
|
||||||
import { DROPBOX_LOGO, ICON_SHARE, JITSI_LOGO } from './styles';
|
import { DROPBOX_LOGO, ICON_CLOUD, JITSI_LOGO } from './styles';
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
@ -162,9 +161,11 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||||
* @returns {React$Component}
|
* @returns {React$Component}
|
||||||
*/
|
*/
|
||||||
_renderFileSharingContent() {
|
_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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,31 +174,22 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||||
_styles: styles,
|
_styles: styles,
|
||||||
isValidating,
|
isValidating,
|
||||||
onSharingSettingChanged,
|
onSharingSettingChanged,
|
||||||
selectedRecordingService,
|
|
||||||
sharingSetting,
|
sharingSetting,
|
||||||
t
|
t
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const controlDisabled = selectedRecordingService !== RECORDING_TYPES.JITSI_REC_SERVICE;
|
|
||||||
let mainContainerClasses = 'recording-header recording-header-line';
|
|
||||||
|
|
||||||
if (controlDisabled) {
|
|
||||||
mainContainerClasses += ' recording-switch-disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
className = { mainContainerClasses }
|
className = 'recording-header'
|
||||||
key = 'fileSharingSetting'
|
key = 'fileSharingSetting'
|
||||||
style = { [
|
style = { [
|
||||||
styles.header,
|
styles.header,
|
||||||
_dialogStyles.topBorderContainer,
|
_dialogStyles.topBorderContainer
|
||||||
controlDisabled ? styles.controlDisabled : null
|
|
||||||
] }>
|
] }>
|
||||||
<Container className = 'recording-icon-container'>
|
<Container className = 'recording-icon-container'>
|
||||||
<Image
|
<Image
|
||||||
className = 'recording-icon'
|
className = 'recording-icon'
|
||||||
src = { ICON_SHARE }
|
src = { ICON_CLOUD }
|
||||||
style = { styles.recordingIcon } />
|
style = { styles.recordingIcon } />
|
||||||
</Container>
|
</Container>
|
||||||
<Text
|
<Text
|
||||||
|
@ -210,12 +202,12 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||||
</Text>
|
</Text>
|
||||||
<Switch
|
<Switch
|
||||||
className = 'recording-switch'
|
className = 'recording-switch'
|
||||||
disabled = { controlDisabled || isValidating }
|
disabled = { isValidating }
|
||||||
onValueChange
|
onValueChange
|
||||||
= { onSharingSettingChanged }
|
= { onSharingSettingChanged }
|
||||||
style = { styles.switch }
|
style = { styles.switch }
|
||||||
trackColor = {{ false: ColorPalette.lightGrey }}
|
trackColor = {{ false: ColorPalette.lightGrey }}
|
||||||
value = { !controlDisabled && sharingSetting } />
|
value = { sharingSetting } />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -248,7 +240,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||||
value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
|
value = { this.props.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE } />
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const icon = isVpaas ? ICON_SHARE : JITSI_LOGO;
|
const icon = isVpaas ? ICON_CLOUD : JITSI_LOGO;
|
||||||
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
|
const label = isVpaas ? t('recording.serviceDescriptionCloud') : t('recording.serviceDescription');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -334,7 +326,7 @@ class StartRecordingDialogContent extends Component<Props> {
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Container
|
<Container
|
||||||
className = 'recording-header'
|
className = 'recording-header recording-header-line'
|
||||||
style = { styles.header }>
|
style = { styles.header }>
|
||||||
<Container
|
<Container
|
||||||
className = 'recording-icon-container'>
|
className = 'recording-icon-container'>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme';
|
||||||
import { BoxModel, ColorPalette } from '../../../base/styles';
|
import { BoxModel, ColorPalette } from '../../../base/styles';
|
||||||
|
|
||||||
export const DROPBOX_LOGO = require('../../../../../images/dropboxLogo_square.png');
|
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');
|
export const JITSI_LOGO = require('../../../../../images/jitsiLogo_square.png');
|
||||||
|
|
||||||
// XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in
|
// XXX The "standard" {@code BoxModel.padding} has been deemed insufficient in
|
||||||
|
|
|
@ -4,6 +4,6 @@ export default {};
|
||||||
|
|
||||||
export const DROPBOX_LOGO = 'images/dropboxLogo_square.png';
|
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';
|
export const JITSI_LOGO = 'images/jitsiLogo_square.png';
|
||||||
|
|
|
@ -46,6 +46,27 @@ export function getSessionById(state: Object, id: string) {
|
||||||
sessionData => sessionData.id === id);
|
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}®ion=${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
|
* 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
|
* 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;
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import {
|
||||||
RECORDING_OFF_SOUND_ID,
|
RECORDING_OFF_SOUND_ID,
|
||||||
RECORDING_ON_SOUND_ID
|
RECORDING_ON_SOUND_ID
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import { getSessionById } from './functions';
|
import { getSessionById, getResourceId } from './functions';
|
||||||
import {
|
import {
|
||||||
LIVE_STREAMING_OFF_SOUND_FILE,
|
LIVE_STREAMING_OFF_SOUND_FILE,
|
||||||
LIVE_STREAMING_ON_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.
|
// Show notification with additional information to the initiator.
|
||||||
dispatch(showRecordingLimitNotification(mode));
|
dispatch(showRecordingLimitNotification(mode));
|
||||||
} else {
|
} else {
|
||||||
dispatch(showStartedRecordingNotification(
|
dispatch(showStartedRecordingNotification(mode, initiator, action.sessionData.id));
|
||||||
mode, initiator && getParticipantDisplayName(getState, initiator.getId())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sendAnalytics(createRecordingEvent('start', mode));
|
sendAnalytics(createRecordingEvent('start', mode));
|
||||||
|
|
||||||
if (disableRecordAudioNotification) {
|
if (disableRecordAudioNotification) {
|
||||||
|
@ -188,8 +186,12 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||||
}
|
}
|
||||||
} else if (updatedSessionData.status === OFF
|
} else if (updatedSessionData.status === OFF
|
||||||
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
&& (!oldSessionData || oldSessionData.status !== OFF)) {
|
||||||
dispatch(showStoppedRecordingNotification(
|
if (terminator) {
|
||||||
mode, terminator && getParticipantDisplayName(getState, terminator.getId())));
|
dispatch(
|
||||||
|
showStoppedRecordingNotification(
|
||||||
|
mode, getParticipantDisplayName(getState, getResourceId(terminator))));
|
||||||
|
}
|
||||||
|
|
||||||
let duration = 0, soundOff, soundOn;
|
let duration = 0, soundOff, soundOn;
|
||||||
|
|
||||||
if (oldSessionData && oldSessionData.timestamp) {
|
if (oldSessionData && oldSessionData.timestamp) {
|
||||||
|
|
Loading…
Reference in New Issue