feat(branding): Add ability to customize logo & background

pull/7008/head jitsi-meet_4715
Vlad Piersec 5 years ago committed by Saúl Ibarra Corretgé
parent 29dc63fbcb
commit 8758c222c6
  1. 17
      config.js
  2. 3
      css/_base.scss
  3. 2
      css/_variables.scss
  4. 3
      css/filmstrip/_tile_view_overrides.scss
  5. 3
      interface_config.js
  6. 26
      modules/UI/videolayout/VideoContainer.js
  7. 1
      react/features/base/config/interfaceConfigWhitelist.js
  8. 75
      react/features/base/react/components/web/Watermarks.js
  9. 9
      react/features/dynamic-branding/actionTypes.js
  10. 66
      react/features/dynamic-branding/actions.js
  11. 15
      react/features/dynamic-branding/functions.js
  12. 4
      react/features/dynamic-branding/index.js
  13. 46
      react/features/dynamic-branding/reducer.js
  14. 62
      react/features/large-video/components/LargeVideo.web.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;
if (this.state.showJitsiWatermark
|| (this.props._isGuest
&& this.state.showJitsiWatermarkForGuests)) {
reactElement = <div className = 'watermark leftwatermark' />;
const { jitsiWatermarkLink } = this.state;
if (jitsiWatermarkLink) {
const {
_customLogoUrl,
_customLogoLink
} = this.props;
if (this._canDisplayJitsiWatermark()) {
const link = _customLogoLink || this.state.jitsiWatermarkLink;
const style = {
backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
maxWidth: 140,
maxHeight: 70
};
reactElement = (<div
className = 'watermark leftwatermark'
style = { style } />);
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…
Cancel
Save