diff --git a/config.js b/config.js index 2bfb82ac4..f1df21b10 100644 --- a/config.js +++ b/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 diff --git a/css/_base.scss b/css/_base.scss index a76ccc1ca..a717cca1f 100644 --- a/css/_base.scss +++ b/css/_base.scss @@ -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 { diff --git a/css/_variables.scss b/css/_variables.scss index 965ab2210..b5cc22c36 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -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; - diff --git a/css/filmstrip/_tile_view_overrides.scss b/css/filmstrip/_tile_view_overrides.scss index 0a75a6d6f..8a13129cf 100644 --- a/css/filmstrip/_tile_view_overrides.scss +++ b/css/filmstrip/_tile_view_overrides.scss @@ -40,9 +40,6 @@ #remotePresenceMessage { display: none !important; } - #largeVideoContainer { - background-color: $defaultBackground !important; - } /** * Thumbnail popover menus can overlap other thumbnails. Setting an auto diff --git a/interface_config.js b/interface_config.js index 6081cc29d..cdde25d57 100644 --- a/interface_config.js +++ b/interface_config.js @@ -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 diff --git a/modules/UI/videolayout/VideoContainer.js b/modules/UI/videolayout/VideoContainer.js index 1ba1a4ed0..445416604 100644 --- a/modules/UI/videolayout/VideoContainer.js +++ b/modules/UI/videolayout/VideoContainer.js @@ -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. * diff --git a/react/features/base/config/interfaceConfigWhitelist.js b/react/features/base/config/interfaceConfigWhitelist.js index d870f0b6e..7306cbbf0 100644 --- a/react/features/base/config/interfaceConfigWhitelist.js +++ b/react/features/base/config/interfaceConfigWhitelist.js @@ -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', diff --git a/react/features/base/react/components/web/Watermarks.js b/react/features/base/react/components/web/Watermarks.js index 32374a23e..e1da87b37 100644 --- a/react/features/base/react/components/web/Watermarks.js +++ b/react/features/base/react/components/web/Watermarks.js @@ -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 { ); } + /** + * 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 { */ _renderJitsiWatermark() { let reactElement = null; + const { + _customLogoUrl, + _customLogoLink + } = this.props; - if (this.state.showJitsiWatermark - || (this.props._isGuest - && this.state.showJitsiWatermarkForGuests)) { - reactElement =
; + 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 = (
); - if (jitsiWatermarkLink) { + if (link) { reactElement = ( { reactElement } @@ -223,12 +268,11 @@ class Watermarks extends Component { * 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 }; } diff --git a/react/features/dynamic-branding/actionTypes.js b/react/features/dynamic-branding/actionTypes.js new file mode 100644 index 000000000..bab4b71bb --- /dev/null +++ b/react/features/dynamic-branding/actionTypes.js @@ -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'; diff --git a/react/features/dynamic-branding/actions.js b/react/features/dynamic-branding/actions.js new file mode 100644 index 000000000..153829a66 --- /dev/null +++ b/react/features/dynamic-branding/actions.js @@ -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 + }; +} diff --git a/react/features/dynamic-branding/functions.js b/react/features/dynamic-branding/functions.js new file mode 100644 index 000000000..951635dc2 --- /dev/null +++ b/react/features/dynamic-branding/functions.js @@ -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]}` : ''; +} diff --git a/react/features/dynamic-branding/index.js b/react/features/dynamic-branding/index.js new file mode 100644 index 000000000..20b2cbc74 --- /dev/null +++ b/react/features/dynamic-branding/index.js @@ -0,0 +1,4 @@ +export * from './actions'; +export * from './functions'; + +import './reducer'; diff --git a/react/features/dynamic-branding/reducer.js b/react/features/dynamic-branding/reducer.js new file mode 100644 index 000000000..ef9ecf785 --- /dev/null +++ b/react/features/dynamic-branding/reducer.js @@ -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; +}); diff --git a/react/features/large-video/components/LargeVideo.web.js b/react/features/large-video/components/LargeVideo.web.js index 2dc69e125..4db151b52 100644 --- a/react/features/large-video/components/LargeVideo.web.js +++ b/react/features/large-video/components/LargeVideo.web.js @@ -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 { + /** + * 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 { * @returns {React$Element} */ render() { + const style = this._getCustomSyles(); + return (
+ id = 'largeVideoContainer' + style = { style }>
@@ -72,6 +100,26 @@ class LargeVideo extends Component {
); } + + /** + * 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 { * * @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);