diff --git a/config.js b/config.js index f774216f6..073b34b3f 100644 --- a/config.js +++ b/config.js @@ -385,6 +385,20 @@ var config = { // 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 // diff --git a/css/_base.scss b/css/_base.scss index d03b5bfc9..a76ccc1ca 100644 --- a/css/_base.scss +++ b/css/_base.scss @@ -37,6 +37,11 @@ body { fill: white; } +.jitsi-icon.gray svg { + fill: #5E6D7A; + cursor: pointer; +} + /** * AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby * preventing transparency in filmstrip-only mode. The selector chosen to diff --git a/css/_chrome-extension-banner.scss b/css/_chrome-extension-banner.scss new file mode 100644 index 000000000..01854b122 --- /dev/null +++ b/css/_chrome-extension-banner.scss @@ -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; + } +} \ No newline at end of file diff --git a/css/_variables.scss b/css/_variables.scss index 0520d6a99..965ab2210 100644 --- a/css/_variables.scss +++ b/css/_variables.scss @@ -260,3 +260,14 @@ $deepLinkingMobileButtonFontWeight: bold; $deepLinkingMobileButtonFontSize: inherit; $primaryDeepLinkingMobileButtonBorderRadius: inherit; + +/** +* Chrome extension banner variables. +*/ +$chromeExtensionBannerDontShowAgainDisplay: flex; +$chromeExtensionBannerHeight: 128px; +$chromeExtensionBannerTop: 80px; +$chromeExtensionBannerRight: 16px; +$chromeExtensionBannerTopInMeeting: 10px; +$chromeExtensionBannerRightInMeeeting: 10px; + diff --git a/css/main.scss b/css/main.scss index fd3bbe41a..c3941e5bc 100644 --- a/css/main.scss +++ b/css/main.scss @@ -85,5 +85,6 @@ $flagsImagePath: "../images/"; @import 'third-party-branding/microsoft'; @import 'avatar'; @import 'promotional-footer'; +@import 'chrome-extension-banner'; /* Modules END */ diff --git a/images/chromeLogo.svg b/images/chromeLogo.svg new file mode 100644 index 000000000..9753e82f5 --- /dev/null +++ b/images/chromeLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/interface_config.js b/interface_config.js index 93f819631..ac9edd137 100644 --- a/interface_config.js +++ b/interface_config.js @@ -199,7 +199,14 @@ var interfaceConfig = { DISABLE_PRESENCE_STATUS: false, // 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 diff --git a/lang/main.json b/lang/main.json index 43649e622..78d01b0cf 100644 --- a/lang/main.json +++ b/lang/main.json @@ -59,6 +59,11 @@ "title": "Chat", "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": { "joiningRoom": "Connecting you to your meeting..." }, @@ -277,7 +282,7 @@ "dialOut": { "statusMessage": "is now {{status}}" }, - "documentSharing" : { + "documentSharing": { "title": "Shared Document" }, "feedback": { @@ -630,7 +635,6 @@ "lowerYourHand": "Lower your hand", "moreActions": "More actions", "mute": "Mute / Unmute", - "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.", "noAudioSignalDescSuggestion": "If you did not purposely mute it from system settings or hardware, consider using the following device:", diff --git a/react/features/base/app/components/BaseApp.js b/react/features/base/app/components/BaseApp.js index ac7ca71e4..2c445df83 100644 --- a/react/features/base/app/components/BaseApp.js +++ b/react/features/base/app/components/BaseApp.js @@ -18,7 +18,9 @@ import { PersistenceRegistry } from '../../storage'; import { appWillMount, appWillUnmount } from '../actions'; import logger from '../logger'; +import { ChromeExtensionBanner } from '../../../chrome-extension-banner'; +declare var interfaceConfig: Object; declare var APP: Object; /** @@ -129,6 +131,11 @@ export default class BaseApp extends Component<*, State> { + { + typeof interfaceConfig !== 'undefined' + && interfaceConfig.SHOW_CHROME_EXTENSION_BANNER + && + } { this._createMainElement(component) } { this._createExtraElement() } diff --git a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.native.js b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.native.js new file mode 100644 index 000000000..e69de29bb diff --git a/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js new file mode 100644 index 000000000..a84f908d1 --- /dev/null +++ b/react/features/chrome-extension-banner/components/ChromeExtensionBanner.web.js @@ -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, + + /** + * 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 { + /** + * 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 ( +
+
+
+
+ { t('chromeExtensionBanner.installExtensionText') } +
+
+ +
+
+
+
+
+ { t('chromeExtensionBanner.buttonText') } +
+
+
+
+ +
+
+ ); + } +} + +/** + * 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)); diff --git a/react/features/chrome-extension-banner/components/index.js b/react/features/chrome-extension-banner/components/index.js new file mode 100644 index 000000000..ea1a43eab --- /dev/null +++ b/react/features/chrome-extension-banner/components/index.js @@ -0,0 +1,2 @@ +export { default as ChromeExtensionBanner } + from './ChromeExtensionBanner'; diff --git a/react/features/chrome-extension-banner/index.js b/react/features/chrome-extension-banner/index.js new file mode 100644 index 000000000..07635cbbc --- /dev/null +++ b/react/features/chrome-extension-banner/index.js @@ -0,0 +1 @@ +export * from './components';