feat(dynamic-branding): Add branding option for virtual backgrounds
This commit is contained in:
parent
57083c174f
commit
f9cc813e91
|
@ -909,6 +909,10 @@ var config = {
|
||||||
*/
|
*/
|
||||||
// dynamicBrandingUrl: '',
|
// dynamicBrandingUrl: '',
|
||||||
|
|
||||||
|
// When true the user cannot add more images to be used as virtual background.
|
||||||
|
// Only the default ones from will be available.
|
||||||
|
// disableAddingBackgroundImages: false,
|
||||||
|
|
||||||
// Sets the background transparency level. '0' is fully transparent, '1' is opaque.
|
// Sets the background transparency level. '0' is fully transparent, '1' is opaque.
|
||||||
// backgroundAlpha: 1,
|
// backgroundAlpha: 1,
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import '../authentication/middleware';
|
import '../authentication/middleware';
|
||||||
import '../base/devices/middleware';
|
import '../base/devices/middleware';
|
||||||
|
import '../dynamic-branding/middleware';
|
||||||
import '../e2ee/middleware';
|
import '../e2ee/middleware';
|
||||||
import '../external-api/middleware';
|
import '../external-api/middleware';
|
||||||
import '../keyboard-shortcuts/middleware';
|
import '../keyboard-shortcuts/middleware';
|
||||||
|
|
|
@ -84,6 +84,7 @@ export default [
|
||||||
'disableAEC',
|
'disableAEC',
|
||||||
'disableAGC',
|
'disableAGC',
|
||||||
'disableAP',
|
'disableAP',
|
||||||
|
'disableAddingBackgroundImages',
|
||||||
'disableAudioLevels',
|
'disableAudioLevels',
|
||||||
'disableChatSmileys',
|
'disableChatSmileys',
|
||||||
'disableDeepLinking',
|
'disableDeepLinking',
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
* @param {string} path - The URL path.
|
* @param {string} path - The URL path.
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function extractFqnFromPath(path: string) {
|
export function extractFqnFromPath() {
|
||||||
const parts = path.split('/');
|
const parts = window.location.pathname.split('/');
|
||||||
const len = parts.length;
|
const len = parts.length;
|
||||||
|
|
||||||
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
|
return parts.length > 2 ? `${parts[len - 2]}/${parts[len - 1]}` : '';
|
||||||
|
@ -28,7 +28,7 @@ export function getDynamicBrandingUrl(state: Object) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = state['features/base/config'].brandingDataUrl;
|
const baseUrl = state['features/base/config'].brandingDataUrl;
|
||||||
const fqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
|
const fqn = extractFqnFromPath();
|
||||||
|
|
||||||
if (baseUrl && fqn) {
|
if (baseUrl && fqn) {
|
||||||
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;
|
return `${baseUrl}?conferenceFqn=${encodeURIComponent(fqn)}`;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { APP_WILL_MOUNT } from '../base/app';
|
||||||
|
import { MiddlewareRegistry } from '../base/redux';
|
||||||
|
|
||||||
|
import { fetchCustomBrandingData } from './actions';
|
||||||
|
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case APP_WILL_MOUNT: {
|
||||||
|
|
||||||
|
store.dispatch(fetchCustomBrandingData());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { ReducerRegistry } from '../base/redux';
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
import { type Image } from '../virtual-background/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SET_DYNAMIC_BRANDING_DATA,
|
SET_DYNAMIC_BRANDING_DATA,
|
||||||
|
@ -113,7 +114,15 @@ const DEFAULT_STATE = {
|
||||||
* @public
|
* @public
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
*/
|
*/
|
||||||
useDynamicBrandingData: false
|
useDynamicBrandingData: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of images to be used as virtual backgrounds instead of the default ones.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
* @type {Array<Object>}
|
||||||
|
*/
|
||||||
|
virtualBackgrounds: []
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -131,7 +140,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||||
inviteDomain,
|
inviteDomain,
|
||||||
logoClickUrl,
|
logoClickUrl,
|
||||||
logoImageUrl,
|
logoImageUrl,
|
||||||
premeetingBackground
|
premeetingBackground,
|
||||||
|
virtualBackgrounds
|
||||||
} = action.value;
|
} = action.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -146,7 +156,8 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||||
premeetingBackground,
|
premeetingBackground,
|
||||||
customizationFailed: false,
|
customizationFailed: false,
|
||||||
customizationReady: true,
|
customizationReady: true,
|
||||||
useDynamicBrandingData: true
|
useDynamicBrandingData: true,
|
||||||
|
virtualBackgrounds: formatImages(virtualBackgrounds || [])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case SET_DYNAMIC_BRANDING_FAILED: {
|
case SET_DYNAMIC_BRANDING_FAILED: {
|
||||||
|
@ -166,3 +177,30 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the branding images into an array of Images objects ready
|
||||||
|
* to be used as virtual backgrounds.
|
||||||
|
*
|
||||||
|
* @param {Array<string>} images -
|
||||||
|
* @private
|
||||||
|
* @returns {{Props}}
|
||||||
|
*/
|
||||||
|
function formatImages(images: Array<string> | Array<Object>): Array<Image> {
|
||||||
|
return images.map((img, i) => {
|
||||||
|
let src;
|
||||||
|
let tooltip;
|
||||||
|
|
||||||
|
if (typeof img === 'object') {
|
||||||
|
({ src, tooltip } = img);
|
||||||
|
} else {
|
||||||
|
src = img;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `branding-${i}`,
|
||||||
|
src,
|
||||||
|
tooltip
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -131,7 +131,7 @@ export function sendJaasFeedbackMetadata(conference: Object, feedback: Object) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const meetingFqn = extractFqnFromPath(state['features/base/connection'].locationURL.pathname);
|
const meetingFqn = extractFqnFromPath();
|
||||||
const feedbackData = {
|
const feedbackData = {
|
||||||
...feedback,
|
...feedback,
|
||||||
sessionId: conference.sessionId,
|
sessionId: conference.sessionId,
|
||||||
|
|
|
@ -5,7 +5,6 @@ import React, { Component } from 'react';
|
||||||
import { Watermarks } from '../../base/react';
|
import { Watermarks } from '../../base/react';
|
||||||
import { connect } from '../../base/redux';
|
import { connect } from '../../base/redux';
|
||||||
import { setColorAlpha } from '../../base/util';
|
import { setColorAlpha } from '../../base/util';
|
||||||
import { fetchCustomBrandingData } from '../../dynamic-branding';
|
|
||||||
import { SharedVideo } from '../../shared-video/components/web';
|
import { SharedVideo } from '../../shared-video/components/web';
|
||||||
import { Captions } from '../../subtitles/';
|
import { Captions } from '../../subtitles/';
|
||||||
|
|
||||||
|
@ -28,11 +27,6 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_customBackgroundImageUrl: string,
|
_customBackgroundImageUrl: string,
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the branding data.
|
|
||||||
*/
|
|
||||||
_fetchCustomBrandingData: Function,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prop that indicates whether the chat is open.
|
* Prop that indicates whether the chat is open.
|
||||||
*/
|
*/
|
||||||
|
@ -52,14 +46,6 @@ type Props = {
|
||||||
* @extends Component
|
* @extends Component
|
||||||
*/
|
*/
|
||||||
class LargeVideo extends Component<Props> {
|
class LargeVideo extends Component<Props> {
|
||||||
/**
|
|
||||||
* Implements React's {@link Component#componentDidMount}.
|
|
||||||
*
|
|
||||||
* @inheritdoc
|
|
||||||
*/
|
|
||||||
componentDidMount() {
|
|
||||||
this.props._fetchCustomBrandingData();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements React's {@link Component#render()}.
|
* Implements React's {@link Component#render()}.
|
||||||
|
@ -167,8 +153,4 @@ function _mapStateToProps(state) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const _mapDispatchToProps = {
|
export default connect(_mapStateToProps)(LargeVideo);
|
||||||
_fetchCustomBrandingData: fetchCustomBrandingData
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(_mapStateToProps, _mapDispatchToProps)(LargeVideo);
|
|
||||||
|
|
|
@ -55,7 +55,6 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
|
||||||
const { webhookProxyUrl: url } = state['features/base/config'];
|
const { webhookProxyUrl: url } = state['features/base/config'];
|
||||||
const { conference } = state['features/base/conference'];
|
const { conference } = state['features/base/conference'];
|
||||||
const { jwt } = state['features/base/jwt'];
|
const { jwt } = state['features/base/jwt'];
|
||||||
const { locationURL } = state['features/base/connection'];
|
|
||||||
const localParticipant = getLocalParticipant(state);
|
const localParticipant = getLocalParticipant(state);
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
|
@ -65,7 +64,7 @@ export async function sendReactionsWebhook(state: Object, reactions: Array<?stri
|
||||||
|
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
meetingFqn: extractFqnFromPath(locationURL.pathname),
|
meetingFqn: extractFqnFromPath(),
|
||||||
sessionId: conference.sessionId,
|
sessionId: conference.sessionId,
|
||||||
submitted: Date.now(),
|
submitted: Date.now(),
|
||||||
reactions,
|
reactions,
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { useCallback, useRef } from 'react';
|
||||||
|
import uuid from 'uuid';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import { Icon, IconPlusCircle } from '../../base/icons';
|
||||||
|
import { VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
|
||||||
|
import { resizeImage } from '../functions';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to set the 'loading' state of the parent component.
|
||||||
|
*/
|
||||||
|
setLoading: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to set the options.
|
||||||
|
*/
|
||||||
|
setOptions: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used to set the storedImages array.
|
||||||
|
*/
|
||||||
|
setStoredImages: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of images locally stored.
|
||||||
|
*/
|
||||||
|
storedImages: Array<Image>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a label should be displayed alongside the button.
|
||||||
|
*/
|
||||||
|
showLabel: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for translation.
|
||||||
|
*/
|
||||||
|
t: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to upload an image.
|
||||||
|
*
|
||||||
|
* @param {Object} Props - The props of the component.
|
||||||
|
* @returns {React$Node}
|
||||||
|
*/
|
||||||
|
function UploadImageButton({
|
||||||
|
setLoading,
|
||||||
|
setOptions,
|
||||||
|
setStoredImages,
|
||||||
|
showLabel,
|
||||||
|
storedImages,
|
||||||
|
t
|
||||||
|
}: Props) {
|
||||||
|
const uploadImageButton: Object = useRef(null);
|
||||||
|
const uploadImageKeyPress = useCallback(e => {
|
||||||
|
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadImageButton.current.click();
|
||||||
|
}
|
||||||
|
}, [ uploadImageButton.current ]);
|
||||||
|
|
||||||
|
|
||||||
|
const uploadImage = useCallback(async e => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
const imageFile = e.target.files;
|
||||||
|
|
||||||
|
reader.readAsDataURL(imageFile[0]);
|
||||||
|
reader.onload = async () => {
|
||||||
|
const url = await resizeImage(reader.result);
|
||||||
|
const uuId = uuid.v4();
|
||||||
|
|
||||||
|
setStoredImages([
|
||||||
|
...storedImages,
|
||||||
|
{
|
||||||
|
id: uuId,
|
||||||
|
src: url
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
setOptions({
|
||||||
|
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
|
||||||
|
enabled: true,
|
||||||
|
url,
|
||||||
|
selectedThumbnail: uuId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
logger.info('New virtual background image uploaded!');
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
setLoading(false);
|
||||||
|
logger.error('Failed to upload virtual image!');
|
||||||
|
};
|
||||||
|
}, [ storedImages ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showLabel && <label
|
||||||
|
aria-label = { t('virtualBackground.uploadImage') }
|
||||||
|
className = 'file-upload-label'
|
||||||
|
htmlFor = 'file-upload'
|
||||||
|
onKeyPress = { uploadImageKeyPress }
|
||||||
|
tabIndex = { 0 } >
|
||||||
|
<Icon
|
||||||
|
className = { 'add-background' }
|
||||||
|
size = { 20 }
|
||||||
|
src = { IconPlusCircle } />
|
||||||
|
{t('virtualBackground.addBackground')}
|
||||||
|
</label>}
|
||||||
|
|
||||||
|
<input
|
||||||
|
accept = 'image/*'
|
||||||
|
className = 'file-upload-btn'
|
||||||
|
id = 'file-upload'
|
||||||
|
onChange = { uploadImage }
|
||||||
|
ref = { uploadImageButton }
|
||||||
|
type = 'file' />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(UploadImageButton);
|
|
@ -3,12 +3,11 @@
|
||||||
import Spinner from '@atlaskit/spinner';
|
import Spinner from '@atlaskit/spinner';
|
||||||
import Bourne from '@hapi/bourne';
|
import Bourne from '@hapi/bourne';
|
||||||
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import uuid from 'uuid';
|
|
||||||
|
|
||||||
import { Dialog, hideDialog, openDialog } from '../../base/dialog';
|
import { Dialog, hideDialog, openDialog } from '../../base/dialog';
|
||||||
import { translate } from '../../base/i18n';
|
import { translate } from '../../base/i18n';
|
||||||
import { Icon, IconCloseSmall, IconPlusCircle, IconShareDesktop } from '../../base/icons';
|
import { Icon, IconCloseSmall, IconShareDesktop } from '../../base/icons';
|
||||||
import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
|
import { browser, JitsiTrackErrors } from '../../base/lib-jitsi-meet';
|
||||||
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
import { createLocalTrack } from '../../base/lib-jitsi-meet/functions';
|
||||||
import { VIDEO_TYPE } from '../../base/media';
|
import { VIDEO_TYPE } from '../../base/media';
|
||||||
|
@ -18,62 +17,20 @@ import { Tooltip } from '../../base/tooltip';
|
||||||
import { getLocalVideoTrack } from '../../base/tracks';
|
import { getLocalVideoTrack } from '../../base/tracks';
|
||||||
import { showErrorNotification } from '../../notifications';
|
import { showErrorNotification } from '../../notifications';
|
||||||
import { toggleBackgroundEffect } from '../actions';
|
import { toggleBackgroundEffect } from '../actions';
|
||||||
import { VIRTUAL_BACKGROUND_TYPE } from '../constants';
|
import { IMAGES, BACKGROUNDS_LIMIT, VIRTUAL_BACKGROUND_TYPE, type Image } from '../constants';
|
||||||
import { resizeImage, toDataURL } from '../functions';
|
import { toDataURL } from '../functions';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
|
||||||
|
import UploadImageButton from './UploadImageButton';
|
||||||
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
|
import VirtualBackgroundPreview from './VirtualBackgroundPreview';
|
||||||
|
|
||||||
|
|
||||||
type Image = {
|
|
||||||
tooltip?: string,
|
|
||||||
id: string,
|
|
||||||
src: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// The limit of virtual background uploads is 24. When the number
|
|
||||||
// of uploads is 25 we trigger the deleteStoredImage function to delete
|
|
||||||
// the first/oldest uploaded background.
|
|
||||||
const backgroundsLimit = 25;
|
|
||||||
const images: Array<Image> = [
|
|
||||||
{
|
|
||||||
tooltip: 'image1',
|
|
||||||
id: '1',
|
|
||||||
src: 'images/virtual-background/background-1.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image2',
|
|
||||||
id: '2',
|
|
||||||
src: 'images/virtual-background/background-2.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image3',
|
|
||||||
id: '3',
|
|
||||||
src: 'images/virtual-background/background-3.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image4',
|
|
||||||
id: '4',
|
|
||||||
src: 'images/virtual-background/background-4.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image5',
|
|
||||||
id: '5',
|
|
||||||
src: 'images/virtual-background/background-5.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image6',
|
|
||||||
id: '6',
|
|
||||||
src: 'images/virtual-background/background-6.jpg'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tooltip: 'image7',
|
|
||||||
id: '7',
|
|
||||||
src: 'images/virtual-background/background-7.jpg'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of Images to choose from.
|
||||||
|
*/
|
||||||
|
_images: Array<Image>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current local flip x status.
|
* The current local flip x status.
|
||||||
*/
|
*/
|
||||||
|
@ -89,6 +46,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
_selectedThumbnail: string,
|
_selectedThumbnail: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the upload button should be displayed or not.
|
||||||
|
*/
|
||||||
|
_showUploadButton: boolean,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the selected virtual background object.
|
* Returns the selected virtual background object.
|
||||||
*/
|
*/
|
||||||
|
@ -128,11 +90,15 @@ const onError = event => {
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state): Object {
|
function _mapStateToProps(state): Object {
|
||||||
const { localFlipX } = state['features/base/settings'];
|
const { localFlipX } = state['features/base/settings'];
|
||||||
|
const dynamicBrandingImages = state['features/dynamic-branding'].virtualBackgrounds;
|
||||||
|
const hasBrandingImages = Boolean(dynamicBrandingImages.length);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_localFlipX: Boolean(localFlipX),
|
_localFlipX: Boolean(localFlipX),
|
||||||
|
_images: (hasBrandingImages && dynamicBrandingImages) || IMAGES,
|
||||||
_virtualBackground: state['features/virtual-background'],
|
_virtualBackground: state['features/virtual-background'],
|
||||||
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
|
_selectedThumbnail: state['features/virtual-background'].selectedThumbnail,
|
||||||
|
_showUploadButton: !(hasBrandingImages || state['features/base/config'].disableAddingBackgroundImages),
|
||||||
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
|
_jitsiTrack: getLocalVideoTrack(state['features/base/tracks'])?.jitsiTrack
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -145,9 +111,11 @@ const VirtualBackgroundDialog = translate(connect(_mapStateToProps)(VirtualBackg
|
||||||
* @returns {ReactElement}
|
* @returns {ReactElement}
|
||||||
*/
|
*/
|
||||||
function VirtualBackground({
|
function VirtualBackground({
|
||||||
_localFlipX,
|
_images,
|
||||||
_jitsiTrack,
|
_jitsiTrack,
|
||||||
|
_localFlipX,
|
||||||
_selectedThumbnail,
|
_selectedThumbnail,
|
||||||
|
_showUploadButton,
|
||||||
_virtualBackground,
|
_virtualBackground,
|
||||||
dispatch,
|
dispatch,
|
||||||
initialOptions,
|
initialOptions,
|
||||||
|
@ -158,7 +126,7 @@ function VirtualBackground({
|
||||||
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
|
const localImages = jitsiLocalStorage.getItem('virtualBackgrounds');
|
||||||
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
|
const [ storedImages, setStoredImages ] = useState<Array<Image>>((localImages && Bourne.parse(localImages)) || []);
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
const uploadImageButton: Object = useRef(null);
|
|
||||||
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
|
const [ activeDesktopVideo ] = useState(_virtualBackground?.virtualSource?.videoType === VIDEO_TYPE.DESKTOP
|
||||||
? _virtualBackground.virtualSource
|
? _virtualBackground.virtualSource
|
||||||
: null);
|
: null);
|
||||||
|
@ -186,7 +154,7 @@ function VirtualBackground({
|
||||||
// Preventing localStorage QUOTA_EXCEEDED_ERR
|
// Preventing localStorage QUOTA_EXCEEDED_ERR
|
||||||
err && setStoredImages(storedImages.slice(1));
|
err && setStoredImages(storedImages.slice(1));
|
||||||
}
|
}
|
||||||
if (storedImages.length === backgroundsLimit) {
|
if (storedImages.length === BACKGROUNDS_LIMIT) {
|
||||||
setStoredImages(storedImages.slice(1));
|
setStoredImages(storedImages.slice(1));
|
||||||
}
|
}
|
||||||
}, [ storedImages ]);
|
}, [ storedImages ]);
|
||||||
|
@ -321,9 +289,10 @@ function VirtualBackground({
|
||||||
|
|
||||||
const setImageBackground = useCallback(async e => {
|
const setImageBackground = useCallback(async e => {
|
||||||
const imageId = e.currentTarget.getAttribute('data-imageid');
|
const imageId = e.currentTarget.getAttribute('data-imageid');
|
||||||
const image = images.find(img => img.id === imageId);
|
const image = _images.find(img => img.id === imageId);
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
|
try {
|
||||||
const url = await toDataURL(image.src);
|
const url = await toDataURL(image.src);
|
||||||
|
|
||||||
setOptions({
|
setOptions({
|
||||||
|
@ -332,50 +301,15 @@ function VirtualBackground({
|
||||||
url,
|
url,
|
||||||
selectedThumbnail: image.id
|
selectedThumbnail: image.id
|
||||||
});
|
});
|
||||||
logger.info('Image setted for virtual background preview!');
|
logger.info('Image set for virtual background preview!');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Could not fetch virtual background image:', err);
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const uploadImage = useCallback(async e => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
const imageFile = e.target.files;
|
|
||||||
|
|
||||||
reader.readAsDataURL(imageFile[0]);
|
|
||||||
reader.onload = async () => {
|
|
||||||
const url = await resizeImage(reader.result);
|
|
||||||
const uuId = uuid.v4();
|
|
||||||
|
|
||||||
setStoredImages([
|
|
||||||
...storedImages,
|
|
||||||
{
|
|
||||||
id: uuId,
|
|
||||||
src: url
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
setOptions({
|
|
||||||
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
|
|
||||||
enabled: true,
|
|
||||||
url,
|
|
||||||
selectedThumbnail: uuId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
logger.info('New virtual background image uploaded!');
|
|
||||||
|
|
||||||
reader.onerror = () => {
|
|
||||||
setLoading(false);
|
|
||||||
logger.error('Failed to upload virtual image!');
|
|
||||||
};
|
|
||||||
}, [ dispatch, storedImages ]);
|
|
||||||
|
|
||||||
const uploadImageKeyPress = useCallback(e => {
|
|
||||||
if (uploadImageButton.current && (e.key === ' ' || e.key === 'Enter')) {
|
|
||||||
e.preventDefault();
|
|
||||||
uploadImageButton.current.click();
|
|
||||||
}
|
|
||||||
}, [ uploadImageButton.current ]);
|
|
||||||
|
|
||||||
const setImageBackgroundKeyPress = useCallback(e => {
|
const setImageBackgroundKeyPress = useCallback(e => {
|
||||||
if (e.key === ' ' || e.key === 'Enter') {
|
if (e.key === ' ' || e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -448,25 +382,13 @@ function VirtualBackground({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{previewIsLoaded && <label
|
{_showUploadButton
|
||||||
aria-label = { t('virtualBackground.uploadImage') }
|
&& <UploadImageButton
|
||||||
className = 'file-upload-label'
|
setLoading = { setLoading }
|
||||||
htmlFor = 'file-upload'
|
setOptions = { setOptions }
|
||||||
onKeyPress = { uploadImageKeyPress }
|
setStoredImages = { setStoredImages }
|
||||||
tabIndex = { 0 } >
|
showLabel = { previewIsLoaded }
|
||||||
<Icon
|
storedImages = { storedImages } />}
|
||||||
className = { 'add-background' }
|
|
||||||
size = { 20 }
|
|
||||||
src = { IconPlusCircle } />
|
|
||||||
{t('virtualBackground.addBackground')}
|
|
||||||
</label> }
|
|
||||||
<input
|
|
||||||
accept = 'image/*'
|
|
||||||
className = 'file-upload-btn'
|
|
||||||
id = 'file-upload'
|
|
||||||
onChange = { uploadImage }
|
|
||||||
ref = { uploadImageButton }
|
|
||||||
type = 'file' />
|
|
||||||
<div
|
<div
|
||||||
className = 'virtual-background-dialog'
|
className = 'virtual-background-dialog'
|
||||||
role = 'radiogroup'
|
role = 'radiogroup'
|
||||||
|
@ -535,7 +457,7 @@ function VirtualBackground({
|
||||||
src = { IconShareDesktop } />
|
src = { IconShareDesktop } />
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{images.map(image => (
|
{_images.map(image => (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
|
content = { image.tooltip && t(`virtualBackground.${image.tooltip}`) }
|
||||||
key = { image.id }
|
key = { image.id }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enumeration of the different virtual background types.
|
* An enumeration of the different virtual background types.
|
||||||
*
|
*
|
||||||
|
@ -9,3 +11,54 @@ export const VIRTUAL_BACKGROUND_TYPE = {
|
||||||
BLUR: 'blur',
|
BLUR: 'blur',
|
||||||
NONE: 'none'
|
NONE: 'none'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type Image = {
|
||||||
|
tooltip?: string,
|
||||||
|
id: string,
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// The limit of virtual background uploads is 24. When the number
|
||||||
|
// of uploads is 25 we trigger the deleteStoredImage function to delete
|
||||||
|
// the first/oldest uploaded background.
|
||||||
|
export const BACKGROUNDS_LIMIT = 25;
|
||||||
|
|
||||||
|
|
||||||
|
export const IMAGES: Array<Image> = [
|
||||||
|
{
|
||||||
|
tooltip: 'image1',
|
||||||
|
id: '1',
|
||||||
|
src: 'images/virtual-background/background-1.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image2',
|
||||||
|
id: '2',
|
||||||
|
src: 'images/virtual-background/background-2.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image3',
|
||||||
|
id: '3',
|
||||||
|
src: 'images/virtual-background/background-3.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image4',
|
||||||
|
id: '4',
|
||||||
|
src: 'images/virtual-background/background-4.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image5',
|
||||||
|
id: '5',
|
||||||
|
src: 'images/virtual-background/background-5.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image6',
|
||||||
|
id: '6',
|
||||||
|
src: 'images/virtual-background/background-6.jpg'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tooltip: 'image7',
|
||||||
|
id: '7',
|
||||||
|
src: 'images/virtual-background/background-7.jpg'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
Loading…
Reference in New Issue