Merge remote-tracking branch 'upstream/v3.0.x-release' into refactor-captions

This commit is contained in:
Tainan Felipe 2024-04-22 19:07:35 -03:00
commit 77ded6548b
45 changed files with 12 additions and 2126 deletions

View File

@ -470,7 +470,7 @@ class ReceivedJsonMsgHandlerActor(
route[CheckGraphqlMiddlewareAlivePongSysMsg](meetingManagerChannel, envelope, jsonNode)
case _ =>
log.error("Cannot route envelope name " + envelope.name)
log.debug("Cannot route envelope name " + envelope.name)
// do nothing
}
}

View File

@ -1776,11 +1776,11 @@ CREATE OR REPLACE FUNCTION "update_caption_locale_owner_func"() RETURNS TRIGGER
BEGIN
WITH upsert AS (
UPDATE "caption_locale" SET
"ownerUserId" = NEW.userId,
"ownerUserId" = NEW."userId",
"updatedAt" = current_timestamp
WHERE "meetingId"=NEW."meetingId" AND "locale"=NEW."locale" AND "captionType"= NEW."captionType"
RETURNING *)
INSERT INTO "user_connectionStatusMetrics"("meetingId","locale","captionType","ownerUserId")
INSERT INTO "caption_locale"("meetingId","locale","captionType","ownerUserId")
SELECT NEW."meetingId", NEW."locale", NEW."captionType", NEW."userId"
WHERE NOT EXISTS (SELECT * FROM upsert);

View File

@ -8,7 +8,6 @@ import UserSettings from '/imports/api/users-settings';
import VideoStreams from '/imports/api/video-streams';
import VoiceUsers from '/imports/api/voice-users';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
import Captions from '/imports/api/captions';
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
import AuthTokenValidation from '/imports/api/auth-token-validation';
import Breakouts from '/imports/api/breakouts';
@ -29,7 +28,6 @@ export const localCollectionRegistry = {
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
localCaptionsSync: new AbstractCollection(Captions, Captions),
localPadsSync: new AbstractCollection(Pads, Pads),
localPadsSessionsSync: new AbstractCollection(PadsSessions, PadsSessions),
localPadsUpdatesSync: new AbstractCollection(PadsUpdates, PadsUpdates),

View File

@ -1,13 +0,0 @@
import { Meteor } from 'meteor/meteor';
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Captions = new Mongo.Collection('captions', collectionOptions);
if (Meteor.isServer) {
Captions.createIndexAsync({ meetingId: 1, locale: 1 });
}
export default Captions;

View File

@ -1,35 +0,0 @@
import axios from 'axios';
import { Meteor } from 'meteor/meteor';
import createCaptions from '/imports/api/captions/server/modifiers/createCaptions';
import Logger from '/imports/startup/server/logger';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const BASENAME = Meteor.settings.public.app.basename;
const HOST = Meteor.settings.private.app.host;
const LOCALES = Meteor.settings.private.app.localesUrl;
const LOCALES_URL = `http://${HOST}:${process.env.PORT}${BASENAME}${LOCALES}`;
const init = (meetingId) => {
axios({
method: 'get',
url: LOCALES_URL,
responseType: 'json',
}).then(async (response) => {
const { status } = response;
if (status !== 200) return;
const locales = response.data;
await Promise.all(locales.map(async (locale) => {
const caption = await createCaptions(meetingId, locale.locale, locale.name);
return caption;
}));
}).catch((error) => Logger.error(`Could not create captions for ${meetingId}: ${error}`));
};
const initCaptions = (meetingId) => {
if (CAPTIONS_CONFIG.enabled) init(meetingId);
};
export {
initCaptions,
};

View File

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

View File

@ -1,12 +0,0 @@
import { Meteor } from 'meteor/meteor';
import updateCaptionsOwner from '/imports/api/captions/server/methods/updateCaptionsOwner';
import startDictation from '/imports/api/captions/server/methods/startDictation';
import stopDictation from '/imports/api/captions/server/methods/stopDictation';
import pushSpeechTranscript from '/imports/api/captions/server/methods/pushSpeechTranscript';
Meteor.methods({
updateCaptionsOwner,
startDictation,
stopDictation,
pushSpeechTranscript,
});

View File

@ -1,36 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
import setTranscript from '/imports/api/captions/server/modifiers/setTranscript';
import updatePad from '/imports/api/pads/server/methods/updatePad';
export default async function pushSpeechTranscript(locale, transcript, type) {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(locale, String);
check(transcript, String);
check(type, String);
const captions = await Captions.findOneAsync({
meetingId,
ownerId: requesterUserId,
locale,
dictating: true,
});
if (captions) {
if (type === 'final') {
const text = `\n${transcript}`;
updatePad(meetingId, requesterUserId, locale, text);
}
await setTranscript(meetingId, locale, transcript);
}
} catch (err) {
Logger.error(`Exception while invoking method pushSpeechTranscript ${err.stack}`);
}
}

View File

@ -1,25 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
import setDictation from '/imports/api/captions/server/modifiers/setDictation';
export default async function startDictation(locale) {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(locale, String);
const captions = await Captions.findOneAsync({
meetingId,
ownerId: requesterUserId,
locale,
});
if (captions) await setDictation(meetingId, locale, true);
} catch (err) {
Logger.error(`Exception while invoking method startDictation ${err.stack}`);
}
}

View File

@ -1,25 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
import setDictation from '/imports/api/captions/server/modifiers/setDictation';
export default async function stopDictation(locale) {
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(locale, String);
const captions = await Captions.findOne({
meetingId,
ownerId: requesterUserId,
locale,
});
if (captions) await setDictation(meetingId, locale, false);
} catch (err) {
Logger.error(`Exception while invoking method stopDictation ${err.stack}`);
}
}

View File

@ -1,30 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers';
import Logger from '/imports/startup/server/logger';
export default function updateCaptionsOwner(locale, name) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'UpdateCaptionOwnerPubMsg';
try {
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(locale, String);
check(name, String);
const payload = {
ownerId: requesterUserId,
locale,
name,
};
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
} catch (err) {
Logger.error(`Exception while invoking method updateCaptionsOwner ${err.stack}`);
}
}

View File

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

View File

@ -1,33 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
export default async function createCaptions(meetingId, locale, name) {
try {
check(meetingId, String);
check(locale, String);
check(name, String);
const selector = {
meetingId,
locale,
};
const modifier = {
meetingId,
locale,
name,
ownerId: '',
dictating: false,
transcript: '',
};
const { numberAffected } = await Captions.upsertAsync(selector, modifier);
if (numberAffected) {
Logger.verbose(`Created captions=${locale} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Creating captions owner to the collection: ${err}`);
}
}

View File

@ -1,33 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
export default async function setDictation(meetingId, locale, dictating) {
try {
check(meetingId, String);
check(locale, String);
check(dictating, Boolean);
const selector = {
meetingId,
locale,
};
const modifier = {
$set: {
dictating,
transcript: '',
},
};
const { numberAffected } = Captions.upsertAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Set captions=${locale} dictating=${dictating} meeting=${meetingId}`);
} else {
Logger.info(`Upserted captions=${locale} dictating=${dictating} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Setting captions dictation to the collection: ${err}`);
}
}

View File

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

View File

@ -1,33 +0,0 @@
import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import Logger from '/imports/startup/server/logger';
export default async function updateCaptionsOwner(meetingId, locale, ownerId) {
try {
check(meetingId, String);
check(locale, String);
check(ownerId, String);
const selector = {
meetingId,
locale,
};
const modifier = {
$set: {
ownerId,
dictating: false, // Refresh dictation mode
},
};
const numberAffected = await Captions.upsert(selector, modifier);
if (numberAffected) {
Logger.info(`Added captions=${locale} owner=${ownerId} meeting=${meetingId}`);
} else {
Logger.info(`Upserted captions=${locale} owner=${ownerId} meeting=${meetingId}`);
}
} catch (err) {
Logger.error(`Adding captions owner to the collection: ${err}`);
}
}

View File

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

View File

@ -11,7 +11,6 @@ import Meetings, {
import Logger from '/imports/startup/server/logger';
import { initPads } from '/imports/api/pads/server/helpers';
import createTimer from '/imports/api/timer/server/methods/createTimer';
import { initCaptions } from '/imports/api/captions/server/helpers';
import { addExternalVideoStreamer } from '/imports/api/external-videos/server/streamer';
import addUserReactionsObserver from '/imports/api/user-reaction/server/helpers';
import { LAYOUT_TYPE } from '/imports/ui/components/layout/enums';
@ -249,9 +248,6 @@ export default async function addMeeting(meeting) {
if (newMeeting.meetingProp.disabledFeatures.indexOf('sharedNotes') === -1) {
initPads(meetingId);
}
if (newMeeting.meetingProp.disabledFeatures.indexOf('captions') === -1) {
await initCaptions(meetingId);
}
if (newMeeting.meetingProp.disabledFeatures.indexOf('reactions') === -1) {
await addUserReactionsObserver(meetingId);
}

View File

@ -6,7 +6,6 @@ import { removeExternalVideoStreamer } from '/imports/api/external-videos/server
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
import clearPads from '/imports/api/pads/server/modifiers/clearPads';
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';

View File

@ -12,7 +12,7 @@ export default async function handleWebcamSync({ body }, meetingId) {
const streamsIds = webcamListSync.map((webcam) => webcam.stream);
const webcamStreams = VideoStreams.find({
const webcamStreams = await VideoStreams.find({
meetingId,
stream: { $in: streamsIds },
}, {
@ -42,7 +42,7 @@ export default async function handleWebcamSync({ body }, meetingId) {
stream: 1,
userId: 1,
},
}).fetchAsynch();
}).fetchAsync();
await Promise.all(videoStreamsToRemove
.map(async (videoStream) => {

View File

@ -4,7 +4,6 @@ import { ActionsBarItemType, ActionsBarPosition } from 'bigbluebutton-html-plugi
import Styled from './styles';
import ActionsDropdown from './actions-dropdown/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';
import AudioControlsContainer from '../audio/audio-graphql/audio-controls/component';
@ -20,20 +19,11 @@ class ActionsBar extends PureComponent {
constructor(props) {
super(props);
this.state = {
isCaptionsReaderMenuModalOpen: false,
};
this.setCaptionsReaderMenuModalIsOpen = this.setCaptionsReaderMenuModalIsOpen.bind(this);
this.setRenderRaiseHand = this.renderRaiseHand.bind(this);
this.actionsBarRef = React.createRef();
this.renderPluginsActionBarItems = this.renderPluginsActionBarItems.bind(this);
}
setCaptionsReaderMenuModalIsOpen(value) {
this.setState({ isCaptionsReaderMenuModalOpen: value });
}
renderPluginsActionBarItems(position) {
const { actionBarItems } = this.props;
return (
@ -110,7 +100,6 @@ class ActionsBar extends PureComponent {
stopExternalVideoShare,
isTimerActive,
isTimerEnabled,
isCaptionsAvailable,
isMeteorConnected,
isPollingEnabled,
isRaiseHandButtonCentered,
@ -125,8 +114,6 @@ class ActionsBar extends PureComponent {
activeCaptions,
} = this.props;
const { isCaptionsReaderMenuModalOpen } = this.state;
const { selectedLayout } = Settings.application;
const shouldShowPresentationButton = selectedLayout !== LAYOUT_TYPE.CAMERAS_ONLY
&& selectedLayout !== LAYOUT_TYPE.PARTICIPANTS_AND_CHAT_ONLY;
@ -165,25 +152,8 @@ class ActionsBar extends PureComponent {
setPresentationFitToWidth,
}}
/>
{isCaptionsAvailable
? (
<>
{
isCaptionsReaderMenuModalOpen ? (
<CaptionsReaderMenuContainer
{...{
onRequestClose: () => this.setCaptionsReaderMenuModalIsOpen(false),
priority: 'low',
setIsOpen: this.setCaptionsReaderMenuModalIsOpen,
isOpen: isCaptionsReaderMenuModalOpen,
}}
/>
) : null
}
</>
)
: null}
<AudioCaptionsButtonContainer activeCaptions={activeCaptions} />
<AudioCaptionsButtonContainer />
</Styled.Left>
<Styled.Center>
{this.renderPluginsActionBarItems(ActionsBarPosition.LEFT)}

View File

@ -129,13 +129,11 @@ const intlMessages = defineMessages({
});
const propTypes = {
captions: PropTypes.element,
darkTheme: PropTypes.bool.isRequired,
};
const defaultProps = {
actionsbar: null,
captions: null,
};
const isLayeredView = window.matchMedia(`(max-width: ${SMALL_VIEWPORT_BREAKPOINT}px)`);
@ -372,31 +370,6 @@ class App extends Component {
&& (isPhone || isLayeredView.matches);
}
renderCaptions() {
const {
captions,
captionsStyle,
} = this.props;
if (!captions) return null;
return (
<Styled.CaptionsWrapper
role="region"
style={
{
position: 'absolute',
left: captionsStyle.left,
right: captionsStyle.right,
maxWidth: captionsStyle.maxWidth,
}
}
>
{captions}
</Styled.CaptionsWrapper>
);
}
renderAudioCaptions() {
const {
audioCaptions,
@ -659,7 +632,6 @@ setRandomUserSelectModalIsOpen(value) {
area="media"
/>
) : null}
{this.renderCaptions()}
<AudioCaptionsSpeechContainer />
{this.renderAudioCaptions()}
<PresentationUploaderToastContainer intl={intl} />

View File

@ -300,6 +300,7 @@ export default withTracker(() => {
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
return {
audioCaptions: <AudioCaptionsLiveContainer />,
fontSize: getFontSize(),
hasBreakoutRooms: getBreakoutRooms().length > 0,
customStyle: getFromUserSettings('bbb_custom_style', false),

View File

@ -1,131 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import Button from '/imports/ui/components/common/button/component';
import PadContainer from '/imports/ui/components/pads/container';
import Service from '/imports/ui/components/captions/service';
import Styled from './styles';
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
import browserInfo from '/imports/utils/browserInfo';
import Header from '/imports/ui/components/common/control-header/component';
const intlMessages = defineMessages({
hide: {
id: 'app.captions.hide',
description: 'Label for hiding closed captions',
},
takeOwnership: {
id: 'app.captions.ownership',
description: 'Label for taking ownership of closed captions',
},
takeOwnershipTooltip: {
id: 'app.captions.ownershipTooltip',
description: 'Text for button for taking ownership of closed captions',
},
dictationStart: {
id: 'app.captions.dictationStart',
description: 'Label for starting speech recognition',
},
dictationStop: {
id: 'app.captions.dictationStop',
description: 'Label for stopping speech recognition',
},
dictationOnDesc: {
id: 'app.captions.dictationOnDesc',
description: 'Aria description for button that turns on speech recognition',
},
dictationOffDesc: {
id: 'app.captions.dictationOffDesc',
description: 'Aria description for button that turns off speech recognition',
},
});
const propTypes = {
locale: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
ownerId: PropTypes.string.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
dictation: PropTypes.bool.isRequired,
dictating: PropTypes.bool.isRequired,
isRTL: PropTypes.bool.isRequired,
hasPermission: PropTypes.bool.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
isResizing: PropTypes.bool.isRequired,
};
const Captions = ({
locale,
intl,
ownerId,
name,
dictation,
dictating,
isRTL,
hasPermission,
layoutContextDispatch,
isResizing,
}) => {
const { isChrome } = browserInfo;
return (
<Styled.Captions isChrome={isChrome}>
<Header
leftButtonProps={{
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
'aria-label': intl.formatMessage(intlMessages.hide),
label: name,
}}
customRightButton={Service.amICaptionsOwner(ownerId) ? (
<span>
<Button
onClick={dictating
? () => Service.stopDictation(locale)
: () => Service.startDictation(locale)}
label={dictating
? intl.formatMessage(intlMessages.dictationStop)
: intl.formatMessage(intlMessages.dictationStart)}
aria-describedby="dictationBtnDesc"
color={dictating ? 'danger' : 'primary'}
disabled={!dictation}
/>
<div id="dictationBtnDesc" hidden>
{dictating
? intl.formatMessage(intlMessages.dictationOffDesc)
: intl.formatMessage(intlMessages.dictationOnDesc)}
</div>
</span>
) : (
<Button
color="primary"
tooltipLabel={intl.formatMessage(intlMessages.takeOwnershipTooltip, { 0: name })}
onClick={() => Service.updateCaptionsOwner(locale, name)}
aria-label={intl.formatMessage(intlMessages.takeOwnership)}
label={intl.formatMessage(intlMessages.takeOwnership)}
/>
)}
/>
<PadContainer
externalId={locale}
hasPermission={hasPermission}
isResizing={isResizing}
isRTL={isRTL}
/>
</Styled.Captions>
);
};
Captions.propTypes = propTypes;
export default injectWbResizeEvent(injectIntl(Captions));

View File

@ -1,99 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import Service from '/imports/ui/components/captions/service';
import Captions from './component';
import Auth from '/imports/ui/services/auth';
import { layoutSelectInput, layoutDispatch } from '../layout/context';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import useCurrentUser from '../../core/hooks/useCurrentUser';
import logger from '/imports/startup/client/logger';
import { useSubscription } from '@apollo/client';
import { getActiveCaptions } from './queries';
const Container = (props) => {
const cameraDock = layoutSelectInput((i) => i.cameraDock);
const { isResizing } = cameraDock;
const layoutContextDispatch = layoutDispatch();
const {
data: currentUserData,
loading: currentUserLoading,
errors: currentUserErrors,
} = useCurrentUser((u) => ({
isModerator: u.isModerator,
userId: u.userId,
}));
const {
loading: captionsLoading,
error: captionsError,
data: captionsData,
} = useSubscription(getActiveCaptions);
if (currentUserLoading || captionsLoading) return null;
if (currentUserErrors) {
logger.info('Error while fetching current user', currentUserErrors);
return (
<div>
{JSON.stringify(currentUserErrors)}
</div>
);
}
if (captionsError) {
logger.info('Error while fetching captions', captionsError);
return (
<div>
{JSON.stringify(captionsError)}
</div>
);
}
if (!captionsData) return null;
const locale = Service.getCaptionsLocale();
const ownerId = captionsData?.caption_typed_activeLocales?.userOwner?.userId
?? currentUserData.userId;
const ownerName = captionsData?.caption_typed_activeLocales?.userOwner?.name
?? currentUserData.name;
const dictating = true;
const dictation = Service.canIDictateThisPad(ownerId);
const hasPermission = ownerId === currentUserData.userId;
if (!currentUserData.isModerator) {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
return null;
}
return (
<Captions
{
...{
...props,
layoutContextDispatch,
isResizing,
locale,
ownerId,
ownerName,
dictating,
dictation,
hasPermission,
}
}
/>
);
};
export default withTracker(() => {
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';
return {
currentUserId: Auth.userID,
isRTL,
};
})(Container);

View File

@ -1,25 +0,0 @@
import React from "react";
import { HexColorPicker } from "react-colorful";
import Styled from './styles';
const ColorPicker = ({ color, onChange, presetColors, colorNames }) => {
return (
<Styled.Picker>
<Styled.ColorPickerGlobalStyle />
<HexColorPicker color={color} onChange={onChange} />
<div>
{presetColors.map((presetColor) => (
<Styled.PickerSwatch
key={presetColor}
style={{ background: presetColor }}
onClick={() => onChange(presetColor)}
title={colorNames[presetColor]}
/>
))}
</div>
</Styled.Picker>
);
};
export default ColorPicker;

View File

@ -1,38 +0,0 @@
import styled, { createGlobalStyle } from 'styled-components';
const ColorPickerGlobalStyle = createGlobalStyle`
.react-colorful {
display: none !important;
}
`;
const Picker = styled.div`
border: 1px solid rgba(0, 0, 0, 0.2);
width: 140px;
box-shadow: rgba(0, 0, 0, 0.15) 0px 3px 12px;
border-radius: 4px;
padding: 5px;
display: flex;
flex-wrap: wrap;
background: #fff;
position: relative;
`;
const PickerSwatch = styled.button`
width: 25px;
height: 25px;
border: none;
&:hover {
cursor: pointer;
border: 1px solid #fff;
border-style: inset;
}
`;
export default {
ColorPickerGlobalStyle,
Picker,
PickerSwatch,
};

View File

@ -1,416 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Button from '/imports/ui/components/common/button/component';
import ColorPicker from "./color-picker/component";
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
const DEFAULT_VALUE = 'select';
const DEFAULT_KEY = -1;
const DEFAULT_INDEX = 0;
const FONT_FAMILIES = ['Arial', 'Calibri', 'Times New Roman', 'Sans-serif'];
const FONT_SIZES = ['12px', '14px', '18px', '24px', '32px', '42px'];
const COLORS = [
"#000000", "#7a7a7a",
"#ff0000", "#ff8800",
"#88ff00", "#ffffff",
"#00ffff", "#0000ff",
"#8800ff", "#ff00ff"
];
// Used to convert hex values to color names for screen reader aria labels
const HEX_COLOR_NAMES = {
'#000000': 'Black',
'#7a7a7a': 'Grey',
'#ff0000': 'Red',
'#ff8800': 'Orange',
'#88ff00': 'Green',
'#ffffff': 'White',
'#00ffff': 'Cyan',
'#0000ff': 'Blue',
'#8800ff': 'Dark violet',
'#ff00ff': 'Magenta',
};
const intlMessages = defineMessages({
closeLabel: {
id: 'app.captions.menu.closeLabel',
description: 'Label for closing captions menu',
},
title: {
id: 'app.captions.menu.title',
description: 'Title for the closed captions menu',
},
start: {
id: 'app.captions.menu.start',
description: 'Write closed captions',
},
select: {
id: 'app.captions.menu.select',
description: 'Select closed captions available language',
},
backgroundColor: {
id: 'app.captions.menu.backgroundColor',
description: 'Select closed captions background color',
},
fontColor: {
id: 'app.captions.menu.fontColor',
description: 'Select closed captions font color',
},
fontFamily: {
id: 'app.captions.menu.fontFamily',
description: 'Select closed captions font family',
},
fontSize: {
id: 'app.captions.menu.fontSize',
description: 'Select closed captions font size',
},
cancelLabel: {
id: 'app.captions.menu.cancelLabel',
description: 'Cancel button label',
},
preview: {
id: 'app.captions.menu.previewLabel',
description: 'Preview area label',
},
ariaSelectLang: {
id: 'app.captions.menu.ariaSelect',
description: 'Captions language select aria label',
},
captionsLabel: {
id: 'app.captions.label',
description: 'Used in font / size aria labels',
},
current: {
id: 'app.submenu.application.currentSize',
description: 'Used in text / background color aria labels',
},
});
const propTypes = {
activateCaptions: PropTypes.func.isRequired,
getCaptionsSettings: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired,
ownedLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
};
class ReaderMenu extends PureComponent {
constructor(props) {
super(props);
const {
backgroundColor,
fontColor,
fontFamily,
fontSize,
} = props.getCaptionsSettings();
const { ownedLocales } = this.props;
this.state = {
locale: (ownedLocales && ownedLocales[0]) ? ownedLocales[0].locale : null,
backgroundColor,
fontColor,
fontFamily,
fontSize,
displayBackgroundColorPicker: false,
displayFontColorPicker: false,
};
this.handleSelectChange = this.handleSelectChange.bind(this);
this.handleColorPickerClick = this.handleColorPickerClick.bind(this);
this.handleCloseColorPicker = this.handleCloseColorPicker.bind(this);
this.handleFontColorChange = this.handleFontColorChange.bind(this);
this.handleBackgroundColorChange = this.handleBackgroundColorChange.bind(this);
this.handleLocaleChange = this.handleLocaleChange.bind(this);
this.handleStart = this.handleStart.bind(this);
this.getPreviewStyle = this.getPreviewStyle.bind(this);
}
handleColorPickerClick(fieldname) {
const obj = {};
// eslint-disable-next-line react/destructuring-assignment
obj[fieldname] = !this.state[fieldname];
this.setState(obj);
}
handleCloseColorPicker() {
this.setState({
displayBackgroundColorPicker: false,
displayFontColorPicker: false,
});
}
handleFontColorChange(color) {
this.setState({ fontColor: color });
this.handleCloseColorPicker();
}
handleBackgroundColorChange(color) {
this.setState({ backgroundColor: color });
this.handleCloseColorPicker();
}
handleLocaleChange(event) {
this.setState({ locale: event.target.value });
}
handleSelectChange(fieldname, options, event) {
const obj = {};
obj[fieldname] = options[event.target.value];
this.setState(obj);
}
handleStart() {
const { closeModal, activateCaptions } = this.props;
const {
locale,
backgroundColor,
fontColor,
fontFamily,
fontSize,
} = this.state;
const settings = {
backgroundColor,
fontColor,
fontFamily,
fontSize,
};
activateCaptions(locale, settings);
closeModal();
}
getPreviewStyle() {
const {
backgroundColor,
fontColor,
fontFamily,
fontSize,
} = this.state;
return {
fontFamily,
fontSize,
color: fontColor,
background: backgroundColor,
};
}
render() {
const {
intl,
ownedLocales,
closeModal,
isOpen,
priority,
} = this.props;
const {
backgroundColor,
displayBackgroundColorPicker,
displayFontColorPicker,
fontColor,
fontFamily,
fontSize,
locale,
} = this.state;
const defaultLocale = locale || DEFAULT_VALUE;
const ariaTextColor = `${intl.formatMessage(intlMessages.fontColor)} ${intl.formatMessage(intlMessages.current, { 0: HEX_COLOR_NAMES[fontColor.toLowerCase()] })}`;
const ariaBackgroundColor = `${intl.formatMessage(intlMessages.backgroundColor)} ${intl.formatMessage(intlMessages.current, { 0: HEX_COLOR_NAMES[backgroundColor.toLowerCase()] })}`;
const ariaFont = `${intl.formatMessage(intlMessages.captionsLabel)} ${intl.formatMessage(intlMessages.fontFamily)}`;
const ariaSize = `${intl.formatMessage(intlMessages.captionsLabel)} ${intl.formatMessage(intlMessages.fontSize)}`;
return (
<Styled.ReaderMenuModal
onRequestClose={closeModal}
hideBorder
contentLabel={intl.formatMessage(intlMessages.title)}
{...{
isOpen,
priority,
}}
>
<Styled.Title>
{intl.formatMessage(intlMessages.title)}
</Styled.Title>
{!locale ? null : (
<div>
<Styled.Col>
<Styled.Row>
<Styled.Label aria-hidden>
{intl.formatMessage(intlMessages.ariaSelectLang)}
</Styled.Label>
<Styled.Select
aria-label={intl.formatMessage(intlMessages.ariaSelectLang)}
onChange={this.handleLocaleChange}
defaultValue={defaultLocale}
lang={locale}
>
<option
disabled
key={DEFAULT_KEY}
value={DEFAULT_VALUE}
>
{intl.formatMessage(intlMessages.select)}
</option>
{ownedLocales.map((loc) => (
<option
key={loc.locale}
value={loc.locale}
lang={loc.locale}
>
{loc.name}
</option>
))}
</Styled.Select>
</Styled.Row>
<Styled.Row>
<Styled.Label aria-hidden>
{intl.formatMessage(intlMessages.fontColor)}
</Styled.Label>
<Styled.Swatch
aria-label={ariaTextColor}
tabIndex={DEFAULT_INDEX}
onClick={this.handleColorPickerClick.bind(this, 'displayFontColorPicker')}
onKeyPress={() => { }}
role="button"
>
<Styled.SwatchInner style={{ background: fontColor }} />
</Styled.Swatch>
{
displayFontColorPicker
? (
<Styled.ColorPickerPopover>
<Styled.ColorPickerOverlay
onClick={this.handleCloseColorPicker.bind(this)}
onKeyPress={() => { }}
role="button"
tabIndex={0}
aria-label={ariaTextColor}
/>
<ColorPicker
color={fontColor}
onChange={this.handleFontColorChange}
presetColors={COLORS}
colorNames={HEX_COLOR_NAMES}
/>
</Styled.ColorPickerPopover>
)
: null
}
</Styled.Row>
<Styled.Row>
<Styled.Label aria-hidden>
{intl.formatMessage(intlMessages.backgroundColor)}
</Styled.Label>
<Styled.Swatch
aria-label={ariaBackgroundColor}
tabIndex={DEFAULT_INDEX}
onClick={this.handleColorPickerClick.bind(this, 'displayBackgroundColorPicker')}
role="button"
onKeyPress={() => { }}
>
<Styled.SwatchInner style={{ background: backgroundColor }} />
</Styled.Swatch>
{
displayBackgroundColorPicker
? (
<Styled.ColorPickerPopover>
<Styled.ColorPickerOverlay
aria-label={ariaBackgroundColor}
onClick={this.handleCloseColorPicker.bind(this)}
tabIndex={0}
role="button"
onKeyPress={() => { }}
/>
<ColorPicker
color={fontColor}
onChange={this.handleBackgroundColorChange}
presetColors={COLORS}
colorNames={HEX_COLOR_NAMES}
/>
</Styled.ColorPickerPopover>
)
: null
}
</Styled.Row>
<Styled.Row>
<Styled.Label aria-hidden>
{intl.formatMessage(intlMessages.fontFamily)}
</Styled.Label>
<Styled.Select
aria-label={ariaFont}
defaultValue={FONT_FAMILIES.indexOf(fontFamily)}
onChange={this.handleSelectChange.bind(this, 'fontFamily', FONT_FAMILIES)}
>
{FONT_FAMILIES.map((family, index) => (
<option
key={family}
value={index}
>
{family}
</option>
))}
</Styled.Select>
</Styled.Row>
<Styled.Row>
<Styled.Label aria-hidden>
{intl.formatMessage(intlMessages.fontSize)}
</Styled.Label>
<Styled.Select
aria-label={ariaSize}
defaultValue={FONT_SIZES.indexOf(fontSize)}
onChange={this.handleSelectChange.bind(this, 'fontSize', FONT_SIZES)}
>
{FONT_SIZES.map((size, index) => (
<option
key={size}
value={index}
>
{size}
</option>
))}
</Styled.Select>
</Styled.Row>
<Styled.Row>
<Styled.Label>{intl.formatMessage(intlMessages.preview)}</Styled.Label>
<span aria-hidden style={this.getPreviewStyle()}>AaBbCc</span>
</Styled.Row>
</Styled.Col>
</div>
)}
<Styled.Footer>
<Styled.Actions>
<Button
label={intl.formatMessage(intlMessages.cancelLabel)}
onClick={closeModal}
/>
<Button
color="primary"
label={intl.formatMessage(intlMessages.start)}
onClick={() => this.handleStart()}
disabled={locale == null}
data-test="startViewingClosedCaptions"
/>
</Styled.Actions>
</Styled.Footer>
</Styled.ReaderMenuModal>
);
}
}
ReaderMenu.propTypes = propTypes;
export default injectIntl(ReaderMenu);

View File

@ -1,13 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import ReaderMenu from './component';
import CaptionsService from '/imports/ui/components/captions/service';
const ReaderMenuContainer = (props) => <ReaderMenu {...props} />;
export default withTracker(({ setIsOpen }) => ({
closeModal: () => setIsOpen(false),
activateCaptions: (locale, settings) => CaptionsService.activateCaptions(locale, settings),
getCaptionsSettings: () => CaptionsService.getCaptionsSettings(),
ownedLocales: CaptionsService.getOwnedLocales(),
}))(ReaderMenuContainer);

View File

@ -1,133 +0,0 @@
import styled from 'styled-components';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import {
colorGrayDark,
colorWhite,
colorGrayLabel,
colorGrayLight,
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import { borderSize, borderSizeLarge } from '/imports/ui/stylesheets/styled-components/general';
const ReaderMenuModal = styled(ModalSimple)`
padding: 1rem;
`;
const Title = styled.header`
display: block;
color: ${colorGrayDark};
font-size: 1.4rem;
text-align: center;
`;
const Col = styled.div`
display: flex;
flex-direction: column;
height: 100%;
margin: 0 1.5rem 0 0;
justify-content: center;
[dir="rtl"] & {
margin: 0 0 0 1.5rem;
}
@media ${smallOnly} {
width: 100%;
height: unset;
}
`;
const Row = styled.div`
display: flex;
justify-content: space-between;
padding: .2rem 0 .2rem 0;
`;
const Label = styled.div`
flex: 1 0 0;
`;
const Select = styled.select`
background-color: ${colorWhite};
border-radius: 0.3rem;
color: ${colorGrayLabel};
height: 1.6rem;
margin-top: 0.4rem;
width: 50%;
`;
const Swatch = styled.div`
flex: 1 0 0;
border-radius: ${borderSize};
border: ${borderSize} solid ${colorGrayLight};
display: inline-block;
vertical-align: middle;
cursor: pointer;
&:focus {
outline: none;
box-shadow: inset 0 0 0 ${borderSizeLarge} ${colorPrimary};
border-radius: ${borderSize};
}
`;
const SwatchInner = styled.div`
width: auto;
height: 1.1rem;
border-radius: ${borderSize};
`;
const ColorPickerPopover = styled.div`
position: absolute;
z-index: 1001;
`;
const ColorPickerOverlay = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
`;
const Footer = styled.div`
display: flex;
`;
const Actions = styled.div`
margin-left: auto;
margin-right: 3px;
[dir="rtl"] & {
margin-right: auto;
margin-left: 3px;
}
> * {
&:first-child {
margin-right: 3px;
margin-left: inherit;
[dir="rtl"] & {
margin-right: inherit;
margin-left: 3px;
}
}
}
`;
export default {
ReaderMenuModal,
Title,
Col,
Row,
Label,
Select,
Swatch,
SwatchInner,
ColorPickerPopover,
ColorPickerOverlay,
Footer,
Actions,
};

View File

@ -1,138 +0,0 @@
import Captions from '/imports/api/captions';
import Auth from '/imports/ui/services/auth';
import PadsService from '/imports/ui/components/pads/service';
import { makeCall } from '/imports/ui/services/api';
import { Session } from 'meteor/session';
import { isCaptionsEnabled } from '/imports/ui/services/features';
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
const LINE_BREAK = '\n';
const updateCaptionsOwner = (locale, name) => makeCall('updateCaptionsOwner', locale, name);
const startDictation = (locale) => makeCall('startDictation', locale);
const stopDictation = (locale) => makeCall('stopDictation', locale);
const getCaptionsSettings = () => {
const settings = Session.get('captionsSettings');
if (settings) return settings;
const {
background,
font,
} = CAPTIONS_CONFIG;
return {
backgroundColor: background,
fontColor: font.color,
fontFamily: font.family,
fontSize: font.size,
};
};
const setCaptionsSettings = (settings) => Session.set('captionsSettings', settings);
const getCaptionsLocale = () => Session.get('captionsLocale') || '';
const setCaptionsLocale = (locale) => Session.set('captionsLocale', locale);
const getCaptionsActive = () => Session.get('captionsActive') || '';
const formatCaptionsText = (text) => {
const splitText = text.split(LINE_BREAK);
const filteredText = splitText.filter((line, index) => {
const lastLine = index === (splitText.length - 1);
const emptyLine = line.length === 0;
return (!emptyLine || lastLine);
});
while (filteredText.length > CAPTIONS_CONFIG.lines) filteredText.shift();
return filteredText.join(LINE_BREAK);
};
const setCaptionsActive = (locale) => Session.set('captionsActive', locale);
const amICaptionsOwner = (ownerId) => ownerId === Auth.userID;
const isCaptionsActive = () => {
const enabled = isCaptionsEnabled();
const activated = getCaptionsActive() !== '';
return (enabled && activated);
};
const deactivateCaptions = () => setCaptionsActive('');
const activateCaptions = (locale, settings) => {
setCaptionsSettings(settings);
setCaptionsActive(locale);
};
const createCaptions = (locale, name) => {
PadsService.createGroup(locale, CAPTIONS_CONFIG.id, name);
updateCaptionsOwner(locale, name);
setCaptionsLocale(locale);
};
const getDictationStatus = (isModerator) => {
if (!CAPTIONS_CONFIG.dictation || !isModerator) {
return {
locale: '',
dictating: false,
};
}
const captions = Captions.findOne({
meetingId: Auth.meetingID,
ownerId: Auth.userID,
}, {
fields: {
locale: 1,
dictating: 1,
},
});
if (captions) {
return {
locale: captions.locale,
dictating: captions.dictating,
};
}
return {
locale: '',
dictating: false,
};
};
const canIDictateThisPad = (ownerId) => {
if (!CAPTIONS_CONFIG.dictation) return false;
if (ownerId !== Auth.userID) return false;
if (!(typeof SpeechRecognitionAPI !== 'undefined')) return false;
return true;
};
export default {
ID: CAPTIONS_CONFIG.id,
updateCaptionsOwner,
startDictation,
stopDictation,
getCaptionsSettings,
amICaptionsOwner,
isCaptionsEnabled,
isCaptionsActive,
deactivateCaptions,
activateCaptions,
formatCaptionsText,
createCaptions,
getCaptionsLocale,
setCaptionsLocale,
getDictationStatus,
canIDictateThisPad,
};

View File

@ -1,24 +0,0 @@
import styled from 'styled-components';
import { colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
const Captions = styled.div`
background-color: ${colorWhite};
padding: ${smPaddingX};
display: flex;
flex-grow: 1;
flex-direction: column;
overflow: hidden;
height: 100%;
${({ isChrome }) => isChrome && `
transform: translateZ(0);
`}
@media ${smallOnly} {
transform: none !important;
}
`;
export default { Captions };

View File

@ -1,163 +0,0 @@
import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import PropTypes from 'prop-types';
import Service from '/imports/ui/components/captions/service';
import LocalesDropdown from '/imports/ui/components/common/locales-dropdown/component';
import Styled from './styles';
import { PANELS, ACTIONS } from '../../layout/enums';
const intlMessages = defineMessages({
closeLabel: {
id: 'app.captions.menu.closeLabel',
description: 'Label for closing captions menu',
},
title: {
id: 'app.captions.menu.title',
description: 'Title for the closed captions menu',
},
subtitle: {
id: 'app.captions.menu.subtitle',
description: 'Subtitle for the closed captions writer menu',
},
start: {
id: 'app.captions.menu.start',
description: 'Write closed captions',
},
ariaStart: {
id: 'app.captions.menu.ariaStart',
description: 'aria label for start captions button',
},
ariaStartDesc: {
id: 'app.captions.menu.ariaStartDesc',
description: 'aria description for start captions button',
},
select: {
id: 'app.captions.menu.select',
description: 'Select closed captions available language',
},
ariaSelect: {
id: 'app.captions.menu.ariaSelect',
description: 'Aria label for captions language selector',
},
});
const propTypes = {
availableLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
};
class WriterMenu extends PureComponent {
constructor(props) {
super(props);
const { availableLocales, intl } = this.props;
const candidate = availableLocales.filter(
(l) => l.locale.substring(0, 2) === intl.locale.substring(0, 2),
);
this.state = {
locale: candidate && candidate[0] ? candidate[0].locale : null,
};
this.handleChange = this.handleChange.bind(this);
this.handleStart = this.handleStart.bind(this);
}
componentWillUnmount() {
const { setIsOpen } = this.props;
setIsOpen(false);
}
handleChange(event) {
this.setState({ locale: event.target.value });
}
handleStart() {
const {
setIsOpen,
layoutContextDispatch,
availableLocales,
} = this.props;
const { locale } = this.state;
const localeObj = availableLocales.find((l) => l.locale === locale);
Service.createCaptions(locale, localeObj.name);
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: true,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.CAPTIONS,
});
setIsOpen(false);
}
render() {
const {
intl,
availableLocales,
isOpen,
onRequestClose,
priority,
setIsOpen
} = this.props;
const { locale } = this.state;
return (
<Styled.WriterMenuModal
hideBorder
contentLabel={intl.formatMessage(intlMessages.title)}
title={intl.formatMessage(intlMessages.title)}
{...{
isOpen,
onRequestClose,
priority,
setIsOpen
}}
>
<Styled.Content>
<span>
{intl.formatMessage(intlMessages.subtitle)}
</span>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label
aria-hidden
htmlFor="captionsLangSelector"
aria-label={intl.formatMessage(intlMessages.ariaSelect)}
/>
<Styled.WriterMenuSelect>
<LocalesDropdown
allLocales={availableLocales}
handleChange={this.handleChange}
value={locale}
elementId="captionsLangSelector"
selectMessage={intl.formatMessage(intlMessages.select)}
/>
</Styled.WriterMenuSelect>
<Styled.StartBtn
label={intl.formatMessage(intlMessages.start)}
aria-label={intl.formatMessage(intlMessages.ariaStart)}
aria-describedby="descriptionStart"
onClick={this.handleStart}
disabled={locale == null}
data-test="startWritingClosedCaptions"
/>
<div id="descriptionStart" hidden>{intl.formatMessage(intlMessages.ariaStartDesc)}</div>
</Styled.Content>
</Styled.WriterMenuModal>
);
}
}
WriterMenu.propTypes = propTypes;
export default injectIntl(WriterMenu);

View File

@ -1,54 +0,0 @@
import React from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import WriterMenu from './component';
import { layoutDispatch } from '../../layout/context';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { useSubscription } from '@apollo/client';
import { getActiveCaptions } from '../queries';
import logger from '/imports/startup/client/logger';
const knownLocales = window.meetingClientSettings.public.captions.locales;
const WriterMenuContainer = (props) => {
const {
loading: captionsLoading,
error: captionsError,
data: captionsData,
} = useSubscription(getActiveCaptions);
const layoutContextDispatch = layoutDispatch();
const {
data: currentUserData,
loading: currentUserLoading,
} = useCurrentUser((user) => ({
isModerator: user.isModerator,
}));
if (captionsLoading || currentUserLoading) return null;
if (captionsError) {
logger.error('Error while fetching captions', captionsError);
return (
<div>
{JSON.stringify(captionsError)}
</div>
);
}
const amIModerator = currentUserData.isModerator;
const activeCaptions = (captionsData?.caption_typed_activeLocales || [])
.map((caption) => caption.lang);
const availableLocales = knownLocales.filter((locale) => !activeCaptions.includes(locale.locale));
return amIModerator
&& (
<WriterMenu {...{
availableLocales,
layoutContextDispatch,
...props,
}}
/>
);
};
export default withTracker(({ setIsOpen }) => ({
closeModal: () => setIsOpen(false),
}))(WriterMenuContainer);

View File

@ -1,81 +0,0 @@
import styled from 'styled-components';
import {
borderSize,
borderSizeLarge,
mdPaddingX,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorWhite,
colorLink,
colorGrayLighter,
colorGrayLabel,
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import Button from '/imports/ui/components/common/button/component';
import ModalSimple from '/imports/ui/components/common/modal/simple/component';
const WriterMenuModal = styled(ModalSimple)`
min-height: 20rem;
`;
const Content = styled.div`
align-items: center;
display: flex;
flex-direction: column;
padding: .3rem 0 0.5rem 0;
`;
const StartBtn = styled(Button)`
align-self: center;
margin: 0;
display: block;
position: absolute;
bottom: ${mdPaddingX};
color: ${colorWhite} !important;
background-color: ${colorLink} !important;
&:focus {
outline: none !important;
}
& > i {
color: #3c5764;
}
`;
const WriterMenuSelect = styled.div`
width: 40%;
& > select {
background-color: ${colorWhite};
border: ${borderSize} solid ${colorWhite};
border-radius: ${borderSize};
border-bottom: 0.1rem solid ${colorGrayLighter};
color: ${colorGrayLabel};
width: 100%;
height: 1.75rem;
padding: 1px;
&:hover {
outline: transparent;
outline-style: dotted;
outline-width: ${borderSize};
}
&:focus {
outline: none;
box-shadow: inset 0 0 0 ${borderSizeLarge} ${colorPrimary};
border-radius: ${borderSize};
outline: transparent;
outline-width: ${borderSize};
outline-style: solid;
}
}
`;
export default {
WriterMenuModal,
Content,
StartBtn,
WriterMenuSelect,
};

View File

@ -5,7 +5,6 @@ import { ACTIONS, PANELS } from '../layout/enums';
import ChatContainer from '/imports/ui/components/chat/chat-graphql/component';
import NotesContainer from '/imports/ui/components/notes/container';
import PollContainer from '/imports/ui/components/poll/container';
import CaptionsContainer from '/imports/ui/components/captions/container';
import BreakoutRoomContainer from '/imports/ui/components/breakout-room/container';
import TimerContainer from '/imports/ui/components/timer/container';
import GuestUsersManagementPanel from '/imports/ui/components/waiting-users/waiting-users-graphql/component';
@ -143,7 +142,6 @@ const SidebarContent = (props) => {
isToSharedNotesBeShow={sidebarContentPanel === PANELS.SHARED_NOTES}
/>
)}
{sidebarContentPanel === PANELS.CAPTIONS && <CaptionsContainer amIModerator={amIModerator} />}
{sidebarContentPanel === PANELS.BREAKOUT && <BreakoutRoomContainer />}
{sidebarContentPanel === PANELS.TIMER && <TimerContainer isModerator={amIModerator} />}
{sidebarContentPanel === PANELS.WAITING_USERS && <GuestUsersManagementPanel />}

View File

@ -1,80 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '/imports/ui/components/common/icon/component';
import CaptionsService from '/imports/ui/components/captions/service';
import { defineMessages, injectIntl } from 'react-intl';
import Styled from './styles';
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
const propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
locale: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
tabIndex: PropTypes.number.isRequired,
};
const intlMessages = defineMessages({
captionLabel: {
id: 'app.captions.label',
description: 'used for captions button aria label',
},
});
const CaptionsListItem = (props) => {
const {
intl,
locale,
name,
tabIndex,
sidebarContentPanel,
layoutContextDispatch,
} = props;
const handleClickToggleCaptions = () => {
if (sidebarContentPanel !== PANELS.CAPTIONS) {
CaptionsService.setCaptionsLocale(locale);
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: true,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.CAPTIONS,
});
} else {
const captionsLocale = CaptionsService.getCaptionsLocale();
if (captionsLocale !== locale) {
CaptionsService.setCaptionsLocale(locale);
} else {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}
}
};
return (
<Styled.ListItem
role="button"
tabIndex={tabIndex}
id={locale}
onClick={handleClickToggleCaptions}
aria-label={`${name} ${intl.formatMessage(intlMessages.captionLabel)}`}
onKeyPress={() => {}}
>
<Icon iconName="closed_caption" />
<span aria-hidden>{name}</span>
</Styled.ListItem>
);
};
CaptionsListItem.propTypes = propTypes;
export default injectIntl(CaptionsListItem);

View File

@ -1,9 +0,0 @@
import styled from 'styled-components';
import StyledContent from '/imports/ui/components/user-list/user-list-content/styles';
const ListItem = styled(StyledContent.ListItem)``;
export default {
ListItem,
};

View File

@ -5,7 +5,6 @@ import UserListParticipants from './user-participants/user-list-participants/com
import ChatList from './user-messages/chat-list/component';
import UserNotesContainer from './user-notes/container';
import TimerContainer from './timer/container';
import UserCaptionsContainer from './user-captions/container';
import GuestPanelOpenerContainer from '../user-list-graphql/user-participants-title/guest-panel-opener/component';
import UserPollsContainer from './user-polls/container';
import BreakoutRoomContainer from './breakout-room/container';
@ -41,7 +40,6 @@ class UserContent extends PureComponent {
return (
<Styled.Content data-test="userListContent">
{isChatEnabled() ? <ChatList /> : null}
{currentUser?.role === ROLE_MODERATOR ? <UserCaptionsContainer /> : null}
<UserNotesContainer />
{isTimerActive && <TimerContainer isModerator={currentUser?.role === ROLE_MODERATOR} />}
{currentUser?.role === ROLE_MODERATOR ? (

View File

@ -1,139 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import CaptionsListItem from '/imports/ui/components/user-list/captions-list-item/component';
import { defineMessages, injectIntl } from 'react-intl';
import KEY_CODES from '/imports/utils/keyCodes';
import Styled from './styles';
import { findDOMNode } from 'react-dom';
const propTypes = {
ownedLocales: PropTypes.arrayOf(PropTypes.object).isRequired,
sidebarContentPanel: PropTypes.string.isRequired,
layoutContextDispatch: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
};
const intlMessages = defineMessages({
title: {
id: 'app.userList.captionsTitle',
description: 'Title for the captions list',
},
});
class UserCaptions extends Component {
constructor() {
super();
this.state = {
selectedCaption: null,
};
this.activeCaptionRefs = [];
this.changeState = this.changeState.bind(this);
this.rove = this.rove.bind(this);
}
componentDidMount() {
if (this._captionsList) {
this._captionsList.addEventListener(
'keydown',
this.rove,
true,
);
}
}
componentDidUpdate(prevProps, prevState) {
const { selectedCaption } = this.state;
if (selectedCaption && selectedCaption !== prevState.selectedCaption) {
const { firstChild } = selectedCaption;
if (firstChild) firstChild.focus();
}
}
changeState(ref) {
this.setState({ selectedCaption: ref });
}
rove(event) {
const { roving } = this.props;
const { selectedCaption } = this.state;
const captionItemsRef = findDOMNode(this._captionItems);
if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event?.which)) {
selectedCaption?.firstChild?.click();
} else {
roving(event, this.changeState, captionItemsRef, selectedCaption);
}
event.stopPropagation();
}
renderCaptions() {
const {
ownedLocales,
sidebarContentPanel,
layoutContextDispatch,
} = this.props;
let index = -1;
return ownedLocales.map((ownedLocale) => (
<CSSTransition
classNames="transition"
appear
enter
exit={false}
timeout={0}
component="div"
key={ownedLocale.locale}
>
<Styled.ListTransition ref={(node) => { this.activeCaptionRefs[index += 1] = node; }}>
<CaptionsListItem
{...{
locale: ownedLocale.locale,
name: ownedLocale.name,
layoutContextDispatch,
sidebarContentPanel,
}}
tabIndex={-1}
/>
</Styled.ListTransition>
</CSSTransition>
));
}
render() {
const {
intl,
ownedLocales,
} = this.props;
if (ownedLocales.length < 1) return null;
return (
<Styled.Messages>
<Styled.Container>
<Styled.SmallTitle>
{intl.formatMessage(intlMessages.title)}
</Styled.SmallTitle>
</Styled.Container>
<Styled.ScrollableList
role="tabpanel"
tabIndex={0}
ref={(ref) => { this._captionsList = ref; }}
>
<Styled.List>
<TransitionGroup ref={(ref) => { this._captionItems = ref; }}>
{this.renderCaptions()}
</TransitionGroup>
</Styled.List>
</Styled.ScrollableList>
</Styled.Messages>
);
}
}
UserCaptions.propTypes = propTypes;
export default injectIntl(UserCaptions);

View File

@ -1,56 +0,0 @@
import React from 'react';
import UserCaptionsItem from './component';
import Service from '/imports/ui/components/user-list/service';
import { layoutSelectInput, layoutDispatch } from '../../../layout/context';
import { useSubscription } from '@apollo/client';
import { getActiveCaptions } from '../../../captions/queries';
import logger from '/imports/startup/client/logger';
const knownLocales = window.meetingClientSettings.public.captions.locales;
const Container = (props) => {
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const layoutContextDispatch = layoutDispatch();
const {
data: activeCaptionsData,
loading: activeCaptionsLoading,
errors: activeCaptionsErrors,
} = useSubscription(getActiveCaptions);
if (activeCaptionsLoading) return null;
if (activeCaptionsErrors) {
logger.info('Error while fetching current user', activeCaptionsErrors);
return (
<div>
{JSON.stringify(activeCaptionsErrors)}
</div>
);
}
if (!activeCaptionsData || activeCaptionsData.length > 0) return null;
const ownedLocales = activeCaptionsData
.caption_typed_activeLocales
.map((caption) => {
const localeName = knownLocales.find((l) => l.locale === caption.lang).name;
return {
locale: caption.lang,
name: localeName,
ownerId: caption.userOwner.userId,
};
});
const { roving } = Service;
return (
<UserCaptionsItem
{
...{
ownedLocales,
sidebarContentPanel,
layoutContextDispatch,
roving,
...props,
}
}
/>
);
};
export default Container;

View File

@ -1,61 +0,0 @@
import styled from 'styled-components';
import Styled from '/imports/ui/components/user-list/styles';
import StyledContent from '/imports/ui/components/user-list/user-list-content/styles';
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
const Messages = styled(Styled.Messages)``;
const Container = styled(StyledContent.Container)``;
const SmallTitle = styled(Styled.SmallTitle)``;
const ScrollableList = styled(StyledContent.ScrollableList)``;
const List = styled(StyledContent.List)``;
const ListTransition = styled.div`
display: flex;
flex-flow: column;
margin: 0;
padding: 0;
padding-top: ${borderSize};
outline: none;
overflow: hidden;
flex-shrink: 1;
&.transition-enter,
&.transition-appear {
opacity: 0.01;
}
&.transition-enter-active,
&.transition-appear-active {
opacity: 1;
&.animationsEnabled {
transition: all 600ms;
}
}
&.transition-leave {
opacity: 1;
}
&.transition-leave-active {
opacity: 0;
&.animationsEnabled {
transition: all 600ms;
}
}
`;
export default {
Messages,
Container,
SmallTitle,
ScrollableList,
List,
ListTransition,
};

View File

@ -7,7 +7,6 @@ import React, {
import LockViewersContainer from '/imports/ui/components/lock-viewers/container';
import GuestPolicyContainer from '/imports/ui/components/waiting-users/guest-policy/container';
import CreateBreakoutRoomContainerGraphql from '../../../../breakout-room/breakout-room-graphql/create-breakout-room/component';
import WriterMenuContainer from '/imports/ui/components/captions/writer-menu/container';
import BBBMenu from '/imports/ui/components/common/menu/component';
import Styled from './styles';
import { defineMessages, useIntl } from 'react-intl';
@ -21,7 +20,7 @@ import {
} from './service';
import { User } from '/imports/ui/Types/user';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { isBreakoutRoomsEnabled, isLearningDashboardEnabled, isCaptionsEnabled } from '/imports/ui/services/features';
import { isBreakoutRoomsEnabled, isLearningDashboardEnabled } from '/imports/ui/services/features';
import { useMutation, useLazyQuery } from '@apollo/client';
import { CLEAR_ALL_EMOJI } from '/imports/ui/core/graphql/mutations/userMutations';
import { SET_MUTED } from './mutations';
@ -102,14 +101,6 @@ const intlMessages = defineMessages({
id: 'app.actionsBar.actionsDropdown.saveUserNames',
description: 'Save user name feature description',
},
captionsLabel: {
id: 'app.actionsBar.actionsDropdown.captionsLabel',
description: 'Captions menu toggle label',
},
captionsDesc: {
id: 'app.actionsBar.actionsDropdown.captionsDesc',
description: 'Captions menu toggle description',
},
newTab: {
id: 'app.modal.newTab',
description: 'label used in aria description',
@ -185,7 +176,6 @@ const UserTitleOptions: React.FC<UserTitleOptionsProps> = ({
const [isCreateBreakoutRoomModalOpen, setCreateBreakoutRoomModalIsOpen] = useState(false);
const [isGuestPolicyModalOpen, setGuestPolicyModalIsOpen] = useState(false);
const [isLockViewersModalOpen, setLockViewersModalIsOpen] = useState(false);
const [isWriterMenuModalOpen, setIsWriterMenuModalOpen] = useState(false);
const [clearAllEmoji] = useMutation(CLEAR_ALL_EMOJI);
const [setMuted] = useMutation(SET_MUTED);
@ -312,15 +302,6 @@ const UserTitleOptions: React.FC<UserTitleOptionsProps> = ({
onClick: () => setCreateBreakoutRoomModalIsOpen(true),
dataTest: 'createBreakoutRooms',
},
{
allow: isModerator && isCaptionsEnabled(),
icon: 'closed_caption',
label: intl.formatMessage(intlMessages.captionsLabel),
description: intl.formatMessage(intlMessages.captionsDesc),
key: uuids.current[7],
onClick: () => setIsWriterMenuModalOpen(true),
dataTest: 'writeClosedCaptions',
},
{
key: 'separator-02',
isSeparator: true,
@ -385,14 +366,6 @@ const UserTitleOptions: React.FC<UserTitleOptionsProps> = ({
otherOptions: {},
})}
{renderModal({
isOpen: isWriterMenuModalOpen,
setIsOpen: setIsWriterMenuModalOpen,
priority: 'low',
Component: WriterMenuContainer,
otherOptions: {},
})}
{renderModal({
isOpen: isLockViewersModalOpen,
setIsOpen: setLockViewersModalIsOpen,

View File

@ -3,7 +3,6 @@ import '/imports/startup/server';
// 2x
import '/imports/api/meetings/server';
import '/imports/api/users/server';
import '/imports/api/captions/server';
import '/imports/api/presentation-upload-token/server';
import '/imports/api/breakouts/server';
import '/imports/api/screenshare/server';

View File

@ -134,6 +134,9 @@ test.describe.parallel('Create Parameters', () => {
});
test.describe.serial(() => {
// current testing code is checking the old (write) captions
// this parameter should works the same way with the automatic captions
test.fixme();
test('Captions', async ({ browser, context, page }) => {
const disabledFeatures = new DisabledFeatures(browser, context);
await disabledFeatures.initModPage(page, true, { createParameter: c.captionsDisabled });