From 8701e9293ec0946ee767ba89d5ea82001b926edc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 10 Jul 2020 15:32:34 +0100 Subject: [PATCH] Add in-app rebranding toasts & prompts Either shows an informational dialog telling you the name has changed, or a more naggy one if the user needs to log in on a different URL. The new URL (if any) is hardcoded based on the current URL, and also with a bonus config param in case other deployments need to do similar. --- res/css/_components.scss | 1 + res/css/structures/_ToastContainer.scss | 6 + res/css/views/dialogs/_RebrandDialog.scss | 63 +++++++ res/img/element-logo.svg | 6 + res/img/riot-logo.svg | 6 + src/@types/global.d.ts | 2 + src/Lifecycle.js | 4 + src/RebrandListener.tsx | 169 ++++++++++++++++++ .../views/dialogs/RebrandDialog.tsx | 116 ++++++++++++ src/i18n/strings/en_EN.json | 9 + 10 files changed, 382 insertions(+) create mode 100644 res/css/views/dialogs/_RebrandDialog.scss create mode 100644 res/img/element-logo.svg create mode 100644 res/img/riot-logo.svg create mode 100644 src/RebrandListener.tsx create mode 100644 src/components/views/dialogs/RebrandDialog.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 8288cf34f6..b25b08a144 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -72,6 +72,7 @@ @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; +@import "./views/dialogs/_RebrandDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 2916c4ffdc..b15f357fc7 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -56,6 +56,8 @@ limitations under the License. grid-row: 1; mask-size: 100%; mask-repeat: no-repeat; + background-size: 100%; + background-repeat: no-repeat; } &.mx_Toast_icon_verification::after { @@ -67,6 +69,10 @@ limitations under the License. background-image: url("$(res)/img/e2e/warning.svg"); } + &.mx_Toast_icon_element_logo::after { + background-image: url("$(res)/img/element-logo.svg"); + } + .mx_Toast_title, .mx_Toast_body { grid-column: 2; } diff --git a/res/css/views/dialogs/_RebrandDialog.scss b/res/css/views/dialogs/_RebrandDialog.scss new file mode 100644 index 0000000000..cd100a7c5e --- /dev/null +++ b/res/css/views/dialogs/_RebrandDialog.scss @@ -0,0 +1,63 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RebrandDialog { + text-align: center; + + a:link, + a:hover, + a:visited { + @mixin mx_Dialog_link; + } + + .mx_Dialog_buttons { + margin-top: 43px; + text-align: center; + } +} + +.mx_RebrandDialog_body { + width: 550px; + margin-left: auto; + margin-right: auto; +} + +.mx_RebrandDialog_logoContainer { + margin-top: 35px; + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.mx_RebrandDialog_logo { + margin-left: 28px; + margin-right: 28px; + width: 64px; + height: 64px; +} + +.mx_RebrandDialog_chevron:after { + content: ''; + display: inline-block; + width: 24px; + height: 24px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $muted-fg-color; + mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); +} diff --git a/res/img/element-logo.svg b/res/img/element-logo.svg new file mode 100644 index 0000000000..2cd11ed193 --- /dev/null +++ b/res/img/element-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/riot-logo.svg b/res/img/riot-logo.svg new file mode 100644 index 0000000000..ac1e547234 --- /dev/null +++ b/res/img/riot-logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 2c2fec759c..bd03702729 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -19,6 +19,7 @@ import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; import DeviceListener from "../DeviceListener"; +import RebrandListener from "../RebrandListener"; import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { PlatformPeg } from "../PlatformPeg"; @@ -33,6 +34,7 @@ declare global { mx_ContentMessages: ContentMessages; mx_ToastStore: ToastStore; mx_DeviceListener: DeviceListener; + mx_RebrandListener: RebrandListener; mx_RoomListStore2: RoomListStore2; mxPlatformPeg: PlatformPeg; } diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 9ae4ae7e03..a05392c3e9 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -40,6 +40,7 @@ import ToastStore from "./stores/ToastStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import {Mjolnir} from "./mjolnir/Mjolnir"; import DeviceListener from "./DeviceListener"; +import RebrandListener from "./RebrandListener"; import {Jitsi} from "./widgets/Jitsi"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; @@ -627,6 +628,8 @@ async function startMatrixClient(startSyncing=true) { // Now that we have a MatrixClientPeg, update the Jitsi info await Jitsi.getInstance().start(); + RebrandListener.sharedInstance().start(); + // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. dis.dispatch({action: 'client_started'}); @@ -688,6 +691,7 @@ export function stopMatrixClient(unsetClient=true) { IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); DeviceListener.sharedInstance().stop(); + RebrandListener.sharedInstance().stop(); if (DMRoomMap.shared()) DMRoomMap.shared().stop(); EventIndexPeg.stop(); const cli = MatrixClientPeg.get(); diff --git a/src/RebrandListener.tsx b/src/RebrandListener.tsx new file mode 100644 index 0000000000..37d49561b8 --- /dev/null +++ b/src/RebrandListener.tsx @@ -0,0 +1,169 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SdkConfig from "./SdkConfig"; +import ToastStore from "./stores/ToastStore"; +import GenericToast from "./components/views/toasts/GenericToast"; +import RebrandDialog from "./components/views/dialogs/RebrandDialog"; +import { RebrandDialogKind } from "./components/views/dialogs/RebrandDialog"; +import Modal from './Modal'; +import { _t } from './languageHandler'; + +const TOAST_KEY = 'rebrand'; +const NAG_INTERVAL = 24 * 60 * 60 * 1000; + +function getRedirectUrl(url) { + const redirectUrl = new URL(url); + redirectUrl.hash = ''; + + if (SdkConfig.get()['redirectToNewBrandUrl']) { + const newUrl = new URL(SdkConfig.get()['redirectToNewBrandUrl']); + if (url.hostname !== newUrl.hostname || url.pathname !== newUrl.pathname) { + redirectUrl.hostname = newUrl.hostname; + redirectUrl.pathname = newUrl.pathname; + return redirectUrl; + } + return null; + } else if (url.hostname === 'riot.im') { + if (url.pathname.startsWith('/app')) { + redirectUrl.hostname = 'app.element.io'; + } else if (url.pathname.startsWith('/staging')) { + redirectUrl.hostname = 'staging.element.io'; + } else if (url.pathname.startsWith('/develop')) { + redirectUrl.hostname = 'develop.element.io'; + } + + return redirectUrl.href; + } else if (url.hostname.endsWith('.riot.im')) { + redirectUrl.hostname = url.hostname.substr(0, url.hostname.length - '.riot.im'.length) + '.element.io'; + return redirectUrl.href; + } else { + return null; + } +} + +/** + * Shows toasts informing the user that the name of the app has changed and, + * potentially, that they should head to a different URL and log in there + */ +export default class RebrandListener { + private _reshowTimer?: number; + private nagAgainAt?: number = null; + + static sharedInstance() { + if (!window.mx_RebrandListener) window.mx_RebrandListener = new RebrandListener(); + return window.mx_RebrandListener; + } + + constructor() { + this._reshowTimer = null; + } + + start() { + this.recheck(); + } + + stop() { + if (this._reshowTimer) { + clearTimeout(this._reshowTimer); + this._reshowTimer = null; + } + } + + onNagToastLearnMore = async () => { + const [doneClicked] = await Modal.createDialog(RebrandDialog, { + kind: RebrandDialogKind.NAG, + targetUrl: getRedirectUrl(window.location), + }).finished; + if (doneClicked) { + // open in new tab: they should come back here & log out + window.open(getRedirectUrl(window.location), '_blank'); + } + + // whatever the user clicks, we go away & nag again after however long: + // If they went to the new URL, we want to nag them to log out if they + // come back to this tab, and if they clicked, 'remind me later' we want + // to, well, remind them later. + this.nagAgainAt = Date.now() + NAG_INTERVAL; + this.recheck(); + } + + onOneTimeToastLearnMore = async () => { + const [doneClicked] = await Modal.createDialog(RebrandDialog, { + kind: RebrandDialogKind.ONE_TIME, + }).finished; + if (doneClicked) { + localStorage.setItem('mx_rename_dialog_dismissed', 'true'); + this.recheck(); + } + } + + onNagTimerFired = () => { + this._reshowTimer = null; + this.nagAgainAt = null; + this.recheck(); + } + + private async recheck() { + // There are two types of toast/dialog we show: a 'one time' informing the user that + // the app is now called a different thing but no action is required from them (they + // may need to look for a different name name/icon to launch the app but don't need to + // log in again) and a nag toast where they need to log in to the app on a different domain. + let nagToast = false; + let oneTimeToast = false; + + if (getRedirectUrl(window.location)) { + if (!this.nagAgainAt) { + // if we have redirectUrl, show the nag toast + nagToast = true; + } + } else { + // otherwise we show the 'one time' toast / dialog + const renameDialogDismissed = localStorage.getItem('mx_rename_dialog_dismissed'); + if (renameDialogDismissed !== 'true') { + oneTimeToast = true; + } + } + + if (nagToast || oneTimeToast) { + let description; + if (nagToast) { + description = _t("Use your account to sign in to the latest version"); + } else { + description = _t("We’re excited to announce Riot is now Element"); + } + + ToastStore.sharedInstance().addOrReplaceToast({ + key: TOAST_KEY, + title: _t("Riot is now Element!"), + icon: 'element_logo', + props: { + description, + acceptLabel: _t("Learn More"), + onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore, + }, + component: GenericToast, + priority: 20, + }); + } else { + ToastStore.sharedInstance().dismissToast(TOAST_KEY); + } + + if (!this._reshowTimer && this.nagAgainAt) { + this._reshowTimer = setTimeout(this.onNagTimerFired, (this.nagAgainAt - Date.now()) + 100); + } + } +} diff --git a/src/components/views/dialogs/RebrandDialog.tsx b/src/components/views/dialogs/RebrandDialog.tsx new file mode 100644 index 0000000000..6daa33f6de --- /dev/null +++ b/src/components/views/dialogs/RebrandDialog.tsx @@ -0,0 +1,116 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from 'react'; +import * as PropTypes from 'prop-types'; +import BaseDialog from './BaseDialog'; +import { _t } from '../../../languageHandler'; +import DialogButtons from '../elements/DialogButtons'; + +export enum RebrandDialogKind { + NAG, + ONE_TIME, +}; + +interface IProps { + onFinished: () => void; + kind: RebrandDialogKind, + targetUrl?: string, +} + +export default class RebrandDialog extends React.PureComponent { + private onDoneClick = () => { + this.props.onFinished(true); + } + + private onGoToElementClick = () => { + this.props.onFinished(true); + } + + private onRemindMeLaterClick = () => { + this.props.onFinished(false); + } + + private getPrettyTargetUrl() { + const u = new URL(this.props.targetUrl); + let ret = u.host; + if (u.pathname !== '/') ret += u.pathname; + return ret; + } + + getBodyText() { + if (this.props.kind === RebrandDialogKind.NAG) { + return _t( + "Use your account to sign in to the latest version of the app at ", {}, + { + a: sub => {this.getPrettyTargetUrl()}, + }, + ); + } else { + return _t( + "You’re already signed in and good to go here, but you can also grab the latest " + + "versions of the app on all platforms at element.io/get-started.", {}, + { + a: sub => {sub}, + }, + ); + } + } + + getDialogButtons() { + if (this.props.kind === RebrandDialogKind.NAG) { + return + } else { + return + } + } + + render() { + return +
{this.getBodyText()}
+
+ Riot Logo + + Element Logo +
+
+ {_t( + "Learn more at element.io/previously-riot", {}, { + a: sub => {sub}, + } + )} +
+ {this.getDialogButtons()} +
; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 58fc6870d1..a3b63b4b93 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -119,6 +119,10 @@ "Unable to enable Notifications": "Unable to enable Notifications", "This email address was not found": "This email address was not found", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", + "Use your account to sign in to the latest version": "Use your account to sign in to the latest version", + "We’re excited to announce Riot is now Element": "We’re excited to announce Riot is now Element", + "Riot is now Element!": "Riot is now Element!", + "Learn More": "Learn More", "Sign In or Create Account": "Sign In or Create Account", "Use your account or create a new one to continue.": "Use your account or create a new one to continue.", "Create Account": "Create Account", @@ -1753,6 +1757,11 @@ "Use this session to verify your new one, granting it access to encrypted messages:": "Use this session to verify your new one, granting it access to encrypted messages:", "If you didn’t sign in to this session, your account may be compromised.": "If you didn’t sign in to this session, your account may be compromised.", "This wasn't me": "This wasn't me", + "Use your account to sign in to the latest version of the app at ": "Use your account to sign in to the latest version of the app at ", + "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.": "You’re already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at element.io/get-started.", + "Go to Element": "Go to Element", + "We’re excited to announce Riot is now Element!": "We’re excited to announce Riot is now Element!", + "Learn more at element.io/previously-riot": "Learn more at element.io/previously-riot", "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.", "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.", "Report bugs & give feedback": "Report bugs & give feedback",