// @ts-expect-error import { jitsiLocalStorage } from '@jitsi/js-utils'; import React, { PureComponent } from 'react'; import { WithTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { createChromeExtensionBannerEvent } from '../../analytics/AnalyticsEvents'; import { sendAnalytics } from '../../analytics/functions'; import { IReduxState } from '../../app/types'; import { getCurrentConference } from '../../base/conference/functions'; import { IJitsiConference } from '../../base/conference/reducer'; import checkChromeExtensionsInstalled from '../../base/environment/checkChromeExtensionsInstalled.web'; import { isMobileBrowser } from '../../base/environment/utils'; import { translate } from '../../base/i18n/functions'; import Icon from '../../base/icons/components/Icon'; import { IconCloseLarge } from '../../base/icons/svg'; import { browser } from '../../base/lib-jitsi-meet'; import { isNarrowScreenWithChatOpen } from '../../base/responsive-ui/functions'; import { isVpaasMeeting } from '../../jaas/functions'; import logger from '../logger'; const emptyObject = {}; /** * 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}. */ interface IProps extends WithTranslation { /** * Contains info about installed/to be installed chrome extension(s). */ bannerCfg: { chromeExtensionsInfo?: string[]; edgeUrl?: string; url?: string; }; /** * Conference data, if any. */ conference?: IJitsiConference; /** * Whether I am the current recorder. */ iAmRecorder: boolean; /** * Whether it's a vpaas meeting or not. */ isVpaas: boolean; /** * Whether the screen is too small to show the banner or not. */ notEnoughAvailableWidth: boolean; } /** * The type of the React {@link PureComponent} state of {@link ChromeExtensionBanner}. */ interface IState { /** * Tells whether user pressed install extension or close button. */ closePressed: boolean; /** * Keeps the current value of dont show again checkbox. */ dontShowAgainChecked: 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 * @augments PureComponent */ class ChromeExtensionBanner extends PureComponent { isEdge: boolean; /** * 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: IProps) { super(props); this.state = { dontShowAgainChecked: false, closePressed: false, shouldShow: false }; this.isEdge = /Edg(e)?/.test(navigator.userAgent); this._onClosePressed = this._onClosePressed.bind(this); this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this); this._shouldNotRender = this._shouldNotRender.bind(this); this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this); this._onCloseKeyPress = this._onCloseKeyPress.bind(this); this._onInstallExtensionKeyPress = this._onInstallExtensionKeyPress.bind(this); } /** * Executed on component update. * Checks whether any chrome extension from the config is installed. * * @inheritdoc */ override async componentDidUpdate(prevProps: IProps) { if (!this._isSupportedEnvironment()) { return; } const { bannerCfg } = this.props; const prevBannerCfg = prevProps.bannerCfg; if (bannerCfg.url && !prevBannerCfg.url) { logger.info('Chrome extension URL found.'); } if ((bannerCfg.chromeExtensionsInfo || []).length && !(prevBannerCfg.chromeExtensionsInfo || []).length) { logger.info('Chrome extension(s) info found.'); } const hasExtensions = await checkChromeExtensionsInstalled(this.props.bannerCfg); if ( hasExtensions?.length && hasExtensions.every(ext => !ext) && !this.state.shouldShow ) { this.setState({ shouldShow: true }); // eslint-disable-line } } /** * Checks whether the feature is enabled and whether the environment(browser/os) * supports it. * * @returns {boolean} */ _isSupportedEnvironment() { return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER && browser.isChromiumBased() && !browser.isTwa() && !isMobileBrowser() && !this.props.isVpaas; } /** * Closes the banner for the current session. * * @returns {void} */ _onClosePressed() { sendAnalytics(createChromeExtensionBannerEvent(false)); this.setState({ closePressed: true }); } /** * KeyPress handler for accessibility. * * @param {Object} e - The key event to handle. * * @returns {void} */ _onCloseKeyPress(e: React.KeyboardEvent) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); this._onClosePressed(); } } /** * Opens the chrome extension page. * * @returns {void} */ _onInstallExtensionClick() { const { edgeUrl, url } = this.props.bannerCfg; sendAnalytics(createChromeExtensionBannerEvent(true)); window.open(this.isEdge && edgeUrl ? edgeUrl : url); this.setState({ closePressed: true }); } /** * KeyPress handler for accessibility. * * @param {Object} e - The key event to handle. * * @returns {void} */ _onInstallExtensionKeyPress(e: React.KeyboardEvent) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); this._onClosePressed(); } } /** * Checks whether the banner should not be rendered. * * @returns {boolean} Whether to show the banner or not. */ _shouldNotRender() { if (!this._isSupportedEnvironment()) { return true; } const dontShowAgain = jitsiLocalStorage.getItem(DONT_SHOW_AGAIN_CHECKED) === 'true'; return !this.props.bannerCfg.url || dontShowAgain || this.state.closePressed || !this.state.shouldShow || this.props.iAmRecorder; } /** * Handles the current `don't show again` checkbox state. * * @param {Object} event - Input change event. * @returns {void} */ _onDontShowAgainChange(event: React.ChangeEvent) { this.setState({ dontShowAgainChecked: event.target.checked }); } /** * Implements React's {@link PureComponent#render()}. * * @inheritdoc * @returns {ReactElement} */ override render(): React.ReactNode { if (this._shouldNotRender()) { if (this.state.dontShowAgainChecked) { jitsiLocalStorage.setItem(DONT_SHOW_AGAIN_CHECKED, 'true'); } return null; } if (!this.props.notEnoughAvailableWidth) { return null; } const { bannerCfg, t } = this.props; const mainClassNames = this.props.conference ? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting' : 'chrome-extension-banner'; return (
{ t('chromeExtensionBanner.installExtensionText') }
{ t(this.isEdge && bannerCfg.edgeUrl ? 'chromeExtensionBanner.buttonTextEdge' : 'chromeExtensionBanner.buttonText') }
); } } /** * Function that maps parts of Redux state tree into component props. * * @param {Object} state - Redux state. * @returns {Object} */ const _mapStateToProps = (state: IReduxState) => { return { // Using emptyObject so that we don't change the reference every time when _mapStateToProps is called. bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject, conference: getCurrentConference(state), iAmRecorder: Boolean(state['features/base/config'].iAmRecorder), isVpaas: isVpaasMeeting(state), notEnoughAvailableWidth: isNarrowScreenWithChatOpen(state) }; }; export default translate(connect(_mapStateToProps)(ChromeExtensionBanner));