extends Component {
);
}
+
+ /**
+ * Renders the {@code TranscribingLabel}.
+ *
+ * @returns {React$Element}
+ * @protected
+ */
+ _renderTranscribingLabel() {
+ return (
+
+ );
+ }
}
/**
diff --git a/react/features/large-video/components/Labels.web.js b/react/features/large-video/components/Labels.web.js
index 6649c3767f31..c5e5c5343129 100644
--- a/react/features/large-video/components/Labels.web.js
+++ b/react/features/large-video/components/Labels.web.js
@@ -85,6 +85,9 @@ class Labels extends AbstractLabels {
this._renderRecordingLabel(
JitsiRecordingConstants.mode.STREAM)
}
+ {
+ this._renderTranscribingLabel()
+ }
{
this._renderVideoQualityLabel()
}
@@ -95,6 +98,8 @@ class Labels extends AbstractLabels {
_renderRecordingLabel: string => React$Element<*>
_renderVideoQualityLabel: () => React$Element<*>
+
+ _renderTranscribingLabel: () => React$Element<*>
}
export default connect(_mapStateToProps)(Labels);
diff --git a/react/features/toolbox/components/web/Toolbox.js b/react/features/toolbox/components/web/Toolbox.js
index 9a3f0668a758..f2678a563fde 100644
--- a/react/features/toolbox/components/web/Toolbox.js
+++ b/react/features/toolbox/components/web/Toolbox.js
@@ -14,7 +14,8 @@ import { translate } from '../../../base/i18n';
import {
getLocalParticipant,
getParticipants,
- participantUpdated
+ participantUpdated,
+ isLocalParticipantModerator
} from '../../../base/participants';
import { getLocalVideoTrack, toggleScreensharing } from '../../../base/tracks';
import { ChatCounter } from '../../../chat';
@@ -56,6 +57,9 @@ import OverflowMenuItem from './OverflowMenuItem';
import OverflowMenuProfileItem from './OverflowMenuProfileItem';
import ToolbarButton from './ToolbarButton';
import VideoMuteButton from '../VideoMuteButton';
+import {
+ ClosedCaptionButton
+} from '../../../transcribing';
/**
* The type of the React {@code Component} props of {@link Toolbox}.
@@ -144,6 +148,11 @@ type Props = {
*/
_sharingVideo: boolean,
+ /**
+ * Whether or not transcribing is enabled.
+ */
+ _transcribingEnabled: boolean,
+
/**
* Flag showing whether toolbar is visible.
*/
@@ -302,6 +311,7 @@ class Toolbox extends Component {
_chatOpen,
_hideInviteButton,
_overflowMenuVisible,
+ _transcribingEnabled,
_raisedHand,
_visible,
_visibleButtons,
@@ -344,6 +354,11 @@ class Toolbox extends Component {
tooltip = { t('toolbar.chat') } />
}
+ {
+ _transcribingEnabled
+ && this._shouldShowButton('closedcaptions')
+ &&
+ }
{
+ const showNotificationAction = showNotification({
+ descriptionKey: 'transcribing.pending',
+ isDismissAllowed: false,
+ titleKey: 'dialog.transcribing'
+ });
+
+ dispatch(showNotificationAction);
+
+ dispatch(setPendingTranscribingNotificationUid(
+ showNotificationAction.uid));
+ };
+}
+
+/**
+ * Sets UID of the the pending transcribing notification to use it when hiding
+ * the notification is necessary, or unsets it when
+ * undefined (or no param) is passed.
+ *
+ * @param {?number} uid - The UID of the notification.
+ * redux.
+ * @returns {{
+ * type: SET_PENDING_TRANSCRIBING_NOTIFICATION_UID,
+ * uid: number
+ * }}
+ */
+export function setPendingTranscribingNotificationUid(uid: ?number) {
+ return {
+ type: SET_PENDING_TRANSCRIBING_NOTIFICATION_UID,
+ uid
+ };
+}
+
+/**
+ * Signals that the pending transcribing notification should be removed from the
+ * screen.
+ *
+ * @returns {Function}
+ */
+export function hidePendingTranscribingNotification() {
+ return (dispatch: Function, getState: Function) => {
+ const { pendingNotificationUid } = getState()['features/transcribing'];
+
+ if (pendingNotificationUid) {
+ dispatch(hideNotification(pendingNotificationUid));
+ dispatch(setPendingTranscribingNotificationUid());
+ }
+ };
+}
+
+/**
+ * Signals that the stopped transcribing notification should be shown on the
+ * screen for a 2500 ms.
+ *
+ * @returns {showNotification}
+ */
+export function showStoppedTranscribingNotification() {
+ return showNotification({
+ descriptionKey: 'transcribing.off',
+ titleKey: 'dialog.transcribing'
+ }, 2500);
+}
+
+
+/**
+ * Signals that the transcribing error notification should be shown.
+ *
+ * @returns {showErrorNotification}
+ */
+export function showTranscribingError() {
+ return showErrorNotification({
+ descriptionKey: 'transcribing.error',
+ titleKey: 'transcribing.failedToStart'
+ });
+}
+
+
diff --git a/react/features/transcribing/components/ClosedCaptionButton.native.js b/react/features/transcribing/components/ClosedCaptionButton.native.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/react/features/transcribing/components/ClosedCaptionButton.web.js b/react/features/transcribing/components/ClosedCaptionButton.web.js
new file mode 100644
index 000000000000..a644046810d9
--- /dev/null
+++ b/react/features/transcribing/components/ClosedCaptionButton.web.js
@@ -0,0 +1,131 @@
+// @flow
+
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { translate } from '../../base/i18n/index';
+
+import { ToolbarButton } from '../../toolbox/';
+
+import { dialTranscriber, stopTranscribing } from '../actions';
+import { createToolbarEvent, sendAnalytics } from '../../analytics';
+
+
+/**
+ * The type of the React {@code Component} props of {@link TranscribingLabel}.
+ */
+type Props = {
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: Function,
+
+ /**
+ * Invoked to Dispatch an Action to the redux store.
+ */
+ dispatch: Function,
+
+ /**
+ * Boolean value indicating current transcribing status
+ */
+ _transcribing: boolean,
+
+ /**
+ * Boolean value indicating current dialing status
+ */
+ _dialing: boolean
+};
+
+/**
+ * React Component for displaying a label when a transcriber is in the
+ * conference.
+ *
+ * @extends Component
+ */
+class ClosedCaptionButton extends Component {
+
+ /**
+ * Initializes a new {@code ClosedCaptionButton} instance.
+ *
+ * @param {Props} props - The read-only properties with which the new
+ * instance is to be initialized.
+ */
+ constructor(props: Props) {
+ super(props);
+
+ // Bind event handler so it is only bound once for every instance.
+ this._onToggleButton = this._onToggleButton.bind(this);
+ }
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ const { _dialing, _transcribing, t } = this.props;
+ const iconClass = `icon-closed_caption ${_dialing || _transcribing
+ ? 'toggled' : ''}`;
+
+ return (
+
+ );
+ }
+
+ _onToggleButton: () => void;
+
+ /**
+ * Dispatch actions for starting or stopping transcription, based on
+ * current state.
+ *
+ * @private
+ * @returns {void}
+ */
+ _onToggleButton() {
+ const { _transcribing, _dialing, dispatch } = this.props;
+
+ sendAnalytics(createToolbarEvent(
+ 'transcribing.ccButton',
+ {
+ 'is_transcribing': Boolean(_transcribing),
+ 'is_dialing': Boolean(_dialing)
+ }));
+
+ if (_dialing) {
+ return;
+ }
+
+ if (_transcribing) {
+ dispatch(stopTranscribing());
+ } else {
+ dispatch(dialTranscriber());
+ }
+ }
+
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code ClosedCaptionButton} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * }}
+ */
+function _mapStateToProps(state) {
+ const { isTranscribing, isDialing } = state['features/transcribing'];
+
+ return {
+ _transcribing: isTranscribing,
+ _dialing: isDialing
+ };
+}
+
+export default translate(connect(_mapStateToProps)(ClosedCaptionButton));
diff --git a/react/features/transcribing/components/TranscribingLabel.native.js b/react/features/transcribing/components/TranscribingLabel.native.js
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/react/features/transcribing/components/TranscribingLabel.web.js b/react/features/transcribing/components/TranscribingLabel.web.js
new file mode 100644
index 000000000000..b220827e121d
--- /dev/null
+++ b/react/features/transcribing/components/TranscribingLabel.web.js
@@ -0,0 +1,75 @@
+// @flow
+
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { translate } from '../../base/i18n/index';
+
+import { CircularLabel } from '../../base/label/index';
+import Tooltip from '@atlaskit/tooltip';
+
+/**
+ * The type of the React {@code Component} props of {@link TranscribingLabel}.
+ */
+type Props = {
+
+ /**
+ * Invoked to obtain translated strings.
+ */
+ t: Function,
+
+ /**
+ * Boolean value indicating current transcribing status
+ */
+ _transcribing: boolean
+};
+
+/**
+ * React Component for displaying a label when a transcriber is in the
+ * conference.
+ *
+ * @extends Component
+ */
+class TranscribingLabel extends Component {
+
+ /**
+ * Implements React's {@link Component#render()}.
+ *
+ * @inheritdoc
+ * @returns {ReactElement}
+ */
+ render() {
+ if (!this.props._transcribing) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+
+}
+
+/**
+ * Maps (parts of) the Redux state to the associated props for the
+ * {@code TranscribingLabel} component.
+ *
+ * @param {Object} state - The Redux state.
+ * @private
+ * @returns {{
+ * }}
+ */
+function _mapStateToProps(state) {
+ const { isTranscribing } = state['features/transcribing'];
+
+ return {
+ _transcribing: isTranscribing
+ };
+}
+
+export default translate(connect(_mapStateToProps)(TranscribingLabel));
diff --git a/react/features/transcribing/components/index.js b/react/features/transcribing/components/index.js
new file mode 100644
index 000000000000..651bc819c0d4
--- /dev/null
+++ b/react/features/transcribing/components/index.js
@@ -0,0 +1,2 @@
+export { default as TranscribingLabel } from './TranscribingLabel';
+export { default as ClosedCaptionButton } from './ClosedCaptionButton';
diff --git a/react/features/transcribing/index.js b/react/features/transcribing/index.js
new file mode 100644
index 000000000000..7f0ef0251d9d
--- /dev/null
+++ b/react/features/transcribing/index.js
@@ -0,0 +1,5 @@
+export * from './actions';
+export * from './components';
+
+import './middleware';
+import './reducer';
diff --git a/react/features/transcribing/middleware.js b/react/features/transcribing/middleware.js
new file mode 100644
index 000000000000..19e963f4423b
--- /dev/null
+++ b/react/features/transcribing/middleware.js
@@ -0,0 +1,100 @@
+// @flow
+
+import { MiddlewareRegistry } from '../base/redux';
+
+import {
+ _TRANSCRIBER_LEFT,
+ DIAL_TRANSCRIBER,
+ STOP_TRANSCRIBING
+} from './actionTypes';
+import {
+ dialError,
+ hidePendingTranscribingNotification,
+ potentialTranscriberJoined,
+ showPendingTranscribingNotification,
+ showStoppedTranscribingNotification,
+ showTranscribingError,
+ transcriberJoined,
+ transcriberLeft
+} from './actions';
+import {
+ HIDDEN_PARTICIPANT_JOINED,
+ HIDDEN_PARTICIPANT_LEFT,
+ PARTICIPANT_UPDATED
+} from './../base/participants';
+
+declare var APP: Object;
+
+const TRANSCRIBER_DIAL_COMMAND = 'jitsi_meet_transcribe';
+const TRANSCRIBER_DISPLAY_NAME = 'Transcriber';
+
+/**
+ * Implements the middleware of the feature transcribing.
+ *
+ * @param {Store} store - The redux store.
+ * @returns {Function}
+ */
+// eslint-disable-next-line no-unused-vars
+MiddlewareRegistry.register(store => next => action => {
+ const {
+ isDialing,
+ isTranscribing,
+ transcriberJID,
+ potentialTranscriberJIDs
+ } = store.getState()['features/transcribing'];
+
+ const { conference } = store.getState()['features/base/conference'];
+
+ switch (action.type) {
+ case DIAL_TRANSCRIBER:
+ if (!(isDialing || isTranscribing)) {
+ store.dispatch(showPendingTranscribingNotification());
+
+ conference.room.dial(TRANSCRIBER_DIAL_COMMAND).catch(
+ () => {
+ store.dispatch(dialError());
+ store.dispatch(hidePendingTranscribingNotification());
+ store.dispatch(showTranscribingError());
+ }
+ );
+ }
+ break;
+ case STOP_TRANSCRIBING:
+ if (isTranscribing) {
+ const participant = conference.getParticipantById(transcriberJID);
+
+ conference.room.kick(participant.getJid());
+ }
+ break;
+ case _TRANSCRIBER_LEFT:
+ store.dispatch(showStoppedTranscribingNotification());
+ break;
+ case HIDDEN_PARTICIPANT_JOINED:
+ if (action.displayName
+ && action.displayName === TRANSCRIBER_DISPLAY_NAME) {
+ store.dispatch(transcriberJoined(action.id));
+ } else {
+ store.dispatch(potentialTranscriberJoined(action.id));
+ }
+
+ break;
+ case HIDDEN_PARTICIPANT_LEFT:
+ if (action.id === transcriberJID) {
+ store.dispatch(transcriberLeft(action.id));
+ }
+ break;
+ case PARTICIPANT_UPDATED: {
+ const { participant } = action;
+
+ if (potentialTranscriberJIDs.includes(participant.id)
+ && participant.name === TRANSCRIBER_DISPLAY_NAME) {
+ store.dispatch(transcriberJoined(participant.id));
+ store.dispatch(hidePendingTranscribingNotification());
+ }
+
+ break;
+ }
+ }
+
+ return next(action);
+});
diff --git a/react/features/transcribing/reducer.js b/react/features/transcribing/reducer.js
new file mode 100644
index 000000000000..68b321d1e0c3
--- /dev/null
+++ b/react/features/transcribing/reducer.js
@@ -0,0 +1,115 @@
+import { ReducerRegistry } from '../base/redux';
+import {
+ _DIAL_ERROR,
+ _TRANSCRIBER_JOINED,
+ _TRANSCRIBER_LEFT,
+ _POTENTIAL_TRANSCRIBER_JOINED,
+ DIAL_TRANSCRIBER,
+ SET_PENDING_TRANSCRIBING_NOTIFICATION_UID,
+ STOP_TRANSCRIBING
+} from '../transcribing/actionTypes';
+
+/**
+ * Returns initial state for transcribing feature part of Redux store.
+ *
+ * @returns {{
+ * isTranscribing: boolean,
+ * isDialing: boolean,
+ * transcriberJID: null,
+ * potentialTranscriberJIDs: Array
+ * }}
+ * @private
+ */
+function _getInitialState() {
+ return {
+ /**
+ * Indicates whether there is currently an active transcriber in the
+ * room
+ *
+ * @type {boolean}
+ */
+ isTranscribing: false,
+
+ /**
+ * Indicates whether the transcriber has been dialed into the room and
+ * we're currently awaiting successfull joining or failure of joining
+ *
+ * @type {boolean}
+ */
+ isDialing: false,
+
+ /**
+ * Indicates whether the transcribing feature is in the process of
+ * terminating; the transcriber has been told to leave.
+ */
+ isTerminating: false,
+
+ /**
+ * The JID of the active transcriber
+ *
+ * @type { string }
+ */
+ transcriberJID: null,
+
+ /**
+ * A list containing potential JID's of transcriber participants
+ *
+ * @type { Array }
+ */
+ potentialTranscriberJIDs: []
+ };
+}
+
+/**
+ * Reduces the Redux actions of the feature features/transcribing.
+ */
+ReducerRegistry.register('features/transcribing',
+ (state = _getInitialState(), action) => {
+ switch (action.type) {
+ case DIAL_TRANSCRIBER:
+ return {
+ ...state,
+ isDialing: true
+ };
+ case STOP_TRANSCRIBING:
+ return {
+ ...state,
+ isTerminating: true
+ };
+ case _DIAL_ERROR:
+ return {
+ ...state,
+ isDialing: false,
+ potentialTranscriberJIDs: []
+ };
+ case _TRANSCRIBER_JOINED:
+ return {
+ ...state,
+ isTranscribing: true,
+ isDialing: false,
+ transcriberJID: action.transcriberJID
+ };
+ case _TRANSCRIBER_LEFT:
+ return {
+ ...state,
+ isTerminating: false,
+ isTranscribing: false,
+ transcriberJID: undefined,
+ potentialTranscriberJIDs: []
+ };
+ case _POTENTIAL_TRANSCRIBER_JOINED:
+ return {
+ ...state,
+ potentialTranscriberJIDs:
+ [ action.transcriberJID ]
+ .concat(state.potentialTranscriberJIDs)
+ };
+ case SET_PENDING_TRANSCRIBING_NOTIFICATION_UID:
+ return {
+ ...state,
+ pendingNotificationUid: action.uid
+ };
+ default:
+ return state;
+ }
+ });