Merge pull request #19533 from ramonlsouza/remove-audiocaptions-collection

Refactor: remove audio-captions collection
This commit is contained in:
Ramón Souza 2024-01-30 12:56:07 -03:00 committed by GitHub
commit 4df0cf81c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 15 additions and 703 deletions

View File

@ -1,9 +0,0 @@
import { Meteor } from 'meteor/meteor';
const AudioCaptions = new Mongo.Collection('audio-captions');
if (Meteor.isServer) {
AudioCaptions.createIndexAsync({ meetingId: 1 });
}
export default AudioCaptions;

View File

@ -1,4 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleTranscriptUpdated from '/imports/api/audio-captions/server/handlers/transcriptUpdated';
RedisPubSub.on('TranscriptUpdatedEvtMsg', handleTranscriptUpdated);

View File

@ -1,12 +0,0 @@
import setTranscript from '/imports/api/audio-captions/server/modifiers/setTranscript';
export default async function transcriptUpdated({ header, body }) {
const { meetingId } = header;
const {
transcriptId,
transcript,
} = body;
await setTranscript(meetingId, transcriptId, transcript);
}

View File

@ -1,2 +0,0 @@
import './eventHandlers';
import './publishers';

View File

@ -1,26 +0,0 @@
import AudioCaptions from '/imports/api/audio-captions';
import Logger from '/imports/startup/server/logger';
export default async function clearAudioCaptions(meetingId) {
if (meetingId) {
try {
const numberAffected = await AudioCaptions.removeAsync({ meetingId });
if (numberAffected) {
Logger.info(`Cleared AudioCaptions (${meetingId})`);
}
} catch (err) {
Logger.error(`Error on clearing audio captions (${meetingId}). ${err}`);
}
} else {
try {
const numberAffected = await AudioCaptions.removeAsync({});
if (numberAffected) {
Logger.info('Cleared AudioCaptions (all)');
}
} catch (err) {
Logger.error(`Error on clearing audio captions (all). ${err}`);
}
}
}

View File

@ -1,30 +0,0 @@
import { check } from 'meteor/check';
import AudioCaptions from '/imports/api/audio-captions';
import Logger from '/imports/startup/server/logger';
export default async function setTranscript(meetingId, transcriptId, transcript) {
try {
check(meetingId, String);
check(transcriptId, String);
check(transcript, String);
const selector = { meetingId };
const modifier = {
$set: {
transcriptId,
transcript,
},
};
const numberAffected = await AudioCaptions.upsertAsync(selector, modifier);
if (numberAffected) {
Logger.debug(`Set transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`);
} else {
Logger.debug(`Upserted transcriptId=${transcriptId} transcript=${transcript} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Setting audio captions transcript to the collection: ${err}`);
}
}

View File

@ -1,26 +0,0 @@
import AudioCaptions from '/imports/api/audio-captions';
import { Meteor } from 'meteor/meteor';
import Logger from '/imports/startup/server/logger';
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
async function audioCaptions() {
const tokenValidation = await AuthTokenValidation
.findOneAsync({ connectionId: this.connection.id });
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
Logger.warn(`Publishing AudioCaptions was requested by unauth connection ${this.connection.id}`);
return AudioCaptions.find({ meetingId: '' });
}
const { meetingId, userId } = tokenValidation;
Logger.debug('Publishing AudioCaptions', { meetingId, requestedBy: userId });
return AudioCaptions.find({ meetingId });
}
function publish(...args) {
const boundAudioCaptions = audioCaptions.bind(this);
return boundAudioCaptions(...args);
}
Meteor.publish('audio-captions', publish);

View File

@ -13,7 +13,6 @@ import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoic
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
import clearScreenshare from '/imports/api/screenshare/server/modifiers/clearScreenshare';
import clearTimer from '/imports/api/timer/server/modifiers/clearTimer';
import clearAudioCaptions from '/imports/api/audio-captions/server/modifiers/clearAudioCaptions';
import clearMeetingTimeRemaining from '/imports/api/meetings/server/modifiers/clearMeetingTimeRemaining';
import clearLocalSettings from '/imports/api/local-settings/server/modifiers/clearLocalSettings';
import clearRecordMeeting from './clearRecordMeeting';
@ -42,7 +41,6 @@ export default async function meetingHasEnded(meetingId) {
clearVoiceUsers(meetingId),
clearUserInfo(meetingId),
clearTimer(meetingId),
clearAudioCaptions(meetingId),
clearLocalSettings(meetingId),
clearMeetingTimeRemaining(meetingId),
clearRecordMeeting(meetingId),

View File

@ -4,7 +4,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import { ActionsBarItemType, ActionsBarPosition } from 'bigbluebutton-html-plugin-sdk/dist/cjs/extensible-areas/actions-bar-item/enums';
import Styled from './styles';
import ActionsDropdown from './actions-dropdown/container';
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/captions/button/container';
import AudioCaptionsButtonContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/button/component';
import CaptionsReaderMenuContainer from '/imports/ui/components/captions/reader-menu/container';
import ScreenshareButtonContainer from '/imports/ui/components/actions-bar/screenshare/container';
import ReactionsButtonContainer from './reactions-button/container';

View File

@ -3,7 +3,7 @@ import { withTracker } from 'meteor/react-meteor-data';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
import Meetings, { LayoutMeetings } from '/imports/api/meetings';
import AudioCaptionsLiveContainer from '/imports/ui/components/audio/captions/live/container';
import AudioCaptionsLiveContainer from '/imports/ui/components/audio/audio-graphql/audio-captions/live/component';
import AudioCaptionsService from '/imports/ui/components/audio/captions/service';
import { notify } from '/imports/ui/services/notification';
import CaptionsContainer from '/imports/ui/components/captions/live/container';

View File

@ -1,259 +0,0 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import Service from '/imports/ui/components/audio/captions/service';
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
import ButtonEmoji from '/imports/ui/components/common/button/button-emoji/ButtonEmoji';
import BBBMenu from '/imports/ui/components/common/menu/component';
import Styled from './styles';
import { useMutation } from '@apollo/client';
import { SET_SPEECH_LOCALE } from '/imports/ui/core/graphql/mutations/userMutations';
const intlMessages = defineMessages({
start: {
id: 'app.audio.captions.button.start',
description: 'Start audio captions',
},
stop: {
id: 'app.audio.captions.button.stop',
description: 'Stop audio captions',
},
transcriptionSettings: {
id: 'app.audio.captions.button.transcriptionSettings',
description: 'Audio captions settings modal',
},
transcription: {
id: 'app.audio.captions.button.transcription',
description: 'Audio speech transcription label',
},
transcriptionOn: {
id: 'app.switch.onLabel',
},
transcriptionOff: {
id: 'app.switch.offLabel',
},
language: {
id: 'app.audio.captions.button.language',
description: 'Audio speech recognition language label',
},
'de-DE': {
id: 'app.audio.captions.select.de-DE',
description: 'Audio speech recognition german language',
},
'en-US': {
id: 'app.audio.captions.select.en-US',
description: 'Audio speech recognition english language',
},
'es-ES': {
id: 'app.audio.captions.select.es-ES',
description: 'Audio speech recognition spanish language',
},
'fr-FR': {
id: 'app.audio.captions.select.fr-FR',
description: 'Audio speech recognition french language',
},
'hi-ID': {
id: 'app.audio.captions.select.hi-ID',
description: 'Audio speech recognition indian language',
},
'it-IT': {
id: 'app.audio.captions.select.it-IT',
description: 'Audio speech recognition italian language',
},
'ja-JP': {
id: 'app.audio.captions.select.ja-JP',
description: 'Audio speech recognition japanese language',
},
'pt-BR': {
id: 'app.audio.captions.select.pt-BR',
description: 'Audio speech recognition portuguese language',
},
'ru-RU': {
id: 'app.audio.captions.select.ru-RU',
description: 'Audio speech recognition russian language',
},
'zh-CN': {
id: 'app.audio.captions.select.zh-CN',
description: 'Audio speech recognition chinese language',
},
});
const DEFAULT_LOCALE = 'en-US';
const DISABLED = '';
const CaptionsButton = ({
intl,
active,
isRTL,
enabled,
currentSpeechLocale,
availableVoices,
isSupported,
isVoiceUser,
}) => {
const isTranscriptionDisabled = () => (
currentSpeechLocale === DISABLED
);
const [setSpeechLocale] = useMutation(SET_SPEECH_LOCALE);
const setUserSpeechLocale = (speechLocale, provider) => {
setSpeechLocale({
variables: {
locale: speechLocale,
provider,
},
});
};
const fallbackLocale = availableVoices.includes(navigator.language)
? navigator.language : DEFAULT_LOCALE;
const getSelectedLocaleValue = (isTranscriptionDisabled() ? fallbackLocale : currentSpeechLocale);
const selectedLocale = useRef(getSelectedLocaleValue);
useEffect(() => {
if (!isTranscriptionDisabled()) selectedLocale.current = getSelectedLocaleValue;
}, [currentSpeechLocale]);
if (!enabled) return null;
const shouldRenderChevron = isSupported && isVoiceUser;
const getAvailableLocales = () => {
let indexToInsertSeparator = -1;
const availableVoicesObjectToMenu = availableVoices.map((availableVoice, index) => {
if (availableVoice === availableVoices[0]) {
indexToInsertSeparator = index;
}
return (
{
icon: '',
label: intl.formatMessage(intlMessages[availableVoice]),
key: availableVoice,
iconRight: selectedLocale.current === availableVoice ? 'check' : null,
customStyles: (selectedLocale.current === availableVoice) && Styled.SelectedLabel,
disabled: isTranscriptionDisabled(),
onClick: () => {
selectedLocale.current = availableVoice;
SpeechService.setSpeechLocale(selectedLocale.current, setUserSpeechLocale);
},
}
);
});
if (indexToInsertSeparator >= 0) {
availableVoicesObjectToMenu.splice(indexToInsertSeparator, 0, {
key: 'separator-01',
isSeparator: true,
});
}
return [
...availableVoicesObjectToMenu,
];
};
const toggleTranscription = () => {
SpeechService.setSpeechLocale(
isTranscriptionDisabled() ? selectedLocale.current : DISABLED, setUserSpeechLocale,
);
};
const getAvailableLocalesList = () => (
[{
key: 'availableLocalesList',
label: intl.formatMessage(intlMessages.language),
customStyles: Styled.TitleLabel,
disabled: true,
},
...getAvailableLocales(),
{
key: 'divider',
label: intl.formatMessage(intlMessages.transcription),
customStyles: Styled.TitleLabel,
disabled: true,
},
{
key: 'separator-02',
isSeparator: true,
},
{
key: 'transcriptionStatus',
label: intl.formatMessage(
isTranscriptionDisabled()
? intlMessages.transcriptionOn
: intlMessages.transcriptionOff,
),
customStyles: isTranscriptionDisabled()
? Styled.EnableTrascription : Styled.DisableTrascription,
disabled: false,
onClick: toggleTranscription,
}]
);
const onToggleClick = (e) => {
e.stopPropagation();
Service.setAudioCaptions(!active);
};
const startStopCaptionsButton = (
<Styled.ClosedCaptionToggleButton
icon={active ? 'closed_caption' : 'closed_caption_stop'}
label={intl.formatMessage(active ? intlMessages.stop : intlMessages.start)}
color={active ? 'primary' : 'default'}
ghost={!active}
hideLabel
circle
size="lg"
onClick={onToggleClick}
/>
);
return (
shouldRenderChevron
? (
<Styled.SpanButtonWrapper>
<BBBMenu
trigger={(
<>
{ startStopCaptionsButton }
<ButtonEmoji
emoji="device_list_selector"
hideLabel
label={intl.formatMessage(intlMessages.transcriptionSettings)}
tabIndex={0}
rotate
/>
</>
)}
actions={getAvailableLocalesList()}
opts={{
id: 'default-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
transformOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
}}
/>
</Styled.SpanButtonWrapper>
) : startStopCaptionsButton
);
};
CaptionsButton.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
active: PropTypes.bool.isRequired,
isRTL: PropTypes.bool.isRequired,
enabled: PropTypes.bool.isRequired,
currentSpeechLocale: PropTypes.string.isRequired,
availableVoices: PropTypes.arrayOf(PropTypes.string).isRequired,
isSupported: PropTypes.bool.isRequired,
isVoiceUser: PropTypes.bool.isRequired,
};
export default injectIntl(CaptionsButton);

View File

@ -1,28 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Service from '/imports/ui/components/audio/captions/service';
import Button from './component';
import SpeechService from '/imports/ui/components/audio/captions/speech/service';
import AudioService from '/imports/ui/components/audio/service';
import AudioCaptionsButtonContainer from '../../audio-graphql/audio-captions/button/component';
const Container = (props) => <Button {...props} />;
withTracker(() => {
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
const availableVoices = SpeechService.getSpeechVoices();
const currentSpeechLocale = SpeechService.getSpeechLocale();
const isSupported = availableVoices.length > 0;
const isVoiceUser = AudioService.isVoiceUser();
return {
isRTL,
enabled: Service.hasAudioCaptions(),
active: Service.getAudioCaptions(),
currentSpeechLocale,
availableVoices,
isSupported,
isVoiceUser,
};
})(Container);
export default AudioCaptionsButtonContainer;

View File

@ -1,61 +0,0 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import Toggle from '/imports/ui/components/common/switch/component';
import {
colorWhite,
colorPrimary,
colorOffWhite,
colorDangerDark,
colorSuccess,
} from '/imports/ui/stylesheets/styled-components/palette';
const ClosedCaptionToggleButton = styled(Button)`
${({ ghost }) => ghost && `
span {
box-shadow: none;
background-color: transparent !important;
border-color: ${colorWhite} !important;
}
i {
margin-top: .4rem;
}
`}
`;
const SpanButtonWrapper = styled.span`
position: relative;
`;
const TranscriptionToggle = styled(Toggle)`
display: flex;
justify-content: flex-start;
padding-left: 1em;
`;
const TitleLabel = {
fontWeight: 'bold',
opacity: 1,
};
const EnableTrascription = {
color: colorSuccess,
};
const DisableTrascription = {
color: colorDangerDark,
};
const SelectedLabel = {
color: colorPrimary,
backgroundColor: colorOffWhite,
};
export default {
ClosedCaptionToggleButton,
SpanButtonWrapper,
TranscriptionToggle,
TitleLabel,
EnableTrascription,
DisableTrascription,
SelectedLabel,
};

View File

@ -1,104 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import UserContainer from './user/container';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
class LiveCaptions extends PureComponent {
constructor(props) {
super(props);
this.state = { clear: true };
this.timer = null;
}
componentDidUpdate(prevProps) {
const { clear } = this.state;
if (clear) {
const { transcript } = this.props;
if (prevProps.transcript !== transcript) {
// eslint-disable-next-line react/no-did-update-set-state
this.setState({ clear: false });
}
} else {
this.resetTimer();
this.timer = setTimeout(() => this.setState({ clear: true }), CAPTIONS_CONFIG.time);
}
}
componentWillUnmount() {
this.resetTimer();
}
resetTimer() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
render() {
const {
transcript,
transcriptId,
} = this.props;
const { clear } = this.state;
const hasContent = transcript.length > 0 && !clear;
const wrapperStyles = {
display: 'flex',
};
const captionStyles = {
whiteSpace: 'pre-line',
wordWrap: 'break-word',
fontFamily: 'Verdana, Arial, Helvetica, sans-serif',
fontSize: '1.5rem',
background: '#000000a0',
color: 'white',
padding: hasContent ? '.5rem' : undefined,
};
const visuallyHidden = {
position: 'absolute',
overflow: 'hidden',
clip: 'rect(0 0 0 0)',
height: '1px',
width: '1px',
margin: '-1px',
padding: '0',
border: '0',
};
return (
<div style={wrapperStyles}>
{clear ? null : (
<UserContainer
background="#000000a0"
transcriptId={transcriptId}
/>
)}
<div style={captionStyles}>
{clear ? '' : transcript}
</div>
<div
style={visuallyHidden}
aria-atomic
aria-live="polite"
>
{clear ? '' : transcript}
</div>
</div>
);
}
}
LiveCaptions.propTypes = {
transcript: PropTypes.string.isRequired,
transcriptId: PropTypes.string.isRequired,
};
export default LiveCaptions;

View File

@ -1,21 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Service from '/imports/ui/components/audio/captions/service';
import LiveCaptions from './component';
import AudioCaptionsLiveContainer from '../../audio-graphql/audio-captions/live/component';
const Container = (props) => <LiveCaptions {...props} />;
withTracker(() => {
const {
transcriptId,
transcript,
} = Service.getAudioCaptionsData();
return {
transcript,
transcriptId,
};
})(Container);
export default AudioCaptionsLiveContainer;

View File

@ -1,39 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import UserAvatar from '/imports/ui/components/user-avatar/component';
const User = ({
avatar,
background,
color,
moderator,
name,
}) => (
<div
style={{
background,
minHeight: '3.25rem',
padding: '.5rem',
textTransform: 'capitalize',
width: '3.25rem',
}}
>
<UserAvatar
avatar={avatar}
color={color}
moderator={moderator}
>
{name.slice(0, 2)}
</UserAvatar>
</div>
);
User.propTypes = {
avatar: PropTypes.string.isRequired,
background: PropTypes.string.isRequired,
color: PropTypes.string.isRequired,
moderator: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
};
export default User;

View File

@ -1,44 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Users from '/imports/api/users';
import User from './component';
const MODERATOR = Meteor.settings.public.user.role_moderator;
const Container = (props) => <User {...props} />;
const getUser = (userId) => {
const user = Users.findOne(
{ userId },
{
fields: {
avatar: 1,
color: 1,
role: 1,
name: 1,
},
},
);
if (user) {
return {
avatar: user.avatar,
color: user.color,
moderator: user.role === MODERATOR,
name: user.name,
};
}
return {
avatar: '',
color: '',
moderator: false,
name: '',
};
};
export default withTracker(({ transcriptId }) => {
const userId = transcriptId.split('-')[0];
return getUser(userId);
})(Container);

View File

@ -1,38 +1,8 @@
import AudioCaptions from '/imports/api/audio-captions';
import Auth from '/imports/ui/services/auth';
const getAudioCaptionsData = () => {
const audioCaptions = AudioCaptions.findOne({ meetingId: Auth.meetingID });
if (audioCaptions) {
return {
transcriptId: audioCaptions.transcriptId,
transcript: audioCaptions.transcript,
};
}
return {
transcriptId: '',
transcript: '',
};
};
const getAudioCaptions = () => Session.get('audioCaptions') || false;
const setAudioCaptions = (value) => Session.set('audioCaptions', value);
const hasAudioCaptions = () => {
const audioCaptions = AudioCaptions.findOne(
{ meetingId: Auth.meetingID },
{ fields: {} },
);
return !!audioCaptions;
};
export default {
getAudioCaptionsData,
getAudioCaptions,
setAudioCaptions,
hasAudioCaptions,
};

View File

@ -12,6 +12,7 @@ import { SMALL_VIEWPORT_BREAKPOINT } from '../../layout/enums';
import { PluginsContext } from '/imports/ui/components/components-data/plugin-context/context';
import { USER_LEAVE_MEETING } from '/imports/ui/core/graphql/mutations/userMutations';
import { useMutation } from '@apollo/client';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
const { isIphone } = deviceInfo;
const { isSafari, isValidSafariVersion } = browserInfo;
@ -30,11 +31,22 @@ const OptionsDropdownContainer = (props) => {
];
}
const {
data: currentMeeting,
} = useMeeting((m) => {
return {
componentsFlags: m.componentsFlags,
};
});
const componentsFlags = currentMeeting?.componentsFlags;
const audioCaptionsEnabled = componentsFlags?.hasCaption;
const [userLeaveMeeting] = useMutation(USER_LEAVE_MEETING);
return (
<OptionsDropdown {...{
isMobile, isRTL, optionsDropdownItems, userLeaveMeeting, ...props,
isMobile, isRTL, optionsDropdownItems, userLeaveMeeting, audioCaptionsEnabled, ...props,
}}
/>
);
@ -44,7 +56,6 @@ export default withTracker((props) => {
const handleToggleFullscreen = () => FullscreenService.toggleFullScreen();
return {
amIModerator: props.amIModerator,
audioCaptionsEnabled: audioCaptionsService.hasAudioCaptions(),
audioCaptionsActive: audioCaptionsService.getAudioCaptions(),
audioCaptionsSet: (value) => audioCaptionsService.setAudioCaptions(value),
isMobile: deviceInfo.isMobile,

View File

@ -31,7 +31,6 @@ const SUBSCRIPTIONS = [
'pads-sessions',
'pads-updates',
'notifications',
'audio-captions',
'layout-meetings',
'user-reaction',
'timer',

View File

@ -21,7 +21,6 @@ import '/imports/api/users-infos/server';
import '/imports/api/users-persistent-data/server';
import '/imports/api/connection-status/server';
import '/imports/api/timer/server';
import '/imports/api/audio-captions/server';
import '/imports/api/pads/server';
import '/imports/api/local-settings/server';
import '/imports/api/voice-call-states/server';