This commit is contained in:
19
react/features/dynamic-branding/actionTypes.ts
Normal file
19
react/features/dynamic-branding/actionTypes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Action used to set custom user properties.
|
||||
*/
|
||||
export const SET_DYNAMIC_BRANDING_DATA = 'SET_DYNAMIC_BRANDING_DATA';
|
||||
|
||||
/**
|
||||
* Action used to signal the customization failed.
|
||||
*/
|
||||
export const SET_DYNAMIC_BRANDING_FAILED = 'SET_DYNAMIC_BRANDING_FAILED';
|
||||
|
||||
/**
|
||||
* Action used to signal the branding elements are ready to be displayed
|
||||
*/
|
||||
export const SET_DYNAMIC_BRANDING_READY = 'SET_DYNAMIC_BRANDING_READY';
|
||||
|
||||
/**
|
||||
* Action used to unset branding elements
|
||||
*/
|
||||
export const UNSET_DYNAMIC_BRANDING = 'UNSET_DYNAMIC_BRANDING';
|
||||
78
react/features/dynamic-branding/actions.any.ts
Normal file
78
react/features/dynamic-branding/actions.any.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { doGetJSON } from '../base/util/httpUtils';
|
||||
|
||||
import {
|
||||
SET_DYNAMIC_BRANDING_DATA,
|
||||
SET_DYNAMIC_BRANDING_FAILED,
|
||||
SET_DYNAMIC_BRANDING_READY
|
||||
} from './actionTypes';
|
||||
import { getDynamicBrandingUrl } from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
* Fetches custom branding data.
|
||||
* If there is no data or the request fails, sets the `customizationReady` flag
|
||||
* so the defaults can be displayed.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function fetchCustomBrandingData() {
|
||||
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const state = getState();
|
||||
const { customizationReady } = state['features/dynamic-branding'];
|
||||
|
||||
if (!customizationReady) {
|
||||
const url = await getDynamicBrandingUrl(state);
|
||||
|
||||
if (url) {
|
||||
try {
|
||||
const res = await doGetJSON(url);
|
||||
|
||||
return dispatch(setDynamicBrandingData(res));
|
||||
} catch (err) {
|
||||
logger.error('Error fetching branding data', err);
|
||||
|
||||
return dispatch(setDynamicBrandingFailed());
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setDynamicBrandingReady());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the user customizations.
|
||||
*
|
||||
* @param {Object} value - The custom data to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDynamicBrandingData(value: Object) {
|
||||
return {
|
||||
type: SET_DYNAMIC_BRANDING_DATA,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to signal the branding elements are ready to be displayed.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDynamicBrandingReady() {
|
||||
return {
|
||||
type: SET_DYNAMIC_BRANDING_READY
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to signal the branding request failed.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setDynamicBrandingFailed() {
|
||||
return {
|
||||
type: SET_DYNAMIC_BRANDING_FAILED
|
||||
};
|
||||
}
|
||||
56
react/features/dynamic-branding/actions.native.ts
Normal file
56
react/features/dynamic-branding/actions.native.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { doGetJSON } from '../base/util/httpUtils';
|
||||
|
||||
import { UNSET_DYNAMIC_BRANDING } from './actionTypes';
|
||||
import {
|
||||
setDynamicBrandingData,
|
||||
setDynamicBrandingFailed,
|
||||
setDynamicBrandingReady
|
||||
} from './actions.any';
|
||||
import { getDynamicBrandingUrl } from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
* Fetches custom branding data.
|
||||
* If there is no data or the request fails, sets the `customizationReady` flag
|
||||
* so the defaults can be displayed.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function fetchCustomBrandingData() {
|
||||
return async function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
const state = getState();
|
||||
const dynamicBrandingUrl = await getDynamicBrandingUrl(state);
|
||||
|
||||
if (dynamicBrandingUrl) {
|
||||
try {
|
||||
return dispatch(
|
||||
setDynamicBrandingData(
|
||||
await doGetJSON(dynamicBrandingUrl))
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error('Error fetching branding data', err);
|
||||
|
||||
return dispatch(
|
||||
setDynamicBrandingFailed()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
dispatch(unsetDynamicBranding());
|
||||
}
|
||||
|
||||
dispatch(setDynamicBrandingReady());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to unset branding elements.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function unsetDynamicBranding() {
|
||||
return {
|
||||
type: UNSET_DYNAMIC_BRANDING
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { Image, ImageStyle, StyleProp, ViewStyle } from 'react-native';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
interface IProps {
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that displays a branding background image.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
const BrandingImageBackground: React.FC<IProps> = ({ uri }: IProps) => {
|
||||
const imageType = uri?.substr(uri.lastIndexOf('/') + 1);
|
||||
const imgSrc = uri ? uri : undefined;
|
||||
|
||||
let backgroundImage;
|
||||
|
||||
if (!uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (imageType?.includes('.svg')) {
|
||||
backgroundImage
|
||||
= (
|
||||
<SvgUri
|
||||
height = '100%'
|
||||
|
||||
// Force uniform scaling.
|
||||
// Align the <min-x> of the element's viewBox
|
||||
// with the smallest X value of the viewport.
|
||||
// Align the <min-y> of the element's viewBox
|
||||
// with the smallest Y value of the viewport.
|
||||
preserveAspectRatio = 'xMinYMin'
|
||||
style = { styles.brandingImageBackgroundSvg as StyleProp<ViewStyle> }
|
||||
uri = { imgSrc ?? null }
|
||||
viewBox = '0 0 400 650'
|
||||
width = '100%' />
|
||||
);
|
||||
} else {
|
||||
backgroundImage
|
||||
= (
|
||||
<Image
|
||||
source = {{ uri: imgSrc }}
|
||||
style = { styles.brandingImageBackground as StyleProp<ImageStyle> } />
|
||||
);
|
||||
}
|
||||
|
||||
return backgroundImage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code DialInLink} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const { backgroundImageUrl } = state['features/dynamic-branding'];
|
||||
|
||||
return {
|
||||
uri: backgroundImageUrl
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(BrandingImageBackground);
|
||||
15
react/features/dynamic-branding/components/native/styles.ts
Normal file
15
react/features/dynamic-branding/components/native/styles.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
|
||||
/**
|
||||
* {@code BrandingImageBackground} Style.
|
||||
*/
|
||||
brandingImageBackgroundSvg: {
|
||||
position: 'absolute'
|
||||
},
|
||||
|
||||
brandingImageBackground: {
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
96
react/features/dynamic-branding/functions.any.ts
Normal file
96
react/features/dynamic-branding/functions.any.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { cleanSvg } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Extracts the fqn part from a path, where fqn represents
|
||||
* tenant/roomName.
|
||||
*
|
||||
* @param {Object} state - A redux state.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function extractFqnFromPath(state?: IReduxState) {
|
||||
let pathname;
|
||||
|
||||
if (window.location.pathname) {
|
||||
pathname = window.location.pathname;
|
||||
} else if (state?.['features/base/connection']) {
|
||||
pathname = state['features/base/connection'].locationURL?.pathname ?? '';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parts = pathname.split('/');
|
||||
const len = parts.length;
|
||||
|
||||
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : parts[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url used for fetching dynamic branding.
|
||||
*
|
||||
* @param {Object | Function} stateful - The redux store, state, or
|
||||
* {@code getState} function.
|
||||
* @returns {string}
|
||||
*/
|
||||
export async function getDynamicBrandingUrl(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
|
||||
// NB: On web this is dispatched really early, before the config has been stored in the
|
||||
// state. Thus, fetch it from the window global.
|
||||
const config
|
||||
= navigator.product === 'ReactNative' ? state['features/base/config'] : window.config;
|
||||
const { dynamicBrandingUrl } = config;
|
||||
|
||||
if (dynamicBrandingUrl) {
|
||||
return dynamicBrandingUrl;
|
||||
}
|
||||
|
||||
const { brandingDataUrl: baseUrl } = config;
|
||||
const fqn = extractFqnFromPath(state);
|
||||
|
||||
if (baseUrl && fqn) {
|
||||
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selector used for getting the load state of the dynamic branding data.
|
||||
*
|
||||
* @param {Object} state - Global state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isDynamicBrandingDataLoaded(state: IReduxState) {
|
||||
return state['features/dynamic-branding'].customizationReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch SVG XMLs from branding icons urls.
|
||||
*
|
||||
* @param {Object} customIcons - The map of branded icons.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const fetchCustomIcons = async (customIcons: Record<string, string>) => {
|
||||
const localCustomIcons: Record<string, string> = {};
|
||||
|
||||
for (const [ key, url ] of Object.entries(customIcons)) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (response.ok) {
|
||||
const svgXml = await response.text();
|
||||
|
||||
localCustomIcons[key] = cleanSvg(svgXml);
|
||||
} else {
|
||||
logger.error(`Failed to fetch ${url}. Status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching ${url}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return localCustomIcons;
|
||||
};
|
||||
9
react/features/dynamic-branding/functions.native.ts
Normal file
9
react/features/dynamic-branding/functions.native.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Sanitizes the given SVG by removing dangerous elements.
|
||||
*
|
||||
* @param {string} svg - The SVG string to clean.
|
||||
* @returns {string} The sanitized SVG string.
|
||||
*/
|
||||
export function cleanSvg(svg: string): string {
|
||||
return svg;
|
||||
}
|
||||
127
react/features/dynamic-branding/functions.web.ts
Normal file
127
react/features/dynamic-branding/functions.web.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import { adaptV4Theme, createTheme } from '@mui/material/styles';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
import { breakpoints, colorMap, font, shape, spacing, typography } from '../base/ui/Tokens';
|
||||
import { createColorTokens } from '../base/ui/utils';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Sanitizes the given SVG by removing dangerous elements.
|
||||
*
|
||||
* @param {string} svg - The SVG string to clean.
|
||||
* @returns {string} The sanitized SVG string.
|
||||
*/
|
||||
export function cleanSvg(svg: string): string {
|
||||
return DOMPurify.sanitize(svg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts unitless fontSize and lineHeight values in a typography style object to rem units.
|
||||
* Backward compatibility: This conversion supports custom themes that may still override
|
||||
* typography values with numeric (pixel-based) values instead of rem strings.
|
||||
*
|
||||
* @param {Object} style - The typography style object to convert.
|
||||
* @returns {void}
|
||||
*/
|
||||
function convertTypographyToRem(style: any): void {
|
||||
if (style) {
|
||||
// Support for backward compatibility with numeric font size overrides
|
||||
if (typeof style.fontSize === 'number') {
|
||||
style.fontSize = `${style.fontSize / DEFAULT_FONT_SIZE}rem`;
|
||||
}
|
||||
// Support for backward compatibility with numeric line height overrides
|
||||
if (typeof style.lineHeight === 'number') {
|
||||
style.lineHeight = `${style.lineHeight / DEFAULT_FONT_SIZE}rem`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates MUI branding theme based on the custom theme json.
|
||||
*
|
||||
* @param {Object} customTheme - The branded custom theme.
|
||||
* @returns {Object} - The MUI theme.
|
||||
*/
|
||||
export function createMuiBrandingTheme(customTheme: Theme) {
|
||||
const {
|
||||
palette: customPalette,
|
||||
shape: customShape,
|
||||
typography: customTypography,
|
||||
breakpoints: customBreakpoints,
|
||||
spacing: customSpacing
|
||||
} = customTheme;
|
||||
|
||||
const newPalette = createColorTokens(colorMap);
|
||||
|
||||
if (customPalette) {
|
||||
overwriteRecurrsive(newPalette, customPalette);
|
||||
}
|
||||
|
||||
const newShape = { ...shape };
|
||||
|
||||
if (customShape) {
|
||||
overwriteRecurrsive(newShape, customShape);
|
||||
}
|
||||
|
||||
const newTypography = {
|
||||
font: { ...font },
|
||||
...typography
|
||||
};
|
||||
|
||||
if (customTypography) {
|
||||
overwriteRecurrsive(newTypography, customTypography);
|
||||
|
||||
// Convert typography values to rem units in case some of the overrides are using the legacy unitless format.
|
||||
// Note: We do the conversion onlt when we do have custom typography overrides. All other values are already in rem.
|
||||
for (const variant of Object.keys(newTypography)) {
|
||||
convertTypographyToRem((newTypography as Record<string, any>)[variant]);
|
||||
}
|
||||
}
|
||||
|
||||
const newBreakpoints = { ...breakpoints };
|
||||
|
||||
if (customBreakpoints) {
|
||||
overwriteRecurrsive(newBreakpoints, customBreakpoints);
|
||||
}
|
||||
|
||||
let newSpacing: any = [ ...spacing ];
|
||||
|
||||
if (customSpacing?.length) {
|
||||
newSpacing = customSpacing;
|
||||
}
|
||||
|
||||
return createTheme(adaptV4Theme({
|
||||
spacing: newSpacing,
|
||||
palette: newPalette,
|
||||
shape: newShape,
|
||||
|
||||
// @ts-ignore
|
||||
typography: newTypography,
|
||||
|
||||
// @ts-ignore
|
||||
breakpoints: newBreakpoints
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites recursively values from object 2 into object 1 based on common keys.
|
||||
* (Merges object2 into object1).
|
||||
*
|
||||
* @param {Object} obj1 - The object holding the merged values.
|
||||
* @param {Object} obj2 - The object to compare to and take values from.
|
||||
* @returns {void}
|
||||
*/
|
||||
function overwriteRecurrsive(obj1: Object, obj2: Object) {
|
||||
Object.keys(obj2).forEach(key => {
|
||||
if (obj1.hasOwnProperty(key)) {
|
||||
if (typeof obj1[key as keyof typeof obj1] === 'object') {
|
||||
overwriteRecurrsive(obj1[key as keyof typeof obj1], obj2[key as keyof typeof obj2]);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
obj1[key] = obj2[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
3
react/features/dynamic-branding/logger.ts
Normal file
3
react/features/dynamic-branding/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/dynamic-branding');
|
||||
103
react/features/dynamic-branding/middleware.any.ts
Normal file
103
react/features/dynamic-branding/middleware.any.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { PARTICIPANT_ROLE_CHANGED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
|
||||
import { fetchCustomIcons } from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { customIcons } = action.value;
|
||||
|
||||
if (customIcons) {
|
||||
fetchCustomIcons(customIcons)
|
||||
.then(localCustomIcons => {
|
||||
action.value.brandedIcons = localCustomIcons;
|
||||
|
||||
return next(action);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
logger.error('Error fetching branded custom icons:', error);
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_ROLE_CHANGED: {
|
||||
const state = store.getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
if (localParticipant?.id !== action.participant.id
|
||||
&& action.participant.role !== PARTICIPANT_ROLE.MODERATOR) {
|
||||
break;
|
||||
}
|
||||
|
||||
maybeUpdatePermissions(state);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_DYNAMIC_BRANDING_READY: {
|
||||
const state = store.getState();
|
||||
|
||||
if (!isLocalParticipantModerator(state)) {
|
||||
break;
|
||||
}
|
||||
|
||||
maybeUpdatePermissions(state);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the permissions metadata for the current conference if the local participant is a moderator.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
function maybeUpdatePermissions(stateful: IStateful): void {
|
||||
const {
|
||||
groupChatRequiresPermission,
|
||||
pollCreationRequiresPermission
|
||||
} = toState(stateful)['features/dynamic-branding'];
|
||||
|
||||
if (groupChatRequiresPermission || pollCreationRequiresPermission) {
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
if (!conference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const permissions: {
|
||||
groupChatRestricted?: boolean;
|
||||
pollCreationRestricted?: boolean;
|
||||
} = conference.getMetadataHandler().getMetadata().permissions || {};
|
||||
let sendUpdate = false;
|
||||
|
||||
if (groupChatRequiresPermission && !permissions.groupChatRestricted) {
|
||||
permissions.groupChatRestricted = true;
|
||||
sendUpdate = true;
|
||||
}
|
||||
|
||||
if (pollCreationRequiresPermission && !permissions.pollCreationRestricted) {
|
||||
permissions.pollCreationRestricted = true;
|
||||
sendUpdate = true;
|
||||
}
|
||||
|
||||
if (sendUpdate) {
|
||||
conference.getMetadataHandler().setMetadata('permissions', permissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
react/features/dynamic-branding/middleware.native.ts
Normal file
35
react/features/dynamic-branding/middleware.native.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
|
||||
import { fetchCustomBrandingData } from './actions.native';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
|
||||
store.dispatch(fetchCustomBrandingData());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { avatarBackgrounds = [] } = action.value;
|
||||
|
||||
// The backend may send an empty string, make sure we skip that.
|
||||
if (Array.isArray(avatarBackgrounds)) {
|
||||
// TODO: implement support for gradients.
|
||||
action.value.avatarBackgrounds = avatarBackgrounds.filter(
|
||||
(color: string) => !color.includes('linear-gradient')
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
27
react/features/dynamic-branding/middleware.web.ts
Normal file
27
react/features/dynamic-branding/middleware.web.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { APP_WILL_MOUNT } from '../base/app/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from './actionTypes';
|
||||
import { fetchCustomBrandingData } from './actions.any';
|
||||
import { createMuiBrandingTheme } from './functions.web';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT: {
|
||||
|
||||
store.dispatch(fetchCustomBrandingData());
|
||||
break;
|
||||
}
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { customTheme } = action.value;
|
||||
|
||||
if (customTheme) {
|
||||
action.value.muiBrandedTheme = createMuiBrandingTheme(customTheme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
290
react/features/dynamic-branding/reducer.ts
Normal file
290
react/features/dynamic-branding/reducer.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { type Image } from '../virtual-background/constants';
|
||||
|
||||
import {
|
||||
SET_DYNAMIC_BRANDING_DATA,
|
||||
SET_DYNAMIC_BRANDING_FAILED,
|
||||
SET_DYNAMIC_BRANDING_READY,
|
||||
UNSET_DYNAMIC_BRANDING
|
||||
} from './actionTypes';
|
||||
|
||||
|
||||
/**
|
||||
* The name of the redux store/state property which is the root of the redux
|
||||
* state of the feature {@code dynamic-branding}.
|
||||
*/
|
||||
const STORE_NAME = 'features/dynamic-branding';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
|
||||
/**
|
||||
* The pool of avatar backgrounds.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
avatarBackgrounds: [],
|
||||
|
||||
/**
|
||||
* The custom background color for the LargeVideo.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
backgroundColor: '',
|
||||
|
||||
/**
|
||||
* The custom background image used on the LargeVideo.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
backgroundImageUrl: '',
|
||||
|
||||
/**
|
||||
* Flag indicating that the branding data can be displayed.
|
||||
* This is used in order to avoid image flickering / text changing(blipping).
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
customizationReady: false,
|
||||
|
||||
/**
|
||||
* Flag indicating that the dynamic branding data request has failed.
|
||||
* When the request fails there is no logo (JitsiWatermark) displayed.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
customizationFailed: false,
|
||||
|
||||
/**
|
||||
* Flag indicating that the dynamic branding has not been modified and should use
|
||||
* the default options.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
defaultBranding: true,
|
||||
|
||||
/**
|
||||
* Url for a custom page for DID numbers list.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
didPageUrl: '',
|
||||
|
||||
/**
|
||||
* Whether participant can only send group chat message if `send-groupchat`
|
||||
* feature is enabled in jwt.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
groupChatRequiresPermission: false,
|
||||
|
||||
/**
|
||||
* The custom invite domain.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
inviteDomain: '',
|
||||
|
||||
/**
|
||||
* An object containing the mapping between the language and url where the translation
|
||||
* bundle is hosted.
|
||||
*
|
||||
* @public
|
||||
* @type {Object}
|
||||
*/
|
||||
labels: null,
|
||||
|
||||
/**
|
||||
* The custom url used when the user clicks the logo.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
logoClickUrl: '',
|
||||
|
||||
/**
|
||||
* The custom logo (JitisWatermark).
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
logoImageUrl: '',
|
||||
|
||||
/**
|
||||
* The generated MUI branded theme based on the custom theme json.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
muiBrandedTheme: undefined,
|
||||
|
||||
/**
|
||||
* Whether participant can only create polls if `create-polls` feature is enabled in jwt.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
pollCreationRequiresPermission: false,
|
||||
|
||||
/**
|
||||
* The lobby/prejoin background.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
premeetingBackground: '',
|
||||
|
||||
/**
|
||||
* Flag used to signal if the app should use a custom logo or not.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
useDynamicBrandingData: false,
|
||||
|
||||
/**
|
||||
* An array of images to be used as virtual backgrounds instead of the default ones.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<Object>}
|
||||
*/
|
||||
virtualBackgrounds: []
|
||||
};
|
||||
|
||||
export interface IDynamicBrandingState {
|
||||
avatarBackgrounds: string[];
|
||||
backgroundColor: string;
|
||||
backgroundImageUrl: string;
|
||||
brandedIcons?: Record<string, string>;
|
||||
customizationFailed: boolean;
|
||||
customizationReady: boolean;
|
||||
defaultBranding: boolean;
|
||||
defaultTranscriptionLanguage?: boolean;
|
||||
didPageUrl: string;
|
||||
groupChatRequiresPermission: boolean;
|
||||
inviteDomain: string;
|
||||
labels: Object | null;
|
||||
logoClickUrl: string;
|
||||
logoImageUrl: string;
|
||||
muiBrandedTheme?: boolean;
|
||||
pollCreationRequiresPermission: boolean;
|
||||
premeetingBackground: string;
|
||||
requireRecordingConsent?: boolean;
|
||||
sharedVideoAllowedURLDomains?: Array<string>;
|
||||
showGiphyIntegration?: boolean;
|
||||
skipRecordingConsentInMeeting?: boolean;
|
||||
supportUrl?: string;
|
||||
useDynamicBrandingData: boolean;
|
||||
virtualBackgrounds: Array<Image>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces redux actions for the purposes of the feature {@code dynamic-branding}.
|
||||
*/
|
||||
ReducerRegistry.register<IDynamicBrandingState>(STORE_NAME, (state = DEFAULT_STATE, action): IDynamicBrandingState => {
|
||||
switch (action.type) {
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const {
|
||||
avatarBackgrounds,
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
defaultBranding,
|
||||
didPageUrl,
|
||||
groupChatRequiresPermission,
|
||||
inviteDomain,
|
||||
labels,
|
||||
logoClickUrl,
|
||||
logoImageUrl,
|
||||
muiBrandedTheme,
|
||||
pollCreationRequiresPermission,
|
||||
premeetingBackground,
|
||||
requireRecordingConsent,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
skipRecordingConsentInMeeting,
|
||||
supportUrl,
|
||||
virtualBackgrounds
|
||||
} = action.value;
|
||||
|
||||
return {
|
||||
avatarBackgrounds,
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
defaultBranding,
|
||||
didPageUrl,
|
||||
groupChatRequiresPermission,
|
||||
inviteDomain,
|
||||
labels,
|
||||
logoClickUrl,
|
||||
logoImageUrl,
|
||||
muiBrandedTheme,
|
||||
pollCreationRequiresPermission,
|
||||
premeetingBackground,
|
||||
requireRecordingConsent,
|
||||
sharedVideoAllowedURLDomains,
|
||||
showGiphyIntegration,
|
||||
skipRecordingConsentInMeeting,
|
||||
supportUrl,
|
||||
customizationFailed: false,
|
||||
customizationReady: true,
|
||||
useDynamicBrandingData: true,
|
||||
virtualBackgrounds: formatImages(virtualBackgrounds || [])
|
||||
};
|
||||
}
|
||||
case SET_DYNAMIC_BRANDING_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
customizationReady: true,
|
||||
customizationFailed: true,
|
||||
useDynamicBrandingData: true
|
||||
};
|
||||
}
|
||||
case SET_DYNAMIC_BRANDING_READY:
|
||||
return {
|
||||
...state,
|
||||
customizationReady: true
|
||||
};
|
||||
|
||||
case UNSET_DYNAMIC_BRANDING:
|
||||
return DEFAULT_STATE;
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Transforms the branding images into an array of Images objects ready
|
||||
* to be used as virtual backgrounds.
|
||||
*
|
||||
* @param {Array<string>} images - The branding images.
|
||||
* @private
|
||||
* @returns {{Props}}
|
||||
*/
|
||||
function formatImages(images: Array<string> | Array<{ src: string; tooltip?: string; }>): Array<Image> {
|
||||
return images.map((img, i) => {
|
||||
let src;
|
||||
let tooltip;
|
||||
|
||||
if (typeof img === 'object') {
|
||||
({ src, tooltip } = img);
|
||||
} else {
|
||||
src = img;
|
||||
}
|
||||
|
||||
return {
|
||||
id: `branding-${i}`,
|
||||
src,
|
||||
tooltip
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user