Migrate auth and settings to graphQL (#19507)

This commit is contained in:
André Castro 2024-03-06 14:28:18 -03:00 committed by GitHub
parent 5518dff623
commit 58a0efe708
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
180 changed files with 4617 additions and 462 deletions

View File

@ -190,4 +190,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""
comment: ""

View File

@ -22,4 +22,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-MeetingId
comment: ""
comment: ""

View File

@ -212,4 +212,4 @@ update_permissions:
_eq: X-Hasura-MeetingId
- userId:
_eq: X-Hasura-UserId
check: null
check: null

View File

@ -0,0 +1,41 @@
import React, {
Suspense,
useContext,
useEffect,
} from 'react';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import { defineMessages, useIntl } from 'react-intl';
const MeetingClientLazy = React.lazy(() => import('./meetingClient'));
const intlMessages = defineMessages({
loadingClientLabel: {
id: 'app.meeting.loadingClient',
description: 'loading client label',
},
});
const ClientStartup: React.FC = () => {
const loadingContextInfo = useContext(LoadingContext);
const intl = useIntl();
useEffect(() => {
loadingContextInfo.setLoading(true, intl.formatMessage(intlMessages.loadingClientLabel));
}, []);
return (
<Suspense>
{
(() => {
try {
return <MeetingClientLazy />;
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error on rendering MeetingClientLazy: '.concat(JSON.stringify(error) || ''));
}
})()
}
</Suspense>
);
};
export default ClientStartup;

View File

@ -0,0 +1,38 @@
import React from 'react';
import ConnectionManager from '/imports/ui/components/connection-manager/component';
// eslint-disable-next-line react/no-deprecated
import { render } from 'react-dom';
import SettingsLoader from '/imports/ui/components/settings-loader/component';
import ErrorBoundary from '/imports/ui/components/common/error-boundary/component';
import { ErrorScreen } from '/imports/ui/components/error-screen/component';
import PresenceManager from '/imports/ui/components/join-handler/presenceManager/component';
import LoadingScreenHOC from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import IntlLoaderContainer from '/imports/startup/client/intlLoader';
import LocatedErrorBoundary from '/imports/ui/components/common/error-boundary/located-error-boundary/component';
import StartupDataFetch from '/imports/ui/components/connection-manager/startup-data-fetch/component';
const Main: React.FC = () => {
return (
<StartupDataFetch>
<ErrorBoundary Fallback={ErrorScreen}>
<LoadingScreenHOC>
<IntlLoaderContainer>
{/* from there the error messages are located */}
<LocatedErrorBoundary Fallback={ErrorScreen}>
<ConnectionManager>
<PresenceManager>
<SettingsLoader />
</PresenceManager>
</ConnectionManager>
</LocatedErrorBoundary>
</IntlLoaderContainer>
</LoadingScreenHOC>
</ErrorBoundary>
</StartupDataFetch>
);
};
render(
<Main />,
document.getElementById('app'),
);

View File

@ -17,43 +17,49 @@
*/
/* eslint no-unused-vars: 0 */
import React from 'react';
import React, { useContext, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import logger from '/imports/startup/client/logger';
import '/imports/ui/services/mobile-app';
import Base from '/imports/startup/client/base';
import JoinHandler from '/imports/ui/components/join-handler/component';
import JoinHandler from '../imports/ui/components/join-handler/component';
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
import Subscriptions from '/imports/ui/components/subscriptions/component';
import IntlStartup from '/imports/startup/client/intl';
import ContextProviders from '/imports/ui/components/context-providers/component';
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
import GraphqlProvider from '/imports/ui/components/graphql-provider/component';
import ConnectionManager from '/imports/ui/components/connection-manager/component';
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
// The adapter import is "unused" as far as static code is concerned, but it
// needs to here to override global prototypes. So: don't remove it - prlanzarin 25 Apr 2022
import adapter from 'webrtc-adapter';
import collectionMirrorInitializer from './collection-mirror-initializer';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import IntlAdapter from '/imports/startup/client/intlAdapter';
import PresenceAdapter from '/imports/ui/components/authenticated-handler/presence-adapter/component';
import CustomUsersSettings from '/imports/ui/components/join-handler/custom-users-settings/component';
import('/imports/api/audio/client/bridge/bridge-whitelist').catch(() => {
// bridge loading
});
const { disableWebsocketFallback } = Meteor.settings.public.app;
if (disableWebsocketFallback) {
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
Meteor.disconnect();
Meteor.reconnect();
}
collectionMirrorInitializer();
liveDataEventBrokerInitializer();
Meteor.startup(() => {
// eslint-disable-next-line import/prefer-default-export
const Startup = () => {
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
const { disableWebsocketFallback } = window.meetingClientSettings.public.app;
loadingContextInfo.setLoading(false, '');
if (disableWebsocketFallback) {
Meteor.connection._stream._sockjsProtocolsWhitelist = function () { return ['websocket']; };
Meteor.disconnect();
Meteor.reconnect();
}
}, []);
// Logs all uncaught exceptions to the client logger
window.addEventListener('error', (e) => {
let message = e.message || e.error.toString();
@ -76,24 +82,20 @@ Meteor.startup(() => {
}, message);
});
// TODO make this a Promise
render(
return (
<ContextProviders>
<>
<JoinHandler>
<AuthenticatedHandler>
<GraphqlProvider>
<Subscriptions>
<IntlStartup>
<Base />
</IntlStartup>
</Subscriptions>
</GraphqlProvider>
</AuthenticatedHandler>
</JoinHandler>
<PresenceAdapter>
<Subscriptions>
<IntlAdapter>
<Base />
</IntlAdapter>
</Subscriptions>
</PresenceAdapter>
<UsersAdapter />
</>
</ContextProviders>,
document.getElementById('app'),
</ContextProviders>
);
});
};
export default Startup;

View File

@ -2,8 +2,10 @@ import { Meteor } from 'meteor/meteor';
import validateAuthToken from './methods/validateAuthToken';
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
import userActivitySign from './methods/userActivitySign';
import validateConnection from './methods/validateConnection';
Meteor.methods({
validateConnection,
validateAuthToken,
setUserEffectiveConnectionType,
userActivitySign,

View File

@ -0,0 +1,65 @@
import { Client } from 'pg';
import upsertValidationState from '/imports/api/auth-token-validation/server/modifiers/upsertValidationState';
import { ValidationStates } from '/imports/api/auth-token-validation';
import userJoin from '/imports/api/users/server/handlers/userJoin';
import Users from '/imports/api/users';
import createDummyUser from '../modifiers/createDummyUser';
import updateUserConnectionId from '../modifiers/updateUserConnectionId';
async function validateConnection(requesterToken, meetingId, userId) {
try {
const client = new Client({
host: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.host,
port: process.env.POSTGRES_PORT || Meteor.settings.private.postgresql.port,
database: process.env.POSTGRES_HOST || Meteor.settings.private.postgresql.database,
user: process.env.POSTGRES_USER || Meteor.settings.private.postgresql.user,
password: process.env.POSTGRES_PASSWORD || Meteor.settings.private.postgresql.password,
query_timeout: process.env.POSTGRES_TIMEOUT || Meteor.settings.private.postgresql.timeout,
});
await client.connect();
const res = await client.query('select "meetingId", "userId" from v_user_connection_auth where "authToken" = $1', [requesterToken]);
if (res.rows.length === 0) {
await upsertValidationState(
meetingId,
userId,
ValidationStates.INVALID,
this.connection.id,
);
} else {
const sessionId = `${meetingId}--${userId}`;
this.setUserId(sessionId);
await upsertValidationState(
meetingId,
userId,
ValidationStates.VALIDATED,
this.connection.id,
);
const User = await Users.findOneAsync({
meetingId,
userId,
});
if (!User) {
await createDummyUser(meetingId, userId, requesterToken);
} else {
await updateUserConnectionId(meetingId, userId, this.connection.id);
}
userJoin(meetingId, userId, requesterToken);
}
await client.end();
} catch (e) {
await upsertValidationState(
meetingId,
userId,
ValidationStates.INVALID,
this.connection.id,
);
}
}
export default validateConnection;

View File

@ -25,7 +25,7 @@ import { useMutation } from '@apollo/client';
import { SET_EXIT_REASON } from '/imports/ui/core/graphql/mutations/userMutations';
import useUserChangedLocalSettings from '/imports/ui/services/settings/hooks/useUserChangedLocalSettings';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_group_id;
const USER_WAS_EJECTED = 'userWasEjected';
@ -395,7 +395,7 @@ export default withTracker(() => {
currentConnectionId: 1,
connectionIdUpdateTime: 1,
};
const User = Users.findOne({ intId: credentials.requesterUserId }, { fields });
const User = Users.findOne({ userId: credentials.requesterUserId }, { fields });
const meeting = Meetings.findOne({ meetingId }, {
fields: {
meetingEnded: 1,
@ -412,30 +412,21 @@ export default withTracker(() => {
const ejected = User?.ejected;
const ejectedReason = User?.ejectedReason;
const meetingEndedReason = meeting?.meetingEndedReason;
const currentConnectionId = User?.currentConnectionId;
const { connectionID, connectionAuthTime } = Auth;
const connectionIdUpdateTime = User?.connectionIdUpdateTime;
if (ejected) {
// use the connectionID to block users, so we can detect if the user was
// blocked by the current connection. This is the case when a a user is
// ejected from a meeting but not permanently ejected. Permanent ejects are
// managed by the server, not by the client.
BBBStorage.setItem(USER_WAS_EJECTED, connectionID);
}
if (currentConnectionId && currentConnectionId !== connectionID && connectionIdUpdateTime > connectionAuthTime) {
Session.set('codeError', '409');
Session.set('errorMessageDescription', 'joined_another_window_reason')
BBBStorage.setItem(USER_WAS_EJECTED, User.userId);
}
let userSubscriptionHandler;
const codeError = Session.get('codeError');
const { streams: usersVideo } = VideoService.getVideoStreams();
return {
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED) == connectionID),
userWasEjected: (BBBStorage.getItem(USER_WAS_EJECTED)),
approved,
ejected,
ejectedReason,

View File

@ -20,9 +20,9 @@ const propTypes = {
children: PropTypes.element.isRequired,
};
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
const DEFAULT_LANGUAGE = window.meetingClientSettings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = window.meetingClientSettings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = window.meetingClientSettings.public.app.fallbackOnEmptyLocaleString;
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
const LARGE_FONT_LANGUAGES = ['te', 'km'];

View File

@ -0,0 +1,52 @@
import React, { useContext, useEffect } from 'react';
import { useIntl } from 'react-intl';
import Settings from '/imports/ui/services/settings';
import { Session } from 'meteor/session';
import { formatLocaleCode } from '/imports/utils/string-utils';
import Intl from '/imports/ui/services/locale';
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
const RTL_LANGUAGES = ['ar', 'dv', 'fa', 'he'];
const LARGE_FONT_LANGUAGES = ['te', 'km'];
interface IntlAdapterProps {
children: React.ReactNode;
}
const IntlAdapter: React.FC<IntlAdapterProps> = ({
children,
}) => {
const [currentLocale] = useCurrentLocale();
const intl = useIntl();
const loadingContextInfo = useContext(LoadingContext);
const setUp = () => {
if (currentLocale) {
const { language, formattedLocale } = formatLocaleCode(currentLocale);
// @ts-ignore - JS code
Settings.application.locale = currentLocale;
Intl.setLocale(formattedLocale, intl.messages);
if (RTL_LANGUAGES.includes(currentLocale.substring(0, 2))) {
// @ts-ignore - JS code
document.body.parentNode.setAttribute('dir', 'rtl');
// @ts-ignore - JS code
Settings.application.isRTL = true;
} else {
// @ts-ignore - JS code
document.body.parentNode.setAttribute('dir', 'ltr');
// @ts-ignore - JS code
Settings.application.isRTL = false;
}
Session.set('isLargeFont', LARGE_FONT_LANGUAGES.includes(currentLocale.substring(0, 2)));
document.getElementsByTagName('html')[0].lang = formattedLocale;
document.body.classList.add(`lang-${language}`);
Settings.save();
}
};
useEffect(setUp, []);
useEffect(setUp, [currentLocale]);
return !loadingContextInfo.isLoading ? children : null;
};
export default IntlAdapter;

View File

@ -0,0 +1,151 @@
import React, { useCallback, useContext, useEffect } from 'react';
import { IntlProvider } from 'react-intl';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
import useCurrentLocale from '/imports/ui/core/local-states/useCurrentLocale';
import logger from './logger';
interface LocaleEndpointResponse {
defaultLocale: string;
fallbackOnEmptyLocaleString: boolean;
normalizedLocale: string;
regionDefaultLocale: string;
}
interface LocaleJson {
[key: string]: string;
}
interface IntlLoaderContainerProps {
children: React.ReactNode;
}
interface IntlLoaderProps extends IntlLoaderContainerProps {
currentLocale: string;
setCurrentLocale: (locale: string) => void;
}
const buildFetchLocale = (locale: string) => {
const localesPath = 'locales';
return new Promise((resolve) => {
fetch(`${localesPath}/${locale}.json`)
.then((response) => {
if (!response.ok) {
return resolve(false);
}
return response.json()
.then((jsonResponse) => resolve(jsonResponse))
.catch(() => {
logger.error({ logCode: 'intl_parse_locale_SyntaxError' }, `Could not parse locale file ${locale}.json, invalid json`);
resolve(false);
});
});
});
};
const IntlLoader: React.FC<IntlLoaderProps> = ({
children,
currentLocale,
setCurrentLocale,
}) => {
const loadingContextInfo = useContext(LoadingContext);
const [fetching, setFetching] = React.useState(false);
const [normalizedLocale, setNormalizedLocale] = React.useState(navigator.language.replace('_', '-'));
const [messages, setMessages] = React.useState<LocaleJson>({});
const [fallbackOnEmptyLocaleString, setFallbackOnEmptyLocaleString] = React.useState(false);
const fetchLocalizedMessages = useCallback((locale: string, init: boolean) => {
const url = `./locale?locale=${locale}&init=${init}`;
setFetching(true);
// fetch localized messages
fetch(url)
.then((response) => {
if (!response.ok) {
loadingContextInfo.setLoading(false, '');
throw new Error('unable to fetch localized messages');
}
return response.json();
}).then((data: LocaleEndpointResponse) => {
const {
defaultLocale,
regionDefaultLocale,
normalizedLocale,
fallbackOnEmptyLocaleString: FOEL,
} = data;
setFallbackOnEmptyLocaleString(FOEL);
const languageSets = Array.from(new Set([
defaultLocale,
regionDefaultLocale,
normalizedLocale,
])).filter((locale) => locale);
Promise.all(languageSets.map((locale) => buildFetchLocale(locale)))
.then((resp) => {
const typedResp = resp as Array<LocaleJson | boolean>;
const foundLocales = typedResp.filter((locale) => locale instanceof Object) as LocaleJson[];
if (foundLocales.length === 0) {
const error = `${{ logCode: 'intl_fetch_locale_error' }},Could not fetch any locale file for ${languageSets.join(', ')}`;
loadingContextInfo.setLoading(false, '');
logger.error(error);
throw new Error(error);
}
const mergedLocale = foundLocales
.reduce((acc, locale: LocaleJson) => Object.assign(acc, locale), {});
const replacedLocale = normalizedLocale.replace('_', '-');
setFetching(false);
setNormalizedLocale(replacedLocale);
setCurrentLocale(replacedLocale);
setMessages(mergedLocale);
if (!init) {
loadingContextInfo.setLoading(false, '');
}
}).catch((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error(error);
});
});
}, []);
useEffect(() => {
const language = navigator.languages ? navigator.languages[0] : navigator.language;
fetchLocalizedMessages(language, true);
}, []);
useEffect(() => {
if (currentLocale !== normalizedLocale) {
fetchLocalizedMessages(currentLocale, false);
}
}, [currentLocale]);
useEffect(() => {
if (fetching) {
loadingContextInfo.setLoading(true, 'Fetching locale');
}
}, [fetching]);
return !fetching || Object.keys(messages).length > 0 ? (
<IntlProvider
fallbackOnEmptyString={fallbackOnEmptyLocaleString}
locale={normalizedLocale.replace('_', '-').replace('@', '-')}
messages={messages}
>
{children}
</IntlProvider>
) : null;
};
const IntlLoaderContainer: React.FC<IntlLoaderContainerProps> = ({
children,
}) => {
const [currentLocale, setCurrentLocale] = useCurrentLocale();
return (
<IntlLoader
currentLocale={currentLocale}
setCurrentLocale={setCurrentLocale}
>
{children}
</IntlLoader>
);
};
export default IntlLoaderContainer;

View File

@ -1,4 +1,3 @@
import Auth from '/imports/ui/services/auth';
import { Meteor } from 'meteor/meteor';
import { createLogger, stdSerializers } from 'browser-bunyan';
import { ConsoleFormattedStream } from '@browser-bunyan/console-formatted-stream';
@ -15,8 +14,43 @@ import { nameFromLevel } from '@browser-bunyan/levels';
// "url": "","method": ""}
// externalURL is the end-point that logs will be sent to
// Call the logger by doing a function call with the level name, I.e, logger.warn('Hi on warn')
const fallback = { console: { enabled: true, level: 'info' } };
const LOG_CONFIG = (JSON.parse(sessionStorage.getItem('clientStartupSettings') || '{}')?.clientLog) || fallback;
const LOG_CONFIG = Meteor.settings.public.clientLog || { console: { enabled: true, level: 'info' } };
export function createStreamForTarget(target, options) {
const TARGET_EXTERNAL = 'external';
const TARGET_CONSOLE = 'console';
const TARGET_SERVER = 'server';
let Stream = ConsoleRawStream;
switch (target) {
case TARGET_EXTERNAL:
Stream = ServerLoggerStream;
break;
case TARGET_CONSOLE:
Stream = ConsoleFormattedStream;
break;
case TARGET_SERVER:
Stream = MeteorStream;
break;
default:
Stream = ConsoleFormattedStream;
}
return new Stream(options);
}
export function generateLoggerStreams(config) {
let result = [];
Object.keys(config).forEach((key) => {
const logOption = config[key];
if (logOption && logOption.enabled) {
const { level, ...streamOptions } = logOption;
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
}
});
return result;
}
// Custom stream that logs to an end-point
class ServerLoggerStream extends ServerStream {
@ -26,16 +60,56 @@ class ServerLoggerStream extends ServerStream {
if (params.logTag) {
this.logTagString = params.logTag;
}
this.auth = null;
}
setConfig(config) {
const streams = generateLoggerStreams(config);
const { addStream } = this;
streams.forEach((stream) => {
addStream(stream);
});
}
setAuth(auth) {
this.auth = auth;
}
getUserData() {
let userInfo = {};
if (this.auth) {
userInfo = this.auth.fullInfo;
} else {
userInfo = {
meetingId: sessionStorage.getItem('meetingId'),
userId: sessionStorage.getItem('userId'),
logoutUrl: sessionStorage.getItem('logoutUrl'),
sessionToken: sessionStorage.getItem('sessionToken'),
userName: sessionStorage.getItem('userName'),
extId: sessionStorage.getItem('extId'),
meetingName: sessionStorage.getItem('meetingName'),
};
}
if (userInfo.meetingId) {
userInfo = {
sessionToken: sessionStorage.getItem('sessionToken'),
};
}
return {
fullInfo: userInfo,
};
}
write(rec) {
const { fullInfo } = Auth;
const { fullInfo } = this.getUserData();
this.rec = rec;
if (fullInfo.meetingId != null) {
this.rec.userInfo = fullInfo;
}
this.rec.clientBuild = Meteor.settings.public.app.html5ClientBuild;
this.rec.clientBuild = window.meetingClientSettings.public.app.html5ClientBuild;
this.rec.connectionId = Meteor.connection._lastSessionId;
if (this.logTagString) {
this.rec.logTag = this.logTagString;
@ -47,7 +121,7 @@ class ServerLoggerStream extends ServerStream {
// Custom stream to log to the meteor server
class MeteorStream {
write(rec) {
const { fullInfo } = Auth;
const { fullInfo } = this.getUserData();
const clientURL = window.location.href;
this.rec = rec;
@ -78,42 +152,6 @@ class MeteorStream {
}
}
function createStreamForTarget(target, options) {
const TARGET_EXTERNAL = 'external';
const TARGET_CONSOLE = 'console';
const TARGET_SERVER = 'server';
let Stream = ConsoleRawStream;
switch (target) {
case TARGET_EXTERNAL:
Stream = ServerLoggerStream;
break;
case TARGET_CONSOLE:
Stream = ConsoleFormattedStream;
break;
case TARGET_SERVER:
Stream = MeteorStream;
break;
default:
Stream = ConsoleFormattedStream;
}
return new Stream(options);
}
function generateLoggerStreams(config) {
let result = [];
Object.keys(config).forEach((key) => {
const logOption = config[key];
if (logOption && logOption.enabled) {
const { level, ...streamOptions } = logOption;
result = result.concat({ level, stream: createStreamForTarget(key, streamOptions) });
}
});
return result;
}
// Creates the logger with the array of streams of the chosen targets
const logger = createLogger({
name: 'clientLogger',
@ -122,5 +160,4 @@ const logger = createLogger({
src: true,
});
export default logger;

View File

@ -13,6 +13,10 @@ import { PrometheusAgent, METRIC_NAMES } from './prom-metrics/index.js'
let guestWaitHtml = '';
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const FALLBACK_ON_EMPTY_STRING = Meteor.settings.public.app.fallbackOnEmptyLocaleString;
const env = Meteor.isDevelopment ? 'development' : 'production';
const meteorRoot = fs.realpathSync(`${process.cwd()}/../`);
@ -273,6 +277,8 @@ WebApp.connectHandlers.use('/locale', (req, res) => {
res.end(JSON.stringify({
normalizedLocale: localeFile,
regionDefaultLocale: (regionDefault && regionDefault !== localeFile) ? regionDefault : '',
defaultLocale: DEFAULT_LANGUAGE,
fallbackOnEmptyLocaleString: FALLBACK_ON_EMPTY_STRING,
}));
});

View File

@ -0,0 +1,846 @@
export interface MeetingClientSettings {
public: Public
private: Private
}
export interface Public {
app: App
externalVideoPlayer: ExternalVideoPlayer
kurento: Kurento
syncUsersWithConnectionManager: SyncUsersWithConnectionManager
poll: Poll
captions: Captions
timer: Timer
chat: Chat
userReaction: UserReaction
userStatus: UserStatus
notes: Notes
layout: Layout
pads: Pads
media: Media
stats: Stats
presentation: Presentation
selectRandomUser: SelectRandomUser
user: User
whiteboard: Whiteboard
clientLog: ClientLog
virtualBackgrounds: VirtualBackgrounds
}
export interface App {
mobileFontSize: string
desktopFontSize: string
audioChatNotification: boolean
autoJoin: boolean
listenOnlyMode: boolean
forceListenOnly: boolean
skipCheck: boolean
skipCheckOnJoin: boolean
enableDynamicAudioDeviceSelection: boolean
clientTitle: string
appName: string
bbbServerVersion: string
displayBbbServerVersion: boolean
copyright: string
html5ClientBuild: string
helpLink: string
delayForUnmountOfSharedNote: number
bbbTabletApp: BbbTabletApp
lockOnJoin: boolean
cdn: string
basename: string
bbbWebBase: string
learningDashboardBase: string
customStyleUrl: string | null
darkTheme: DarkTheme
askForFeedbackOnLogout: boolean
askForConfirmationOnLeave: boolean
wakeLock: WakeLock
allowDefaultLogoutUrl: boolean
allowUserLookup: boolean
dynamicGuestPolicy: boolean
enableGuestLobbyMessage: boolean
guestPolicyExtraAllowOptions: boolean
alwaysShowWaitingRoomUI: boolean
enableLimitOfViewersInWebcam: boolean
enableMultipleCameras: boolean
enableCameraAsContent: boolean
enableWebcamSelectorButton: boolean
enableTalkingIndicator: boolean
enableCameraBrightness: boolean
mirrorOwnWebcam: boolean
viewersInWebcam: number
ipv4FallbackDomain: string
allowLogout: boolean
allowFullscreen: boolean
preloadNextSlides: number
warnAboutUnsavedContentOnMeetingEnd: boolean
audioCaptions: AudioCaptions
mutedAlert: MutedAlert
remainingTimeThreshold: number
remainingTimeAlertThresholdArray: number[]
enableDebugWindow: boolean
breakouts: Breakouts
customHeartbeat: boolean
showAllAvailableLocales: boolean
showAudioFilters: boolean
raiseHandActionButton: RaiseHandActionButton
reactionsButton: ReactionsButton
emojiRain: EmojiRain
enableNetworkStats: boolean
enableCopyNetworkStatsButton: boolean
userSettingsStorage: string
defaultSettings: DefaultSettings
shortcuts: Shortcuts
branding: Branding
connectionTimeout: number
showHelpButton: boolean
effectiveConnection: string[]
fallbackOnEmptyLocaleString: boolean
disableWebsocketFallback: boolean
}
export interface BbbTabletApp {
enabled: boolean
iosAppStoreUrl: string
iosAppUrlScheme: string
}
export interface DarkTheme {
enabled: boolean
}
export interface WakeLock {
enabled: boolean
}
export interface AudioCaptions {
enabled: boolean
mobile: boolean
provider: string
language: Language
}
export interface Language {
available: string[]
forceLocale: boolean
defaultSelectLocale: boolean
locale: string
}
export interface MutedAlert {
enabled: boolean
interval: number
threshold: number
duration: number
}
export interface Breakouts {
allowUserChooseRoomByDefault: boolean
captureWhiteboardByDefault: boolean
captureSharedNotesByDefault: boolean
sendInvitationToAssignedModeratorsByDefault: boolean
breakoutRoomLimit: number
}
export interface RaiseHandActionButton {
enabled: boolean
centered: boolean
}
export interface ReactionsButton {
enabled: boolean
}
export interface EmojiRain {
enabled: boolean
intervalEmojis: number
numberOfEmojis: number
emojiSize: number
}
export interface DefaultSettings {
application: Application
audio: Audio
dataSaving: DataSaving
}
export interface Application {
selectedLayout: string
animations: boolean
chatAudioAlerts: boolean
chatPushAlerts: boolean
userJoinAudioAlerts: boolean
userJoinPushAlerts: boolean
userLeaveAudioAlerts: boolean
userLeavePushAlerts: boolean
raiseHandAudioAlerts: boolean
raiseHandPushAlerts: boolean
guestWaitingAudioAlerts: boolean
guestWaitingPushAlerts: boolean
wakeLock: boolean
paginationEnabled: boolean
whiteboardToolbarAutoHide: boolean
autoCloseReactionsBar: boolean
darkTheme: boolean
fallbackLocale: string
overrideLocale: string | null
}
export interface Audio {
inputDeviceId: string
outputDeviceId: string
}
export interface DataSaving {
viewParticipantsWebcams: boolean
viewScreenshare: boolean
}
export interface Shortcuts {
openOptions: OpenOptions
toggleUserList: ToggleUserList
toggleMute: ToggleMute
joinAudio: JoinAudio
leaveAudio: LeaveAudio
togglePublicChat: TogglePublicChat
hidePrivateChat: HidePrivateChat
closePrivateChat: ClosePrivateChat
raiseHand: RaiseHand
openActions: OpenActions
openDebugWindow: OpenDebugWindow
}
export interface OpenOptions {
accesskey: string
descId: string
}
export interface ToggleUserList {
accesskey: string
descId: string
}
export interface ToggleMute {
accesskey: string
descId: string
}
export interface JoinAudio {
accesskey: string
descId: string
}
export interface LeaveAudio {
accesskey: string
descId: string
}
export interface TogglePublicChat {
accesskey: string
descId: string
}
export interface HidePrivateChat {
accesskey: string
descId: string
}
export interface ClosePrivateChat {
accesskey: string
descId: string
}
export interface RaiseHand {
accesskey: string
descId: string
}
export interface OpenActions {
accesskey: string
descId: string
}
export interface OpenDebugWindow {
accesskey: string
descId: string
}
export interface Branding {
displayBrandingArea: boolean
}
export interface ExternalVideoPlayer {
enabled: boolean
}
export interface Kurento {
wsUrl: string
cameraWsOptions: CameraWsOptions
gUMTimeout: number
signalCandidates: boolean
traceLogs: boolean
cameraTimeouts: CameraTimeouts
screenshare: Screenshare
cameraProfiles: CameraProfile[]
enableScreensharing: boolean
enableVideo: boolean
enableVideoMenu: boolean
enableVideoPin: boolean
autoShareWebcam: boolean
skipVideoPreview: boolean
skipVideoPreviewOnFirstJoin: boolean
cameraSortingModes: CameraSortingModes
cameraQualityThresholds: CameraQualityThresholds
pagination: Pagination
paginationThresholds: PaginationThresholds
}
export interface CameraWsOptions {
wsConnectionTimeout: number
maxRetries: number
debug: boolean
heartbeat: Heartbeat
}
export interface Heartbeat {
interval: number
delay: number
reconnectOnFailure: boolean
}
export interface CameraTimeouts {
baseTimeout: number
maxTimeout: number
}
export interface Screenshare {
enableVolumeControl: boolean
subscriberOffering: boolean
bitrate: number
mediaTimeouts: MediaTimeouts
constraints: Constraints
}
export interface MediaTimeouts {
maxConnectionAttempts: number
baseTimeout: number
baseReconnectionTimeout: number
maxTimeout: number
timeoutIncreaseFactor: number
}
export interface Constraints {
video: Video
audio: boolean
}
export interface Video {
frameRate: FrameRate
width: Width
height: Height
}
export interface FrameRate {
ideal: number
max: number
}
export interface Width {
max: number
}
export interface Height {
max: number
}
export interface CameraProfile {
id: string
name: string
bitrate: number
hidden?: boolean
default?: boolean
constraints?: Constraints2
}
export interface Constraints2 {
width: number
height: number
frameRate?: number
}
export interface CameraSortingModes {
defaultSorting: string
paginationSorting: string
}
export interface CameraQualityThresholds {
enabled: boolean
applyConstraints: boolean
privilegedStreams: boolean
debounceTime: number
thresholds: Threshold[]
}
export interface Threshold {
threshold: number
profile: string
}
export interface Pagination {
paginationToggleEnabled: boolean
pageChangeDebounceTime: number
desktopPageSizes: DesktopPageSizes
mobilePageSizes: MobilePageSizes
desktopGridSizes: DesktopGridSizes
mobileGridSizes: MobileGridSizes
}
export interface DesktopPageSizes {
moderator: number
viewer: number
}
export interface MobilePageSizes {
moderator: number
viewer: number
}
export interface DesktopGridSizes {
moderator: number
viewer: number
}
export interface MobileGridSizes {
moderator: number
viewer: number
}
export interface PaginationThresholds {
enabled: boolean
thresholds: Threshold2[]
}
export interface Threshold2 {
users: number
desktopPageSizes: DesktopPageSizes2
}
export interface DesktopPageSizes2 {
moderator: number
viewer: number
}
export interface SyncUsersWithConnectionManager {
enabled: boolean
syncInterval: number
}
export interface Poll {
enabled: boolean
allowCustomResponseInput: boolean
maxCustom: number
maxTypedAnswerLength: number
chatMessage: boolean
}
export interface Captions {
enabled: boolean
id: string
dictation: boolean
background: string
font: Font
lines: number
time: number
}
export interface Font {
color: string
family: string
size: string
}
export interface Timer {
enabled: boolean
alarm: boolean
music: Music
interval: Interval
time: number
tabIndicator: boolean
}
export interface Music {
enabled: boolean
volume: number
track1: string
track2: string
track3: string
}
export interface Interval {
clock: number
offset: number
}
export interface Chat {
enabled: boolean
itemsPerPage: number
timeBetweenFetchs: number
enableSaveAndCopyPublicChat: boolean
bufferChatInsertsMs: number
startClosed: boolean
min_message_length: number
max_message_length: number
grouping_messages_window: number
type_system: string
type_public: string
type_private: string
system_userid: string
system_username: string
public_id: string
public_group_id: string
public_userid: string
public_username: string
storage_key: string
system_messages_keys: SystemMessagesKeys
typingIndicator: TypingIndicator
moderatorChatEmphasized: boolean
autoConvertEmoji: boolean
emojiPicker: EmojiPicker
disableEmojis: string[]
allowedElements: string[]
}
export interface SystemMessagesKeys {
chat_clear: string
chat_poll_result: string
chat_exported_presentation: string
chat_status_message: string
}
export interface TypingIndicator {
enabled: boolean
showNames: boolean
}
export interface EmojiPicker {
enable: boolean
}
export interface UserReaction {
enabled: boolean
expire: number
reactions: Reaction[]
}
export interface Reaction {
id: string
native: string
}
export interface UserStatus {
enabled: boolean
}
export interface Notes {
enabled: boolean
id: string
pinnable: boolean
}
export interface Layout {
hidePresentationOnJoin: boolean
showParticipantsOnLogin: boolean
showPushLayoutButton: boolean
showPushLayoutToggle: boolean
}
export interface Pads {
url: string
cookie: Cookie
}
export interface Cookie {
path: string
sameSite: string
secure: boolean
}
export interface Media {
audio: Audio2
stunTurnServersFetchAddress: string
cacheStunTurnServers: boolean
fallbackStunServer: string
forceRelay: boolean
forceRelayOnFirefox: boolean
mediaTag: string
callTransferTimeout: number
callHangupTimeout: number
callHangupMaximumRetries: number
echoTestNumber: string
listenOnlyCallTimeout: number
transparentListenOnly: boolean
fullAudioOffering: boolean
listenOnlyOffering: boolean
iceGatheringTimeout: number
audioConnectionTimeout: number
audioReconnectionDelay: number
audioReconnectionAttempts: number
sipjsHackViaWs: boolean
sipjsAllowMdns: boolean
sip_ws_host: string
toggleMuteThrottleTime: number
websocketKeepAliveInterval: number
websocketKeepAliveDebounce: number
traceSip: boolean
sdpSemantics: string
localEchoTest: LocalEchoTest
showVolumeMeter: boolean
}
export interface Audio2 {
defaultFullAudioBridge: string
defaultListenOnlyBridge: string
bridges: Bridge[]
retryThroughRelay: boolean
}
export interface Bridge {
name: string
path: string
}
export interface LocalEchoTest {
enabled: boolean
initialHearingState: boolean
useRtcLoopbackInChromium: boolean
delay: Delay
}
export interface Delay {
enabled: boolean
delayTime: number
maxDelayTime: number
}
export interface Stats {
enabled: boolean
interval: number
timeout: number
log: boolean
notification: Notification
jitter: number[]
loss: number[]
rtt: number[]
level: string[]
help: string
}
export interface Notification {
warning: boolean
error: boolean
}
export interface Presentation {
allowDownloadOriginal: boolean
allowDownloadWithAnnotations: boolean
allowSnapshotOfCurrentSlide: boolean
panZoomThrottle: number
restoreOnUpdate: boolean
uploadEndpoint: string
fileUploadConstraintsHint: boolean
mirroredFromBBBCore: MirroredFromBbbcore
uploadValidMimeTypes: UploadValidMimeType[]
}
export interface MirroredFromBbbcore {
uploadSizeMax: number
uploadPagesMax: number
}
export interface UploadValidMimeType {
extension: string
mime: string
}
export interface SelectRandomUser {
enabled: boolean
countdown: boolean
}
export interface User {
role_moderator: string
role_viewer: string
label: Label
}
export interface Label {
moderator: boolean
mobile: boolean
guest: boolean
sharingWebcam: boolean
}
export interface Whiteboard {
annotationsQueueProcessInterval: number
cursorInterval: number
pointerDiameter: number
maxStickyNoteLength: number
maxNumberOfAnnotations: number
annotations: Annotations
styles: Styles
toolbar: Toolbar
}
export interface Annotations {
status: Status
}
export interface Status {
start: string
update: string
end: string
}
export interface Styles {
text: Text
}
export interface Text {
family: string
}
export interface Toolbar {
multiUserPenOnly: boolean
colors: Color[]
thickness: Thickness[]
font_sizes: FontSize[]
tools: Tool[]
presenterTools: string[]
multiUserTools: string[]
}
export interface Color {
label: string
value: string
}
export interface Thickness {
value: number
}
export interface FontSize {
value: number
}
export interface Tool {
icon: string
value: string
}
export interface ClientLog {
server: Server
console: Console
external: External
}
export interface Server {
enabled: boolean
level: string
}
export interface Console {
enabled: boolean
level: string
}
export interface External {
enabled: boolean
level: string
url: string
method: string
throttleInterval: number
flushOnClose: boolean
logTag: string
}
export interface VirtualBackgrounds {
enabled: boolean
enableVirtualBackgroundUpload: boolean
storedOnBBB: boolean
showThumbnails: boolean
imagesPath: string
thumbnailsPath: string
fileNames: string[]
}
export interface Private {
analytics: Analytics
app: App2
redis: Redis
serverLog: ServerLog
minBrowserVersions: MinBrowserVersion[]
prometheus: Prometheus
}
export interface Analytics {
includeChat: boolean
}
export interface App2 {
host: string
localesUrl: string
pencilChunkLength: number
loadSlidesFromHttpAlways: boolean
}
export interface Redis {
host: string
port: string
timeout: number
password: string | null
debug: boolean
metrics: Metrics
channels: Channels
subscribeTo: string[]
async: string[]
ignored: string[]
}
export interface Metrics {
queueMetrics: boolean
metricsDumpIntervalMs: number
metricsFolderPath: string
removeMeetingOnEnd: boolean
}
export interface Channels {
toAkkaApps: string
toThirdParty: string
}
export interface ServerLog {
level: string
streamerLog: boolean
includeServerInfo: boolean
healthChecker: HealthChecker
}
export interface HealthChecker {
enable: boolean
intervalMs: number
}
export interface MinBrowserVersion {
browser: string
version: number | number[] | string
}
export interface Prometheus {
enabled: boolean
path: string
collectDefaultMetrics: boolean
collectRedisMetrics: boolean
}
export default MeetingClientSettings;

View File

@ -14,7 +14,7 @@ const AboutContainer = (props) => {
const getClientBuildInfo = () => (
{
settings: Meteor.settings.public.app,
settings: window.meetingClientSettings.public.app,
}
);

View File

@ -77,13 +77,13 @@ const ActionsBarContainer = (props) => {
);
};
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
const RAISE_HAND_BUTTON_ENABLED = Meteor.settings.public.app.raiseHandActionButton.enabled;
const RAISE_HAND_BUTTON_CENTERED = Meteor.settings.public.app.raiseHandActionButton.centered;
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
const RAISE_HAND_BUTTON_ENABLED = window.meetingClientSettings.public.app.raiseHandActionButton.enabled;
const RAISE_HAND_BUTTON_CENTERED = window.meetingClientSettings.public.app.raiseHandActionButton.centered;
const isReactionsButtonEnabled = () => {
const USER_REACTIONS_ENABLED = Meteor.settings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = Meteor.settings.public.app.reactionsButton.enabled;
const USER_REACTIONS_ENABLED = window.meetingClientSettings.public.userReaction.enabled;
const REACTIONS_BUTTON_ENABLED = window.meetingClientSettings.public.app.reactionsButton.enabled;
return USER_REACTIONS_ENABLED && REACTIONS_BUTTON_ENABLED;
};

View File

@ -7,7 +7,7 @@ import { PANELS, ACTIONS } from '../../layout/enums';
import { uniqueId, safeMatch } from '/imports/utils/string-utils';
import PollService from '/imports/ui/components/poll/service';
const POLL_SETTINGS = Meteor.settings.public.poll;
const POLL_SETTINGS = window.meetingClientSettings.public.poll;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const MAX_CHAR_LIMIT = POLL_SETTINGS.maxTypedAnswerLength;
const CANCELED_POLL_DELAY = 250;

View File

@ -11,7 +11,7 @@ import { useMutation } from '@apollo/client';
import Styled from './styles';
const REACTIONS = Meteor.settings.public.userReaction.reactions;
const REACTIONS = window.meetingClientSettings.public.userReaction.reactions;
const ReactionsButton = (props) => {
const {

View File

@ -81,7 +81,7 @@ class ActivityCheck extends Component {
}
playAudioAlert() {
this.alert = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/notify.mp3`);
this.alert = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/notify.mp3`);
this.alert.addEventListener('ended', () => { this.alert.src = null; });
this.alert.play();
}

View File

@ -57,11 +57,11 @@ import FloatingWindowContainer from '/imports/ui/components/floating-window/cont
import ChatAlertContainerGraphql from '../chat/chat-graphql/alert/component';
const MOBILE_MEDIA = 'only screen and (max-width: 40em)';
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
const DESKTOP_FONT_SIZE = APP_CONFIG.desktopFontSize;
const MOBILE_FONT_SIZE = APP_CONFIG.mobileFontSize;
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const CONFIRMATION_ON_LEAVE = Meteor.settings.public.app.askForConfirmationOnLeave;
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
const CONFIRMATION_ON_LEAVE = window.meetingClientSettings.public.app.askForConfirmationOnLeave;
const intlMessages = defineMessages({
userListLabel: {

View File

@ -37,7 +37,7 @@ import App from './component';
import useToggleVoice from '../audio/audio-graphql/hooks/useToggleVoice';
import useUserChangedLocalSettings from '../../services/settings/hooks/useUserChangedLocalSettings';
const CUSTOM_STYLE_URL = Meteor.settings.public.app.customStyleUrl;
const CUSTOM_STYLE_URL = window.meetingClientSettings.public.app.customStyleUrl;
const endMeeting = (code, ejectedReason) => {
Session.set('codeError', code);
@ -339,7 +339,7 @@ export default withTracker(() => {
customStyleUrl = CUSTOM_STYLE_URL;
}
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
return {
captions: CaptionsService.isCaptionsActive() ? <CaptionsContainer /> : null,
@ -376,7 +376,7 @@ export default withTracker(() => {
isLargeFont: Session.get('isLargeFont'),
presentationRestoreOnUpdate: getFromUserSettings(
'bbb_force_restore_presentation_on_new_events',
Meteor.settings.public.presentation.restoreOnUpdate,
window.meetingClientSettings.public.presentation.restoreOnUpdate,
),
hidePresentationOnJoin: getFromUserSettings('bbb_hide_presentation_on_join', LAYOUT_CONFIG.hidePresentationOnJoin),
hideActionsBar: getFromUserSettings('bbb_hide_actions_bar', false),

View File

@ -21,9 +21,9 @@ import ListenOnly from './buttons/listenOnly';
import LiveSelection from './buttons/LiveSelection';
// @ts-ignore - temporary, while meteor exists in the project
const { enableDynamicAudioDeviceSelection } = Meteor.settings.public.app;
const { enableDynamicAudioDeviceSelection } = window.meetingClientSettings.public.app;
// @ts-ignore - temporary, while meteor exists in the project
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
const MUTE_ALERT_CONFIG = window.meetingClientSettings.public.app.mutedAlert;
// @ts-ignore - temporary while settings are still in .js
// eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -7,9 +7,9 @@ import AudioManager from '/imports/ui/services/audio-manager';
const MUTED_KEY = 'muted';
// @ts-ignore - temporary, while meteor exists in the project
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
// @ts-ignore - temporary, while meteor exists in the project
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
const DEVICE_LABEL_MAX_LENGTH = 40;
const CLIENT_DID_USER_SELECTED_MICROPHONE_KEY = 'clientUserSelectedMicrophone';
const CLIENT_DID_USER_SELECTED_LISTEN_ONLY_KEY = 'clientUserSelectedListenOnly';

View File

@ -19,7 +19,7 @@ import Service from '../service';
const AudioModalContainer = (props) => <AudioModal {...props} />;
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
const invalidDialNumbers = ['0', '613-555-1212', '613-555-1234', '0000'];
const isRTL = document.documentElement.getAttribute('dir') === 'rtl';

View File

@ -8,7 +8,7 @@ const AudioTestContainer = (props) => <AudioTest {...props} />;
export default withTracker(() => ({
outputDeviceId: Service.outputDeviceId(),
handlePlayAudioSample: (deviceId) => {
const sound = new Audio(`${Meteor.settings.public.app.cdn + Meteor.settings.public.app.basename + Meteor.settings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
const sound = new Audio(`${window.meetingClientSettings.public.app.cdn + window.meetingClientSettings.public.app.basename + window.meetingClientSettings.public.app.instanceId}/resources/sounds/audioSample.mp3`);
sound.addEventListener('ended', () => { sound.src = null; });
if (deviceId && sound.setSinkId) sound.setSinkId(deviceId);
sound.play();

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import deviceInfo from '/imports/utils/deviceInfo';
import { isLiveTranscriptionEnabled } from '/imports/ui/services/features';
import { unique } from 'radash';
const CONFIG = Meteor.settings.public.app.audioCaptions;
const CONFIG = window.meetingClientSettings.public.app.audioCaptions;
const ENABLED = CONFIG.enabled;
const PROVIDER = CONFIG.provider;
const LANGUAGES = CONFIG.language.available;

View File

@ -25,8 +25,8 @@ import Settings from '/imports/ui/services/settings';
import useToggleVoice from './audio-graphql/hooks/useToggleVoice';
import { usePreviousValue } from '/imports/ui/components/utils/hooks';
const APP_CONFIG = Meteor.settings.public.app;
const KURENTO_CONFIG = Meteor.settings.public.kurento;
const APP_CONFIG = window.meetingClientSettings.public.app;
const KURENTO_CONFIG = window.meetingClientSettings.public.kurento;
const intlMessages = defineMessages({
joinedAudio: {

View File

@ -1,13 +1,13 @@
import LocalPCLoopback from '/imports/ui/services/webrtc-base/local-pc-loopback';
import browserInfo from '/imports/utils/browserInfo';
const MEDIA_TAG = Meteor.settings.public.media.mediaTag;
const USE_RTC_LOOPBACK_CHR = Meteor.settings.public.media.localEchoTest.useRtcLoopbackInChromium;
const MEDIA_TAG = window.meetingClientSettings.public.media.mediaTag;
const USE_RTC_LOOPBACK_CHR = window.meetingClientSettings.public.media.localEchoTest.useRtcLoopbackInChromium;
const {
enabled: DELAY_ENABLED = true,
delayTime = 0.5,
maxDelayTime = 2,
} = Meteor.settings.public.media.localEchoTest.delay;
} = window.meetingClientSettings.public.media.localEchoTest.delay;
let audioContext = null;
let sourceContext = null;

View File

@ -8,13 +8,13 @@ import VoiceUsers from '/imports/api/voice-users';
import logger from '/imports/startup/client/logger';
import Storage from '../../services/storage/session';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const TOGGLE_MUTE_THROTTLE_TIME = Meteor.settings.public.media.toggleMuteThrottleTime;
const SHOW_VOLUME_METER = Meteor.settings.public.media.showVolumeMeter;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const TOGGLE_MUTE_THROTTLE_TIME = window.meetingClientSettings.public.media.toggleMuteThrottleTime;
const SHOW_VOLUME_METER = window.meetingClientSettings.public.media.showVolumeMeter;
const {
enabled: LOCAL_ECHO_TEST_ENABLED,
initialHearingState: LOCAL_ECHO_INIT_HEARING_STATE,
} = Meteor.settings.public.media.localEchoTest;
} = window.meetingClientSettings.public.media.localEchoTest;
const MUTED_KEY = 'muted';

View File

@ -38,12 +38,10 @@ class AuthenticatedHandler extends Component {
if (Auth.loggedIn) {
callback();
}
AuthenticatedHandler.addReconnectObservable();
const setReason = (reason) => {
const log = reason.error === 403 ? 'warn' : 'error';
logger[log]({
logCode: 'authenticatedhandlercomponent_setreason',
extraInfo: { reason },
@ -70,6 +68,7 @@ class AuthenticatedHandler extends Component {
componentDidMount() {
if (Session.get('codeError')) {
console.log('Session.get(codeError)', Session.get('codeError'));
this.setState({ authenticated: true });
}
AuthenticatedHandler.authenticatedRouteHandler((value, error) => {

View File

@ -0,0 +1,44 @@
import React, { useEffect } from 'react';
import useAuthData from '/imports/ui/core/local-states/useAuthData';
import Auth from '/imports/ui/services/auth';
import { Session } from 'meteor/session';
interface PresenceAdapterProps {
children: React.ReactNode;
}
const PresenceAdapter: React.FC<PresenceAdapterProps> = ({ children }) => {
const [authData] = useAuthData();
const [authSetted, setAuthSetted] = React.useState(false);
useEffect(() => {
const {
authToken,
logoutUrl,
meetingId,
sessionToken,
userId,
userName,
extId,
meetingName,
} = authData;
Auth.clearCredentials();
Auth.set(
meetingId,
userId,
authToken,
logoutUrl,
sessionToken,
userName,
extId,
meetingName,
);
Auth.loggedIn = true;
Auth.connectionAuthTime = new Date().getTime();
Session.set('userWillAuth', false);
setAuthSetted(true);
}, []);
return authSetted ? children : null;
};
export default PresenceAdapter;

View File

@ -2,7 +2,6 @@ import React, { useMemo } from 'react';
import ModalFullscreen from '/imports/ui/components/common/modal/fullscreen/component';
import { defineMessages, useIntl } from 'react-intl';
import { range } from 'ramda';
import { Meteor } from 'meteor/meteor';
import { uniqueId } from '/imports/utils/string-utils';
import { isImportPresentationWithAnnotationsFromBreakoutRoomsEnabled, isImportSharedNotesFromBreakoutRoomsEnabled } from '/imports/ui/services/features';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
@ -27,7 +26,7 @@ import {
} from './room-managment-state/types';
import { BREAKOUT_ROOM_CREATE, BREAKOUT_ROOM_MOVE_USER } from '../../mutations';
const BREAKOUT_LIM = Meteor.settings.public.app.breakouts.breakoutRoomLimit;
const BREAKOUT_LIM = window.meetingClientSettings.public.app.breakouts.breakoutRoomLimit;
const MIN_BREAKOUT_ROOMS = 2;
const MAX_BREAKOUT_ROOMS = BREAKOUT_LIM > MIN_BREAKOUT_ROOMS ? BREAKOUT_LIM : MIN_BREAKOUT_ROOMS;
const MIN_BREAKOUT_TIME = 5;

View File

@ -4,7 +4,7 @@ import MessageForm from './component';
import ChatService from '/imports/ui/components/chat/service';
import { BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL } from '../mutations';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const MessageFormContainer = (props) => {
const [sendMessageToAllBreakouts] = useMutation(BREAKOUT_ROOM_SEND_MESSAGE_TO_ALL);

View File

@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import Service from '/imports/ui/components/captions/service';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
class LiveCaptions extends PureComponent {
constructor(props) {

View File

@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { isCaptionsEnabled } from '/imports/ui/services/features';
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const CAPTIONS_CONFIG = window.meetingClientSettings.public.captions;
const LINE_BREAK = '\n';
const getAvailableLocales = () => {

View File

@ -17,10 +17,11 @@ import { generateExportedMessages } from './services';
import { getDateString } from '/imports/utils/string-utils';
import { ChatCommands } from '/imports/ui/core/enums/chat';
import { CHAT_PUBLIC_CLEAR_HISTORY } from './mutations';
import useMeetingSettings from '/imports/ui/core/local-states/useMeetingSettings';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
// const CHAT_CONFIG = window.meetingClientSettings.public.chat;
// const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
const intlMessages = defineMessages({
clear: {
@ -54,6 +55,9 @@ const intlMessages = defineMessages({
});
const ChatActions: React.FC = () => {
const [MeetingSettings] = useMeetingSettings();
const chatConfig = MeetingSettings.public.chat;
const { enableSaveAndCopyPublicChat } = chatConfig;
const intl = useIntl();
const isRTL = layoutSelect((i: Layout) => i.isRTL);
const uniqueIdsRef = useRef<string[]>([uid(1), uid(2), uid(3), uid(4)]);
@ -116,7 +120,7 @@ const ChatActions: React.FC = () => {
const dropdownActions = [
{
key: uniqueIdsRef.current[0],
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
enable: enableSaveAndCopyPublicChat,
icon: 'download',
dataTest: 'chatSave',
label: intl.formatMessage(intlMessages.save),
@ -127,7 +131,7 @@ const ChatActions: React.FC = () => {
},
{
key: uniqueIdsRef.current[1],
enable: ENABLE_SAVE_AND_COPY_PUBLIC_CHAT,
enable: enableSaveAndCopyPublicChat,
icon: 'copy',
id: 'clipboardButton',
dataTest: 'chatCopy',

View File

@ -38,7 +38,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { throttle } from '/imports/utils/throttle';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
@ -114,9 +114,9 @@ const messages = defineMessages({
});
// @ts-ignore - temporary, while meteor exists in the project
const AUTO_CONVERT_EMOJI = Meteor.settings.public.chat.autoConvertEmoji;
const AUTO_CONVERT_EMOJI = window.meetingClientSettings.public.chat.autoConvertEmoji;
// @ts-ignore - temporary, while meteor exists in the project
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.emojiPicker.enable;
const ENABLE_EMOJI_PICKER = window.meetingClientSettings.public.chat.emojiPicker.enable;
const ENABLE_TYPING_INDICATOR = CHAT_CONFIG.typingIndicator.enabled;
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({

View File

@ -5,7 +5,6 @@ import React, {
useState,
useMemo,
} from 'react';
import { Meteor } from 'meteor/meteor';
import { makeVar, useMutation } from '@apollo/client';
import { defineMessages, useIntl } from 'react-intl';
import LAST_SEEN_MUTATION from './queries';
@ -27,7 +26,7 @@ import { Layout } from '../../../layout/layoutTypes';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;

View File

@ -4,7 +4,7 @@ import Styled from './styles';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
interface ChatMessagePresentationContentProps {
metadata: string;

View File

@ -14,7 +14,7 @@ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
}) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const { allowedElements } = Meteor.settings.public.chat;
const { allowedElements } = window.meetingClientSettings.public.chat;
return (
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test="messageContent">

View File

@ -16,7 +16,7 @@ import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscr
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
interface ChatListPageContainerProps {

View File

@ -16,7 +16,7 @@ interface ChatPopupProps {
const WELCOME_MSG_KEY = 'welcomeMsg';
const WELCOME_MSG_FOR_MODERATORS_KEY = 'welcomeMsgForModerators';
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const setWelcomeMsgsOnSession = (key: string, value: boolean) => {

View File

@ -24,7 +24,7 @@ const DEBUG_CONSOLE = false;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;

View File

@ -8,7 +8,8 @@ import { meetingIsBreakout } from '/imports/ui/components/app/service';
import { defineMessages } from 'react-intl';
import PollService from '/imports/ui/components/poll/service';
const CHAT_CONFIG = Meteor.settings.public.chat;
const APP = window.meetingClientSettings.public.app;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
@ -19,7 +20,7 @@ const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const ScrollCollection = new Mongo.Collection(null);

View File

@ -1,16 +1,43 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import logger from '/imports/startup/client/logger';
import logger, { generateLoggerStreams } from '/imports/startup/client/logger';
const propTypes = {
children: PropTypes.element.isRequired,
Fallback: PropTypes.func.isRequired,
Fallback: PropTypes.element,
errorMessage: PropTypes.string,
};
const defaultProps = {
Fallback: null,
errorMessage: 'Something went wrong',
};
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: false, errorInfo: null };
this.state = { error: '', errorInfo: null };
}
componentDidMount() {
const data = JSON.parse((sessionStorage.getItem('clientStartupSettings')) || {});
const logConfig = data?.clientLog;
if (logConfig) {
generateLoggerStreams(logConfig).forEach((stream) => {
logger.addStream(stream);
});
}
}
componentDidUpdate() {
const { code, error, errorInfo } = this.state;
const log = code === '403' ? 'warn' : 'error';
if (error || errorInfo) {
logger[log]({
logCode: 'Error_Boundary_wrapper',
extraInfo: { error, errorInfo },
}, 'generic error boundary logger');
}
}
componentDidCatch(error, errorInfo) {
@ -18,26 +45,27 @@ class ErrorBoundary extends Component {
error,
errorInfo,
});
logger.error({
logCode: 'Error_Boundary_wrapper',
extraInfo: { error, errorInfo },
}, 'generic error boundary logger');
}
render() {
const { error } = this.state;
const { children, Fallback } = this.props;
const { error, errorInfo } = this.state;
const { children, Fallback, errorMessage } = this.props;
return (error ? (<Fallback {...this.state} />) : children);
const fallbackElement = Fallback && error
? <Fallback error={error || {}} errorInfo={errorInfo} /> : <div>{errorMessage}</div>;
return (error
? fallbackElement
: children);
}
}
ErrorBoundary.propTypes = propTypes;
ErrorBoundary.defaultProps = defaultProps;
export default ErrorBoundary;
export const withErrorBoundary = (WrappedComponent, FallbackComponent) => (props) => (
<ErrorBoundary Fallback={FallbackComponent}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
export default ErrorBoundary;

View File

@ -0,0 +1,24 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import ErrorBoundary from '../component';
const intlMessages = defineMessages({
errorMessage: {
id: 'app.presentationUploder.genericError',
defaultMessage: 'Something went wrong',
},
});
const LocatedErrorBoundary = ({ children, ...props }) => {
const intl = useIntl();
return (
<ErrorBoundary
{...props}
errorMessage={intl.formatMessage(intlMessages.errorMessage)}
>
{children}
</ErrorBoundary>
);
};
export default LocatedErrorBoundary;

View File

@ -1,14 +1,11 @@
import React from 'react';
import Styled from './styles';
import Settings from '/imports/ui/services/settings';
const { animations } = Settings.application;
const LoadingScreen = ({ children }) => (
<Styled.Background>
<Styled.Spinner animations={animations}>
<Styled.Bounce1 animations={animations} />
<Styled.Bounce2 animations={animations} />
<Styled.Spinner animations>
<Styled.Bounce1 animations />
<Styled.Bounce2 animations />
<div />
</Styled.Spinner>
<Styled.Message>

View File

@ -0,0 +1,57 @@
import React from 'react';
import LoadingScreen from '../component';
interface LoadingContent {
isLoading: boolean;
loadingMessage: string;
}
interface LoadingContextContent extends LoadingContent {
setLoading: (isLoading: boolean, loadingMessage: string) => void;
}
export const LoadingContext = React.createContext<LoadingContextContent>({
isLoading: false,
loadingMessage: '',
setLoading: () => { },
});
interface LoadingScreenHOCProps {
children: React.ReactNode;
}
const LoadingScreenHOC: React.FC<LoadingScreenHOCProps> = ({
children,
}) => {
const [loading, setLoading] = React.useState<LoadingContent>({
isLoading: false,
loadingMessage: '',
});
return (
<LoadingContext.Provider value={{
loadingMessage: loading.loadingMessage,
isLoading: loading.isLoading,
setLoading: (isLoading: boolean, loadingMessage: string = '') => {
setLoading({
isLoading,
loadingMessage,
});
},
}}
>
{
loading.isLoading
? (
<LoadingScreen>
<h1>{loading.loadingMessage}</h1>
</LoadingScreen>
)
: null
}
{children}
</LoadingContext.Provider>
);
};
export default LoadingScreenHOC;

View File

@ -22,7 +22,7 @@ class LocalesDropdown extends PureComponent {
filterLocaleVariations(value) {
const { allLocales } = this.props;
if (allLocales) {
if (Meteor.settings.public.app.showAllAvailableLocales) {
if (window.meetingClientSettings.public.app.showAllAvailableLocales) {
return allLocales;
}

View File

@ -5,7 +5,7 @@ import ModalSimple from '/imports/ui/components/common/modal/simple/component';
import AudioService from '/imports/ui/components/audio/service';
import Styled from './styles';
const SELECT_RANDOM_USER_COUNTDOWN = Meteor.settings.public.selectRandomUser.countdown;
const SELECT_RANDOM_USER_COUNTDOWN = window.meetingClientSettings.public.selectRandomUser.countdown;
const messages = defineMessages({
noViewers: {
@ -102,9 +102,9 @@ class RandomUserSelect extends Component {
}
play() {
AudioService.playAlertSound(`${Meteor.settings.public.app.cdn
+ Meteor.settings.public.app.basename
+ Meteor.settings.public.app.instanceId}`
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
+ window.meetingClientSettings.public.app.basename
+ window.meetingClientSettings.public.app.instanceId}`
+ '/resources/sounds/Poll.mp3');
}

View File

@ -11,7 +11,7 @@ import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { useMutation } from '@apollo/client';
import { PICK_RANDOM_VIEWER } from '/imports/ui/core/graphql/mutations/userMutations';
const SELECT_RANDOM_USER_ENABLED = Meteor.settings.public.selectRandomUser.enabled;
const SELECT_RANDOM_USER_ENABLED = window.meetingClientSettings.public.selectRandomUser.enabled;
// A value that is used by component to remember
// whether it should be open or closed after a render

View File

@ -0,0 +1,107 @@
import {
ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject,
} from '@apollo/client';
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import React, { useContext, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
import { LoadingContext } from '/imports/ui/components/common/loading-screen/loading-screen-HOC/component';
interface ConnectionManagerProps {
children: React.ReactNode;
}
interface Response {
response: {
returncode: string;
version: string;
apiVersion: string;
bbbVersion: string;
graphqlWebsocketUrl: string;
}
}
const ConnectionManager: React.FC<ConnectionManagerProps> = ({ children }): React.ReactNode => {
const [graphqlUrlApolloClient, setApolloClient] = React.useState<ApolloClient<NormalizedCacheObject> | null>(null);
const [graphqlUrl, setGraphqlUrl] = React.useState<string>('');
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
fetch(`https://${window.location.hostname}/bigbluebutton/api`, {
headers: {
'Content-Type': 'application/json',
},
}).then(async (response) => {
const responseJson: Response = await response.json();
setGraphqlUrl(responseJson.response.graphqlWebsocketUrl);
}).catch((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error fetching GraphQL URL: '.concat(error.message || ''));
});
loadingContextInfo.setLoading(true, 'Fetching GraphQL URL');
}, []);
useEffect(() => {
loadingContextInfo.setLoading(true, 'Connecting to GraphQL server');
if (graphqlUrl) {
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken');
if (!sessionToken) {
loadingContextInfo.setLoading(false, '');
throw new Error('Missing session token');
}
sessionStorage.setItem('sessionToken', sessionToken);
let wsLink;
try {
const subscription = new SubscriptionClient(graphqlUrl, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
'X-Session-Token': sessionToken,
},
},
});
subscription.onError(() => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error: on subscription to server');
});
wsLink = new WebSocketLink(
subscription,
);
wsLink.setOnError((error) => {
loadingContextInfo.setLoading(false, '');
throw new Error('Error: on apollo connection'.concat(JSON.stringify(error) || ''));
});
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error creating WebSocketLink: '.concat(JSON.stringify(error) || ''));
}
let client;
try {
client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache(),
connectToDevTools: Meteor.isDevelopment,
});
setApolloClient(client);
} catch (error) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error creating Apollo Client: '.concat(JSON.stringify(error) || ''));
}
}
},
[graphqlUrl]);
return (
graphqlUrlApolloClient
? (
<ApolloProvider
client={graphqlUrlApolloClient}
>
{children}
</ApolloProvider>
) : null
);
};
export default ConnectionManager;

View File

@ -0,0 +1,98 @@
import React, { useEffect } from 'react';
import { ErrorScreen } from '../../error-screen/component';
import LoadingScreen from '../../common/loading-screen/component';
const connectionTimeout = 60000;
interface Response {
meeting_clientSettings: Array<{
askForFeedbackOnLogout: boolean,
allowDefaultLogoutUrl: boolean,
learningDashboardBase: string,
fallbackLocale: string,
fallbackOnEmptyString: boolean,
clientLog: {
server: {
level: string,
enabled: boolean
},
console: {
level: string,
enabled: true
},
external: {
url: string,
level: string,
logTag: string,
method: string,
enabled: boolean,
flushOnClose: boolean,
throttleInterval: number,
}
}
}>
}
interface StartupDataFetchProps {
children: React.ReactNode;
}
const StartupDataFetch: React.FC<StartupDataFetchProps> = ({
children,
}) => {
const [settingsFetched, setSettingsFetched] = React.useState<boolean>(false);
const [error, setError] = React.useState<string>('');
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
const [loading, setLoading] = React.useState<boolean>(false);
useEffect(() => {
setLoading(true);
timeoutRef.current = setTimeout(() => {
setError('Timeout on fetching startup data');
setLoading(false);
}, connectionTimeout);
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken');
if (!sessionToken) {
setError('Missing session token');
setLoading(false);
return;
}
const clientStartupSettings = '/api/rest/clientStartupSettings/';
const url = new URL(`${window.location.origin}${clientStartupSettings}`);
const headers = new Headers({ 'X-Session-Token': sessionToken, 'Content-Type': 'application/json' });
fetch(url, { method: 'get', headers })
.then((resp) => resp.json())
.then((data: Response) => {
const settings = data.meeting_clientSettings[0];
sessionStorage.setItem('clientStartupSettings', JSON.stringify(settings));
setSettingsFetched(true);
clearTimeout(timeoutRef.current);
setLoading(false);
});
}, []);
return (
<>
{settingsFetched ? children : null}
{error
? (
<ErrorScreen
endedReason={error}
/>
)
: null}
{loading
? (
<LoadingScreen>
<div style={{ display: 'none' }}>
Loading...
</div>
</LoadingScreen>
)
: null}
</>
);
};
export default StartupDataFetch;

View File

@ -3,7 +3,7 @@ import { useMutation, useSubscription } from '@apollo/client';
import { CONNECTION_STATUS_SUBSCRIPTION } from './queries';
import { UPDATE_CONNECTION_ALIVE_AT, UPDATE_USER_CLIENT_RTT } from './mutations';
const STATS_INTERVAL = Meteor.settings.public.stats.interval;
const STATS_INTERVAL = window.meetingClientSettings.public.stats.interval;
const ConnectionStatus = () => {
const networkRttInMs = useRef(null); // Ref to store the current timeout

View File

@ -391,7 +391,7 @@ class ConnectionStatusComponent extends PureComponent {
* @return {Object} The component to be renderized.
*/
renderNetworkData() {
const { enableNetworkStats } = Meteor.settings.public.app;
const { enableNetworkStats } = window.meetingClientSettings.public.app;
if (!enableNetworkStats) {
return null;
@ -497,7 +497,7 @@ class ConnectionStatusComponent extends PureComponent {
* @return {Object} - The component to be renderized
*/
renderCopyDataButton() {
const { enableCopyNetworkStatsButton } = Meteor.settings.public.app;
const { enableCopyNetworkStatsButton } = window.meetingClientSettings.public.app;
if (!enableCopyNetworkStatsButton) {
return null;

View File

@ -7,9 +7,9 @@ import AudioService from '/imports/ui/components/audio/service';
import VideoService from '/imports/ui/components/video-provider/service';
import ScreenshareService from '/imports/ui/components/screenshare/service';
const STATS = Meteor.settings.public.stats;
const STATS = window.meetingClientSettings.public.stats;
const NOTIFICATION = STATS.notification;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const intlMessages = defineMessages({
saved: {

View File

@ -43,8 +43,8 @@ const intlMessages = defineMessages({
},
});
const DEBUG_WINDOW_ENABLED = Meteor.settings.public.app.enableDebugWindow;
const SHOW_DEBUG_WINDOW_ACCESSKEY = Meteor.settings.public.app.shortcuts.openDebugWindow.accesskey;
const DEBUG_WINDOW_ENABLED = window.meetingClientSettings.public.app.enableDebugWindow;
const SHOW_DEBUG_WINDOW_ACCESSKEY = window.meetingClientSettings.public.app.shortcuts.openDebugWindow.accesskey;
class DebugWindow extends Component {
constructor(props) {

View File

@ -4,7 +4,7 @@ import { injectIntl } from 'react-intl';
import data from '@emoji-mart/data';
import Picker from '@emoji-mart/react';
const DISABLE_EMOJIS = Meteor.settings.public.chat.disableEmojis;
const DISABLE_EMOJIS = window.meetingClientSettings.public.chat.disableEmojis;
const propTypes = {
intl: PropTypes.shape({

View File

@ -5,9 +5,9 @@ import Service from './service';
const EmojiRain = ({ reactions }) => {
const containerRef = useRef(null);
const [isAnimating, setIsAnimating] = useState(false);
const EMOJI_SIZE = Meteor.settings.public.app.emojiRain.emojiSize;
const NUMBER_OF_EMOJIS = Meteor.settings.public.app.emojiRain.numberOfEmojis;
const EMOJI_RAIN_ENABLED = Meteor.settings.public.app.emojiRain.enabled;
const EMOJI_SIZE = window.meetingClientSettings.public.app.emojiRain.emojiSize;
const NUMBER_OF_EMOJIS = window.meetingClientSettings.public.app.emojiRain.numberOfEmojis;
const EMOJI_RAIN_ENABLED = window.meetingClientSettings.public.app.emojiRain.enabled;
const { animations } = Settings.application;

View File

@ -26,7 +26,7 @@ const intlMessages = defineMessages({
},
});
const { warnAboutUnsavedContentOnMeetingEnd } = Meteor.settings.public.app;
const { warnAboutUnsavedContentOnMeetingEnd } = window.meetingClientSettings.public.app;
const propTypes = {
intl: PropTypes.shape({

View File

@ -3,8 +3,6 @@ import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import AudioManager from '/imports/ui/services/audio-manager';
import logger from '/imports/startup/client/logger';
import Styled from './styles';
const intlMessages = defineMessages({
@ -80,21 +78,29 @@ const propTypes = {
PropTypes.string,
PropTypes.number,
]),
error: PropTypes.object,
errorInfo: PropTypes.object,
};
const defaultProps = {
code: '500',
callback: () => {},
endedReason: null,
error: {},
errorInfo: null,
};
class ErrorScreen extends PureComponent {
componentDidMount() {
const { code, callback, endedReason } = this.props;
const log = code === '403' ? 'warn' : 'error';
AudioManager.exitAudio();
// stop audio
document.querySelector('audio').pause();
navigator
.mediaDevices
.getUserMedia({ audio: true, video: true })
.then((m) => m.getTracks().forEach((t) => t.stop()));
callback(endedReason, () => Meteor.disconnect());
logger[log]({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
console.error({ logCode: 'startup_client_usercouldnotlogin_error' }, `User could not log in HTML5, hit ${code}`);
}
render() {
@ -102,18 +108,27 @@ class ErrorScreen extends PureComponent {
intl,
code,
children,
error,
errorInfo,
} = this.props;
let formatedMessage = 'Oops, something went wrong';
let errorMessageDescription = Session.get('errorMessageDescription');
if (intl) {
formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]);
let formatedMessage = intl.formatMessage(intlMessages[defaultProps.code]);
if (code in intlMessages) {
formatedMessage = intl.formatMessage(intlMessages[code]);
}
if (code in intlMessages) {
formatedMessage = intl.formatMessage(intlMessages[code]);
errorMessageDescription = Session.get('errorMessageDescription');
if (errorMessageDescription in intlMessages) {
errorMessageDescription = intl.formatMessage(intlMessages[errorMessageDescription]);
}
}
let errorMessageDescription = Session.get('errorMessageDescription');
if (errorMessageDescription in intlMessages) {
errorMessageDescription = intl.formatMessage(intlMessages[errorMessageDescription]);
if (error) {
errorMessageDescription = error.message;
}
return (
@ -130,6 +145,19 @@ class ErrorScreen extends PureComponent {
</Styled.SessionMessage>
)
}
{
errorInfo
? (
<textarea
rows="5"
cols="33"
disabled
>
{JSON.stringify(errorInfo)}
</textarea>
)
: null
}
<Styled.Separator />
<Styled.CodeError>
{code}
@ -144,5 +172,7 @@ class ErrorScreen extends PureComponent {
export default injectIntl(ErrorScreen);
export { ErrorScreen };
ErrorScreen.propTypes = propTypes;
ErrorScreen.defaultProps = defaultProps;

View File

@ -1,56 +0,0 @@
import {
ApolloClient, ApolloProvider, InMemoryCache, NormalizedCacheObject,
} from '@apollo/client';
// import { WebSocketLink } from "@apollo/client/link/ws";
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import React, { useEffect } from 'react';
import Auth from '/imports/ui/services/auth';
import { Meteor } from 'meteor/meteor';
interface Props {
children: React.ReactNode;
}
const GraphqlProvider = ({ children }: Props): React.ReactNode => {
// const [link, setLink] = React.useState<WebSocketLink | null>(null);
const [apolloClient, setApolloClient] = React.useState<ApolloClient<NormalizedCacheObject> | null>(null);
useEffect(() => {
let GRAPHQL_URL = null;
if ('graphqlUrl' in Meteor.settings.public.app) {
GRAPHQL_URL = Meteor.settings.public.app.graphqlUrl;
} else {
GRAPHQL_URL = `wss://${window.location.hostname}/v1/graphql`;
}
const wsLink = new WebSocketLink(
new SubscriptionClient(GRAPHQL_URL, {
reconnect: true,
timeout: 30000,
connectionParams: {
headers: {
'X-Session-Token': Auth.sessionToken,
},
},
}),
);
// setLink(wsLink);
const client = new ApolloClient({
link: wsLink,
cache: new InMemoryCache(),
connectToDevTools: Meteor.isDevelopment,
});
setApolloClient(client);
}, []);
return (
apolloClient
&& (
<ApolloProvider
client={apolloClient}
>
{children}
</ApolloProvider>
)
);
};
export default GraphqlProvider;

View File

@ -85,7 +85,7 @@ class JoinHandler extends Component {
async fetchToken() {
const { hasAlreadyJoined } = this.state;
const APP = Meteor.settings.public.app;
const APP = window.meetingClientSettings.public.app;
if (!this._isMounted) return;
const urlParams = new URLSearchParams(window.location.search);
@ -112,7 +112,7 @@ class JoinHandler extends Component {
userAgent: userInfo.userAgent,
screenSize: { width: window.screen.width, height: window.screen.height },
windowSize: { width: window.innerWidth, height: window.innerHeight },
bbbVersion: Meteor.settings.public.app.bbbServerVersion,
bbbVersion: window.meetingClientSettings.public.app.bbbServerVersion,
location: window.location.href,
};
@ -210,14 +210,13 @@ class JoinHandler extends Component {
},
}, 'User successfully went through main.joinRouteHandler');
} else {
if(['missingSession','meetingForciblyEnded','notFound'].includes(response.messageKey)) {
if (['missingSession', 'meetingForciblyEnded', 'notFound'].includes(response.messageKey)) {
JoinHandler.setError('410');
Session.set('errorMessageDescription', 'meeting_ended');
} else if(response.messageKey == "guestDeny") {
} else if (response.messageKey == "guestDeny") {
JoinHandler.setError('401');
Session.set('errorMessageDescription', 'guest_deny');
} else if(response.messageKey == "maxParticipantsReached") {
} else if (response.messageKey == "maxParticipantsReached") {
JoinHandler.setError('401');
Session.set('errorMessageDescription', 'max_participants_reason');
} else {

View File

@ -0,0 +1,55 @@
import { useQuery } from '@apollo/client';
import React, { useCallback, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
import { UserCustomParameterResponse, getCustomParameter } from './queries';
interface CustomUsersSettingsProps {
children: React.ReactNode;
}
const CustomUsersSettings: React.FC<CustomUsersSettingsProps> = ({
children,
}) => {
const {
data: customParameterData,
loading: customParameterLoading,
error: customParameterError,
} = useQuery<UserCustomParameterResponse>(getCustomParameter);
const [allowToRender, setAllowToRender] = React.useState(false);
const sendToServer = useCallback((data: Array<{[x: string]: string}>, count = 0) => {
Meteor.callAsync('addUserSettings', data).then(() => {
setAllowToRender(true);
})
.catch(() => {
if (count < 3) {
setTimeout(() => {
sendToServer(data, count + 1);
}, 500);
} else {
throw new Error('Error on sending user settings to server');
}
});
}, []);
useEffect(() => {
if (customParameterData && !customParameterLoading) {
const filteredData = customParameterData.user_customParameter.map((uc) => {
const { parameter, value } = uc;
return { [parameter]: value };
});
sendToServer(filteredData);
}
}, [
customParameterData,
customParameterLoading,
]);
useEffect(() => {
if (customParameterError) {
throw new Error(`Error on requesting custom parameter data: ${customParameterError}`);
}
}, [customParameterError]);
return allowToRender ? <>{children}</> : null;
};
export default CustomUsersSettings;

View File

@ -0,0 +1,23 @@
import { gql } from '@apollo/client';
interface CustomParameter {
parameter: string;
value: string;
}
export interface UserCustomParameterResponse {
user_customParameter: CustomParameter[];
}
export const getCustomParameter = gql`
query getCustomParameter {
user_customParameter {
parameter
value
}
}
`;
export default {
getCustomParameter,
};

View File

@ -0,0 +1,215 @@
import { useMutation, useQuery, useSubscription } from '@apollo/client';
import React, { useContext, useEffect } from 'react';
import { Meteor } from 'meteor/meteor';
// @ts-ignore - type avaible only to server package
import { DDP } from 'meteor/ddp-client';
import { Session } from 'meteor/session';
import {
getUserCurrent,
GetUserCurrentResponse,
getUserInfo,
GetUserInfoResponse,
userJoinMutation,
} from './queries';
import { setAuthData } from '/imports/ui/core/local-states/useAuthData';
import MeetingEndedContainer from '../../meeting-ended/meeting-ended-ts/component';
import { setUserDataToSessionStorage } from './service';
import { LoadingContext } from '../../common/loading-screen/loading-screen-HOC/component';
const connectionTimeout = 60000;
interface PresenceManagerContainerProps {
children: React.ReactNode;
}
interface PresenceManagerProps extends PresenceManagerContainerProps {
authToken: string;
logoutUrl: string;
meetingId: string;
meetingName: string;
userName: string;
extId: string;
userId: string;
joinErrorCode: string;
joinErrorMessage: string;
joined: boolean;
meetingEnded: boolean;
endedReasonCode: string;
endedBy: string;
ejectReasonCode: string;
bannerColor: string;
bannerText: string;
}
const PresenceManager: React.FC<PresenceManagerProps> = ({
authToken,
children,
logoutUrl,
meetingId,
meetingName,
userName,
extId,
userId,
joinErrorCode,
joinErrorMessage,
joined,
meetingEnded,
endedReasonCode,
endedBy,
ejectReasonCode,
bannerColor,
bannerText,
}) => {
const [allowToRender, setAllowToRender] = React.useState(false);
const [dispatchUserJoin] = useMutation(userJoinMutation);
const timeoutRef = React.useRef<ReturnType<typeof setTimeout>>();
const loadingContextInfo = useContext(LoadingContext);
useEffect(() => {
timeoutRef.current = setTimeout(() => {
loadingContextInfo.setLoading(false, '');
throw new Error('Authentication timeout');
}, connectionTimeout);
DDP.onReconnect(() => {
Meteor.callAsync('validateConnection', authToken, meetingId, userId);
});
const urlParams = new URLSearchParams(window.location.search);
const sessionToken = urlParams.get('sessionToken') as string;
setAuthData({
meetingId,
userId,
authToken,
logoutUrl,
sessionToken,
userName,
extId,
meetingName,
});
setUserDataToSessionStorage({
meetingId,
userId,
authToken,
logoutUrl,
sessionToken,
userName,
extId,
meetingName,
});
}, []);
useEffect(() => {
if (bannerColor || bannerText) {
Session.set('bannerText', bannerText);
Session.set('bannerColor', bannerColor);
}
}, [bannerColor, bannerText]);
useEffect(() => {
if (authToken && !joined) {
dispatchUserJoin({
variables: {
authToken,
clientType: 'HTML5',
},
});
}
}, [joined, authToken]);
useEffect(() => {
if (joined) {
clearTimeout(timeoutRef.current);
Meteor.callAsync('validateConnection', authToken, meetingId, userId).then(() => {
setAllowToRender(true);
});
}
}, [joined]);
useEffect(() => {
if (joinErrorCode) {
loadingContextInfo.setLoading(false, '');
}
},
[joinErrorCode, joinErrorMessage]);
return (
<>
{allowToRender && !(meetingEnded || joinErrorCode || ejectReasonCode) ? children : null}
{
meetingEnded || joinErrorCode || ejectReasonCode
? (
<MeetingEndedContainer
meetingEndedCode={endedReasonCode}
endedBy={endedBy}
joinErrorCode={joinErrorCode || ejectReasonCode}
/>
)
: null
}
</>
);
};
const PresenceManagerContainer: React.FC<PresenceManagerContainerProps> = ({ children }) => {
const { loading, error, data } = useSubscription<GetUserCurrentResponse>(getUserCurrent);
const {
loading: userInfoLoading,
error: userInfoError,
data: userInfoData,
} = useQuery<GetUserInfoResponse>(getUserInfo);
const loadingContextInfo = useContext(LoadingContext);
if (loading || userInfoLoading) return null;
if (error || userInfoError) {
loadingContextInfo.setLoading(false, '');
throw new Error('Error on user authentication: ', error);
}
if (!data || data.user_current.length === 0) return null;
if (!userInfoData
|| userInfoData.meeting.length === 0
|| userInfoData.user_current.length === 0) return null;
const {
authToken,
joinErrorCode,
joinErrorMessage,
joined,
ejectReasonCode,
meeting,
} = data.user_current[0];
const {
logoutUrl,
meetingId,
name: meetingName,
bannerColor,
bannerText,
} = userInfoData.meeting[0];
const { extId, name: userName, userId } = userInfoData.user_current[0];
return (
<PresenceManager
authToken={authToken}
logoutUrl={logoutUrl}
meetingId={meetingId}
meetingName={meetingName}
userName={userName}
extId={extId}
userId={userId}
joined={joined}
joinErrorCode={joinErrorCode}
joinErrorMessage={joinErrorMessage}
meetingEnded={meeting.ended}
endedReasonCode={meeting.endedReasonCode}
endedBy={meeting.endedBy}
ejectReasonCode={ejectReasonCode}
bannerColor={bannerColor}
bannerText={bannerText}
>
{children}
</PresenceManager>
);
};
export default PresenceManagerContainer;

View File

@ -0,0 +1,80 @@
import { gql } from '@apollo/client';
export interface GetUserCurrentResponse {
user_current: Array<{
userId: string;
authToken: string;
joined: boolean;
joinErrorCode: string;
joinErrorMessage: string;
ejectReasonCode: string;
meeting: {
ended: boolean;
endedReasonCode: string;
endedBy: string;
};
}>;
}
export interface GetUserInfoResponse {
meeting: Array<{
meetingId: string;
name: string;
logoutUrl: string;
bannerColor: string;
bannerText: string;
}>;
user_current: Array<{
extId: string;
name: string;
userId: string;
}>;
}
export const getUserInfo = gql`
query getUserInfo {
meeting {
meetingId
name
logoutUrl
bannerColor
bannerText
}
user_current {
extId
name
userId
}
}
`;
export const getUserCurrent = gql`
subscription getUserCurrent {
user_current {
userId
authToken
joinErrorCode
joinErrorMessage
joined
ejectReasonCode
meeting {
ended
endedReasonCode
endedBy
}
}
}
`;
export const userJoinMutation = gql`
mutation UserJoin($authToken: String!, $clientType: String!) {
userJoinMeeting(
authToken: $authToken,
clientType: $clientType,
)
}
`;
export default {
getUserCurrent,
userJoinMutation,
getUserInfo,
};

View File

@ -0,0 +1,36 @@
export const JoinErrorCodeTable = {
NOT_EJECT: 'not_eject_reason',
DUPLICATE_USER: 'duplicate_user_in_meeting_eject_reason',
PERMISSION_FAILED: 'not_enough_permission_eject_reason',
EJECT_USER: 'user_requested_eject_reason',
SYSTEM_EJECT_USER: 'system_requested_eject_reason',
VALIDATE_TOKEN: 'validate_token_failed_eject_reason',
USER_INACTIVITY: 'user_inactivity_eject_reason',
BANNED_USER_REJOINING: 'banned_user_rejoining_reason',
USER_LOGGED_OUT: 'user_logged_out_reason',
MAX_PARTICIPANTS: 'max_participants_reason',
};
export const setUserDataToSessionStorage = (userData: {
meetingId: string,
userId: string,
authToken: string,
logoutUrl: string,
sessionToken: string,
userName: string,
extId: string,
meetingName: string,
}) => {
sessionStorage.setItem('meetingId', userData.meetingId);
sessionStorage.setItem('userId', userData.userId);
sessionStorage.setItem('logoutUrl', userData.logoutUrl);
sessionStorage.setItem('sessionToken', userData.sessionToken);
sessionStorage.setItem('userName', userData.userName);
sessionStorage.setItem('extId', userData.extId);
sessionStorage.setItem('meetingName', userData.meetingName);
};
export default {
JoinErrorCodeTable,
setUserDataToSessionStorage,
};

View File

@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { LAYOUT_TYPE, CAMERADOCK_POSITION, PANELS } from './enums';
const CHAT_CONFIG = Meteor.settings.public.chat;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const DEFAULT_VALUES = {

View File

@ -22,7 +22,7 @@ const LayoutModalComponent = (props) => {
const [selectedLayout, setSelectedLayout] = useState(application.selectedLayout);
const BASE_NAME = Meteor.settings.public.app.basename;
const BASE_NAME = window.meetingClientSettings.public.app.basename;
const LAYOUTS_PATH = `${BASE_NAME}/resources/images/layouts/`;
const isKeepPushingLayoutEnabled = SettingsService.isKeepPushingLayoutEnabled();

View File

@ -9,7 +9,7 @@ import { isMobile } from '../utils';
import { updateSettings } from '/imports/ui/components/settings/service';
import { Session } from 'meteor/session';
const HIDE_PRESENTATION = Meteor.settings.public.layout.hidePresentationOnJoin;
const HIDE_PRESENTATION = window.meetingClientSettings.public.layout.hidePresentationOnJoin;
const equalDouble = (n1, n2) => {
const precision = 0.01;

View File

@ -2,7 +2,7 @@ import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const isModerator = () => {
const user = Users.findOne(
@ -40,7 +40,7 @@ const setLearningDashboardCookie = () => {
};
const openLearningDashboardUrl = (lang) => {
const APP = Meteor.settings.public.app;
const APP = window.meetingClientSettings.public.app;
if (getLearningDashboardAccessToken() && setLearningDashboardCookie()) {
window.open(`${APP.learningDashboardBase}/?meeting=${Auth.meetingID}&lang=${lang}`, '_blank');
} else {

View File

@ -70,8 +70,8 @@ const FETCHING = 'fetching';
const FALLBACK = 'fallback';
const READY = 'ready';
const supportedBrowsers = ['Chrome', 'Firefox', 'Safari', 'Opera', 'Microsoft Edge', 'Yandex Browser'];
const DEFAULT_LANGUAGE = Meteor.settings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = Meteor.settings.public.app.html5ClientBuild;
const DEFAULT_LANGUAGE = window.meetingClientSettings.public.app.defaultSettings.application.fallbackLocale;
const CLIENT_VERSION = window.meetingClientSettings.public.app.html5ClientBuild;
export default class Legacy extends Component {
constructor(props) {

View File

@ -5,7 +5,7 @@ import { LockStruct } from './context';
import Users from '/imports/api/users';
import { withLockContext } from './withContext';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const lockContextContainer = (component) => withTracker(() => {
const lockSetting = new LockStruct();

View File

@ -10,9 +10,9 @@ import NotesService from '/imports/ui/components/notes/service';
import VideoStreams from '/imports/api/video-streams';
import Auth from '/imports/ui/services/auth/index';
const LAYOUT_CONFIG = Meteor.settings.public.layout;
const KURENTO_CONFIG = Meteor.settings.public.kurento;
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
const LAYOUT_CONFIG = window.meetingClientSettings.public.layout;
const KURENTO_CONFIG = window.meetingClientSettings.public.kurento;
const PRESENTATION_CONFIG = window.meetingClientSettings.public.presentation;
function shouldShowWhiteboard() {
return true;

View File

@ -176,7 +176,7 @@ class MeetingEnded extends PureComponent {
shouldShowFeedback() {
const { dispatched } = this.state;
return getFromUserSettings('bbb_ask_for_feedback_on_logout', Meteor.settings.public.app.askForFeedbackOnLogout) && !dispatched;
return getFromUserSettings('bbb_ask_for_feedback_on_logout', window.meetingClientSettings.public.app.askForFeedbackOnLogout) && !dispatched;
}
confirmRedirect() {

View File

@ -0,0 +1,451 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { isEmpty } from 'radash';
import { useQuery } from '@apollo/client';
import {
JoinErrorCodeTable,
MeetingEndedTable,
openLearningDashboardUrl,
setLearningDashboardCookie,
} from './service';
import { MeetingEndDataResponse, getMeetingEndData } from './queries';
import useAuthData from '/imports/ui/core/local-states/useAuthData';
import Icon from '/imports/ui/components/common/icon/icon-ts/component';
import Styled from './styles';
import Rating from '../rating/component';
import { LoadingContext } from '../../common/loading-screen/loading-screen-HOC/component';
import logger from '/imports/startup/client/logger';
const intlMessage = defineMessages({
410: {
id: 'app.meeting.ended',
description: 'message when meeting is ended',
},
403: {
id: 'app.error.removed',
description: 'Message to display when user is removed from the conference',
},
430: {
id: 'app.error.meeting.ended',
description: 'user logged conference',
},
'acl-not-allowed': {
id: 'app.error.removed',
description: 'Message to display when user is removed from the conference',
},
messageEnded: {
id: 'app.meeting.endedMessage',
description: 'message saying to go back to home screen',
},
messageEndedByUser: {
id: 'app.meeting.endedByUserMessage',
description: 'message informing who ended the meeting',
},
messageEndedByNoModeratorSingular: {
id: 'app.meeting.endedByNoModeratorMessageSingular',
description: 'message informing that the meeting was ended due to no moderator present (singular)',
},
messageEndedByNoModeratorPlural: {
id: 'app.meeting.endedByNoModeratorMessagePlural',
description: 'message informing that the meeting was ended due to no moderator present (plural)',
},
buttonOkay: {
id: 'app.meeting.endNotification.ok.label',
description: 'label okay for button',
},
title: {
id: 'app.feedback.title',
description: 'title for feedback screen',
},
subtitle: {
id: 'app.feedback.subtitle',
description: 'subtitle for feedback screen',
},
textarea: {
id: 'app.feedback.textarea',
description: 'placeholder for textarea',
},
confirmDesc: {
id: 'app.leaveConfirmation.confirmDesc',
description: 'adds context to confim option',
},
sendLabel: {
id: 'app.feedback.sendFeedback',
description: 'send feedback button label',
},
sendDesc: {
id: 'app.feedback.sendFeedbackDesc',
description: 'adds context to send feedback option',
},
[JoinErrorCodeTable.DUPLICATE_USER]: {
id: 'app.meeting.logout.duplicateUserEjectReason',
description: 'message for duplicate users',
},
[JoinErrorCodeTable.PERMISSION_FAILED]: {
id: 'app.meeting.logout.permissionEjectReason',
description: 'message for whom was kicked by doing something without permission',
},
[JoinErrorCodeTable.EJECT_USER]: {
id: 'app.meeting.logout.ejectedFromMeeting',
description: 'message when the user is removed by someone',
},
[JoinErrorCodeTable.SYSTEM_EJECT_USER]: {
id: 'app.meeting.logout.ejectedFromMeeting',
description: 'message when the user is removed by the system',
},
[JoinErrorCodeTable.MAX_PARTICIPANTS]: {
id: 'app.meeting.logout.maxParticipantsReached',
description: 'message when the user is rejected due to max participants limit',
},
[JoinErrorCodeTable.VALIDATE_TOKEN]: {
id: 'app.meeting.logout.validateTokenFailedEjectReason',
description: 'invalid auth token',
},
[JoinErrorCodeTable.USER_INACTIVITY]: {
id: 'app.meeting.logout.userInactivityEjectReason',
description: 'message to whom was kicked by inactivity',
},
[JoinErrorCodeTable.USER_LOGGED_OUT]: {
id: 'app.feedback.title',
description: 'message to whom was kicked by logging out',
},
[JoinErrorCodeTable.BANNED_USER_REJOINING]: {
id: 'app.error.userBanned',
description: 'message to whom was banned',
},
open_activity_report_btn: {
id: 'app.learning-dashboard.clickHereToOpen',
description: 'description of link to open activity report',
},
[MeetingEndedTable.ENDED_FROM_API]: {
id: 'app.meeting.endedFromAPI',
description: '',
},
[MeetingEndedTable.ENDED_WHEN_NOT_JOINED]: {
id: 'app.meeting.endedWhenNoUserJoined',
description: '',
},
[MeetingEndedTable.ENDED_WHEN_LAST_USER_LEFT]: {
id: 'app.meeting.endedWhenLastUserLeft',
description: '',
},
[MeetingEndedTable.ENDED_AFTER_USER_LOGGED_OUT]: {
id: 'app.meeting.endedWhenLastUserLeft',
description: '',
},
[MeetingEndedTable.ENDED_AFTER_EXCEEDING_DURATION]: {
id: 'app.meeting.endedAfterExceedingDuration',
description: '',
},
[MeetingEndedTable.BREAKOUT_ENDED_EXCEEDING_DURATION]: {
id: 'app.meeting.breakoutEndedAfterExceedingDuration',
description: '',
},
[MeetingEndedTable.BREAKOUT_ENDED_BY_MOD]: {
id: 'app.meeting.breakoutEndedByModerator',
description: '',
},
[MeetingEndedTable.ENDED_DUE_TO_NO_AUTHED_USER]: {
id: 'app.meeting.endedDueNoAuthed',
description: '',
},
[MeetingEndedTable.ENDED_DUE_TO_NO_MODERATOR]: {
id: 'app.meeting.endedDueNoModerators',
description: '',
},
});
interface MeetingEndedContainerProps {
endedBy: string;
meetingEndedCode: string;
joinErrorCode: string;
}
interface MeetingEndedProps extends MeetingEndedContainerProps {
allowDefaultLogoutUrl: boolean;
askForFeedbackOnLogout: boolean
learningDashboardAccessToken: string;
role: string;
learningDashboardBase: string;
isBreakout: boolean;
}
const MeetingEnded: React.FC<MeetingEndedProps> = ({
endedBy,
joinErrorCode,
meetingEndedCode,
allowDefaultLogoutUrl,
askForFeedbackOnLogout,
learningDashboardAccessToken,
role,
learningDashboardBase,
isBreakout,
}) => {
const loadingContextInfo = useContext(LoadingContext);
const intl = useIntl();
const [{
authToken,
meetingId,
logoutUrl,
userName,
userId,
}] = useAuthData();
const [selectedStars, setSelectedStars] = useState(0);
const [dispatched, setDispatched] = useState(false);
const generateEndMessage = useCallback((joinErrorCode: string, meetingEndedCode: string, endedBy: string) => {
if (!isEmpty(endedBy)) {
return intl.formatMessage(intlMessage.messageEndedByUser, { 0: endedBy });
}
// OR opetaror always returns the first truthy value
const code = meetingEndedCode || joinErrorCode || '410';
return intl.formatMessage(intlMessage[code]);
}, []);
const sendFeedback = useCallback(() => {
const textarea = document.getElementById('feedbackComment') as HTMLTextAreaElement;
const comment = (textarea?.value || '').trim();
const message = {
rating: selectedStars,
userId,
userName,
authToken,
meetingId,
comment,
userRole: role,
};
const url = './feedback';
const options = {
method: 'POST',
body: JSON.stringify(message),
headers: {
'Content-Type': 'application/json',
},
};
setDispatched(true);
fetch(url, options).then(() => {
if (role === 'VIEWER') {
const REDIRECT_WAIT_TIME = 5000;
setTimeout(() => {
window.location.href = logoutUrl;
}, REDIRECT_WAIT_TIME);
}
}).catch((e) => {
logger.warn({
logCode: 'user_feedback_not_sent_error',
extraInfo: {
errorName: e.name,
errorMessage: e.message,
},
}, `Unable to send feedback: ${e.message}`);
});
}, [selectedStars]);
const confirmRedirect = (isBreakout: boolean, allowRedirect: boolean) => {
if (isBreakout) window.close();
if (allowRedirect) {
window.location.href = logoutUrl;
}
};
const logoutButton = useMemo(() => {
const { locale } = intl;
return (
(
<Styled.Wrapper>
{
learningDashboardAccessToken && role === 'moderator'
// Always set cookie in case Dashboard is already opened
&& setLearningDashboardCookie(learningDashboardAccessToken, meetingId) === true
? (
<Styled.Text>
<Styled.MeetingEndedButton
color="default"
onClick={() => openLearningDashboardUrl(learningDashboardAccessToken,
meetingId,
authToken,
learningDashboardBase,
locale)}
aria-description={intl.formatMessage(intlMessage.open_activity_report_btn)}
>
<Icon
iconName="multi_whiteboard"
/>
</Styled.MeetingEndedButton>
</Styled.Text>
) : null
}
<Styled.Text>
{intl.formatMessage(intlMessage.messageEnded)}
</Styled.Text>
<Styled.MeetingEndedButton
color="primary"
onClick={() => confirmRedirect(isBreakout, allowDefaultLogoutUrl)}
aria-description={intl.formatMessage(intlMessage.confirmDesc)}
>
{intl.formatMessage(intlMessage.buttonOkay)}
</Styled.MeetingEndedButton>
</Styled.Wrapper>
)
);
}, [learningDashboardAccessToken, role, meetingId, authToken, learningDashboardBase]);
const feedbackScreen = useMemo(() => {
const shouldShowFeedback = askForFeedbackOnLogout && !dispatched;
const noRating = selectedStars === 0;
return (
<>
<Styled.Text>
{shouldShowFeedback
? intl.formatMessage(intlMessage.subtitle)
: intl.formatMessage(intlMessage.messageEnded)}
</Styled.Text>
{shouldShowFeedback ? (
<div data-test="rating">
<Rating
total="5"
onRate={setSelectedStars}
/>
{!noRating ? (
<Styled.TextArea
rows={5}
id="feedbackComment"
placeholder={intl.formatMessage(intlMessage.textarea)}
aria-describedby="textareaDesc"
/>
) : null}
</div>
) : null}
{noRating ? (
<Styled.MeetingEndedButton
color="primary"
onClick={() => setDispatched(true)}
aria-description={intl.formatMessage(intlMessage.confirmDesc)}
>
{intl.formatMessage(intlMessage.buttonOkay)}
</Styled.MeetingEndedButton>
) : null}
{!noRating ? (
<Styled.MeetingEndedButton
onClick={sendFeedback}
aria-description={intl.formatMessage(intlMessage.sendDesc)}
>
{intl.formatMessage(intlMessage.sendLabel)}
</Styled.MeetingEndedButton>
) : null}
</>
);
}, []);
useEffect(() => {
loadingContextInfo.setLoading(false, '');
}, []);
return (
<Styled.Parent>
<Styled.Modal data-test="meetingEndedModal">
<Styled.Content>
<Styled.Title>
{generateEndMessage(joinErrorCode, meetingEndedCode, endedBy)}
</Styled.Title>
{allowDefaultLogoutUrl && !askForFeedbackOnLogout ? logoutButton : null}
{askForFeedbackOnLogout ? feedbackScreen : null}
</Styled.Content>
</Styled.Modal>
</Styled.Parent>
);
};
const MeetingEndedContainer: React.FC<MeetingEndedContainerProps> = ({
endedBy,
meetingEndedCode,
joinErrorCode,
}) => {
const {
loading: meetingEndLoading,
error: meetingEndError,
data: meetingEndData,
} = useQuery<MeetingEndDataResponse>(getMeetingEndData);
if (meetingEndLoading || !meetingEndData) {
return (
<MeetingEnded
endedBy=""
joinErrorCode=""
meetingEndedCode=""
allowDefaultLogoutUrl={false}
askForFeedbackOnLogout={false}
learningDashboardAccessToken=""
// eslint-disable-next-line jsx-a11y/aria-role
role=""
learningDashboardBase=""
isBreakout={false}
/>
);
}
if (meetingEndError) {
logger.error('Error on fetching meeting end data: ', meetingEndError);
return (
<MeetingEnded
endedBy=""
joinErrorCode=""
meetingEndedCode=""
allowDefaultLogoutUrl={false}
askForFeedbackOnLogout={false}
learningDashboardAccessToken=""
// eslint-disable-next-line jsx-a11y/aria-role
role=""
learningDashboardBase=""
isBreakout={false}
/>
);
}
const {
user_current,
} = meetingEndData;
const {
role,
meeting,
} = user_current[0];
const {
learningDashboard,
isBreakout,
clientSettings,
} = meeting;
const {
askForFeedbackOnLogout,
allowDefaultLogoutUrl,
learningDashboardBase,
} = clientSettings;
return (
<MeetingEnded
endedBy={endedBy}
joinErrorCode={joinErrorCode}
meetingEndedCode={meetingEndedCode}
allowDefaultLogoutUrl={allowDefaultLogoutUrl}
askForFeedbackOnLogout={askForFeedbackOnLogout}
learningDashboardAccessToken={learningDashboard?.learningDashboardAccessToken}
role={role}
learningDashboardBase={learningDashboardBase}
isBreakout={isBreakout}
/>
);
};
export default MeetingEndedContainer;

View File

@ -0,0 +1,43 @@
import { gql } from '@apollo/client';
export interface MeetingEndDataResponse {
user_current: Array<{
role: string;
meeting: {
learningDashboard: {
learningDashboardAccessToken: string;
}
isBreakout: boolean;
logoutUrl: string;
clientSettings: {
askForFeedbackOnLogout: boolean;
allowDefaultLogoutUrl: boolean;
learningDashboardBase: string;
};
};
}>;
}
export const getMeetingEndData = gql`
query getMeetingEndData {
user_current {
role
meeting {
learningDashboard {
learningDashboardAccessToken
}
isBreakout
logoutUrl
clientSettings {
askForFeedbackOnLogout: clientSettingsJson(path: "$.public.app.askForFeedbackOnLogout")
allowDefaultLogoutUrl: clientSettingsJson(path: "$.public.app.allowDefaultLogoutUrl")
learningDashboardBase: clientSettingsJson(path: "$.public.app.learningDashboardBase")
}
}
}
}
`;
export default {
getMeetingEndData,
};

View File

@ -0,0 +1,55 @@
export const JoinErrorCodeTable = {
NOT_EJECT: 'not_eject_reason',
DUPLICATE_USER: 'duplicate_user_in_meeting_eject_reason',
PERMISSION_FAILED: 'not_enough_permission_eject_reason',
EJECT_USER: 'user_requested_eject_reason',
SYSTEM_EJECT_USER: 'system_requested_eject_reason',
VALIDATE_TOKEN: 'validate_token_failed_eject_reason',
USER_INACTIVITY: 'user_inactivity_eject_reason',
BANNED_USER_REJOINING: 'banned_user_rejoining_reason',
USER_LOGGED_OUT: 'user_logged_out_reason',
MAX_PARTICIPANTS: 'max_participants_reason',
};
export const MeetingEndedTable = {
ENDED_FROM_API: 'ENDED_FROM_API',
ENDED_WHEN_NOT_JOINED: 'ENDED_WHEN_NOT_JOINED',
ENDED_WHEN_LAST_USER_LEFT: 'ENDED_WHEN_LAST_USER_LEFT',
ENDED_AFTER_USER_LOGGED_OUT: 'ENDED_AFTER_USER_LOGGED_OUT',
ENDED_AFTER_EXCEEDING_DURATION: 'ENDED_AFTER_EXCEEDING_DURATION',
BREAKOUT_ENDED_EXCEEDING_DURATION: 'BREAKOUT_ENDED_EXCEEDING_DURATION',
BREAKOUT_ENDED_BY_MOD: 'BREAKOUT_ENDED_BY_MOD',
ENDED_DUE_TO_NO_AUTHED_USER: 'ENDED_DUE_TO_NO_AUTHED_USER',
ENDED_DUE_TO_NO_MODERATOR: 'ENDED_DUE_TO_NO_MODERATOR',
};
export const openLearningDashboardUrl = (
accessToken: string,
mId: string,
sToken:string,
learningDashboardBase: string,
lang: string,
) => {
if (accessToken && setLearningDashboardCookie(accessToken, mId)) {
window.open(`${learningDashboardBase}/?meeting=${mId}&lang=${lang}`, '_blank');
} else {
window.open(`${learningDashboardBase}/?meeting=${mId}&sessionToken=${sToken}&lang=${lang}`, '_blank');
}
};
export const setLearningDashboardCookie = (accessToken: string, mId: string) => {
if (accessToken !== null) {
const lifetime = new Date();
lifetime.setTime(lifetime.getTime() + (3600000)); // 1h (extends 7d when open Dashboard)
document.cookie = `ld-${mId}=${accessToken}; expires=${lifetime.toUTCString()}; path=/`;
return true;
}
return false;
};
export default {
JoinErrorCodeTable,
MeetingEndedTable,
setLearningDashboardCookie,
openLearningDashboardUrl,
};

View File

@ -0,0 +1,114 @@
import { smallOnly } from '/imports/ui/stylesheets/styled-components/breakpoints';
import styled from 'styled-components';
import {
borderRadius,
lgPaddingX,
} from '/imports/ui/stylesheets/styled-components/general';
import {
fontSizeSmall,
fontSizeBase,
fontSizeLarge,
headingsFontWeight,
lineHeightComputed,
} from '/imports/ui/stylesheets/styled-components/typography';
import {
colorWhite,
colorText,
colorBackground,
} from '/imports/ui/stylesheets/styled-components/palette';
const Parent = styled.div`
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: ${colorBackground};
`;
const Modal = styled.div`
display: flex;
padding: ${lgPaddingX};
background-color: ${colorWhite};
flex-direction: column;
border-radius: ${borderRadius};
max-width: 95vw;
width: 600px;
`;
const Content = styled.div`
text-align: center;
`;
const Title = styled.h1`
margin: 0;
font-size: ${fontSizeLarge};
font-weight: ${headingsFontWeight};
`;
const Text = styled.div`
color: ${colorText};
font-weight: normal;
padding: ${lineHeightComputed} 0;
@media ${smallOnly} {
font-size: ${fontSizeSmall};
}
`;
const Wrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const MeetingEndedButton = styled.button`
border: none;
overflow: visible;
border-radius: 2px;
font-weight: 600;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
height: 3rem;
width: 3rem;
display: flex !important;
align-items: center;
box-align: center;
flex-align: center;
box-pack: center;
justify-content: center;
flex-pack: center;
color: var(--btn-primary-color, var(--color-white, #FFF));
background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7));
border: 3px solid transparent;
padding: calc(1.25rem / 2);
@media ${smallOnly} {
font-size: ${fontSizeBase};
}
`;
const TextArea = styled.textarea`
resize: none;
margin: 1rem auto;
width: 100%;
&::placeholder {
text-align: center;
}
`;
export default {
Parent,
Modal,
Content,
Title,
Text,
MeetingEndedButton,
TextArea,
Wrapper,
};

View File

@ -3,7 +3,7 @@ import Auth from '/imports/ui/services/auth';
export default function allowRedirectToLogoutURL() {
const ALLOW_DEFAULT_LOGOUT_URL = Meteor.settings.public.app.allowDefaultLogoutUrl;
const ALLOW_DEFAULT_LOGOUT_URL = window.meetingClientSettings.public.app.allowDefaultLogoutUrl;
const protocolPattern = /^((http|https):\/\/)/;
if (Auth.logoutURL) {
// default logoutURL

View File

@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import Styled from './styles';
import Meetings from '/imports/api/meetings';
const BBB_TABLET_APP_CONFIG = Meteor.settings.public.app.bbbTabletApp;
const BBB_TABLET_APP_CONFIG = window.meetingClientSettings.public.app.bbbTabletApp;
const intlMessages = defineMessages({
title: {

View File

@ -7,7 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl';
import { notify } from '/imports/ui/services/notification';
import TooltipContainer from '/imports/ui/components/common/tooltip/container';
const MUTE_ALERT_CONFIG = Meteor.settings.public.app.mutedAlert;
const MUTE_ALERT_CONFIG = window.meetingClientSettings.public.app.mutedAlert;
const propTypes = {
inputStream: PropTypes.objectOf(PropTypes.any).isRequired,

View File

@ -13,7 +13,7 @@ import { PANELS } from '/imports/ui/components/layout/enums';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import useChat from '/imports/ui/core/hooks/useChat';
const PUBLIC_CONFIG = Meteor.settings.public;
const PUBLIC_CONFIG = window.meetingClientSettings.public;
const NavBarContainer = ({ children, ...props }) => {
const { pluginsExtensibleAreasAggregatedState } = useContext(PluginsContext);

View File

@ -21,7 +21,7 @@ interface TalkingIndicatorSubscriptionData {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const APP_CONFIG = Meteor.settings.public.app;
const APP_CONFIG = window.meetingClientSettings.public.app;
const { enableTalkingIndicator } = APP_CONFIG;
const TALKING_INDICATORS_MAX = 8;

View File

@ -130,8 +130,8 @@ const defaultProps = {
audioCaptionsEnabled: false,
};
const ALLOW_FULLSCREEN = Meteor.settings.public.app.allowFullscreen;
const BBB_TABLET_APP_CONFIG = Meteor.settings.public.app.bbbTabletApp;
const ALLOW_FULLSCREEN = window.meetingClientSettings.public.app.allowFullscreen;
const BBB_TABLET_APP_CONFIG = window.meetingClientSettings.public.app.bbbTabletApp;
const { isSafari, isTabletApp } = browserInfo;
const FULLSCREEN_CHANGE_EVENT = isSafari ? 'webkitfullscreenchange' : 'fullscreenchange';
@ -252,7 +252,7 @@ class OptionsDropdown extends PureComponent {
showHelpButton: helpButton,
helpLink,
allowLogout: allowLogoutSetting,
} = Meteor.settings.public.app;
} = window.meetingClientSettings.public.app;
this.menuItems = [];

View File

@ -11,9 +11,9 @@ import Header from '/imports/ui/components/common/control-header/component';
import NotesDropdown from '/imports/ui/components/notes/notes-dropdown/container';
import { isPresentationEnabled } from '../../services/features';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_group_id;
const DELAY_UNMOUNT_SHARED_NOTES = Meteor.settings.public.app.delayForUnmountOfSharedNote;
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const DELAY_UNMOUNT_SHARED_NOTES = window.meetingClientSettings.public.app.delayForUnmountOfSharedNote;
const intlMessages = defineMessages({
hide: {
id: 'app.notes.hide',

View File

@ -7,7 +7,7 @@ import Service from './service';
import { uniqueId } from '/imports/utils/string-utils';
const DEBOUNCE_TIMEOUT = 15000;
const NOTES_CONFIG = Meteor.settings.public.notes;
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
const NOTES_IS_PINNABLE = NOTES_CONFIG.pinnable;
const intlMessages = defineMessages({

View File

@ -5,7 +5,7 @@ import NotesService from '/imports/ui/components/notes/service';
import { UploadingPresentations } from '/imports/api/presentations';
import { uniqueId } from '/imports/utils/string-utils';
const PADS_CONFIG = Meteor.settings.public.pads;
const PADS_CONFIG = window.meetingClientSettings.public.pads;
async function convertAndUpload(presentations, setPresentation, removePresentation) {
let filename = 'Shared_Notes';

View File

@ -6,8 +6,8 @@ import { Session } from 'meteor/session';
import { ACTIONS, PANELS } from '/imports/ui/components/layout/enums';
import { isSharedNotesEnabled } from '/imports/ui/services/features';
const NOTES_CONFIG = Meteor.settings.public.notes;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const NOTES_CONFIG = window.meetingClientSettings.public.notes;
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
const hasPermission = () => {
const user = Users.findOne(

View File

@ -23,7 +23,7 @@ const STATUS_FAILED = 'failed';
// failed to connect and waiting to try to reconnect
const STATUS_WAITING = 'waiting';
const METEOR_SETTINGS_APP = Meteor.settings.public.app;
const METEOR_SETTINGS_APP = window.meetingClientSettings.public.app;
const REMAINING_TIME_THRESHOLD = METEOR_SETTINGS_APP.remainingTimeThreshold;

View File

@ -48,7 +48,7 @@ let timeRemaining = 0;
let prevTimeRemaining = 0;
let lastAlertTime = null;
const METEOR_SETTINGS_APP = Meteor.settings.public.app;
const METEOR_SETTINGS_APP = window.meetingClientSettings.public.app;
const REMAINING_TIME_ALERT_THRESHOLD_ARRAY = METEOR_SETTINGS_APP.remainingTimeAlertThresholdArray;
const timeRemainingDep = new Tracker.Dependency();

View File

@ -8,7 +8,7 @@ import {
isScreenBroadcasting,
} from '/imports/ui/components/screenshare/service';
const PADS_CONFIG = Meteor.settings.public.pads;
const PADS_CONFIG = window.meetingClientSettings.public.pads;
const THROTTLE_TIMEOUT = 2000;
const getLang = () => {

View File

@ -1,6 +1,6 @@
import { PadsSessions } from '/imports/api/pads';
const COOKIE_CONFIG = Meteor.settings.public.pads.cookie;
const COOKIE_CONFIG = window.meetingClientSettings.public.pads.cookie;
const PATH = COOKIE_CONFIG.path;
const SAME_SITE = COOKIE_CONFIG.sameSite;
const SECURE = COOKIE_CONFIG.secure;

View File

@ -18,7 +18,7 @@ import PluginDomElementManipulationManager from './dom-element-manipulation/mana
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - temporary, while meteor exists in the project
const PLUGINS_CONFIG = Meteor.settings.public.plugins;
const PLUGINS_CONFIG = window.meetingClientSettings.public.plugins;
const PluginsEngineManager = () => {
// If there is no plugin to load, the engine simply returns null

View File

@ -214,7 +214,7 @@ const intlMessages = defineMessages({
}
});
const POLL_SETTINGS = Meteor.settings.public.poll;
const POLL_SETTINGS = window.meetingClientSettings.public.poll;
const ALLOW_CUSTOM_INPUT = POLL_SETTINGS.allowCustomResponseInput;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;

Some files were not shown because too many files have changed in this diff Show More