Add `Install chrome extension` banner (#4996)
This commit is contained in:
parent
ad68a87dba
commit
63a411168e
14
config.js
14
config.js
|
@ -385,6 +385,20 @@ var config = {
|
||||||
// userRegion: "asia"
|
// userRegion: "asia"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Information for the chrome extension banner
|
||||||
|
// chromeExtensionBanner: {
|
||||||
|
// // The chrome extension to be installed address
|
||||||
|
// url: 'https://chrome.google.com/webstore/detail/jitsi-meetings/kglhbbefdnlheedjiejgomgmfplipfeb',
|
||||||
|
|
||||||
|
// // Extensions info which allows checking if they are installed or not
|
||||||
|
// chromeExtensionsInfo: [
|
||||||
|
// {
|
||||||
|
// id: 'kglhbbefdnlheedjiejgomgmfplipfeb',
|
||||||
|
// path: 'jitsi-logo-48x48.png'
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
|
||||||
// Local Recording
|
// Local Recording
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,11 @@ body {
|
||||||
fill: white;
|
fill: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.jitsi-icon.gray svg {
|
||||||
|
fill: #5E6D7A;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
|
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
|
||||||
* preventing transparency in filmstrip-only mode. The selector chosen to
|
* preventing transparency in filmstrip-only mode. The selector chosen to
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
.chrome-extension-banner {
|
||||||
|
position: fixed;
|
||||||
|
width: 406px;
|
||||||
|
height: $chromeExtensionBannerHeight;
|
||||||
|
background: #FFF;
|
||||||
|
box-shadow: 0px 2px 48px rgba(0, 0, 0, 0.25);
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1000;
|
||||||
|
float: right;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 20px;
|
||||||
|
top: $chromeExtensionBannerTop;
|
||||||
|
right: $chromeExtensionBannerRight;
|
||||||
|
&__pos_in_meeting {
|
||||||
|
top: $chromeExtensionBannerTopInMeeting;
|
||||||
|
right: $chromeExtensionBannerRightInMeeeting;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkbox-container {
|
||||||
|
display: $chromeExtensionBannerDontShowAgainDisplay;
|
||||||
|
margin-left: 45px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__checkbox-label {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
letter-spacing: -0.006em;
|
||||||
|
color: #1C2025;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon-container {
|
||||||
|
display: flex;
|
||||||
|
background: url('../images/chromeLogo.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
width: 27px;
|
||||||
|
height: 27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text-container {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
letter-spacing: -0.006em;
|
||||||
|
color: #151531;
|
||||||
|
width: 329px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__close-container {
|
||||||
|
display: flex;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__gray-close-icon {
|
||||||
|
fill: #5E6D7A;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-open-url {
|
||||||
|
background: #0A57EB;
|
||||||
|
border-radius: 24px;
|
||||||
|
margin-left: 45px;
|
||||||
|
width: 236px;
|
||||||
|
height: 40px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__button-text {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: -0.006em;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
|
@ -260,3 +260,14 @@ $deepLinkingMobileButtonFontWeight: bold;
|
||||||
$deepLinkingMobileButtonFontSize: inherit;
|
$deepLinkingMobileButtonFontSize: inherit;
|
||||||
|
|
||||||
$primaryDeepLinkingMobileButtonBorderRadius: inherit;
|
$primaryDeepLinkingMobileButtonBorderRadius: inherit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome extension banner variables.
|
||||||
|
*/
|
||||||
|
$chromeExtensionBannerDontShowAgainDisplay: flex;
|
||||||
|
$chromeExtensionBannerHeight: 128px;
|
||||||
|
$chromeExtensionBannerTop: 80px;
|
||||||
|
$chromeExtensionBannerRight: 16px;
|
||||||
|
$chromeExtensionBannerTopInMeeting: 10px;
|
||||||
|
$chromeExtensionBannerRightInMeeeting: 10px;
|
||||||
|
|
||||||
|
|
|
@ -85,5 +85,6 @@ $flagsImagePath: "../images/";
|
||||||
@import 'third-party-branding/microsoft';
|
@import 'third-party-branding/microsoft';
|
||||||
@import 'avatar';
|
@import 'avatar';
|
||||||
@import 'promotional-footer';
|
@import 'promotional-footer';
|
||||||
|
@import 'chrome-extension-banner';
|
||||||
|
|
||||||
/* Modules END */
|
/* Modules END */
|
||||||
|
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 134 KiB |
|
@ -199,7 +199,14 @@ var interfaceConfig = {
|
||||||
DISABLE_PRESENCE_STATUS: false,
|
DISABLE_PRESENCE_STATUS: false,
|
||||||
|
|
||||||
// If true, notifications regarding joining/leaving are no longer displayed
|
// If true, notifications regarding joining/leaving are no longer displayed
|
||||||
DISABLE_JOIN_LEAVE_NOTIFICATIONS: false
|
DISABLE_JOIN_LEAVE_NOTIFICATIONS: false,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides whether the chrome extension banner should be rendered on the landing page and during the meeting.
|
||||||
|
* If this is set to false, the banner will not be rendered at all. If set to true, the check for extension(s)
|
||||||
|
* being already installed is done before rendering.
|
||||||
|
*/
|
||||||
|
SHOW_CHROME_EXTENSION_BANNER: false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How many columns the tile view can expand to. The respected range is
|
* How many columns the tile view can expand to. The respected range is
|
||||||
|
|
|
@ -59,6 +59,11 @@
|
||||||
"title": "Chat",
|
"title": "Chat",
|
||||||
"you": "you"
|
"you": "you"
|
||||||
},
|
},
|
||||||
|
"chromeExtensionBanner": {
|
||||||
|
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration",
|
||||||
|
"buttonText": "Install Chrome Extension",
|
||||||
|
"dontShowAgain": "Don’t show me this again"
|
||||||
|
},
|
||||||
"connectingOverlay": {
|
"connectingOverlay": {
|
||||||
"joiningRoom": "Connecting you to your meeting..."
|
"joiningRoom": "Connecting you to your meeting..."
|
||||||
},
|
},
|
||||||
|
@ -277,7 +282,7 @@
|
||||||
"dialOut": {
|
"dialOut": {
|
||||||
"statusMessage": "is now {{status}}"
|
"statusMessage": "is now {{status}}"
|
||||||
},
|
},
|
||||||
"documentSharing" : {
|
"documentSharing": {
|
||||||
"title": "Shared Document"
|
"title": "Shared Document"
|
||||||
},
|
},
|
||||||
"feedback": {
|
"feedback": {
|
||||||
|
@ -630,7 +635,6 @@
|
||||||
"lowerYourHand": "Lower your hand",
|
"lowerYourHand": "Lower your hand",
|
||||||
"moreActions": "More actions",
|
"moreActions": "More actions",
|
||||||
"mute": "Mute / Unmute",
|
"mute": "Mute / Unmute",
|
||||||
|
|
||||||
"noAudioSignalTitle": "There is no input coming from your mic!",
|
"noAudioSignalTitle": "There is no input coming from your mic!",
|
||||||
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider changing the device.",
|
"noAudioSignalDesc": "If you did not purposely mute it from system settings or hardware, consider changing the device.",
|
||||||
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider using the following device:",
|
"noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider using the following device:",
|
||||||
|
|
|
@ -18,7 +18,9 @@ import { PersistenceRegistry } from '../../storage';
|
||||||
|
|
||||||
import { appWillMount, appWillUnmount } from '../actions';
|
import { appWillMount, appWillUnmount } from '../actions';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
import { ChromeExtensionBanner } from '../../../chrome-extension-banner';
|
||||||
|
|
||||||
|
declare var interfaceConfig: Object;
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,6 +131,11 @@ export default class BaseApp extends Component<*, State> {
|
||||||
<I18nextProvider i18n = { i18next }>
|
<I18nextProvider i18n = { i18next }>
|
||||||
<Provider store = { store }>
|
<Provider store = { store }>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
{
|
||||||
|
typeof interfaceConfig !== 'undefined'
|
||||||
|
&& interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
|
||||||
|
&& <ChromeExtensionBanner />
|
||||||
|
}
|
||||||
{ this._createMainElement(component) }
|
{ this._createMainElement(component) }
|
||||||
<SoundCollection />
|
<SoundCollection />
|
||||||
{ this._createExtraElement() }
|
{ this._createExtraElement() }
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
// @flow
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from '../../base/redux';
|
||||||
|
import { Icon, IconClose } from '../../base/icons';
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import { getCurrentConference } from '../../base/conference/functions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local storage key name for flag telling if user checked 'Don't show again' checkbox on the banner
|
||||||
|
* If the user checks this before closing the banner, next time he will access a jitsi domain
|
||||||
|
* the banner will not be shown regardless of extensions being installed or not.
|
||||||
|
*/
|
||||||
|
const DONT_SHOW_AGAIN_CHECKED = 'hide_chrome_extension_banner';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@code PureComponent} props of {@link ChromeExtensionBanner}.
|
||||||
|
*/
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conference data, if any
|
||||||
|
*/
|
||||||
|
conference: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The url of the chrome extension
|
||||||
|
*/
|
||||||
|
chromeExtensionUrl: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array containing info for identifying a chrome extension
|
||||||
|
*/
|
||||||
|
chromeExtensionsInfo: Array<Object>,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked to obtain translated strings.
|
||||||
|
*/
|
||||||
|
t: Function,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the React {@link PureComponent} state of {@link ChromeExtensionBanner}.
|
||||||
|
*/
|
||||||
|
type State = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the current value of dont show again checkbox
|
||||||
|
*/
|
||||||
|
dontShowAgainChecked: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether user pressed install extension or close button.
|
||||||
|
*/
|
||||||
|
closePressed: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether should show the banner or not based on extension being installed or not.
|
||||||
|
*/
|
||||||
|
shouldShow: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a React {@link PureComponent} which displays a banner having a link to the chrome extension.
|
||||||
|
* @class ChromeExtensionBanner
|
||||||
|
* @extends PureComponent
|
||||||
|
*/
|
||||||
|
class ChromeExtensionBanner extends PureComponent<Props, State> {
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code ChromeExtensionBanner} instance.
|
||||||
|
*
|
||||||
|
* @param {Object} props - The read-only React {@code PureComponent} props with
|
||||||
|
* which the new instance is to be initialized.
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
dontShowAgainChecked: false,
|
||||||
|
closePressed: false,
|
||||||
|
shouldShow: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onClosePressed = this._onClosePressed.bind(this);
|
||||||
|
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
|
||||||
|
this._checkExtensionsInstalled = this._checkExtensionsInstalled.bind(this);
|
||||||
|
this._shouldNotRender = this._shouldNotRender.bind(this);
|
||||||
|
this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executed on component update.
|
||||||
|
* Checks whether any chrome extension from the config is installed.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async componentDidUpdate() {
|
||||||
|
const hasExtensions = await this._checkExtensionsInstalled();
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasExtensions
|
||||||
|
&& hasExtensions.length
|
||||||
|
&& hasExtensions.every(ext => !ext)
|
||||||
|
&& !this.state.shouldShow
|
||||||
|
) {
|
||||||
|
this.setState({ shouldShow: true }); // eslint-disable-line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClosePressed: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the banner for the current session.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClosePressed() {
|
||||||
|
this.setState({ closePressed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInstallExtensionClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the chrome extension page.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onInstallExtensionClick() {
|
||||||
|
window.open(this.props.chromeExtensionUrl);
|
||||||
|
this.setState({ closePressed: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkExtensionsInstalled: () => Promise<*>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the chrome extensions defined in the config file are installed or not.
|
||||||
|
*
|
||||||
|
* @returns {Promise[]}
|
||||||
|
*/
|
||||||
|
_checkExtensionsInstalled() {
|
||||||
|
const isExtensionInstalled = info => new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
|
||||||
|
img.src = `chrome-extension://${info.id}/${info.path}`;
|
||||||
|
img.onload = function() {
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
img.onerror = function() {
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const extensionInstalledFunction = info => isExtensionInstalled(info);
|
||||||
|
|
||||||
|
if (!this.props.chromeExtensionsInfo.length) {
|
||||||
|
console.warn('Further configuration needed, missing chrome extension(s) info');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(
|
||||||
|
this.props.chromeExtensionsInfo.map(info => extensionInstalledFunction(info))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_shouldNotRender: () => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the banner should be displayed based on:
|
||||||
|
* Whether there is a configuration issue with the chrome extensions data.
|
||||||
|
* Whether the user checked don't show again checkbox in a previous session.
|
||||||
|
* Whether the user closed the banner.
|
||||||
|
* Whether the extension is already installed.
|
||||||
|
*
|
||||||
|
* @returns {boolean} whether to show the banner or not.
|
||||||
|
*/
|
||||||
|
_shouldNotRender() {
|
||||||
|
if (!this.props.chromeExtensionUrl) {
|
||||||
|
console.warn('Further configuration needed, missing chrome extension URL');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dontShowAgain = localStorage.getItem(DONT_SHOW_AGAIN_CHECKED) === 'true';
|
||||||
|
|
||||||
|
return dontShowAgain
|
||||||
|
|| this.state.closePressed
|
||||||
|
|| !this.state.shouldShow;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDontShowAgainChange: (object: Object) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the current `don't show again` checkbox state.
|
||||||
|
*
|
||||||
|
* @param {Object} event - Input change event.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDontShowAgainChange(event) {
|
||||||
|
this.setState({ dontShowAgainChecked: event.target.checked });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link PureComponent#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (this._shouldNotRender()) {
|
||||||
|
if (this.state.dontShowAgainChecked) {
|
||||||
|
localStorage.setItem(DONT_SHOW_AGAIN_CHECKED, 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { t } = this.props;
|
||||||
|
const mainClassNames = this.props.conference
|
||||||
|
? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting'
|
||||||
|
: 'chrome-extension-banner';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className = { mainClassNames }>
|
||||||
|
<div className = 'chrome-extension-banner__container'>
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__icon-container' />
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__text-container'>
|
||||||
|
{ t('chromeExtensionBanner.installExtensionText') }
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__close-container'
|
||||||
|
onClick = { this._onClosePressed }>
|
||||||
|
<Icon
|
||||||
|
className = 'gray'
|
||||||
|
size = { 12 }
|
||||||
|
src = { IconClose } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__button-container'>
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__button-open-url'
|
||||||
|
onClick = { this._onInstallExtensionClick }>
|
||||||
|
<div
|
||||||
|
className = 'chrome-extension-banner__button-text'>
|
||||||
|
{ t('chromeExtensionBanner.buttonText') }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className = 'chrome-extension-banner__checkbox-container'>
|
||||||
|
<label className = 'chrome-extension-banner__checkbox-label'>
|
||||||
|
<input
|
||||||
|
checked = { this.state.dontShowAgainChecked }
|
||||||
|
onChange = { this._onDontShowAgainChange }
|
||||||
|
type = 'checkbox' />
|
||||||
|
{ t('chromeExtensionBanner.dontShowAgain') }
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function that maps parts of Redux state tree into component props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - Redux state.
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
const _mapStateToProps = state => {
|
||||||
|
const bannerCfg = state['features/base/config'].chromeExtensionBanner || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
chromeExtensionUrl: bannerCfg.url,
|
||||||
|
chromeExtensionsInfo: bannerCfg.chromeExtensionsInfo || [],
|
||||||
|
conference: getCurrentConference(state)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps)(ChromeExtensionBanner));
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as ChromeExtensionBanner }
|
||||||
|
from './ChromeExtensionBanner';
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './components';
|
Loading…
Reference in New Issue