mirror of https://github.com/jitsi/jitsi-meet
[WEB] add UI for transcription (#3213)
* [WEB] add UI for transcription * add analytics event for button, do not use global APP object * use props instead of state, use local conference to kick participant * put imports in alphabetical order * add translation for TranscribingLabel * fix merge conflict * add closed caption button * purge OverFlowMenuItem which starts and stops Transcription * readd closed caption icon and fix small issues due to purge * delete unused icon in _font.scsspull/3312/head jitsi-meet_3252
parent
39f1958300
commit
b8daf0a9f9
Binary file not shown.
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,78 @@ |
||||
/** |
||||
* The type of Redux action triggering the transcriber to join (be 'dialed' in) |
||||
* |
||||
* { |
||||
* type: DIAL_TRANSCRIBER |
||||
* } |
||||
* @public |
||||
*/ |
||||
export const DIAL_TRANSCRIBER = Symbol('DIAL_TRANSCRIBER'); |
||||
|
||||
/** |
||||
* The type of Redux action triggering the transcriber to leave. |
||||
* |
||||
* { |
||||
* type: STOP_TRANSCRBIBING |
||||
* } |
||||
* @public |
||||
*/ |
||||
export const STOP_TRANSCRIBING = Symbol('STOP_TRANSCRBIBING'); |
||||
|
||||
/** |
||||
* The type of Redux action triggering storage of participantId of transcriber, |
||||
* so that it can later be kicked |
||||
* |
||||
* { |
||||
* type: TRANSCRIBER_JOINED, |
||||
* participantId: String |
||||
* } |
||||
* @private |
||||
*/ |
||||
export const _TRANSCRIBER_JOINED = Symbol('TRANSCRIBER_JOINED'); |
||||
|
||||
/** |
||||
* The type of Redux action signalling that the transcriber has left |
||||
* |
||||
* { |
||||
* type: TRANSCRIBER_LEFT, |
||||
* participantId: String |
||||
* } |
||||
* @private |
||||
*/ |
||||
export const _TRANSCRIBER_LEFT = Symbol('TRANSCRIBER_LEFT'); |
||||
|
||||
/** |
||||
* The type of a Redux action signalling that a hidden participant has joined, |
||||
* which can be candidate for being a transcriber. |
||||
* |
||||
* { |
||||
* type: _POTENTIAL_TRANSCRIBER_JOINED, |
||||
* } |
||||
* @private |
||||
*/ |
||||
export const _POTENTIAL_TRANSCRIBER_JOINED |
||||
= Symbol('POTENTIAL_TRANSCRIBER_JOINED'); |
||||
|
||||
/** |
||||
* The type of a Redux action signalling that dialing the transcriber failed. |
||||
* |
||||
* { |
||||
* type: _DIAL_ERROR, |
||||
* } |
||||
* @private |
||||
*/ |
||||
export const _DIAL_ERROR = Symbol('DIAL_ERROR'); |
||||
|
||||
/** |
||||
* The type of Redux action which sets the pending transcribing notification UID |
||||
* to use it for when hiding the notification is necessary, or unsets it when |
||||
* undefined (or no param) is passed. |
||||
* |
||||
* { |
||||
* type: SET_PENDING_TRANSCRIBING_NOTIFICATION_UID, |
||||
* uid: ?number |
||||
* } |
||||
* @public |
||||
*/ |
||||
export const SET_PENDING_TRANSCRIBING_NOTIFICATION_UID |
||||
= Symbol('SET_PENDING_TRANSCRIBING_NOTIFICATION_UID'); |
@ -0,0 +1,189 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
_DIAL_ERROR, |
||||
_POTENTIAL_TRANSCRIBER_JOINED, |
||||
_TRANSCRIBER_JOINED, |
||||
_TRANSCRIBER_LEFT, |
||||
DIAL_TRANSCRIBER, |
||||
SET_PENDING_TRANSCRIBING_NOTIFICATION_UID, |
||||
STOP_TRANSCRIBING |
||||
} from './actionTypes'; |
||||
import { |
||||
hideNotification, |
||||
showErrorNotification, |
||||
showNotification |
||||
} from '../notifications'; |
||||
|
||||
/** |
||||
* Dial the transcriber into the room. |
||||
* |
||||
* @public |
||||
* @returns {{ |
||||
* type: DIAL_TRANSCRIBER |
||||
* }} |
||||
*/ |
||||
export function dialTranscriber() { |
||||
return { |
||||
type: DIAL_TRANSCRIBER |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Stop the transcribing by kicking the transcriber participant. |
||||
* |
||||
* @returns {{ |
||||
* type: STOP_TRANSCRIBING |
||||
* }} |
||||
*/ |
||||
export function stopTranscribing() { |
||||
return { |
||||
type: STOP_TRANSCRIBING |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Notify that the transcriber, with a unique ID, has joined. |
||||
* |
||||
* @param {string} participantId - The participant id of the transcriber. |
||||
* @returns {{ |
||||
* type: _TRANSCRIBER_JOINED, |
||||
* participantId: string |
||||
* }} |
||||
*/ |
||||
export function transcriberJoined(participantId: string) { |
||||
return { |
||||
type: _TRANSCRIBER_JOINED, |
||||
transcriberJID: participantId |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Notify that the transcriber, with a unique ID, has left. |
||||
* |
||||
* @param {string} participantId - The participant id of the transcriber. |
||||
* @returns {{ |
||||
* type: _TRANSCRIBER_LEFT, |
||||
* participantId: string |
||||
* }} |
||||
*/ |
||||
export function transcriberLeft(participantId: string) { |
||||
return { |
||||
type: _TRANSCRIBER_LEFT, |
||||
transcriberJID: participantId |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Notify that a potential transcriber, with a unique ID, has joined. |
||||
* |
||||
* @param {string} participantId - The participant id of the transcriber. |
||||
* @returns {{ |
||||
* type: _POTENTIAL_TRANSCRIBER_JOINED, |
||||
* participantId: string |
||||
* }} |
||||
*/ |
||||
export function potentialTranscriberJoined(participantId: string) { |
||||
return { |
||||
type: _POTENTIAL_TRANSCRIBER_JOINED, |
||||
transcriberJID: participantId |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Notify that dialing the transcriber resulted in an error. |
||||
* |
||||
* @returns {{ |
||||
* type: _DIAL_ERROR |
||||
* }} |
||||
*/ |
||||
export function dialError() { |
||||
return { |
||||
type: _DIAL_ERROR |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Signals that the pending transcribing notification should be shown on the |
||||
* screen. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function showPendingTranscribingNotification() { |
||||
return (dispatch: Function) => { |
||||
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' |
||||
}); |
||||
} |
||||
|
||||
|
@ -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<Props> { |
||||
|
||||
/** |
||||
* 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 ( |
||||
<ToolbarButton |
||||
accessibilityLabel |
||||
= { t('toolbar.accessibilityLabel.cc') } |
||||
iconName = { iconClass } |
||||
onClick = { this._onToggleButton } |
||||
tooltip = { t('transcribing.ccButtonTooltip') } /> |
||||
); |
||||
} |
||||
|
||||
_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)); |
@ -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<Props> { |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
if (!this.props._transcribing) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<Tooltip |
||||
content = { this.props.t('transcribing.labelToolTip') } |
||||
position = { 'left' }> |
||||
<CircularLabel |
||||
className = 'recording-label' |
||||
label = { this.props.t('transcribing.tr') } /> |
||||
</Tooltip> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
/** |
||||
* 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)); |
@ -0,0 +1,2 @@ |
||||
export { default as TranscribingLabel } from './TranscribingLabel'; |
||||
export { default as ClosedCaptionButton } from './ClosedCaptionButton'; |
@ -0,0 +1,5 @@ |
||||
export * from './actions'; |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -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); |
||||
}); |
@ -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; |
||||
} |
||||
}); |
Loading…
Reference in new issue