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.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",