You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ChromeExtensionBanner.web.js 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. // @flow
  2. import { jitsiLocalStorage } from '@jitsi/js-utils';
  3. import React, { PureComponent } from 'react';
  4. import {
  5. createChromeExtensionBannerEvent,
  6. sendAnalytics
  7. } from '../../analytics';
  8. import { getCurrentConference } from '../../base/conference/functions';
  9. import {
  10. checkChromeExtensionsInstalled,
  11. isMobileBrowser
  12. } from '../../base/environment/utils';
  13. import { translate } from '../../base/i18n';
  14. import { Icon, IconClose } from '../../base/icons';
  15. import { browser } from '../../base/lib-jitsi-meet';
  16. import { connect } from '../../base/redux';
  17. import logger from '../logger';
  18. declare var interfaceConfig: Object;
  19. const emptyObject = {};
  20. /**
  21. * Local storage key name for flag telling if user checked 'Don't show again' checkbox on the banner
  22. * If the user checks this before closing the banner, next time he will access a jitsi domain
  23. * the banner will not be shown regardless of extensions being installed or not.
  24. */
  25. const DONT_SHOW_AGAIN_CHECKED = 'hide_chrome_extension_banner';
  26. /**
  27. * The type of the React {@code PureComponent} props of {@link ChromeExtensionBanner}.
  28. */
  29. type Props = {
  30. /**
  31. * Contains info about installed/to be installed chrome extension(s).
  32. */
  33. bannerCfg: Object,
  34. /**
  35. * Conference data, if any
  36. */
  37. conference: Object,
  38. /**
  39. * Whether I am the current recorder.
  40. */
  41. iAmRecorder: boolean,
  42. /**
  43. * Invoked to obtain translated strings.
  44. */
  45. t: Function,
  46. };
  47. /**
  48. * The type of the React {@link PureComponent} state of {@link ChromeExtensionBanner}.
  49. */
  50. type State = {
  51. /**
  52. * Keeps the current value of dont show again checkbox
  53. */
  54. dontShowAgainChecked: boolean,
  55. /**
  56. * Tells whether user pressed install extension or close button.
  57. */
  58. closePressed: boolean,
  59. /**
  60. * Tells whether should show the banner or not based on extension being installed or not.
  61. */
  62. shouldShow: boolean,
  63. };
  64. /**
  65. * Implements a React {@link PureComponent} which displays a banner having a link to the chrome extension.
  66. * @class ChromeExtensionBanner
  67. * @extends PureComponent
  68. */
  69. class ChromeExtensionBanner extends PureComponent<Props, State> {
  70. /**
  71. * Initializes a new {@code ChromeExtensionBanner} instance.
  72. *
  73. * @param {Object} props - The read-only React {@code PureComponent} props with
  74. * which the new instance is to be initialized.
  75. */
  76. constructor(props: Props) {
  77. super(props);
  78. this.state = {
  79. dontShowAgainChecked: false,
  80. closePressed: false,
  81. shouldShow: false
  82. };
  83. this._onClosePressed = this._onClosePressed.bind(this);
  84. this._onInstallExtensionClick = this._onInstallExtensionClick.bind(this);
  85. this._shouldNotRender = this._shouldNotRender.bind(this);
  86. this._onDontShowAgainChange = this._onDontShowAgainChange.bind(this);
  87. }
  88. /**
  89. * Executed on component update.
  90. * Checks whether any chrome extension from the config is installed.
  91. *
  92. * @inheritdoc
  93. */
  94. async componentDidUpdate(prevProps) {
  95. if (!this._isSupportedEnvironment()) {
  96. return;
  97. }
  98. const { bannerCfg } = this.props;
  99. const prevBannerCfg = prevProps.bannerCfg;
  100. if (bannerCfg.url && !prevBannerCfg.url) {
  101. logger.info('Chrome extension URL found.');
  102. }
  103. if ((bannerCfg.chromeExtensionsInfo || []).length && !(prevBannerCfg.chromeExtensionsInfo || []).length) {
  104. logger.info('Chrome extension(s) info found.');
  105. }
  106. const hasExtensions = await checkChromeExtensionsInstalled(this.props.bannerCfg);
  107. if (
  108. hasExtensions
  109. && hasExtensions.length
  110. && hasExtensions.every(ext => !ext)
  111. && !this.state.shouldShow
  112. ) {
  113. this.setState({ shouldShow: true }); // eslint-disable-line
  114. }
  115. }
  116. /**
  117. * Checks whether the feature is enabled and whether the environment(browser/os)
  118. * supports it.
  119. *
  120. * @returns {boolean}
  121. */
  122. _isSupportedEnvironment() {
  123. return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
  124. && browser.isChrome()
  125. && !isMobileBrowser();
  126. }
  127. _onClosePressed: () => void;
  128. /**
  129. * Closes the banner for the current session.
  130. *
  131. * @returns {void}
  132. */
  133. _onClosePressed() {
  134. sendAnalytics(createChromeExtensionBannerEvent(false));
  135. this.setState({ closePressed: true });
  136. }
  137. _onInstallExtensionClick: () => void;
  138. /**
  139. * Opens the chrome extension page.
  140. *
  141. * @returns {void}
  142. */
  143. _onInstallExtensionClick() {
  144. sendAnalytics(createChromeExtensionBannerEvent(true));
  145. window.open(this.props.bannerCfg.url);
  146. this.setState({ closePressed: true });
  147. }
  148. _shouldNotRender: () => boolean;
  149. /**
  150. * Checks whether the banner should not be rendered.
  151. *
  152. * @returns {boolean} Whether to show the banner or not.
  153. */
  154. _shouldNotRender() {
  155. if (!this._isSupportedEnvironment()) {
  156. return true;
  157. }
  158. const dontShowAgain = jitsiLocalStorage.getItem(DONT_SHOW_AGAIN_CHECKED) === 'true';
  159. return !this.props.bannerCfg.url
  160. || dontShowAgain
  161. || this.state.closePressed
  162. || !this.state.shouldShow
  163. || this.props.iAmRecorder;
  164. }
  165. _onDontShowAgainChange: (object: Object) => void;
  166. /**
  167. * Handles the current `don't show again` checkbox state.
  168. *
  169. * @param {Object} event - Input change event.
  170. * @returns {void}
  171. */
  172. _onDontShowAgainChange(event) {
  173. this.setState({ dontShowAgainChecked: event.target.checked });
  174. }
  175. /**
  176. * Implements React's {@link PureComponent#render()}.
  177. *
  178. * @inheritdoc
  179. * @returns {ReactElement}
  180. */
  181. render() {
  182. if (this._shouldNotRender()) {
  183. if (this.state.dontShowAgainChecked) {
  184. jitsiLocalStorage.setItem(DONT_SHOW_AGAIN_CHECKED, 'true');
  185. }
  186. return null;
  187. }
  188. const { t } = this.props;
  189. const mainClassNames = this.props.conference
  190. ? 'chrome-extension-banner chrome-extension-banner__pos_in_meeting'
  191. : 'chrome-extension-banner';
  192. return (
  193. <div className = { mainClassNames }>
  194. <div className = 'chrome-extension-banner__container'>
  195. <div
  196. className = 'chrome-extension-banner__icon-container' />
  197. <div
  198. className = 'chrome-extension-banner__text-container'>
  199. { t('chromeExtensionBanner.installExtensionText') }
  200. </div>
  201. <div
  202. className = 'chrome-extension-banner__close-container'
  203. onClick = { this._onClosePressed }>
  204. <Icon
  205. className = 'gray'
  206. size = { 12 }
  207. src = { IconClose } />
  208. </div>
  209. </div>
  210. <div
  211. className = 'chrome-extension-banner__button-container'>
  212. <div
  213. className = 'chrome-extension-banner__button-open-url'
  214. onClick = { this._onInstallExtensionClick }>
  215. <div
  216. className = 'chrome-extension-banner__button-text'>
  217. { t('chromeExtensionBanner.buttonText') }
  218. </div>
  219. </div>
  220. </div>
  221. <div className = 'chrome-extension-banner__checkbox-container'>
  222. <label className = 'chrome-extension-banner__checkbox-label'>
  223. <input
  224. checked = { this.state.dontShowAgainChecked }
  225. onChange = { this._onDontShowAgainChange }
  226. type = 'checkbox' />
  227. &nbsp;{ t('chromeExtensionBanner.dontShowAgain') }
  228. </label>
  229. </div>
  230. </div>
  231. );
  232. }
  233. }
  234. /**
  235. * Function that maps parts of Redux state tree into component props.
  236. *
  237. * @param {Object} state - Redux state.
  238. * @returns {Object}
  239. */
  240. const _mapStateToProps = state => {
  241. return {
  242. // Using emptyObject so that we don't change the reference every time when _mapStateToProps is called.
  243. bannerCfg: state['features/base/config'].chromeExtensionBanner || emptyObject,
  244. conference: getCurrentConference(state),
  245. iAmRecorder: state['features/base/config'].iAmRecorder
  246. };
  247. };
  248. export default translate(connect(_mapStateToProps)(ChromeExtensionBanner));