Add `Install chrome extension` banner (#4996)

This commit is contained in:
horymury 2020-01-23 11:32:52 +02:00 committed by Дамян Минков
parent ad68a87dba
commit 63a411168e
13 changed files with 433 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

9
images/chromeLogo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 134 KiB

View File

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

View File

@ -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": "Dont 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:",

View File

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

View File

@ -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' />
&nbsp;{ 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));

View File

@ -0,0 +1,2 @@
export { default as ChromeExtensionBanner }
from './ChromeExtensionBanner';

View File

@ -0,0 +1 @@
export * from './components';