Merge remote-tracking branch 'upstream/v3.0.x-release' into refactor-captions
This commit is contained in:
commit
77ded6548b
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -1,2 +0,0 @@
|
||||
import './methods';
|
||||
import './publishers';
|
@ -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,
|
||||
});
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
@ -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)}
|
||||
|
@ -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} />
|
||||
|
@ -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),
|
||||
|
@ -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));
|
@ -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);
|
@ -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;
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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 };
|
@ -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);
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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 />}
|
||||
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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 ? (
|
||||
|
@ -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);
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
|
@ -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';
|
||||
|
@ -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 });
|
||||
|
Loading…
Reference in New Issue
Block a user