extends Component {
+ /**
+ * 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)
+ };
+}
diff --git a/react/features/recording/components/Recording/web/HighlightButton.js b/react/features/recording/components/Recording/web/HighlightButton.js
new file mode 100644
index 000000000..630a45087
--- /dev/null
+++ b/react/features/recording/components/Recording/web/HighlightButton.js
@@ -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 {
+
+ /**
+ * 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 (
+
+
+
+ );
+ }
+}
+
+export default withStyles(styles)(translate(connect(_abstractMapStateToProps)(HighlightButton)));
diff --git a/react/features/recording/components/Recording/web/index.js b/react/features/recording/components/Recording/web/index.js
index c59badd89..6ea324514 100644
--- a/react/features/recording/components/Recording/web/index.js
+++ b/react/features/recording/components/Recording/web/index.js
@@ -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';
diff --git a/react/features/recording/functions.js b/react/features/recording/functions.js
index 82592e333..aff18baee 100644
--- a/react/features/recording/functions.js
+++ b/react/features/recording/functions.js
@@ -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;
+}
diff --git a/react/features/recording/reducer.js b/react/features/recording/reducer.js
index e63efa8a1..99bc4f535 100644
--- a/react/features/recording/reducer.js
+++ b/react/features/recording/reducer.js
@@ -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;
}