Compare commits

...

2 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
505ed15323 WIP 2020-06-10 17:10:02 +02:00
Vlad Piersec
8758c222c6 feat(branding): Add ability to customize logo & background 2020-06-10 14:58:27 +02:00
19 changed files with 443 additions and 50 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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;

View File

@@ -40,9 +40,6 @@
#remotePresenceMessage {
display: none !important;
}
#largeVideoContainer {
background-color: $defaultBackground !important;
}
/**
* Thumbnail popover menus can overlap other thumbnails. Setting an auto

46
docker/Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
ARG JITSI_REPO=jitsi
ARG JITSI_GIT_REPO=https://github.com/jitsi/jitsi-meet.git
ARG JITSI_GIT_REF=HEAD
FROM node:12 as builder
ARG JITSI_GIT_REPO
ARG JITSI_GIT_REF
WORKDIR /src
RUN \
git clone $JITSI_GIT_REPO && \
cd jitsi-meet && \
git reset --hard $JITSI_GIT_REF && \
npm install && \
make
FROM ${JITSI_REPO}/base
RUN \
apt-dpkg-wrap apt-get update && \
apt-dpkg-wrap apt-get install -y nginx-extras && \
apt-cleanup && \
rm -f /etc/nginx/conf.d/default.conf
COPY rootfs/ /
COPY --from=builder /src/jitsi-meet/libs /usr/share/jitsi-meet/libs
COPY --from=builder /src/jitsi-meet/static /usr/share/jitsi-meet/static
COPY --from=builder /src/jitsi-meet/sounds /usr/share/jitsi-meet/sounds
COPY --from=builder /src/jitsi-meet/fonts /usr/share/jitsi-meet/fonts
COPY --from=builder /src/jitsi-meet/images /usr/share/jitsi-meet/images
COPY --from=builder /src/jitsi-meet/lang /usr/share/jitsi-meet/lang
COPY --from=builder /src/jitsi-meet/connection_optimization /usr/share/jitsi-meet/connection_optimization
COPY --from=builder /src/jitsi-meet/css/all.css /usr/share/jitsi-meet/css/
COPY --from=builder /src/jitsi-meet/resources/*.sh /usr/share/jitsi-meet/scripts/
COPY --from=builder /src/jitsi-meet/*.html /usr/share/jitsi-meet/
COPY --from=builder /src/jitsi-meet/*.ico /usr/share/jitsi-meet/
COPY --from=builder /src/jitsi-meet/resources/robots.txt /usr/share/jitsi-meet/
ENV XMPP_BOSH_URL_BASE=https://meet.jit.si
ENV XMPP_DOMAIN=meet.jit.si
EXPOSE 8000
VOLUME ["/config"]

View File

@@ -0,0 +1,46 @@
server_name _;
client_max_body_size 0;
root /usr/share/jitsi-meet;
# ssi on with javascript for multidomain variables in config.js
ssi on;
ssi_types application/x-javascript application/javascript;
index index.html index.htm;
error_page 404 /static/404.html;
location = /config.js {
alias /config/config.js;
}
location = /interface_config.js {
alias /config/interface_config.js;
}
location = /external_api.js {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
# ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
alias /usr/share/jitsi-meet/$1/$2;
}
# BOSH
location = /http-bind {
proxy_pass {{ .Env.XMPP_BOSH_URL_BASE }}/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host {{ .Env.XMPP_DOMAIN }};
}
location ~ ^/([^/?&:'"]+)$ {
try_files $uri @root_path;
}
location @root_path {
rewrite ^/(.*)$ / break;
}

View File

@@ -0,0 +1,60 @@
user www-data;
worker_processes 4;
pid /run/nginx.pid;
daemon off;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
client_max_body_size 0;
include /etc/nginx/mime.types;
types {
# add support for wasm MIME type, that is required by specification and it is not part of default mime.types file
application/wasm wasm;
}
default_type application/octet-stream;
##
# Logging Settings
##
access_log /dev/stdout;
error_log /dev/stderr;
##
# Gzip Settings
##
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_vary on;
gzip_min_length 860;
##
## The Sever
##
server {
listen 8000 default_server;
include /config/nginx/meet.conf;
}
}

View File

@@ -0,0 +1,12 @@
#!/usr/bin/with-contenv bash
# make our folders
mkdir -p \
/config/nginx \
/run \
/var/lib/nginx/tmp/client_body \
/var/tmp/nginx
# copy config files
cp /defaults/nginx.conf /config/nginx/nginx.conf
tpl /defaults/meet.conf > /config/nginx/meet.conf

View File

@@ -0,0 +1,3 @@
#!/usr/bin/with-contenv bash
exec nginx -c /config/nginx/nginx.conf

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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',

View File

@@ -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
};
}

View File

@@ -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';

View File

@@ -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
};
}

View File

@@ -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]}` : '';
}

View File

@@ -0,0 +1,4 @@
export * from './actions';
export * from './functions';
import './reducer';

View File

@@ -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;
});

View File

@@ -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);