浏览代码

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

master
Vlad Piersec 5 年前
父节点
当前提交
8758c222c6

+ 17
- 0
config.js 查看文件

512
     // If set to true all muting operations of remote participants will be disabled.
512
     // If set to true all muting operations of remote participants will be disabled.
513
     // disableRemoteMute: true,
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
     // List of undocumented settings used in jitsi-meet
532
     // List of undocumented settings used in jitsi-meet
516
     /**
533
     /**
517
      _immediateReloadThreshold
534
      _immediateReloadThreshold

+ 2
- 1
css/_base.scss 查看文件

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

+ 0
- 2
css/_variables.scss 查看文件

101
  * Misc.
101
  * Misc.
102
  */
102
  */
103
 $borderRadius: 4px;
103
 $borderRadius: 4px;
104
-$defaultWatermarkLink: '../images/watermark.png';
105
 $popoverMenuPadding: 13px;
104
 $popoverMenuPadding: 13px;
106
 $happySoftwareBackground: transparent;
105
 $happySoftwareBackground: transparent;
107
 $desktopAppDragBarHeight: 25px;
106
 $desktopAppDragBarHeight: 25px;
270
 $chromeExtensionBannerRight: 16px;
269
 $chromeExtensionBannerRight: 16px;
271
 $chromeExtensionBannerTopInMeeting: 10px;
270
 $chromeExtensionBannerTopInMeeting: 10px;
272
 $chromeExtensionBannerRightInMeeeting: 10px;
271
 $chromeExtensionBannerRightInMeeeting: 10px;
273
-

+ 0
- 3
css/filmstrip/_tile_view_overrides.scss 查看文件

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

+ 1
- 2
interface_config.js 查看文件

1
 /* eslint-disable no-unused-vars, no-var, max-len */
1
 /* eslint-disable no-unused-vars, no-var, max-len */
2
 
2
 
3
 var interfaceConfig = {
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
     DEFAULT_BACKGROUND: '#474747',
4
     DEFAULT_BACKGROUND: '#474747',
5
+    DEFAULT_LOGO_URL: '../images/watermark.png',
7
 
6
 
8
     /**
7
     /**
9
      * Whether or not the blurred video background for large video should be
8
      * Whether or not the blurred video background for large video should be

+ 0
- 26
modules/UI/videolayout/VideoContainer.js 查看文件

498
         });
498
         });
499
 
499
 
500
         this._updateBackground();
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
      * @param {boolean} show
530
      * @param {boolean} show
534
      */
531
      */
535
     showAvatar(show) {
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
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
533
         this.$avatar.css('visibility', show ? 'visible' : 'hidden');
545
         this.avatarDisplayed = show;
534
         this.avatarDisplayed = show;
546
 
535
 
596
         return false;
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
      * Callback invoked when the video element changes dimensions.
589
      * Callback invoked when the video element changes dimensions.
616
      *
590
      *

+ 1
- 0
react/features/base/config/interfaceConfigWhitelist.js 查看文件

18
     'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
18
     'CONNECTION_INDICATOR_AUTO_HIDE_TIMEOUT',
19
     'CONNECTION_INDICATOR_DISABLED',
19
     'CONNECTION_INDICATOR_DISABLED',
20
     'DEFAULT_BACKGROUND',
20
     'DEFAULT_BACKGROUND',
21
+    'DEFAULT_LOGO_URL',
21
     'DISABLE_PRESENCE_STATUS',
22
     'DISABLE_PRESENCE_STATUS',
22
     'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
23
     'DISABLE_JOIN_LEAVE_NOTIFICATIONS',
23
     'DEFAULT_LOCAL_DISPLAY_NAME',
24
     'DEFAULT_LOCAL_DISPLAY_NAME',

+ 61
- 14
react/features/base/react/components/web/Watermarks.js 查看文件

21
  */
21
  */
22
 type Props = {
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
      * Whether or not the current user is logged in through a JWT.
35
      * Whether or not the current user is logged in through a JWT.
26
      */
36
      */
27
     _isGuest: boolean,
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
      * Invoked to obtain translated strings.
46
      * Invoked to obtain translated strings.
31
      */
47
      */
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
      * Renders a brand watermark if it is enabled.
173
      * Renders a brand watermark if it is enabled.
138
      *
174
      *
173
      */
209
      */
174
     _renderJitsiWatermark() {
210
     _renderJitsiWatermark() {
175
         let reactElement = null;
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
                 reactElement = (
230
                 reactElement = (
186
                     <a
231
                     <a
187
-                        href = { jitsiWatermarkLink }
232
+                        href = { link }
188
                         target = '_new'>
233
                         target = '_new'>
189
                         { reactElement }
234
                         { reactElement }
190
                     </a>
235
                     </a>
223
  * Maps parts of Redux store to component prop types.
268
  * Maps parts of Redux store to component prop types.
224
  *
269
  *
225
  * @param {Object} state - Snapshot of Redux store.
270
  * @param {Object} state - Snapshot of Redux store.
226
- * @returns {{
227
- *      _isGuest: boolean
228
- * }}
271
+ * @returns {Props}
229
  */
272
  */
230
 function _mapStateToProps(state) {
273
 function _mapStateToProps(state) {
231
     const { isGuest } = state['features/base/jwt'];
274
     const { isGuest } = state['features/base/jwt'];
275
+    const { customizationReady, logoClickUrl, logoImageUrl } = state['features/dynamic-branding'];
232
 
276
 
233
     return {
277
     return {
234
         /**
278
         /**
238
          * @private
282
          * @private
239
          * @type {boolean}
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 查看文件

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 查看文件

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 查看文件

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 查看文件

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

+ 46
- 0
react/features/dynamic-branding/reducer.js 查看文件

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 查看文件

4
 
4
 
5
 import { Watermarks } from '../../base/react';
5
 import { Watermarks } from '../../base/react';
6
 import { connect } from '../../base/redux';
6
 import { connect } from '../../base/redux';
7
+import { fetchCustomBrandingData } from '../../dynamic-branding';
7
 import { Captions } from '../../subtitles/';
8
 import { Captions } from '../../subtitles/';
8
 
9
 
9
 declare var interfaceConfig: Object;
10
 declare var interfaceConfig: Object;
10
 
11
 
11
 type Props = {
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
      * Used to determine the value of the autoplay attribute of the underlying
30
      * Used to determine the value of the autoplay attribute of the underlying
15
      * video element.
31
      * video element.
24
  * @extends Component
40
  * @extends Component
25
  */
41
  */
26
 class LargeVideo extends Component<Props> {
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
      * Implements React's {@link Component#render()}.
53
      * Implements React's {@link Component#render()}.
29
      *
54
      *
31
      * @returns {React$Element}
56
      * @returns {React$Element}
32
      */
57
      */
33
     render() {
58
     render() {
59
+        const style = this._getCustomSyles();
60
+
34
         return (
61
         return (
35
             <div
62
             <div
36
                 className = 'videocontainer'
63
                 className = 'videocontainer'
37
-                id = 'largeVideoContainer'>
64
+                id = 'largeVideoContainer'
65
+                style = { style }>
38
                 <div id = 'sharedVideo'>
66
                 <div id = 'sharedVideo'>
39
                     <div id = 'sharedVideoIFrame' />
67
                     <div id = 'sharedVideoIFrame' />
40
                 </div>
68
                 </div>
72
             </div>
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
  *
128
  *
81
  * @param {Object} state - The Redux state.
129
  * @param {Object} state - The Redux state.
82
  * @private
130
  * @private
83
- * @returns {{
84
- *     _noAutoPlayVideo: boolean
85
- * }}
131
+ * @returns {Props}
86
  */
132
  */
87
 function _mapStateToProps(state) {
133
 function _mapStateToProps(state) {
88
     const testingConfig = state['features/base/config'].testing;
134
     const testingConfig = state['features/base/config'].testing;
135
+    const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
89
 
136
 
90
     return {
137
     return {
138
+        _customBackgroundColor: backgroundColor,
139
+        _customBackgroundImageUrl: backgroundImageUrl,
91
         _noAutoPlayVideo: testingConfig?.noAutoPlayVideo
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);

正在加载...
取消
保存