This commit is contained in:
169
react/features/subtitles/components/web/Captions.tsx
Normal file
169
react/features/subtitles/components/web/Captions.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getVideospaceFloatingElementsBottomSpacing, pixelsToRem, remToPixels } from '../../../base/ui/functions.web';
|
||||
import { getStageParticipantNameLabelHeight } from '../../../display-name/components/web/styles';
|
||||
import { shouldDisplayStageParticipantBadge } from '../../../display-name/functions';
|
||||
import {
|
||||
getTransitionParamsForElementsAboveToolbox,
|
||||
isToolboxVisible,
|
||||
toCSSTransitionValue
|
||||
} from '../../../toolbox/functions.web';
|
||||
import { calculateSubtitlesFontSize } from '../../functions.web';
|
||||
import {
|
||||
AbstractCaptions,
|
||||
type IAbstractCaptionsProps,
|
||||
_abstractMapStateToProps
|
||||
} from '../AbstractCaptions';
|
||||
|
||||
interface IProps extends IAbstractCaptionsProps {
|
||||
|
||||
/**
|
||||
* The height of the visible area.
|
||||
*/
|
||||
_clientHeight?: number;
|
||||
|
||||
/**
|
||||
* Whether the subtitles container is lifted above the invite box.
|
||||
*/
|
||||
_isLifted: boolean | undefined;
|
||||
|
||||
/**
|
||||
* Whether toolbar is shifted up or not.
|
||||
*/
|
||||
_shiftUp: boolean;
|
||||
|
||||
/**
|
||||
* Whether the toolbox is visible or not.
|
||||
*/
|
||||
_toolboxVisible: boolean;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
}
|
||||
|
||||
|
||||
const styles = (theme: Theme, props: IProps) => {
|
||||
const { _isLifted = false, _clientHeight, _shiftUp = false, _toolboxVisible = false } = props;
|
||||
const fontSize = calculateSubtitlesFontSize(_clientHeight);
|
||||
|
||||
// Normally we would use 0.2 * fontSize in order to cover the background gap from line-height: 1.2 but it seems
|
||||
// the current font is a little bit larger than it is supposed to be.
|
||||
const padding = 0.1 * fontSize;
|
||||
let bottom = getVideospaceFloatingElementsBottomSpacing(theme, _toolboxVisible);
|
||||
let marginBottom = 0;
|
||||
|
||||
// This is the case where we display the onstage participant display name
|
||||
// below the subtitles.
|
||||
if (_isLifted) {
|
||||
// 10px is the space between the onstage participant display name label and subtitles. We also need
|
||||
// to add the padding of the subtitles because it will decrease the gap between the label and subtitles.
|
||||
bottom += remToPixels(getStageParticipantNameLabelHeight(theme, _clientHeight)) + 10 + padding;
|
||||
}
|
||||
|
||||
if (_shiftUp) {
|
||||
// The toolbar is shifted up with 30px from the css.
|
||||
marginBottom += 30;
|
||||
}
|
||||
|
||||
return {
|
||||
transcriptionSubtitles: {
|
||||
bottom: `${bottom}px`,
|
||||
marginBottom: `${marginBottom}px`,
|
||||
fontSize: pixelsToRem(fontSize),
|
||||
left: '50%',
|
||||
maxWidth: '50vw',
|
||||
overflowWrap: 'break-word' as const,
|
||||
pointerEvents: 'none' as const,
|
||||
position: 'absolute' as const,
|
||||
textShadow: `
|
||||
0px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 1px 1px rgba(0,0,0,0.3),
|
||||
1px 0px 1px rgba(0,0,0,0.3),
|
||||
0px 0px 1px rgba(0,0,0,0.3)`,
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 7, // The popups are with z-index 8. This z-index has to be lower.
|
||||
lineHeight: 1.2,
|
||||
transition: `bottom ${toCSSTransitionValue(getTransitionParamsForElementsAboveToolbox(_toolboxVisible))}`,
|
||||
|
||||
span: {
|
||||
color: '#fff',
|
||||
background: 'black',
|
||||
|
||||
// without this when the text is wrapped on 2+ lines there will be a gap in the background:
|
||||
padding: `${padding}px 8px`,
|
||||
boxDecorationBreak: 'clone' as const
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* React {@code Component} which can display speech-to-text results from
|
||||
* Jigasi as subtitles.
|
||||
*/
|
||||
class Captions extends AbstractCaptions<IProps> {
|
||||
|
||||
/**
|
||||
* Renders the transcription text.
|
||||
*
|
||||
* @param {string} id - The ID of the transcript message from which the
|
||||
* {@code text} has been created.
|
||||
* @param {string} text - Subtitles text formatted with the participant's
|
||||
* name.
|
||||
* @protected
|
||||
* @returns {ReactElement} - The React element which displays the text.
|
||||
*/
|
||||
override _renderParagraph(id: string, text: string): ReactElement {
|
||||
return (
|
||||
<p key = { id }>
|
||||
<span>{ text }</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the subtitles container.
|
||||
*
|
||||
* @param {Array<ReactElement>} paragraphs - An array of elements created
|
||||
* for each subtitle using the {@link _renderParagraph} method.
|
||||
* @protected
|
||||
* @returns {ReactElement} - The subtitles container.
|
||||
*/
|
||||
override _renderSubtitlesContainer(paragraphs: Array<ReactElement>): ReactElement {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
return (
|
||||
<div className = { classes.transcriptionSubtitles } >
|
||||
{ paragraphs }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@code }'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { clientHeight } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_isLifted: shouldDisplayStageParticipantBadge(state),
|
||||
_clientHeight: clientHeight,
|
||||
_shiftUp: state['features/toolbox'].shiftUp,
|
||||
_toolboxVisible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(Captions, styles));
|
||||
@@ -0,0 +1,92 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconSubtitles } from '../../../base/icons/svg';
|
||||
import { openCCPanel } from '../../../chat/actions.any';
|
||||
import { toggleLanguageSelectorDialog } from '../../actions.web';
|
||||
import { canStartSubtitles, isCCTabEnabled } from '../../functions.any';
|
||||
import {
|
||||
AbstractClosedCaptionButton,
|
||||
IAbstractProps,
|
||||
_abstractMapStateToProps
|
||||
} from '../AbstractClosedCaptionButton';
|
||||
|
||||
/**
|
||||
* A button which starts/stops the transcriptions.
|
||||
*/
|
||||
class ClosedCaptionButton
|
||||
extends AbstractClosedCaptionButton {
|
||||
override icon = IconSubtitles;
|
||||
override labelProps = {
|
||||
language: this.props.t(this.props._language ?? 'transcribing.subtitlesOff'),
|
||||
languages: this.props.t(this.props.languages ?? ''),
|
||||
languagesHead: this.props.t(this.props.languagesHead ?? '')
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current button label based on the CC tab state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override _getLabel() {
|
||||
const { _isCCTabEnabled } = this.props;
|
||||
|
||||
return _isCCTabEnabled ? 'toolbar.closedCaptions' : 'toolbar.startSubtitles';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the accessibility label for the button.
|
||||
*
|
||||
* @returns {string} Accessibility label.
|
||||
*/
|
||||
override _getAccessibilityLabel() {
|
||||
const { _isCCTabEnabled } = this.props;
|
||||
|
||||
return _isCCTabEnabled ? 'toolbar.accessibilityLabel.closedCaptions' : 'toolbar.accessibilityLabel.cc';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tooltip text based on the CC tab state.
|
||||
*
|
||||
* @returns {string} The tooltip text.
|
||||
*/
|
||||
override _getTooltip() {
|
||||
const { _isCCTabEnabled } = this.props;
|
||||
|
||||
return _isCCTabEnabled ? 'transcribing.openClosedCaptions' : 'transcribing.ccButtonTooltip';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle language selection dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClickOpenLanguageSelector() {
|
||||
const { dispatch, _isCCTabEnabled } = this.props;
|
||||
|
||||
if (_isCCTabEnabled) {
|
||||
dispatch(openCCPanel());
|
||||
} else {
|
||||
dispatch(toggleLanguageSelectorDialog());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The component's own props.
|
||||
* @returns {Object} Mapped props for the component.
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: IAbstractProps) {
|
||||
const { visible = canStartSubtitles(state) || isCCTabEnabled(state) } = ownProps;
|
||||
|
||||
return _abstractMapStateToProps(state, {
|
||||
...ownProps,
|
||||
visible
|
||||
});
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(ClosedCaptionButton));
|
||||
52
react/features/subtitles/components/web/LanguageList.tsx
Normal file
52
react/features/subtitles/components/web/LanguageList.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
|
||||
import LanguageListItem from './LanguageListItem';
|
||||
|
||||
interface ILanguageListProps {
|
||||
items: Array<ILanguageItem>;
|
||||
onLanguageSelected: (lang: string) => void;
|
||||
selectedLanguage: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(() => {
|
||||
return {
|
||||
itemsContainer: {
|
||||
display: 'flex',
|
||||
flexFlow: 'column'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
interface ILanguageItem {
|
||||
id: string;
|
||||
lang: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the security options dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
const LanguageList = ({
|
||||
items,
|
||||
onLanguageSelected
|
||||
}: ILanguageListProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const listItems = items.map(item => (
|
||||
<LanguageListItem
|
||||
key = { item.id }
|
||||
lang = { item.lang }
|
||||
onLanguageSelected = { onLanguageSelected }
|
||||
selected = { item.selected } />
|
||||
));
|
||||
|
||||
return (
|
||||
<div className = { styles.itemsContainer }>{listItems}</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageList;
|
||||
77
react/features/subtitles/components/web/LanguageListItem.tsx
Normal file
77
react/features/subtitles/components/web/LanguageListItem.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCheck } from '../../../base/icons/svg';
|
||||
|
||||
interface ILanguageListItemProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Language string.
|
||||
*/
|
||||
lang: string;
|
||||
|
||||
/**
|
||||
* Callback for language selection.
|
||||
*/
|
||||
onLanguageSelected: (lang: string) => void;
|
||||
|
||||
/**
|
||||
* If language item is selected or not.
|
||||
*/
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
itemContainer: {
|
||||
display: 'flex',
|
||||
color: theme.palette.text02,
|
||||
alignItems: 'center',
|
||||
fontSize: '0.875rem',
|
||||
cursor: 'pointer',
|
||||
padding: '5px 0',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui04
|
||||
}
|
||||
},
|
||||
iconWrapper: {
|
||||
margin: '4px 10px',
|
||||
width: '22px',
|
||||
height: '22px'
|
||||
},
|
||||
activeItemContainer: {
|
||||
fontWeight: 700
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders the language list item.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
|
||||
const LanguageListItem = ({
|
||||
t,
|
||||
lang,
|
||||
selected,
|
||||
onLanguageSelected
|
||||
}: ILanguageListItemProps) => {
|
||||
const { classes: styles } = useStyles();
|
||||
const onLanguageSelectedWrapper = useCallback(() => onLanguageSelected(lang), [ lang ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { `${styles.itemContainer} ${selected ? styles.activeItemContainer : ''}` }
|
||||
onClick = { onLanguageSelectedWrapper }>
|
||||
<span className = { styles.iconWrapper }>{ selected
|
||||
&& <Icon src = { IconCheck } /> }</span>
|
||||
{ t(lang) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(LanguageListItem);
|
||||
103
react/features/subtitles/components/web/LanguageSelector.tsx
Normal file
103
react/features/subtitles/components/web/LanguageSelector.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React, { ChangeEvent, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import { setRequestingSubtitles } from '../../actions.any';
|
||||
import { getAvailableSubtitlesLanguages } from '../../functions.any';
|
||||
|
||||
/**
|
||||
* The styles for the LanguageSelector component.
|
||||
*
|
||||
* @param {Theme} theme - The MUI theme.
|
||||
* @returns {Object} The styles object.
|
||||
*/
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(2),
|
||||
gap: theme.spacing(2)
|
||||
},
|
||||
select: {
|
||||
flex: 1,
|
||||
minWidth: 200
|
||||
},
|
||||
label: {
|
||||
...theme.typography.bodyShortRegular,
|
||||
color: theme.palette.text01,
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component that renders a language selection dropdown.
|
||||
* Uses the same language options as LanguageSelectorDialog and
|
||||
* updates the subtitles language preference in Redux.
|
||||
*
|
||||
* @param {IProps} props - The component props.
|
||||
* @returns {JSX.Element} - The rendered component.
|
||||
*/
|
||||
function LanguageSelector() {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const selectedLanguage = useSelector((state: IReduxState) => state['features/subtitles']._language);
|
||||
const languageCodes = useSelector((state: IReduxState) => getAvailableSubtitlesLanguages(
|
||||
state,
|
||||
selectedLanguage?.replace('translation-languages:', '')
|
||||
));
|
||||
|
||||
/**
|
||||
* Maps available languages to Select component options format.
|
||||
*
|
||||
* @type {Array<{value: string, label: string}>}
|
||||
*/
|
||||
const languages = [ 'transcribing.original', ...languageCodes.map(lang => `translation-languages:${lang}`) ]
|
||||
.map(lang => {
|
||||
return {
|
||||
value: lang,
|
||||
label: t(lang)
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles language selection changes.
|
||||
* Dispatches the setRequestingSubtitles action with the new language.
|
||||
*
|
||||
* @param {string} value - The selected language code.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onLanguageChange = useCallback((e: ChangeEvent<HTMLSelectElement>) => {
|
||||
let { value }: { value?: string | null; } = e.target;
|
||||
|
||||
if (value === 'transcribing.original') {
|
||||
value = null;
|
||||
}
|
||||
dispatch(setRequestingSubtitles(true, true, value));
|
||||
|
||||
if (value !== null) {
|
||||
value = value.replace('translation-languages:', '');
|
||||
}
|
||||
}, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<span className = { classes.label }>
|
||||
{t('transcribing.translateTo')}:
|
||||
</span>
|
||||
<Select
|
||||
className = { classes.select }
|
||||
id = 'subtitles-language-select'
|
||||
onChange = { onLanguageChange }
|
||||
options = { languages }
|
||||
value = { selectedLanguage || 'transcribing.original' } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LanguageSelector;
|
||||
@@ -0,0 +1,78 @@
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate, translateToHTML } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { openSettingsDialog } from '../../../settings/actions.web';
|
||||
import { SETTINGS_TABS } from '../../../settings/constants';
|
||||
import { toggleLanguageSelectorDialog } from '../../actions.web';
|
||||
import AbstractLanguageSelectorDialog, {
|
||||
IAbstractLanguageSelectorDialogProps
|
||||
} from '../AbstractLanguageSelectorDialog';
|
||||
|
||||
import LanguageList from './LanguageList';
|
||||
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
paragraphWrapper: {
|
||||
fontSize: '0.875rem',
|
||||
margin: '10px 0px',
|
||||
color: theme.palette.text01
|
||||
},
|
||||
spanWrapper: {
|
||||
fontWeight: 700,
|
||||
cursor: 'pointer',
|
||||
color: theme.palette.link01,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui04,
|
||||
color: theme.palette.link01Hover
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const LanguageSelectorDialog = (props: IAbstractLanguageSelectorDialogProps) => {
|
||||
const { dispatch, language, listItems, onLanguageSelected, subtitles, t } = props;
|
||||
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const onSelected = useCallback((e: string) => {
|
||||
onLanguageSelected(e);
|
||||
dispatch(toggleLanguageSelectorDialog());
|
||||
}, [ language ]);
|
||||
|
||||
const onSourceLanguageClick = useCallback(() => {
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.MORE, false));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = 'transcribing.subtitles'>
|
||||
<p className = { styles.paragraphWrapper } >
|
||||
{
|
||||
translateToHTML(t, 'transcribing.sourceLanguageDesc', {
|
||||
'sourceLanguage': t(`languages:${i18next.language}`).toLowerCase()
|
||||
})
|
||||
}<span
|
||||
className = { styles.spanWrapper }
|
||||
onClick = { onSourceLanguageClick }>{t('transcribing.sourceLanguageHere')}.</span>
|
||||
</p>
|
||||
<LanguageList
|
||||
items = { listItems }
|
||||
onLanguageSelected = { onSelected }
|
||||
selectedLanguage = { subtitles } />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* We apply AbstractLanguageSelector to fill in the AbstractProps common
|
||||
* to both the web and native implementations.
|
||||
*/
|
||||
// eslint-disable-next-line new-cap
|
||||
export default translate(AbstractLanguageSelectorDialog(LanguageSelectorDialog));
|
||||
Reference in New Issue
Block a user