Kaynağa Gözat

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

master
Vlad Piersec 5 yıl önce
ebeveyn
işleme
8758c222c6

+ 17
- 0
config.js Dosyayı Görüntüle

@@ -512,6 +512,23 @@ var config = {
512 512
     // If set to true all muting operations of remote participants will be disabled.
513 513
     // disableRemoteMute: true,
514 514
 
515
+    /**
516
+     External API url used to receive branding specific information.
517
+     If there is no url set or there are missing fields, the defaults are applied.
518
+     None of the fieds are mandatory and the response must have the shape:
519
+     {
520
+         // The hex value for the colour used as background
521
+         backgroundColor: '#fff',
522
+         // The url for the image used as background
523
+         backgroundImageUrl: 'https://example.com/background-img.png',
524
+         // The anchor url used when clicking the logo image
525
+         logoClickUrl: 'https://example-company.org',
526
+         // The url used for the image used as logo
527
+         logoImageUrl: 'https://example.com/logo-img.png'
528
+     }
529
+    */
530
+    // brandingDataUrl: '',
531
+
515 532
     // List of undocumented settings used in jitsi-meet
516 533
     /**
517 534
      _immediateReloadThreshold

+ 2
- 1
css/_base.scss Dosyayı Görüntüle

@@ -115,8 +115,9 @@ form {
115 115
 .leftwatermark {
116 116
     left: 32px;
117 117
     top: 32px;
118
-    background-image: url($defaultWatermarkLink);
119 118
     background-position: center left;
119
+    background-repeat: no-repeat;
120
+    background-size: contain;
120 121
 }
121 122
 
122 123
 .rightwatermark {

+ 0
- 2
css/_variables.scss Dosyayı Görüntüle

@@ -101,7 +101,6 @@ $sidebarWidth: 375px;
101 101
  * Misc.
102 102
  */
103 103
 $borderRadius: 4px;
104
-$defaultWatermarkLink: '../images/watermark.png';
105 104
 $popoverMenuPadding: 13px;
106 105
 $happySoftwareBackground: transparent;
107 106
 $desktopAppDragBarHeight: 25px;
@@ -270,4 +269,3 @@ $chromeExtensionBannerTop: 80px;
270 269
 $chromeExtensionBannerRight: 16px;
271 270
 $chromeExtensionBannerTopInMeeting: 10px;
272 271
 $chromeExtensionBannerRightInMeeeting: 10px;
273
-

+ 0
- 3
css/filmstrip/_tile_view_overrides.scss Dosyayı Görüntüle

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

+ 1
- 2
interface_config.js Dosyayı Görüntüle

@@ -1,9 +1,8 @@
1 1
 /* eslint-disable no-unused-vars, no-var, max-len */
2 2
 
3 3
 var interfaceConfig = {
4
-    // TO FIX: this needs to be handled from SASS variables. There are some
5
-    // methods allowing to use variables both in css and js.
6 4
     DEFAULT_BACKGROUND: '#474747',
5
+    DEFAULT_LOGO_URL: '../images/watermark.png',
7 6
 
8 7
     /**
9 8
      * Whether or not the blurred video background for large video should be

+ 0
- 26
modules/UI/videolayout/VideoContainer.js Dosyayı Görüntüle

@@ -498,9 +498,6 @@ export class VideoContainer extends LargeContainer {
498 498
         });
499 499
 
500 500
         this._updateBackground();
501
-
502
-        // Reset the large video background depending on the stream.
503
-        this.setLargeVideoBackground(this.avatarDisplayed);
504 501
     }
505 502
 
506 503
     /**
@@ -533,14 +530,6 @@ export class VideoContainer extends LargeContainer {
533 530
      * @param {boolean} show
534 531
      */
535 532
     showAvatar(show) {
536
-        // TO FIX: Video background need to be black, so that we don't have a
537
-        // flickering effect when scrolling between videos and have the screen
538
-        // move to grey before going back to video. Avatars though can have the
539
-        // default background set.
540
-        // In order to fix this code we need to introduce video background or
541
-        // find a workaround for the video flickering.
542
-        this.setLargeVideoBackground(show);
543
-
544 533
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
545 534
         this.avatarDisplayed = show;
546 535
 
@@ -596,21 +585,6 @@ export class VideoContainer extends LargeContainer {
596 585
         return false;
597 586
     }
598 587
 
599
-    /**
600
-     * Sets the large video container background depending on the container
601
-     * type and the parameter indicating if an avatar is currently shown on
602
-     * large.
603
-     *
604
-     * @param {boolean} isAvatar - Indicates if the avatar is currently shown
605
-     * on the large video.
606
-     * @returns {void}
607
-     */
608
-    setLargeVideoBackground(isAvatar) {
609
-        $('#largeVideoContainer').css('background',
610
-            this.videoType === VIDEO_CONTAINER_TYPE && !isAvatar
611
-                ? '#000' : interfaceConfig.DEFAULT_BACKGROUND);
612
-    }
613
-
614 588
     /**
615 589
      * Callback invoked when the video element changes dimensions.
616 590
      *

+ 1
- 0
react/features/base/config/interfaceConfigWhitelist.js Dosyayı Görüntüle

@@ -18,6 +18,7 @@ export default [
18 18
     'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
19 19
     'CONNECTION_INDICATOR_DISABLED',
20 20
     'DEFAULT_BACKGROUND',
21
+    'DEFAULT_LOGO_URL',
21 22
     'DISABLE_PRESENCE_STATUS',
22 23
     'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
23 24
     'DEFAULT_LOCAL_DISPLAY_NAME',

+ 61
- 14
react/features/base/react/components/web/Watermarks.js Dosyayı Görüntüle

@@ -21,11 +21,27 @@ const _RIGHT_WATERMARK_STYLE = {
21 21
  */
22 22
 type Props = {
23 23
 
24
+    /**
25
+     * The user selected url used to navigate to on logo click.
26
+     */
27
+    _customLogoLink: string,
28
+
29
+    /**
30
+     * The url of the user selected logo.
31
+     */
32
+    _customLogoUrl: string,
33
+
24 34
     /**
25 35
      * Whether or not the current user is logged in through a JWT.
26 36
      */
27 37
     _isGuest: boolean,
28 38
 
39
+    /**
40
+     * Flag used to signal that the logo can be displayed.
41
+     * It becomes true after the user customization options are fetched.
42
+     */
43
+    _readyToDisplayJitsiWatermark: boolean,
44
+
29 45
     /**
30 46
      * Invoked to obtain translated strings.
31 47
      */
@@ -133,6 +149,26 @@ class Watermarks extends Component<Props, State> {
133 149
         );
134 150
     }
135 151
 
152
+    /**
153
+     * Returns true if the watermark is ready to be displayed.
154
+     *
155
+     * @private
156
+     * @returns {boolean}
157
+     */
158
+    _canDisplayJitsiWatermark() {
159
+        const {
160
+            showJitsiWatermark,
161
+            showJitsiWatermarkForGuests
162
+        } = this.state;
163
+        const {
164
+            _isGuest,
165
+            _readyToDisplayJitsiWatermark
166
+        } = this.props;
167
+
168
+        return _readyToDisplayJitsiWatermark
169
+            && (showJitsiWatermark || (_isGuest && showJitsiWatermarkForGuests));
170
+    }
171
+
136 172
     /**
137 173
      * Renders a brand watermark if it is enabled.
138 174
      *
@@ -173,18 +209,27 @@ class Watermarks extends Component<Props, State> {
173 209
      */
174 210
     _renderJitsiWatermark() {
175 211
         let reactElement = null;
176
-
177
-        if (this.state.showJitsiWatermark
178
-                || (this.props._isGuest
179
-                    && this.state.showJitsiWatermarkForGuests)) {
180
-            reactElement = <div className = 'watermark leftwatermark' />;
181
-
182
-            const { jitsiWatermarkLink } = this.state;
183
-
184
-            if (jitsiWatermarkLink) {
212
+        const {
213
+            _customLogoUrl,
214
+            _customLogoLink
215
+        } = this.props;
216
+
217
+        if (this._canDisplayJitsiWatermark()) {
218
+            const link = _customLogoLink || this.state.jitsiWatermarkLink;
219
+            const style = {
220
+                backgroundImage: `url(${_customLogoUrl || interfaceConfig.DEFAULT_LOGO_URL})`,
221
+                maxWidth: 140,
222
+                maxHeight: 70
223
+            };
224
+
225
+            reactElement = (<div
226
+                className = 'watermark leftwatermark'
227
+                style = { style } />);
228
+
229
+            if (link) {
185 230
                 reactElement = (
186 231
                     <a
187
-                        href = { jitsiWatermarkLink }
232
+                        href = { link }
188 233
                         target = '_new'>
189 234
                         { reactElement }
190 235
                     </a>
@@ -223,12 +268,11 @@ class Watermarks extends Component<Props, State> {
223 268
  * Maps parts of Redux store to component prop types.
224 269
  *
225 270
  * @param {Object} state - Snapshot of Redux store.
226
- * @returns {{
227
- *      _isGuest: boolean
228
- * }}
271
+ * @returns {Props}
229 272
  */
230 273
 function _mapStateToProps(state) {
231 274
     const { isGuest } = state['features/base/jwt'];
275
+    const { customizationReady, logoClickUrl, logoImageUrl } = state['features/dynamic-branding'];
232 276
 
233 277
     return {
234 278
         /**
@@ -238,7 +282,10 @@ function _mapStateToProps(state) {
238 282
          * @private
239 283
          * @type {boolean}
240 284
          */
241
-        _isGuest: isGuest
285
+        _customLogoLink: logoClickUrl,
286
+        _customLogoUrl: logoImageUrl,
287
+        _isGuest: isGuest,
288
+        _readyToDisplayJitsiWatermark: customizationReady
242 289
     };
243 290
 }
244 291
 

+ 9
- 0
react/features/dynamic-branding/actionTypes.js Dosyayı Görüntüle

@@ -0,0 +1,9 @@
1
+/**
2
+ * Action used to set custom user properties.
3
+ */
4
+export const SET_DYNAMIC_BRANDING_DATA = 'SET_DYNAMIC_BRANDING_DATA';
5
+
6
+/**
7
+ * Action used to signal the branding elements are ready to be displayed
8
+ */
9
+export const SET_DYNAMIC_BRANDING_READY = 'SET_DYNAMIC_BRANDING_READY';

+ 66
- 0
react/features/dynamic-branding/actions.js Dosyayı Görüntüle

@@ -0,0 +1,66 @@
1
+// @flow
2
+
3
+import { getLogger } from 'jitsi-meet-logger';
4
+
5
+import { doGetJSON } from '../base/util';
6
+
7
+import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
8
+import { extractFqnFromPath } from './functions';
9
+
10
+const logger = getLogger(__filename);
11
+
12
+/**
13
+ * Fetches custom branding data.
14
+ * If there is no data or the request fails, sets the `customizationReady` flag
15
+ * so the defaults can be displayed.
16
+ *
17
+ * @returns {Function}
18
+ */
19
+export function fetchCustomBrandingData() {
20
+    return async function(dispatch: Function, getState: Function) {
21
+        const state = getState();
22
+        const baseUrl = state['features/base/config'].brandingDataUrl;
23
+        const { customizationReady } = state['features/dynamic-branding'];
24
+
25
+        if (!customizationReady) {
26
+            const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
27
+
28
+            if (baseUrl && fqn) {
29
+                try {
30
+                    const res = await doGetJSON(`${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`);
31
+
32
+                    return dispatch(setDynamicBrandingData(res));
33
+                } catch (err) {
34
+                    logger.error('Error fetching branding data', err);
35
+                }
36
+            }
37
+
38
+            dispatch(setDynamicBrandingReady());
39
+        }
40
+    };
41
+}
42
+
43
+/**
44
+ * Action used to set the user customizations.
45
+ *
46
+ * @param {Object} value - The custom data to be set.
47
+ * @returns {Object}
48
+ */
49
+function setDynamicBrandingData(value) {
50
+    return {
51
+        type: SET_DYNAMIC_BRANDING_DATA,
52
+        value
53
+    };
54
+}
55
+
56
+
57
+/**
58
+ * Action used to signal the branding elements are ready to be displayed.
59
+ *
60
+ * @returns {Object}
61
+ */
62
+function setDynamicBrandingReady() {
63
+    return {
64
+        type: SET_DYNAMIC_BRANDING_READY
65
+    };
66
+}

+ 15
- 0
react/features/dynamic-branding/functions.js Dosyayı Görüntüle

@@ -0,0 +1,15 @@
1
+// @flow
2
+
3
+/**
4
+ * Extracts the fqn part from a path, where fqn represents
5
+ * tenant/roomName.
6
+ *
7
+ * @param {string} path - The URL path.
8
+ * @returns {string}
9
+ */
10
+export function extractFqnFromPath(path: string) {
11
+    const parts = path.split('/');
12
+    const len = parts.length;
13
+
14
+    return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
15
+}

+ 4
- 0
react/features/dynamic-branding/index.js Dosyayı Görüntüle

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

+ 46
- 0
react/features/dynamic-branding/reducer.js Dosyayı Görüntüle

@@ -0,0 +1,46 @@
1
+// @flow
2
+
3
+import { ReducerRegistry } from '../base/redux';
4
+
5
+import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
6
+
7
+/**
8
+ * The name of the redux store/state property which is the root of the redux
9
+ * state of the feature {@code dynamic-branding}.
10
+ */
11
+const STORE_NAME = 'features/dynamic-branding';
12
+
13
+const DEFAULT_STATE = {
14
+    backgroundColor: '',
15
+    backgroundImageUrl: '',
16
+    customizationReady: false,
17
+    logoClickUrl: '',
18
+    logoImageUrl: ''
19
+};
20
+
21
+/**
22
+ * Reduces redux actions for the purposes of the feature {@code dynamic-branding}.
23
+ */
24
+ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
25
+    switch (action.type) {
26
+    case SET_DYNAMIC_BRANDING_DATA: {
27
+        const { backgroundColor, backgroundImageUrl, logoClickUrl, logoImageUrl } = action.value;
28
+
29
+        return {
30
+            backgroundColor,
31
+            backgroundImageUrl,
32
+            logoClickUrl,
33
+            logoImageUrl,
34
+            customizationReady: true
35
+        };
36
+    }
37
+    case SET_DYNAMIC_BRANDING_READY:
38
+        return {
39
+            ...state,
40
+            customizationReady: true
41
+        };
42
+
43
+    }
44
+
45
+    return state;
46
+});

+ 57
- 5
react/features/large-video/components/LargeVideo.web.js Dosyayı Görüntüle

@@ -4,12 +4,28 @@ import React, { Component } from 'react';
4 4
 
5 5
 import { Watermarks } from '../../base/react';
6 6
 import { connect } from '../../base/redux';
7
+import { fetchCustomBrandingData } from '../../dynamic-branding';
7 8
 import { Captions } from '../../subtitles/';
8 9
 
9 10
 declare var interfaceConfig: Object;
10 11
 
11 12
 type Props = {
12 13
 
14
+    /**
15
+     * The user selected background color.
16
+     */
17
+     _customBackgroundColor: string,
18
+
19
+    /**
20
+     * The user selected background image url.
21
+     */
22
+     _customBackgroundImageUrl: string,
23
+
24
+    /**
25
+     * Fetches the branding data.
26
+     */
27
+    _fetchCustomBrandingData: Function,
28
+
13 29
     /**
14 30
      * Used to determine the value of the autoplay attribute of the underlying
15 31
      * video element.
@@ -24,6 +40,15 @@ type Props = {
24 40
  * @extends Component
25 41
  */
26 42
 class LargeVideo extends Component<Props> {
43
+    /**
44
+     * Implements React's {@link Component#componentDidMount}.
45
+     *
46
+     * @inheritdoc
47
+     */
48
+    componentDidMount() {
49
+        this.props._fetchCustomBrandingData();
50
+    }
51
+
27 52
     /**
28 53
      * Implements React's {@link Component#render()}.
29 54
      *
@@ -31,10 +56,13 @@ class LargeVideo extends Component<Props> {
31 56
      * @returns {React$Element}
32 57
      */
33 58
     render() {
59
+        const style = this._getCustomSyles();
60
+
34 61
         return (
35 62
             <div
36 63
                 className = 'videocontainer'
37
-                id = 'largeVideoContainer'>
64
+                id = 'largeVideoContainer'
65
+                style = { style }>
38 66
                 <div id = 'sharedVideo'>
39 67
                     <div id = 'sharedVideoIFrame' />
40 68
                 </div>
@@ -72,6 +100,26 @@ class LargeVideo extends Component<Props> {
72 100
             </div>
73 101
         );
74 102
     }
103
+
104
+    /**
105
+     * Creates the custom styles object.
106
+     *
107
+     * @private
108
+     * @returns {Object}
109
+     */
110
+    _getCustomSyles() {
111
+        const styles = {};
112
+        const { _customBackgroundColor, _customBackgroundImageUrl } = this.props;
113
+
114
+        styles.backgroundColor = _customBackgroundColor || interfaceConfig.DEFAULT_BACKGROUND;
115
+
116
+        if (_customBackgroundImageUrl) {
117
+            styles.backgroundImage = `url(${_customBackgroundImageUrl})`;
118
+            styles.backgroundSize = 'cover';
119
+        }
120
+
121
+        return styles;
122
+    }
75 123
 }
76 124
 
77 125
 
@@ -80,17 +128,21 @@ class LargeVideo extends Component<Props> {
80 128
  *
81 129
  * @param {Object} state - The Redux state.
82 130
  * @private
83
- * @returns {{
84
- *     _noAutoPlayVideo: boolean
85
- * }}
131
+ * @returns {Props}
86 132
  */
87 133
 function _mapStateToProps(state) {
88 134
     const testingConfig = state['features/base/config'].testing;
135
+    const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
89 136
 
90 137
     return {
138
+        _customBackgroundColor: backgroundColor,
139
+        _customBackgroundImageUrl: backgroundImageUrl,
91 140
         _noAutoPlayVideo: testingConfig?.noAutoPlayVideo
92 141
     };
93 142
 }
94 143
 
144
+const _mapDispatchToProps = {
145
+    _fetchCustomBrandingData: fetchCustomBrandingData
146
+};
95 147
 
96
-export default connect(_mapStateToProps)(LargeVideo);
148
+export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);

Loading…
İptal
Kaydet