feat(recording) allow highlighting meeting recording moments (#10981)
This commit is contained in:
parent
faac45b5bc
commit
d651ecb166
|
@ -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'
|
||||
// ]
|
||||
// },
|
||||
|
||||
|
|
|
@ -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>.",
|
||||
|
|
|
@ -26,6 +26,7 @@ export const TOOLBAR_BUTTONS = [
|
|||
'fullscreen',
|
||||
'hangup',
|
||||
'help',
|
||||
'highlight',
|
||||
'invite',
|
||||
'linktosalesforce',
|
||||
'livestreaming',
|
||||
|
|
|
@ -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 |
|
@ -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';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export const CONFERENCE_INFO = {
|
||||
alwaysVisible: [ 'recording', 'local-recording', 'raised-hands-count' ],
|
||||
autoHide: [
|
||||
'highlight-moment',
|
||||
'subject',
|
||||
'conference-timer',
|
||||
'participants-count',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
}
|
|
@ -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)));
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue