2020-01-23 09:32:52 +00:00
|
|
|
// @flow
|
2020-05-01 19:48:08 +00:00
|
|
|
|
2020-07-09 07:17:23 +00:00
|
|
|
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
2020-01-23 09:32:52 +00:00
|
|
|
import React, { PureComponent } from 'react';
|
2020-05-01 19:48:08 +00:00
|
|
|
|
|
|
|
import {
|
|
|
|
createChromeExtensionBannerEvent,
|
|
|
|
sendAnalytics
|
|
|
|
} from '../../analytics';
|
|
|
|
import { getCurrentConference } from '../../base/conference/functions';
|
2020-02-25 12:41:13 +00:00
|
|
|
import {
|
|
|
|
checkChromeExtensionsInstalled,
|
|
|
|
isMobileBrowser
|
|
|
|
} from '../../base/environment/utils';
|
2020-05-20 10:57:03 +00:00
|
|
|
import { translate } from '../../base/i18n';
|
|
|
|
import { Icon, IconClose } from '../../base/icons';
|
|
|
|
import { browser } from '../../base/lib-jitsi-meet';
|
|
|
|
import { connect } from '../../base/redux';
|
2021-07-20 08:58:42 +00:00
|
|
|
import { isVpaasMeeting } from '../../jaas/functions';
|
2020-02-06 11:58:16 +00:00
|
|
|
import logger from '../logger';
|
2020-05-01 19:48:08 +00:00
|
|
|
|
2020-01-29 12:30:17 +00:00
|
|
|
|
|
|
|
declare var interfaceConfig: Object;
|
2020-01-23 09:32:52 +00:00
|
|
|
|
2020-02-25 15:09:52 +00:00
|
|
|
const emptyObject = {};
|
|
|
|
|
2020-01-23 09:32:52 +00:00
|
|
|
/**
|
|
|
|
* 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 = {
|
|
|
|
|
|
|
|
/**
|
2020-02-25 12:41:13 +00:00
|
|
|
* Contains info about installed/to be installed chrome extension(s).
|
2020-01-23 09:32:52 +00:00
|
|
|
*/
|
2020-02-25 12:41:13 +00:00
|
|
|
bannerCfg: Object,
|
2020-01-23 09:32:52 +00:00
|
|
|
|
|
|
|
/**
|
2021-11-04 21:10:43 +00:00
|
|
|
* Conference data, if any.
|
2020-01-23 09:32:52 +00:00
|
|
|
*/
|
2020-02-25 12:41:13 +00:00
|
|
|
conference: Object,
|
2020-01-23 09:32:52 +00:00
|
|
|
|
2020-01-29 12:30:17 +00:00
|
|
|
/**
|
|
|
|
* Whether I am the current recorder.
|
|
|
|
*/
|
|
|
|
iAmRecorder: boolean,
|
|
|
|
|
2020-08-25 11:57:35 +00:00
|
|
|
/**
|
|
|
|
* Whether it's a vpaas meeting or not.
|
|
|
|
*/
|
|
|
|
isVpaas: boolean,
|
|
|
|
|
2020-01-23 09:32:52 +00:00
|
|
|
/**
|
|
|
|
* Invoked to obtain translated strings.
|
|
|
|
*/
|
|
|
|
t: Function,
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The type of the React {@link PureComponent} state of {@link ChromeExtensionBanner}.
|
|
|
|
*/
|
|
|
|
type State = {
|
|
|
|
|
|
|
|
/**
|
2021-11-04 21:10:43 +00:00
|
|
|
* Keeps the current value of dont show again checkbox.
|
2020-01-23 09:32:52 +00:00
|
|
|
*/
|
|
|
|
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.
|
2021-11-04 21:10:43 +00:00
|
|
|
*
|
2020-01-23 09:32:52 +00:00
|
|
|
* @class ChromeExtensionBanner
|
2021-11-04 21:10:43 +00:00
|
|
|
* @augments PureComponent
|
2020-01-23 09:32:52 +00:00
|
|
|
*/
|
|
|
|
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
|
|
|
|
};
|
|
|
|
|
2022-06-16 06:55:30 +00:00
|
|
|
this.isEdge = /Edg(e)?/.test(navigator.userAgent);
|
2020-01-23 09:32:52 +00:00
|
|
|
this._onClosePressed = this._onClosePressed.bind(this);
|
|
|
|
this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
|
|
|
|
this._shouldNotRender = this._shouldNotRender.bind(this);
|
|
|
|
this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
|
2021-06-10 12:48:44 +00:00
|
|
|
this._onCloseKeyPress = this._onCloseKeyPress.bind(this);
|
|
|
|
this._onInstallExtensionKeyPress = this._onInstallExtensionKeyPress.bind(this);
|
2020-01-23 09:32:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Executed on component update.
|
|
|
|
* Checks whether any chrome extension from the config is installed.
|
|
|
|
*
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
2020-02-18 09:56:09 +00:00
|
|
|
async componentDidUpdate(prevProps) {
|
2020-02-06 11:58:16 +00:00
|
|
|
if (!this._isSupportedEnvironment()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-02-25 12:41:13 +00:00
|
|
|
const { bannerCfg } = this.props;
|
|
|
|
const prevBannerCfg = prevProps.bannerCfg;
|
|
|
|
|
|
|
|
if (bannerCfg.url && !prevBannerCfg.url) {
|
2020-02-18 09:56:09 +00:00
|
|
|
logger.info('Chrome extension URL found.');
|
|
|
|
}
|
|
|
|
|
2020-02-25 12:41:13 +00:00
|
|
|
if ((bannerCfg.chromeExtensionsInfo || []).length && !(prevBannerCfg.chromeExtensionsInfo || []).length) {
|
2020-02-18 09:56:09 +00:00
|
|
|
logger.info('Chrome extension(s) info found.');
|
|
|
|
}
|
|
|
|
|
2020-02-25 12:41:13 +00:00
|
|
|
const hasExtensions = await checkChromeExtensionsInstalled(this.props.bannerCfg);
|
2020-01-23 09:32:52 +00:00
|
|
|
|
|
|
|
if (
|
|
|
|
hasExtensions
|
|
|
|
&& hasExtensions.length
|
|
|
|
&& hasExtensions.every(ext => !ext)
|
|
|
|
&& !this.state.shouldShow
|
|
|
|
) {
|
|
|
|
this.setState({ shouldShow: true }); // eslint-disable-line
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-06 11:58:16 +00:00
|
|
|
/**
|
|
|
|
* Checks whether the feature is enabled and whether the environment(browser/os)
|
|
|
|
* supports it.
|
|
|
|
*
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
_isSupportedEnvironment() {
|
|
|
|
return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
|
|
|
|
&& browser.isChrome()
|
2020-10-20 09:25:38 +00:00
|
|
|
&& !browser.isTwa()
|
2020-08-25 11:57:35 +00:00
|
|
|
&& !isMobileBrowser()
|
|
|
|
&& !this.props.isVpaas;
|
2020-02-06 11:58:16 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 09:32:52 +00:00
|
|
|
_onClosePressed: () => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes the banner for the current session.
|
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onClosePressed() {
|
2020-02-25 12:41:13 +00:00
|
|
|
sendAnalytics(createChromeExtensionBannerEvent(false));
|
2020-01-23 09:32:52 +00:00
|
|
|
this.setState({ closePressed: true });
|
|
|
|
}
|
|
|
|
|
2021-06-10 12:48:44 +00:00
|
|
|
_onCloseKeyPress: (Object) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* KeyPress handler for accessibility.
|
|
|
|
*
|
|
|
|
* @param {Object} e - The key event to handle.
|
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onCloseKeyPress(e) {
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
|
|
e.preventDefault();
|
|
|
|
this._onClosePressed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-23 09:32:52 +00:00
|
|
|
_onInstallExtensionClick: () => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Opens the chrome extension page.
|
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onInstallExtensionClick() {
|
2022-06-16 06:55:30 +00:00
|
|
|
const { edgeUrl, url } = this.props.bannerCfg;
|
|
|
|
|
2020-02-25 12:41:13 +00:00
|
|
|
sendAnalytics(createChromeExtensionBannerEvent(true));
|
2022-06-16 06:55:30 +00:00
|
|
|
window.open(this.isEdge && edgeUrl ? edgeUrl : url);
|
2020-01-23 09:32:52 +00:00
|
|
|
this.setState({ closePressed: true });
|
|
|
|
}
|
|
|
|
|
2021-06-10 12:48:44 +00:00
|
|
|
_onInstallExtensionKeyPress: (Object) => void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* KeyPress handler for accessibility.
|
|
|
|
*
|
|
|
|
* @param {Object} e - The key event to handle.
|
|
|
|
*
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
_onInstallExtensionKeyPress(e) {
|
|
|
|
if (e.key === ' ' || e.key === 'Enter') {
|
|
|
|
e.preventDefault();
|
|
|
|
this._onClosePressed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-23 09:32:52 +00:00
|
|
|
_shouldNotRender: () => boolean;
|
|
|
|
|
|
|
|
/**
|
2020-01-29 12:30:17 +00:00
|
|
|
* Checks whether the banner should not be rendered.
|
2020-01-23 09:32:52 +00:00
|
|
|
*
|
2020-01-23 14:20:04 +00:00
|
|
|
* @returns {boolean} Whether to show the banner or not.
|
2020-01-23 09:32:52 +00:00
|
|
|
*/
|
|
|
|
_shouldNotRender() {
|
2020-02-06 11:58:16 +00:00
|
|
|
if (!this._isSupportedEnvironment()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-05-01 19:48:08 +00:00
|
|
|
const dontShowAgain = jitsiLocalStorage.getItem(DONT_SHOW_AGAIN_CHECKED) === 'true';
|
2020-01-23 09:32:52 +00:00
|
|
|
|
2020-02-25 12:41:13 +00:00
|
|
|
return !this.props.bannerCfg.url
|
2020-02-18 09:56:09 +00:00
|
|
|
|| dontShowAgain
|
2020-01-29 12:30:17 +00:00
|
|
|
|| this.state.closePressed
|
|
|
|
|| !this.state.shouldShow
|
|
|
|
|| this.props.iAmRecorder;
|
2020-01-23 09:32:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
_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) {
|
2020-05-01 19:48:08 +00:00
|
|
|
jitsiLocalStorage.setItem(DONT_SHOW_AGAIN_CHECKED, 'true');
|
2020-01-23 09:32:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2022-06-16 06:55:30 +00:00
|
|
|
const { bannerCfg, t } = this.props;
|
2020-01-23 09:32:52 +00:00
|
|
|
const mainClassNames = this.props.conference
|
|
|
|
? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting'
|
|
|
|
: 'chrome-extension-banner';
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className = { mainClassNames }>
|
2021-06-10 12:48:44 +00:00
|
|
|
<div
|
|
|
|
aria-aria-describedby = 'chrome-extension-banner__text-container'
|
|
|
|
className = 'chrome-extension-banner__container'
|
|
|
|
role = 'banner'>
|
|
|
|
<div className = 'chrome-extension-banner__icon-container' />
|
2020-01-23 09:32:52 +00:00
|
|
|
<div
|
2021-06-10 12:48:44 +00:00
|
|
|
className = 'chrome-extension-banner__text-container'
|
|
|
|
id = 'chrome-extension-banner__text-container'>
|
2020-01-23 09:32:52 +00:00
|
|
|
{ t('chromeExtensionBanner.installExtensionText') }
|
|
|
|
</div>
|
|
|
|
<div
|
2021-06-10 12:48:44 +00:00
|
|
|
aria-label = { t('chromeExtensionBanner.close') }
|
2020-01-23 09:32:52 +00:00
|
|
|
className = 'chrome-extension-banner__close-container'
|
2021-06-10 12:48:44 +00:00
|
|
|
onClick = { this._onClosePressed }
|
|
|
|
onKeyPress = { this._onCloseKeyPress }
|
|
|
|
role = 'button'
|
|
|
|
tabIndex = { 0 }>
|
2020-01-23 09:32:52 +00:00
|
|
|
<Icon
|
|
|
|
className = 'gray'
|
|
|
|
size = { 12 }
|
|
|
|
src = { IconClose } />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div
|
|
|
|
className = 'chrome-extension-banner__button-container'>
|
|
|
|
<div
|
2021-06-10 12:48:44 +00:00
|
|
|
aria-labelledby = 'chrome-extension-banner__button-text'
|
2020-01-23 09:32:52 +00:00
|
|
|
className = 'chrome-extension-banner__button-open-url'
|
2021-06-10 12:48:44 +00:00
|
|
|
onClick = { this._onInstallExtensionClick }
|
|
|
|
onKeyPress = { this._onInstallExtensionKeyPress }
|
|
|
|
role = 'button'
|
|
|
|
tabIndex = { 0 }>
|
2020-01-23 09:32:52 +00:00
|
|
|
<div
|
2021-06-10 12:48:44 +00:00
|
|
|
className = 'chrome-extension-banner__button-text'
|
|
|
|
id = 'chrome-extension-banner__button-text'>
|
2022-06-16 06:55:30 +00:00
|
|
|
{ t(this.isEdge && bannerCfg.edgeUrl
|
|
|
|
? 'chromeExtensionBanner.buttonTextEdge'
|
|
|
|
: 'chromeExtensionBanner.buttonText')
|
|
|
|
}
|
2020-01-23 09:32:52 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className = 'chrome-extension-banner__checkbox-container'>
|
2021-06-10 12:48:44 +00:00
|
|
|
<label
|
|
|
|
className = 'chrome-extension-banner__checkbox-label'
|
|
|
|
htmlFor = 'chrome-extension-banner__checkbox'
|
|
|
|
id = 'chrome-extension-banner__checkbox-label'>
|
2020-01-23 09:32:52 +00:00
|
|
|
<input
|
2021-06-10 12:48:44 +00:00
|
|
|
aria-labelledby = 'chrome-extension-banner__checkbox-label'
|
2020-01-23 09:32:52 +00:00
|
|
|
checked = { this.state.dontShowAgainChecked }
|
2021-06-10 12:48:44 +00:00
|
|
|
id = 'chrome-extension-banner__checkbox'
|
2020-01-23 09:32:52 +00:00
|
|
|
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 => {
|
|
|
|
return {
|
2020-02-25 15:09:52 +00:00
|
|
|
// Using emptyObject so that we don't change the reference every time when _mapStateToProps is called.
|
|
|
|
bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject,
|
2020-01-29 12:30:17 +00:00
|
|
|
conference: getCurrentConference(state),
|
2020-08-25 11:57:35 +00:00
|
|
|
iAmRecorder: state['features/base/config'].iAmRecorder,
|
2021-07-16 12:14:37 +00:00
|
|
|
isVpaas: isVpaasMeeting(state)
|
2020-01-23 09:32:52 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
export default translate(connect(_mapStateToProps)(ChromeExtensionBanner));
|