Merge branch 'dbkr/in-app-rebrand-prompt' into 'element'

Add in-app rebranding toasts & prompts

See merge request new-vector/element/element-web/matrix-react-sdk!1
This commit is contained in:
David Baker 2020-07-10 17:09:17 +00:00
commit 503159c1c8
10 changed files with 382 additions and 0 deletions

View file

@ -73,6 +73,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";

View file

@ -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;
}

View file

@ -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');
}

6
res/img/element-logo.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.64001 1.44C8.64001 0.64471 9.28472 0 10.08 0C15.3819 0 19.68 4.29807 19.68 9.6C19.68 10.3953 19.0353 11.04 18.24 11.04C17.4447 11.04 16.8 10.3953 16.8 9.6C16.8 5.88865 13.7914 2.88 10.08 2.88C9.28472 2.88 8.64001 2.23529 8.64001 1.44Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.36 22.56C15.36 23.3553 14.7153 24 13.92 24C8.61805 24 4.31999 19.7019 4.31999 14.4C4.31999 13.6047 4.9647 12.96 5.75999 12.96C6.55528 12.96 7.19999 13.6047 7.19999 14.4C7.19999 18.1114 10.2086 21.12 13.92 21.12C14.7153 21.12 15.36 21.7647 15.36 22.56Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.44 15.36C0.64471 15.36 -1.40906e-08 14.7153 -3.14722e-08 13.92C-1.4735e-07 8.61805 4.29807 4.31999 9.6 4.31998C10.3953 4.31998 11.04 4.96469 11.04 5.75998C11.04 6.55527 10.3953 7.19998 9.6 7.19998C5.88865 7.19998 2.88 10.2086 2.88 13.92C2.88 14.7153 2.23529 15.36 1.44 15.36Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.56 8.64001C23.3553 8.64001 24 9.28472 24 10.08C24 15.3819 19.7019 19.68 14.4 19.68C13.6047 19.68 12.96 19.0353 12.96 18.24C12.96 17.4447 13.6047 16.8 14.4 16.8C18.1114 16.8 21.12 13.7914 21.12 10.08C21.12 9.28472 21.7647 8.64001 22.56 8.64001Z" fill="#0DBD8B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

6
res/img/riot-logo.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="42" height="50" viewBox="0 0 42 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.04021 42.174C1.04021 45.944 4.27657 49 8.26822 49C12.2603 49 15.4962 45.944 15.4962 42.174V35.2587L22.8119 35.2518C23.2234 35.2518 23.6386 35.2385 24.0415 35.2122C33.5209 34.6096 40.9465 27.1046 40.9465 18.1261C40.9465 8.68273 32.8114 1 22.8119 1H8.26822C4.27657 1 1.04021 4.05637 1.04021 7.82645V27.7141C1.01327 27.9548 0.999582 28.1987 1.00001 28.4459C1.00001 28.6887 1.01412 28.9286 1.04021 29.1645V42.174ZM15.4963 21.6066V14.6525H22.812C24.8405 14.6525 26.4901 16.2103 26.4901 18.1261C26.4901 19.9469 24.9881 21.4692 23.0665 21.5916C22.9822 21.5969 22.8975 21.5993 22.8047 21.5993L15.4963 21.6066Z" fill="#A2DDEF"/>
<path d="M15.4963 14.6525V14.0405H14.8844V14.6525H15.4963ZM15.4963 21.6066H14.8844V22.2191L15.4969 22.2185L15.4963 21.6066ZM22.8047 21.5993V20.9874H22.8041L22.8047 21.5993ZM23.0665 21.5916L23.1045 22.2024L23.1053 22.2023L23.0665 21.5916ZM1.04021 29.1645H1.65214V29.1308L1.64843 29.0972L1.04021 29.1645ZM1.00001 28.4459H1.61194L1.61193 28.4449L1.00001 28.4459ZM1.04021 27.7141L1.64834 27.7821L1.65214 27.7482V27.7141H1.04021ZM24.0415 35.2122L24.0027 34.6015L24.0017 34.6016L24.0415 35.2122ZM22.8119 35.2518V34.6399H22.8113L22.8119 35.2518ZM15.4962 35.2587L15.4957 34.6467L14.8843 34.6473V35.2587H15.4962ZM14.8844 14.6525V21.6066H16.1082V14.6525H14.8844ZM15.4969 22.2185L22.8053 22.2112L22.8041 20.9874L15.4957 20.9946L15.4969 22.2185ZM22.8047 22.2112C22.9085 22.2112 23.006 22.2085 23.1045 22.2024L23.0284 20.9809C22.9584 20.9852 22.8865 20.9874 22.8047 20.9874V22.2112ZM23.1053 22.2023C25.3203 22.0612 27.1021 20.2979 27.1021 18.1261H25.8782C25.8782 19.5959 24.6559 20.8772 23.0276 20.9809L23.1053 22.2023ZM27.1021 18.1261C27.1021 15.8399 25.145 14.0405 22.812 14.0405V15.2644C24.536 15.2644 25.8782 16.5808 25.8782 18.1261H27.1021ZM22.812 14.0405H15.4963V15.2644H22.812V14.0405ZM8.26822 48.3881C4.58104 48.3881 1.65214 45.5735 1.65214 42.174H0.428288C0.428288 46.3145 3.97209 49.6119 8.26822 49.6119V48.3881ZM1.65214 42.174V29.1645H0.428288V42.174H1.65214ZM1.64843 29.0972C1.62467 28.8824 1.61193 28.665 1.61193 28.4459H0.388085C0.388085 28.7124 0.403576 28.9748 0.431997 29.2318L1.64843 29.0972ZM1.61193 28.4449C1.61155 28.221 1.62394 28.0001 1.64834 27.7821L0.432085 27.646C0.402599 27.9094 0.387617 28.1765 0.388085 28.447L1.61193 28.4449ZM1.65214 27.7141V7.82645H0.428288V27.7141H1.65214ZM1.65214 7.82645C1.65214 4.42682 4.58109 1.61193 8.26822 1.61193V0.388075C3.97204 0.388075 0.428288 3.68592 0.428288 7.82645H1.65214ZM8.26822 1.61193H22.8119V0.388075H8.26822V1.61193ZM22.8119 1.61193C32.5069 1.61193 40.3346 9.05321 40.3346 18.1261H41.5584C41.5584 8.31226 33.1159 0.388075 22.8119 0.388075V1.61193ZM40.3346 18.1261C40.3346 26.7525 33.1898 34.0175 24.0027 34.6015L24.0804 35.8229C33.8521 35.2017 41.5584 27.4566 41.5584 18.1261H40.3346ZM24.0017 34.6016C23.6124 34.6269 23.2104 34.6399 22.8119 34.6399V35.8637C23.2363 35.8637 23.6649 35.85 24.0813 35.8228L24.0017 34.6016ZM22.8113 34.6399L15.4957 34.6467L15.4968 35.8706L22.8125 35.8637L22.8113 34.6399ZM14.8843 35.2587V42.174H16.1082V35.2587H14.8843ZM14.8843 42.174C14.8843 45.5736 11.9558 48.3881 8.26822 48.3881V49.6119C12.5648 49.6119 16.1082 46.3145 16.1082 42.174H14.8843Z" fill="#368BD6"/>
<path d="M8.26823 42.174V7.82642H22.8119C28.8351 7.82642 33.7181 12.4378 33.7181 18.1261C33.7181 23.5784 29.232 28.0412 23.5561 28.4019C23.3098 28.4176 23.0621 28.4257 22.8119 28.4257L8.22803 28.4395" stroke="#368BD6" stroke-width="1.22385" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4227 9.0113C15.758 7.21673 15.3325 5.4048 14.2243 3.91155C11.9366 0.828522 7.41753 0.0768486 4.15037 2.23574C2.56747 3.28105 1.51106 4.84579 1.17575 6.64116C0.84001 8.43653 1.26557 10.2481 2.37372 11.7413C4.66146 14.8243 9.18092 15.576 12.4481 13.4171C14.0306 12.3714 15.087 10.8071 15.4227 9.0113ZM27.852 46.0868C29.2587 47.9824 31.4998 48.9962 33.7777 48.9962C35.21 48.9962 36.6569 48.5951 37.9195 47.7594C41.1883 45.5965 41.9817 41.3397 39.6905 38.2522L29.4751 24.4846C27.1843 21.3972 22.6769 20.6475 19.408 22.8121C16.1392 24.975 15.3458 29.2318 17.6365 32.3192L27.852 46.0868Z" fill="#368BD6"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -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";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
@ -34,6 +35,7 @@ declare global {
mx_ContentMessages: ContentMessages;
mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener;
mx_RebrandListener: RebrandListener;
mx_RoomListStore2: RoomListStore2;
mx_RoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg;

View file

@ -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();

169
src/RebrandListener.tsx Normal file
View file

@ -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("Were 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);
}
}
}

View file

@ -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<IProps> {
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 />", {},
{
a: sub => <a href={this.props.targetUrl} rel="noopener noreferrer" target="_blank">{this.getPrettyTargetUrl()}</a>,
},
);
} else {
return _t(
"Youre already signed in and good to go here, but you can also grab the latest " +
"versions of the app on all platforms at <a>element.io/get-started</a>.", {},
{
a: sub => <a href="https://element.io/get-started" rel="noopener noreferrer" target="_blank">{sub}</a>,
},
);
}
}
getDialogButtons() {
if (this.props.kind === RebrandDialogKind.NAG) {
return <DialogButtons primaryButton={_t("Go to Element")}
primaryButtonClass='primary'
onPrimaryButtonClick={this.onGoToElementClick}
hasCancel={true}
cancelButton={"Remind me later"}
onCancel={this.onRemindMeLaterClick}
focus={true}
/>
} else {
return <DialogButtons primaryButton={_t("Done")}
primaryButtonClass='primary'
hasCancel={false}
onPrimaryButtonClick={this.onDoneClick}
focus={true}
/>
}
}
render() {
return <BaseDialog title={_t("Were excited to announce Riot is now Element!")}
className='mx_RebrandDialog'
contentId='mx_Dialog_content'
onFinished={this.props.onFinished}
hasCancel={false}
>
<div className="mx_RebrandDialog_body">{this.getBodyText()}</div>
<div className="mx_RebrandDialog_logoContainer">
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/riot-logo.svg")} alt="Riot Logo" />
<span className="mx_RebrandDialog_chevron" />
<img className="mx_RebrandDialog_logo" src={require("../../../../res/img/element-logo.svg")} alt="Element Logo" />
</div>
<div>
{_t(
"Learn more at <a>element.io/previously-riot</a>", {}, {
a: sub => <a href="https://element.io/previously-riot" rel="noopener noreferrer" target="_blank">{sub}</a>,
}
)}
</div>
{this.getDialogButtons()}
</BaseDialog>;
}
}

View file

@ -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",
"Were excited to announce Riot is now Element": "Were 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",
@ -1761,6 +1765,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 didnt sign in to this session, your account may be compromised.": "If you didnt 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 <a />": "Use your account to sign in to the latest version of the app at <a />",
"Youre already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at <a>element.io/get-started</a>.": "Youre already signed in and good to go here, but you can also grab the latest versions of the app on all platforms at <a>element.io/get-started</a>.",
"Go to Element": "Go to Element",
"Were excited to announce Riot is now Element!": "Were excited to announce Riot is now Element!",
"Learn more at <a>element.io/previously-riot</a>": "Learn more at <a>element.io/previously-riot</a>",
"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 <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",