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

This commit is contained in:
Avram Tudor 2022-03-14 10:31:08 +02:00 committed by GitHub
parent faac45b5bc
commit d651ecb166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 305 additions and 2 deletions

View File

@ -603,6 +603,7 @@ var config = {
// 'fullscreen',
// 'hangup',
// 'help',
// 'highlight',
// 'invite',
// 'livestreaming',
// 'microphone',
@ -1118,7 +1119,8 @@ var config = {
// 'e2ee',
// 'transcribing',
// 'video-quality',
// 'insecure-room'
// 'insecure-room',
// 'highlight-moment'
// ]
// },

View File

@ -865,6 +865,10 @@
"expandedPending": "Recording is being started...",
"failedToStart": "Recording failed to start",
"fileSharingdescription": "Share the recording link with the meeting participants",
"highlightMoment": "Highlight moment",
"highlightMomentDisabled": "You can highlight moments when the recording starts",
"highlightMomentSuccess": "Moment highlighted",
"highlightMomentSucessDescription": "Your highlighted moment will be added to the meeting summary.",
"inProgress": "Recording or live streaming in progress",
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
"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>.",

View File

@ -26,6 +26,7 @@ export const TOOLBAR_BUTTONS = [
'fullscreen',
'hangup',
'help',
'highlight',
'invite',
'linktosalesforce',
'livestreaming',

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<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" />
</svg>

After

Width:  |  Height:  |  Size: 962 B

View File

@ -60,6 +60,7 @@ export { default as IconFullScreen } from './full-screen.svg';
export { default as IconGoogle } from './google.svg';
export { default as IconHangup } from './hangup.svg';
export { default as IconHelp } from './help.svg';
export { default as IconHighlight } from './highlight.svg';
export { default as IconHome } from './home.svg';
export { default as IconHorizontalPoints } from './horizontal-points.svg';
export { default as IconInfo } from './info.svg';

View File

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

View File

@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
import { E2EELabel } from '../../../e2ee';
import { LocalRecordingLabel } from '../../../local-recording';
import { RecordingLabel } from '../../../recording';
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
import { isToolboxVisible } from '../../../toolbox/functions.web';
import { TranscribingLabel } from '../../../transcribing';
import { VideoQualityLabel } from '../../../video-quality';
@ -38,6 +39,10 @@ type Props = {
};
const COMPONENTS = [
{
Component: HighlightButton,
id: 'highlight-moment'
},
{
Component: SubjectText,
id: 'subject'

View File

@ -56,3 +56,13 @@ export const SET_SELECTED_RECORDING_SERVICE = 'SET_SELECTED_RECORDING_SERVICE';
* }
*/
export const SET_STREAM_KEY = 'SET_STREAM_KEY';
/**
* Sets the enable state of the meeting highlight button.
*
* {
* type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
* disabled: boolean
* }
*/
export const SET_MEETING_HIGHLIGHT_BUTTON_STATE = 'SET_MEETING_HIGHLIGHT_BUTTON_STATE';

View File

@ -16,11 +16,12 @@ import {
import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_SELECTED_RECORDING_SERVICE,
SET_STREAM_KEY
} from './actionTypes';
import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox } from './functions';
import { getRecordingLink, getResourceId, isSavingRecordingOnDropbox, sendMeetingHighlight } from './functions';
import logger from './logger';
declare var APP: Object;
@ -38,6 +39,21 @@ export function clearRecordingSessions() {
};
}
/**
* Sets the meeting highlight button disable state.
*
* @param {boolean} disabled - The disabled state value.
* @returns {{
* type: CLEAR_RECORDING_SESSIONS
* }}
*/
export function setHighlightMomentButtonState(disabled: boolean) {
return {
type: SET_MEETING_HIGHLIGHT_BUTTON_STATE,
disabled
};
}
/**
* Signals that the pending recording notification should be removed from the
* screen.
@ -105,6 +121,31 @@ export function showPendingRecordingNotification(streamType: string) {
};
}
/**
* Highlights a meeting moment.
*
* {@code stream}).
*
* @returns {Function}
*/
export function highlightMeetingMoment() {
return async (dispatch: Function, getState: Function) => {
dispatch(setHighlightMomentButtonState(true));
try {
await sendMeetingHighlight(getState());
dispatch(showNotification({
descriptionKey: 'recording.highlightMomentSucessDescription',
titleKey: 'recording.highlightMomentSuccess'
}));
} catch (err) {
logger.error('Could not highlight meeting moment', err);
}
dispatch(setHighlightMomentButtonState(false));
};
}
/**
* Signals that the recording error notification should be shown.
*

View File

@ -0,0 +1,71 @@
// @flow
import { Component } from 'react';
import { getActiveSession, isHighlightMeetingMomentDisabled } from '../..';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { highlightMeetingMoment } from '../../actions.any';
export type Props = {
/**
* Whether or not the conference is in audio only mode.
*/
_audioOnly: boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Abstract class for the {@code AbstractHighlightButton} component.
*/
export default class AbstractHighlightButton<P: Props> extends Component<P> {
/**
* Initializes a new AbstractVideoTrack instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._onClick = this._onClick.bind(this);
}
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_onClick() {
const { dispatch } = this.props;
dispatch(highlightMeetingMoment());
}
}
/**
* Maps (parts of) the Redux state to the associated
* {@code AbstractVideoQualityLabel}'s props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _audioOnly: boolean
* }}
*/
export function _abstractMapStateToProps(state: Object) {
const isRecordingRunning = getActiveSession(state, JitsiRecordingConstants.mode.FILE);
const isButtonDisabled = isHighlightMeetingMomentDisabled(state);
const { webhookProxyUrl } = state['features/base/config'];
return {
_disabled: !isRecordingRunning || isButtonDisabled,
_visible: Boolean(webhookProxyUrl)
};
}

View File

@ -0,0 +1,94 @@
// @flow
import { withStyles } from '@material-ui/core';
import React from 'react';
import { translate } from '../../../../base/i18n';
import { IconHighlight } from '../../../../base/icons';
import { Label } from '../../../../base/label';
import { connect } from '../../../../base/redux';
import { Tooltip } from '../../../../base/tooltip';
import BaseTheme from '../../../../base/ui/components/BaseTheme';
import AbstractHighlightButton, {
_abstractMapStateToProps,
type Props as AbstractProps
} from '../AbstractHighlightButton';
type Props = AbstractProps & {
_disabled: boolean,
/**
* The message to show within the label's tooltip.
*/
_tooltipKey: string,
/**
* Flag controlling visibility of the component.
*/
_visible: boolean,
};
/**
* Creates the styles for the component.
*
* @param {Object} theme - The current UI theme.
*
* @returns {Object}
*/
const styles = theme => {
return {
regular: {
background: theme.palette.field02,
margin: '0 4px 4px 4px'
},
disabled: {
background: theme.palette.text02,
margin: '0 4px 4px 4px'
}
};
};
/**
* React {@code Component} responsible for displaying an action that
* allows users to highlight a meeting moment.
*/
export class HighlightButton extends AbstractHighlightButton<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
_disabled,
_visible,
classes,
t
} = this.props;
if (!_visible) {
return null;
}
const className = _disabled ? classes.disabled : classes.regular;
const tooltipKey = _disabled ? 'recording.highlightMomentDisabled' : 'recording.highlightMoment';
return (
<Tooltip
content = { t(tooltipKey) }
position = { 'bottom' }>
<Label
className = { className }
icon = { IconHighlight }
iconColor = { _disabled ? BaseTheme.palette.text03 : BaseTheme.palette.field01 }
id = 'highlightMeetingLabel'
onClick = { this._onClick } />
</Tooltip>
);
}
}
export default withStyles(styles)(translate(connect(_abstractMapStateToProps)(HighlightButton)));

View File

@ -1,5 +1,6 @@
// @flow
export { default as HighlightButton } from './HighlightButton';
export { default as RecordButton } from './RecordButton';
export { default as StartRecordingDialog } from './StartRecordingDialog';
export { default as StopRecordingDialog } from './StopRecordingDialog';

View File

@ -1,9 +1,12 @@
// @flow
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant } from '../base/participants';
import { isEnabled as isDropboxEnabled } from '../dropbox';
import { extractFqnFromPath } from '../dynamic-branding';
import { RECORDING_STATUS_PRIORITIES, RECORDING_TYPES } from './constants';
import logger from './logger';
/**
* Searches in the passed in redux state for an active recording session of the
@ -79,6 +82,16 @@ export function isSavingRecordingOnDropbox(state: Object) {
&& state['features/recording'].selectedRecordingService === RECORDING_TYPES.DROPBOX;
}
/**
* Selector used for determining disable state for the meeting highlight button.
*
* @param {Object} state - The redux state to search in.
* @returns {string}
*/
export function isHighlightMeetingMomentDisabled(state: Object) {
return state['features/recording'].disableHighlightMeetingMoment;
}
/**
* 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
@ -120,3 +133,51 @@ export function getResourceId(recorder: string | Object) {
: recorder.getId();
}
}
/**
* Sends a meeting highlight to backend.
*
* @param {Object} state - Redux state.
* @returns {boolean} - True if sent, false otherwise.
*/
export async function sendMeetingHighlight(state: Object) {
const { webhookProxyUrl: url } = state['features/base/config'];
const { conference } = state['features/base/conference'];
const { jwt } = state['features/base/jwt'];
const { connection } = state['features/base/connection'];
const jid = connection.getJid();
const localParticipant = getLocalParticipant(state);
const headers = {
...jwt ? { 'Authorization': `Bearer ${jwt}` } : {},
'Content-Type': 'application/json'
};
const reqBody = {
meetingFqn: extractFqnFromPath(),
sessionId: conference.sessionId,
submitted: Date.now(),
participantId: localParticipant.jwtId,
participantName: localParticipant.name,
participantJid: jid
};
if (url) {
try {
const res = await fetch(`${url}/v2/highlights`, {
method: 'POST',
headers,
body: JSON.stringify(reqBody)
});
if (res.ok) {
return true;
}
logger.error('Status error:', res.status);
} catch (err) {
logger.error('Could not send request', err);
}
}
return false;
}

View File

@ -3,12 +3,14 @@ import { ReducerRegistry } from '../base/redux';
import {
CLEAR_RECORDING_SESSIONS,
RECORDING_SESSION_UPDATED,
SET_MEETING_HIGHLIGHT_BUTTON_STATE,
SET_PENDING_RECORDING_NOTIFICATION_UID,
SET_SELECTED_RECORDING_SERVICE,
SET_STREAM_KEY
} from './actionTypes';
const DEFAULT_STATE = {
disableHighlightMeetingMoment: false,
pendingNotificationUids: {},
selectedRecordingService: '',
sessionDatas: []
@ -65,6 +67,12 @@ ReducerRegistry.register(STORE_NAME,
streamKey: action.streamKey
};
case SET_MEETING_HIGHLIGHT_BUTTON_STATE:
return {
...state,
disableHighlightMeetingMoment: action.disabled
};
default:
return state;
}