Merge pull request #6012 from matrix-org/t3chguy/fix/17238

Add feedback mechanism for beta features, namely Spaces
This commit is contained in:
Michael Telatynski 2021-05-11 17:46:21 +01:00 committed by GitHub
commit ea9be0dd4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 318 additions and 17 deletions

View file

@ -63,6 +63,7 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";

View file

@ -93,6 +93,10 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
&:not(.mx_SpaceRoomView_landing) .mx_SpaceFeedbackPrompt {
width: $SpaceRoomViewInnerWidth;
}
.mx_SpaceRoomView_buttons {
display: block;
margin-top: 44px;
@ -335,10 +339,19 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SearchBox {
margin: 0 0 20px;
}
.mx_SpaceFeedbackPrompt {
margin-bottom: 16px;
// hide the HR as we have our own
& + hr {
display: none;
}
}
}
.mx_SpaceRoomView_privateScope {
.mx_AccessibleButton {
> .mx_AccessibleButton {
@mixin SpacePillButton;
}
@ -450,3 +463,66 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
}
.mx_SpaceFeedbackPrompt {
margin-top: 18px;
margin-bottom: 12px;
> hr {
border: none;
border-top: 1px solid $input-border-color;
margin-bottom: 12px;
}
> div {
display: flex;
flex-direction: row;
font-size: $font-15px;
line-height: $font-24px;
> span {
color: $secondary-fg-color;
position: relative;
padding-left: 32px;
font-size: inherit;
line-height: inherit;
margin-right: auto;
&::before {
content: '';
position: absolute;
left: 0;
top: 2px;
height: 20px;
width: 20px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_AccessibleButton_kind_link {
color: $accent-color;
position: relative;
padding: 0 0 0 24px;
margin-left: 8px;
font-size: inherit;
line-height: inherit;
&::before {
content: '';
position: absolute;
left: 0;
height: 16px;
width: 16px;
background-color: $accent-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
mask-position: center;
}
}
}
}

View file

@ -39,19 +39,21 @@ limitations under the License.
font-size: $font-15px;
line-height: $font-20px;
color: $secondary-fg-color;
margin-bottom: 20px;
}
.mx_AccessibleButton {
display: block;
margin: 20px 0;
padding: 12px 40px;
width: max-content;
margin: 12px 0;
padding: 7px 40px;
width: auto;
}
.mx_BetaCard_disclaimer {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
margin-top: 20px;
}
}

View file

@ -0,0 +1,30 @@
/*
Copyright 2021 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_BetaFeedbackDialog {
.mx_BetaFeedbackDialog_subheading {
color: $secondary-fg-color;
font-size: $font-14px;
line-height: $font-20px;
margin-bottom: 24px;
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
line-height: inherit;
}
}

View file

@ -574,7 +574,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
return <>
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") }
placeholder={ _t("Search names and descriptions") }
onSearch={setQuery}
autoFocus={true}
initialValue={initialText}

View file

@ -64,6 +64,9 @@ import {BetaPill} from "../views/beta/BetaCard";
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
import SettingsStore from "../../settings/SettingsStore";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
import SdkConfig from "../../SdkConfig";
interface IProps {
space: Room;
@ -89,6 +92,26 @@ enum Phase {
PrivateExistingRooms,
}
// XXX: Temporary for the Spaces Beta only
export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
if (!SdkConfig.get().bug_report_endpoint_url) return null;
return <div className="mx_SpaceFeedbackPrompt">
<hr />
<div>
<span className="mx_SpaceFeedbackPrompt_text">{ _t("Spaces are a beta feature.") }</span>
<AccessibleButton kind="link" onClick={() => {
if (onClick) onClick();
Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
featureId: "feature_spaces",
});
}}>
{ _t("Feedback") }
</AccessibleButton>
</div>
</div>;
};
const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room);
const count = members.length;
@ -398,6 +421,7 @@ const SpaceLanding = ({ space }) => {
<div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} />
</div>
<SpaceFeedbackPrompt />
<hr />
<SpaceHierarchy
@ -487,6 +511,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -555,6 +580,7 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ buttonLabel }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -574,6 +600,7 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished }) => {
{ _t("Go to my first room") }
</AccessibleButton>
</div>
<SpaceFeedbackPrompt />
</div>;
};
@ -600,6 +627,7 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
</AccessibleButton>
<SpaceFeedbackPrompt />
</div>;
};
@ -721,6 +749,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
value={buttonLabel}
/>
</div>
<SpaceFeedbackPrompt />
</div>;
};

View file

@ -22,6 +22,9 @@ import AccessibleButton from "../elements/AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
interface IProps {
title?: string;
@ -63,9 +66,21 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId);
if (!info) return null; // Beta is invalid/disabled
const { title, caption, disclaimer, image } = info;
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
const value = SettingsStore.getValue(featureId);
let feedbackButton;
if (value && feedbackLabel && feedbackSubheading && SdkConfig.get().bug_report_endpoint_url) {
feedbackButton = <AccessibleButton
onClick={() => {
Modal.createTrackedDialog("Beta Feedback", featureId, BetaFeedbackDialog, { featureId });
}}
kind="primary"
>
{ _t("Feedback") }
</AccessibleButton>;
}
return <div className="mx_BetaCard">
<div>
<h3 className="mx_BetaCard_title">
@ -73,12 +88,15 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind="primary"
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
<div>
{ feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind={feedbackButton ? "primary_outline" : "primary"}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">
{ disclaimer(value) }
</div> }

View file

@ -36,6 +36,7 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -307,6 +308,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
<div className="mx_AddExistingToSpaceDialog_footer">
{ footer }
</div>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
</BaseDialog>;
};

View file

@ -0,0 +1,105 @@
/*
Copyright 2021 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 React, {useState} from "react";
import QuestionDialog from './QuestionDialog';
import { _t } from '../../../languageHandler';
import Field from "../elements/Field";
import SdkConfig from "../../../SdkConfig";
import {IDialogProps} from "./IDialogProps";
import SettingsStore from "../../../settings/SettingsStore";
import {submitFeedback} from "../../../rageshake/submit-rageshake";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {USER_LABS_TAB} from "./UserSettingsDialog";
interface IProps extends IDialogProps {
featureId: string;
}
const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
const info = SettingsStore.getBetaInfo(featureId);
const [comment, setComment] = useState("");
const [canContact, setCanContact] = useState(false);
const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
title: _t("Beta feedback"),
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
hasCloseButton: false,
fixedWidth: false,
});
};
return (<QuestionDialog
className="mx_BetaFeedbackDialog"
hasCancelButton={true}
title={_t("Beta feedback")}
description={<React.Fragment>
<div className="mx_BetaFeedbackDialog_subheading">
{ _t(info.feedbackSubheading) }
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.")}
<AccessibleButton kind="link" onClick={() => {
onFinished(false);
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB,
});
}}>
{ _t("To leave the beta, visit your settings.") }
</AccessibleButton>
</div>
<Field
id="feedbackComment"
label={_t("Feedback")}
type="text"
autoComplete="off"
value={comment}
element="textarea"
onChange={(ev) => {
setComment(ev.target.value);
}}
/>
<StyledCheckbox
checked={canContact}
onClick={e => setCanContact((e.target as HTMLInputElement).checked)}
>
{ _t("You may contact me if you have any follow up questions") }
</StyledCheckbox>
</React.Fragment>}
button={_t("Send feedback")}
buttonDisabled={!comment}
onFinished={sendFeedback}
/>);
};
export default BetaFeedbackDialog;

View file

@ -32,6 +32,7 @@ import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise";
import {useDispatcher} from "../../../hooks/useDispatcher";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@ -111,6 +112,8 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
<SpaceFeedbackPrompt onClick={() => onFinished(false)} />
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
avatarDisabled={!canSetAvatar}

View file

@ -32,6 +32,7 @@ import {Action} from "../../../dispatcher/actions";
import {USER_LABS_TAB} from "../dialogs/UserSettingsDialog";
import Field from "../elements/Field";
import withValidation from "../elements/Validation";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
return (
@ -152,6 +153,8 @@ const SpaceCreateMenu = ({ onFinished }) => {
/>
<p>{ _t("You can change this later") }</p>
<SpaceFeedbackPrompt onClick={onFinished} />
</React.Fragment>;
} else {
body = <React.Fragment>

View file

@ -787,11 +787,12 @@
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Spaces": "Spaces",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
"%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
"If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.",
"Beta available for web, desktop and Android. Thank you for trying the beta.": "Beta available for web, desktop and Android. Thank you for trying the beta.",
"%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.",
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Youre using an early version of Spaces, your feedback will really help inform the next versions.": "Youre using an early version of Spaces, your feedback will really help inform the next versions.",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
@ -2063,6 +2064,13 @@
"Invite anyway and never warn me again": "Invite anyway and never warn me again",
"Invite anyway": "Invite anyway",
"Close dialog": "Close dialog",
"Beta feedback": "Beta feedback",
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
"Your platform and username will be noted to help us use your feedback as much as we can.": "Your platform and username will be noted to help us use your feedback as much as we can.",
"To leave the beta, visit your settings.": "To leave the beta, visit your settings.",
"Feedback": "Feedback",
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
"Send feedback": "Send feedback",
"Please tell us what went wrong or, better, create a GitHub issue that describes the problem.": "Please tell us what went wrong or, better, create a GitHub issue that describes the problem.",
"Preparing to send logs": "Preparing to send logs",
"Logs sent": "Logs sent",
@ -2193,10 +2201,8 @@
"Comment": "Comment",
"There are two ways you can provide feedback and help us improve %(brand)s.": "There are two ways you can provide feedback and help us improve %(brand)s.",
"PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.": "PRO TIP: If you start a bug, please submit <debugLogsLink>debug logs</debugLogsLink> to help us track down the problem.",
"Feedback": "Feedback",
"Report a bug": "Report a bug",
"Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.": "Please view <existingIssuesLink>existing bugs on Github</existingIssuesLink> first. No match? <newIssueLink>Start a new one</newIssueLink>.",
"Send feedback": "Send feedback",
"Confirm abort of host creation": "Confirm abort of host creation",
"Are you sure you wish to abort creation of the host? The process cannot be continued.": "Are you sure you wish to abort creation of the host? The process cannot be continued.",
"Abort": "Abort",
@ -2680,9 +2686,10 @@
"Mark as suggested": "Mark as suggested",
"No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Search names and description": "Search names and description",
"Search names and descriptions": "Search names and descriptions",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Spaces are a beta feature.": "Spaces are a beta feature.",
"Public space": "Public space",
"Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",

View file

@ -28,6 +28,7 @@ import * as rageshake from './rageshake';
// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
import SettingsStore from "../settings/SettingsStore";
import SdkConfig from "../SdkConfig";
let TextEncoder = window.TextEncoder;
if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder;
@ -268,6 +269,25 @@ function uint8ToString(buf: Buffer) {
return out;
}
export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) {
let version = "UNKNOWN";
try {
version = await PlatformPeg.get().getAppVersion();
} catch (err) {} // PlatformPeg already logs this.
const body = new FormData();
body.append("label", label);
body.append("text", comment);
body.append("can_contact", canContact ? "yes" : "no");
body.append("app", "element-web");
body.append("version", version);
body.append("platform", PlatformPeg.get().getHumanReadableName());
body.append("user_id", MatrixClientPeg.get()?.getUserId());
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
}
function _submitReport(endpoint: string, body: FormData, progressCallback: (string) => void) {
return new Promise<void>((resolve, reject) => {
const req = new XMLHttpRequest();

View file

@ -125,6 +125,8 @@ export interface ISetting {
caption: string; // _td
disclaimer?: (enabled: boolean) => ReactNode;
image: string; // require(...)
feedbackSubheading?: string;
feedbackLabel?: string;
};
}
@ -142,7 +144,7 @@ export const SETTINGS: {[setting: string]: ISetting} = {
disclaimer: (enabled) => {
if (enabled) {
return <>
<p>{ _t("%(brand)s will reload with Spaces disabled. " +
<p>{ _t("If you leave, %(brand)s will reload with Spaces disabled. " +
"Communities and custom tags will be visible again.", {
brand: SdkConfig.get().brand,
}) }</p>
@ -162,6 +164,9 @@ export const SETTINGS: {[setting: string]: ISetting} = {
</>;
},
image: require("../../res/img/betas/spaces.png"),
feedbackSubheading: _td("Youre using an early version of Spaces, " +
"your feedback will really help inform the next versions."),
feedbackLabel: "spaces-feedback",
},
},
"feature_dnd": {