feat(branding): Add ability to customize logo & background
This commit is contained in:
parent
29dc63fbcb
commit
8758c222c6
17
config.js
17
config.js
|
@ -512,6 +512,23 @@ var config = {
|
|||
// If set to true all muting operations of remote participants will be disabled.
|
||||
// disableRemoteMute: true,
|
||||
|
||||
/**
|
||||
External API url used to receive branding specific information.
|
||||
If there is no url set or there are missing fields, the defaults are applied.
|
||||
None of the fieds are mandatory and the response must have the shape:
|
||||
{
|
||||
// The hex value for the colour used as background
|
||||
backgroundColor: '#fff',
|
||||
// The url for the image used as background
|
||||
backgroundImageUrl: 'https://example.com/background-img.png',
|
||||
// The anchor url used when clicking the logo image
|
||||
logoClickUrl: 'https://example-company.org',
|
||||
// The url used for the image used as logo
|
||||
logoImageUrl: 'https://example.com/logo-img.png'
|
||||
}
|
||||
*/
|
||||
// brandingDataUrl: '',
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
|
|
|
@ -115,8 +115,9 @@ form {
|
|||
.leftwatermark {
|
||||
left: 32px;
|
||||
top: 32px;
|
||||
background-image: url($defaultWatermarkLink);
|
||||
background-position: center left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.rightwatermark {
|
||||
|
|
|
@ -101,7 +101,6 @@ $sidebarWidth: 375px;
|
|||
* Misc.
|
||||
*/
|
||||
$borderRadius: 4px;
|
||||
$defaultWatermarkLink: '../images/watermark.png';
|
||||
$popoverMenuPadding: 13px;
|
||||
$happySoftwareBackground: transparent;
|
||||
$desktopAppDragBarHeight: 25px;
|
||||
|
@ -270,4 +269,3 @@ $chromeExtensionBannerTop: 80px;
|
|||
$chromeExtensionBannerRight: 16px;
|
||||
$chromeExtensionBannerTopInMeeting: 10px;
|
||||
$chromeExtensionBannerRightInMeeeting: 10px;
|
||||
|
||||
|
|
|
@ -40,9 +40,6 @@
|
|||
#remotePresenceMessage {
|
||||
display: none !important;
|
||||
}
|
||||
#largeVideoContainer {
|
||||
background-color: $defaultBackground !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thumbnail popover menus can overlap other thumbnails. Setting an auto
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
/* eslint-disable no-unused-vars, no-var, max-len */
|
||||
|
||||
var interfaceConfig = {
|
||||
// TO FIX: this needs to be handled from SASS variables. There are some
|
||||
// methods allowing to use variables both in css and js.
|
||||
DEFAULT_BACKGROUND: '#474747',
|
||||
DEFAULT_LOGO_URL: '../images/watermark.png',
|
||||
|
||||
/**
|
||||
* Whether or not the blurred video background for large video should be
|
||||
|
|
|
@ -498,9 +498,6 @@ export class VideoContainer extends LargeContainer {
|
|||
});
|
||||
|
||||
this._updateBackground();
|
||||
|
||||
// Reset the large video background depending on the stream.
|
||||
this.setLargeVideoBackground(this.avatarDisplayed);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -533,14 +530,6 @@ export class VideoContainer extends LargeContainer {
|
|||
* @param {boolean} show
|
||||
*/
|
||||
showAvatar(show) {
|
||||
// TO FIX: Video background need to be black, so that we don't have a
|
||||
// flickering effect when scrolling between videos and have the screen
|
||||
// move to grey before going back to video. Avatars though can have the
|
||||
// default background set.
|
||||
// In order to fix this code we need to introduce video background or
|
||||
// find a workaround for the video flickering.
|
||||
this.setLargeVideoBackground(show);
|
||||
|
||||
this.$avatar.css('visibility', show ? 'visible' : 'hidden');
|
||||
this.avatarDisplayed = show;
|
||||
|
||||
|
@ -596,21 +585,6 @@ export class VideoContainer extends LargeContainer {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the large video container background depending on the container
|
||||
* type and the parameter indicating if an avatar is currently shown on
|
||||
* large.
|
||||
*
|
||||
* @param {boolean} isAvatar - Indicates if the avatar is currently shown
|
||||
* on the large video.
|
||||
* @returns {void}
|
||||
*/
|
||||
setLargeVideoBackground(isAvatar) {
|
||||
$('#largeVideoContainer').css('background',
|
||||
this.videoType === VIDEO_CONTAINER_TYPE && !isAvatar
|
||||
? '#000' : interfaceConfig.DEFAULT_BACKGROUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the video element changes dimensions.
|
||||
*
|
||||
|
|
|
@ -18,6 +18,7 @@ export default [
|
|||
'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
|
||||
'CONNECTION_INDICATOR_DISABLED',
|
||||
'DEFAULT_BACKGROUND',
|
||||
'DEFAULT_LOGO_URL',
|
||||
'DISABLE_PRESENCE_STATUS',
|
||||
'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
|
||||
'DEFAULT_LOCAL_DISPLAY_NAME',
|
||||
|
|
|
@ -21,11 +21,27 @@ const _RIGHT_WATERMARK_STYLE = {
|
|||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The user selected url used to navigate to on logo click.
|
||||
*/
|
||||
_customLogoLink: string,
|
||||
|
||||
/**
|
||||
* The url of the user selected logo.
|
||||
*/
|
||||
_customLogoUrl: string,
|
||||
|
||||
/**
|
||||
* Whether or not the current user is logged in through a JWT.
|
||||
*/
|
||||
_isGuest: boolean,
|
||||
|
||||
/**
|
||||
* Flag used to signal that the logo can be displayed.
|
||||
* It becomes true after the user customization options are fetched.
|
||||
*/
|
||||
_readyToDisplayJitsiWatermark: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
@ -133,6 +149,26 @@ class Watermarks extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the watermark is ready to be displayed.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_canDisplayJitsiWatermark() {
|
||||
const {
|
||||
showJitsiWatermark,
|
||||
showJitsiWatermarkForGuests
|
||||
} = this.state;
|
||||
const {
|
||||
_isGuest,
|
||||
_readyToDisplayJitsiWatermark
|
||||
} = this.props;
|
||||
|
||||
return _readyToDisplayJitsiWatermark
|
||||
&& (showJitsiWatermark || (_isGuest && showJitsiWatermarkForGuests));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a brand watermark if it is enabled.
|
||||
*
|
||||
|
@ -173,18 +209,27 @@ class Watermarks extends Component<Props, State> {
|
|||
*/
|
||||
_renderJitsiWatermark() {
|
||||
let reactElement = null;
|
||||
const {
|
||||
_customLogoUrl,
|
||||
_customLogoLink
|
||||
} = this.props;
|
||||
|
||||
if (this.state.showJitsiWatermark
|
||||
|| (this.props._isGuest
|
||||
&& this.state.showJitsiWatermarkForGuests)) {
|
||||
reactElement = <div className = 'watermark leftwatermark' />;
|
||||
if (this._canDisplayJitsiWatermark()) {
|
||||
const link = _customLogoLink || this.state.jitsiWatermarkLink;
|
||||
const style = {
|
||||
backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
|
||||
maxWidth: 140,
|
||||
maxHeight: 70
|
||||
};
|
||||
|
||||
const { jitsiWatermarkLink } = this.state;
|
||||
reactElement = (<div
|
||||
className = 'watermark leftwatermark'
|
||||
style = { style } />);
|
||||
|
||||
if (jitsiWatermarkLink) {
|
||||
if (link) {
|
||||
reactElement = (
|
||||
<a
|
||||
href = { jitsiWatermarkLink }
|
||||
href = { link }
|
||||
target = '_new'>
|
||||
{ reactElement }
|
||||
</a>
|
||||
|
@ -223,12 +268,11 @@ class Watermarks extends Component<Props, State> {
|
|||
* Maps parts of Redux store to component prop types.
|
||||
*
|
||||
* @param {Object} state - Snapshot of Redux store.
|
||||
* @returns {{
|
||||
* _isGuest: boolean
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { isGuest } = state['features/base/jwt'];
|
||||
const { customizationReady, logoClickUrl, logoImageUrl } = state['features/dynamic-branding'];
|
||||
|
||||
return {
|
||||
/**
|
||||
|
@ -238,7 +282,10 @@ function _mapStateToProps(state) {
|
|||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
_isGuest: isGuest
|
||||
_customLogoLink: logoClickUrl,
|
||||
_customLogoUrl: logoImageUrl,
|
||||
_isGuest: isGuest,
|
||||
_readyToDisplayJitsiWatermark: customizationReady
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Action used to set custom user properties.
|
||||
*/
|
||||
export const SET_DYNAMIC_BRANDING_DATA = 'SET_DYNAMIC_BRANDING_DATA';
|
||||
|
||||
/**
|
||||
* Action used to signal the branding elements are ready to be displayed
|
||||
*/
|
||||
export const SET_DYNAMIC_BRANDING_READY = 'SET_DYNAMIC_BRANDING_READY';
|
|
@ -0,0 +1,66 @@
|
|||
// @flow
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import { doGetJSON } from '../base/util';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
|
||||
import { extractFqnFromPath } from './functions';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* 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: Function, getState: Function) {
|
||||
const state = getState();
|
||||
const baseUrl = state['features/base/config'].brandingDataUrl;
|
||||
const { customizationReady } = state['features/dynamic-branding'];
|
||||
|
||||
if (!customizationReady) {
|
||||
const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
|
||||
|
||||
if (baseUrl && fqn) {
|
||||
try {
|
||||
const res = await doGetJSON(`${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`);
|
||||
|
||||
return dispatch(setDynamicBrandingData(res));
|
||||
} catch (err) {
|
||||
logger.error('Error fetching branding data', err);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(setDynamicBrandingReady());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the user customizations.
|
||||
*
|
||||
* @param {Object} value - The custom data to be set.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function setDynamicBrandingData(value) {
|
||||
return {
|
||||
type: SET_DYNAMIC_BRANDING_DATA,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Action used to signal the branding elements are ready to be displayed.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
function setDynamicBrandingReady() {
|
||||
return {
|
||||
type: SET_DYNAMIC_BRANDING_READY
|
||||
};
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
|
||||
/**
|
||||
* Extracts the fqn part from a path, where fqn represents
|
||||
* tenant/roomName.
|
||||
*
|
||||
* @param {string} path - The URL path.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function extractFqnFromPath(path: string) {
|
||||
const parts = path.split('/');
|
||||
const len = parts.length;
|
||||
|
||||
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './actions';
|
||||
export * from './functions';
|
||||
|
||||
import './reducer';
|
|
@ -0,0 +1,46 @@
|
|||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } 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 = {
|
||||
backgroundColor: '',
|
||||
backgroundImageUrl: '',
|
||||
customizationReady: false,
|
||||
logoClickUrl: '',
|
||||
logoImageUrl: ''
|
||||
};
|
||||
|
||||
/**
|
||||
* Reduces redux actions for the purposes of the feature {@code dynamic-branding}.
|
||||
*/
|
||||
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { backgroundColor, backgroundImageUrl, logoClickUrl, logoImageUrl } = action.value;
|
||||
|
||||
return {
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
logoClickUrl,
|
||||
logoImageUrl,
|
||||
customizationReady: true
|
||||
};
|
||||
}
|
||||
case SET_DYNAMIC_BRANDING_READY:
|
||||
return {
|
||||
...state,
|
||||
customizationReady: true
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
|
@ -4,12 +4,28 @@ import React, { Component } from 'react';
|
|||
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { connect } from '../../base/redux';
|
||||
import { fetchCustomBrandingData } from '../../dynamic-branding';
|
||||
import { Captions } from '../../subtitles/';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The user selected background color.
|
||||
*/
|
||||
_customBackgroundColor: string,
|
||||
|
||||
/**
|
||||
* The user selected background image url.
|
||||
*/
|
||||
_customBackgroundImageUrl: string,
|
||||
|
||||
/**
|
||||
* Fetches the branding data.
|
||||
*/
|
||||
_fetchCustomBrandingData: Function,
|
||||
|
||||
/**
|
||||
* Used to determine the value of the autoplay attribute of the underlying
|
||||
* video element.
|
||||
|
@ -24,6 +40,15 @@ type Props = {
|
|||
* @extends Component
|
||||
*/
|
||||
class LargeVideo extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#componentDidMount}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props._fetchCustomBrandingData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -31,10 +56,13 @@ class LargeVideo extends Component<Props> {
|
|||
* @returns {React$Element}
|
||||
*/
|
||||
render() {
|
||||
const style = this._getCustomSyles();
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'videocontainer'
|
||||
id = 'largeVideoContainer'>
|
||||
id = 'largeVideoContainer'
|
||||
style = { style }>
|
||||
<div id = 'sharedVideo'>
|
||||
<div id = 'sharedVideoIFrame' />
|
||||
</div>
|
||||
|
@ -72,6 +100,26 @@ class LargeVideo extends Component<Props> {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the custom styles object.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getCustomSyles() {
|
||||
const styles = {};
|
||||
const { _customBackgroundColor, _customBackgroundImageUrl } = this.props;
|
||||
|
||||
styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
|
||||
|
||||
if (_customBackgroundImageUrl) {
|
||||
styles.backgroundImage = `url(${_customBackgroundImageUrl})`;
|
||||
styles.backgroundSize = 'cover';
|
||||
}
|
||||
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -80,17 +128,21 @@ class LargeVideo extends Component<Props> {
|
|||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _noAutoPlayVideo: boolean
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const testingConfig = state['features/base/config'].testing;
|
||||
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
|
||||
|
||||
return {
|
||||
_customBackgroundColor: backgroundColor,
|
||||
_customBackgroundImageUrl: backgroundImageUrl,
|
||||
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo
|
||||
};
|
||||
}
|
||||
|
||||
const _mapDispatchToProps = {
|
||||
_fetchCustomBrandingData: fetchCustomBrandingData
|
||||
};
|
||||
|
||||
export default connect(_mapStateToProps)(LargeVideo);
|
||||
export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);
|
||||
|
|
Loading…
Reference in New Issue