Move new search experience to a Beta (#7718)

This commit is contained in:
Michael Telatynski 2022-02-08 14:02:36 +00:00 committed by GitHub
parent 5201c9b285
commit ed185240a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 329 additions and 116 deletions

View file

@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import React, { ReactNode, useState } from "react";
import classNames from "classnames";
import { sleep } from "matrix-js-sdk/src/utils";
import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
@ -26,6 +27,8 @@ import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import InlineSpinner from "../elements/InlineSpinner";
// XXX: Keep this around for re-use in future Betas
@ -42,10 +45,10 @@ export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
})}
tooltip={<div>
<div className="mx_Tooltip_title">
{ _t("Spaces is a beta feature") }
{ _t("This is a beta feature") }
</div>
<div className="mx_Tooltip_sub">
{ _t("Tap for more info") }
{ _t("Click for more info") }
</div>
</div>}
onClick={onClick}
@ -67,10 +70,20 @@ export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId);
const value = useFeatureEnabled(featureId);
const [busy, setBusy] = useState(false);
if (!info) return null; // Beta is invalid/disabled
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
const value = SettingsStore.getValue(featureId);
const {
title,
caption,
disclaimer,
image,
feedbackLabel,
feedbackSubheading,
extraSettings,
requiresRefresh,
} = info;
let feedbackButton;
if (value && feedbackLabel && feedbackSubheading && SdkConfig.get().bug_report_endpoint_url) {
@ -84,6 +97,15 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
</AccessibleButton>;
}
let content: ReactNode;
if (busy) {
content = <InlineSpinner />;
} else if (value) {
content = _t("Leave the beta");
} else {
content = _t("Join the beta");
}
return <div className="mx_BetaCard">
<div className="mx_BetaCard_columns">
<div>
@ -91,14 +113,26 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
{ titleOverride || _t(title) }
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<span className="mx_BetaCard_caption">{ caption() }</span>
<div className="mx_BetaCard_buttons">
{ feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
onClick={async () => {
setBusy(true);
// make it look like we're doing something for two seconds,
// otherwise users think clicking did nothing
if (!requiresRefresh) {
await sleep(2000);
}
await SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value);
if (!requiresRefresh) {
setBusy(false);
}
}}
kind={feedbackButton ? "primary_outline" : "primary"}
disabled={busy}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
{ content }
</AccessibleButton>
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">

View file

@ -52,8 +52,8 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
Modal.createTrackedDialog("Feedback Sent", rageshakeLabel, InfoDialog, {
title,
description: _t("Thank you for your feedback, we really appreciate it."),
button: _t("Done"),
description: _t("Feedback sent! Thanks, we appreciate it!"),
button: _t("Close"),
hasCloseButton: false,
fixedWidth: false,
});
@ -68,7 +68,7 @@ const GenericFeatureFeedbackDialog: React.FC<IProps> = ({
{ subheading }
&nbsp;
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
&nbsp;
{ children }
</div>

View file

@ -18,6 +18,7 @@ import React, {
ChangeEvent,
ComponentProps,
KeyboardEvent,
RefObject,
useCallback,
useContext,
useEffect,
@ -52,11 +53,10 @@ import DMRoomMap from "../../../utils/DMRoomMap";
import { mediaFromMxc } from "../../../customisations/Media";
import BaseAvatar from "../avatars/BaseAvatar";
import Spinner from "../elements/Spinner";
import { roomContextDetailsText } from "../../../Rooms";
import { roomContextDetailsText, spaceContextDetailsText } from "../../../Rooms";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import { Action } from "../../../dispatcher/actions";
import Modal from "../../../Modal";
import GenericFeatureFeedbackDialog from "./GenericFeatureFeedbackDialog";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import RoomViewStore from "../../../stores/RoomViewStore";
import { showStartChatInviteDialog } from "../../../RoomInvite";
@ -64,6 +64,10 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel";
import NotificationBadge from "../rooms/NotificationBadge";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import { BetaPill } from "../beta/BetaCard";
import { UserTab } from "./UserSettingsDialog";
import BetaFeedbackDialog from "./BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
const MAX_RECENT_SEARCHES = 10;
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
@ -106,10 +110,10 @@ const useRecentSearches = (): [Room[], () => void] => {
};
const ResultDetails = ({ room }: { room: Room }) => {
const roomContextDetails = roomContextDetailsText(room);
if (roomContextDetails) {
const contextDetails = room.isSpaceRoom() ? spaceContextDetailsText(room) : roomContextDetailsText(room);
if (contextDetails) {
return <div className="mx_SpotlightDialog_result_details">
{ roomContextDetails }
{ contextDetails }
</div>;
}
@ -166,6 +170,10 @@ const useSpaceResults = (space?: Room, query?: string): [IHierarchyRoom[], boole
return [results, hierarchy?.loading ?? false];
};
function refIsForRecentlyViewed(ref: RefObject<HTMLElement>): boolean {
return ref.current?.id.startsWith("mx_SpotlightDialog_button_recentlyViewed_");
}
const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) => {
const cli = MatrixClientPeg.get();
const rovingContext = useContext(RovingTabIndexContext);
@ -245,7 +253,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
viewRoom(room.roomId, true);
}}
>
<DecoratedRoomAvatar room={room} avatarSize={20} />
<DecoratedRoomAvatar room={room} avatarSize={20} tooltipProps={{ tabIndex: -1 }} />
{ room.name }
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} />
<ResultDetails room={room} />
@ -385,9 +393,10 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
viewRoom(room.roomId, true);
}}
>
<DecoratedRoomAvatar room={room} avatarSize={20} />
<DecoratedRoomAvatar room={room} avatarSize={20} tooltipProps={{ tabIndex: -1 }} />
{ room.name }
<NotificationBadge notification={RoomNotificationStateStore.instance.getRoomState(room)} />
<ResultDetails room={room} />
<div className="mx_SpotlightDialog_enterPrompt"></div>
</Option>
)) }
@ -450,6 +459,8 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
};
const onKeyDown = (ev: KeyboardEvent) => {
let ref: RefObject<HTMLElement>;
switch (ev.key) {
case Key.ARROW_UP:
case Key.ARROW_DOWN:
@ -457,18 +468,36 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
ev.preventDefault();
if (rovingContext.state.refs.length > 0) {
const idx = rovingContext.state.refs.indexOf(rovingContext.state.activeRef);
const ref = findSiblingElement(rovingContext.state.refs, idx + (ev.key === Key.ARROW_UP ? -1 : 1));
if (ref) {
rovingContext.dispatch({
type: Type.SetFocus,
payload: { ref },
});
ref.current?.scrollIntoView({
block: "nearest",
});
let refs = rovingContext.state.refs;
if (!query) {
// If the current selection is not in the recently viewed row then only include the
// first recently viewed so that is the target when the user is switching into recently viewed.
const keptRecentlyViewedRef = refIsForRecentlyViewed(rovingContext.state.activeRef)
? rovingContext.state.activeRef
: refs.find(refIsForRecentlyViewed);
// exclude all other recently viewed items from the list so up/down arrows skip them
refs = refs.filter(ref => ref === keptRecentlyViewedRef || !refIsForRecentlyViewed(ref));
}
const idx = refs.indexOf(rovingContext.state.activeRef);
ref = findSiblingElement(refs, idx + (ev.key === Key.ARROW_UP ? -1 : 1));
}
break;
case Key.ARROW_LEFT:
case Key.ARROW_RIGHT:
// only handle these keys when we are in the recently viewed row of options
if (!query &&
rovingContext.state.refs.length > 0 &&
refIsForRecentlyViewed(rovingContext.state.activeRef)
) {
// we only intercept left/right arrows when the field is empty, and they'd do nothing anyway
ev.stopPropagation();
ev.preventDefault();
const refs = rovingContext.state.refs.filter(refIsForRecentlyViewed);
const idx = refs.indexOf(rovingContext.state.activeRef);
ref = findSiblingElement(refs, idx + (ev.key === Key.ARROW_LEFT ? -1 : 1));
}
break;
@ -478,16 +507,34 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
rovingContext.state.activeRef?.current?.click();
break;
}
if (ref) {
rovingContext.dispatch({
type: Type.SetFocus,
payload: { ref },
});
ref.current?.scrollIntoView({
block: "nearest",
});
}
};
const openFeedback = SdkConfig.get().bug_report_endpoint_url ? () => {
Modal.createTrackedDialog("Spotlight Feedback", "feature_spotlight", BetaFeedbackDialog, {
featureId: "feature_spotlight",
});
} : null;
const activeDescendant = rovingContext.state.activeRef?.current?.id;
return <>
<div className="mx_SpotlightDialog_keyboardPrompt">
{ _t("Use <arrows/> to scroll results", {}, {
{ _t("Use <arrows/> to scroll", {}, {
arrows: () => <>
<div></div>
<div></div>
{ !query && <div></div> }
{ !query && <div></div> }
</>,
}) }
</div>
@ -517,24 +564,24 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", onFinished }) =>
</div>
<div className="mx_SpotlightDialog_footer">
<span>
{ activeSpace
? _t("Searching rooms and chats you're in and %(spaceName)s", { spaceName: activeSpace.name })
: _t("Searching rooms and chats you're in") }
</span>
<AccessibleButton
<BetaPill onClick={() => {
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
onFinished();
}} />
{ openFeedback && _t("Results not as expected? Please <a>give feedback</a>.", {}, {
a: sub => <AccessibleButton kind="link_inline" onClick={openFeedback}>
{ sub }
</AccessibleButton>,
}) }
{ openFeedback && <AccessibleButton
kind="primary_outline"
onClick={() => {
Modal.createTrackedDialog("Spotlight Feedback", "", GenericFeatureFeedbackDialog, {
title: _t("Spotlight search feedback"),
subheading: _t("Thank you for trying Spotlight search. " +
"Your feedback will help inform the next versions."),
rageshakeLabel: "spotlight-feedback",
});
}}
onClick={openFeedback}
>
{ _t("Feedback") }
</AccessibleButton>
</AccessibleButton> }
</div>
</BaseDialog>
</>;