mirror of https://github.com/vector-im/riot-web
Move new search experience to a Beta (#7718)
parent
5201c9b285
commit
ed185240a5
res
css
structures
views
img
element-icons/settings
src
components
structures
views
i18n/strings
settings
stores/spaces
|
@ -35,6 +35,7 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
background-color: $secondary-content;
|
||||
margin-left: 7px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mx_RoomSearch_input {
|
||||
|
@ -44,14 +45,18 @@ limitations under the License.
|
|||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-16px;
|
||||
|
||||
&:not(.mx_RoomSearch_inputExpanded)::placeholder {
|
||||
color: $tertiary-content !important; // !important to override default app-wide styles
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSearch_input,
|
||||
.mx_RoomSearch_spotlightTriggerText {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-16px;
|
||||
}
|
||||
|
||||
&.mx_RoomSearch_hasQuery {
|
||||
border-color: $secondary-content;
|
||||
}
|
||||
|
@ -108,4 +113,36 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_RoomSearch_spotlightTrigger {
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
|
||||
.mx_RoomSearch_spotlightTriggerText {
|
||||
color: $tertiary-content;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
// the following rules are to match that of a real input field
|
||||
overflow: hidden;
|
||||
margin: 9px;
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $tertiary-content;
|
||||
|
||||
.mx_RoomSearch_spotlightTriggerText {
|
||||
color: $background;
|
||||
}
|
||||
|
||||
.mx_RoomSearch_shortcutPrompt {
|
||||
background-color: $background;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_RoomSearch_icon {
|
||||
background-color: $background;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,14 @@ limitations under the License.
|
|||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
margin-top: 20px;
|
||||
|
||||
> h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,6 +72,7 @@ limitations under the License.
|
|||
width: 300px;
|
||||
object-fit: contain;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +99,7 @@ limitations under the License.
|
|||
border-radius: 8px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: 15px;
|
||||
color: #FFFFFF;
|
||||
display: inline-block;
|
||||
|
|
|
@ -223,18 +223,13 @@ limitations under the License.
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_SpotlightDialog_recentSearches {
|
||||
overflow-y: hidden;
|
||||
height: calc(100% - 190px);
|
||||
|
||||
> h4 > .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
float: right;
|
||||
font-weight: normal;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
.mx_SpotlightDialog_recentSearches > h4 > .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
float: right;
|
||||
font-weight: normal;
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_SpotlightDialog_enterPrompt {
|
||||
|
@ -253,28 +248,21 @@ limitations under the License.
|
|||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
color: $secondary-content;
|
||||
padding: 16px 16px 20px;
|
||||
padding: 12px 16px 16px;
|
||||
display: flex;
|
||||
border-top: 1px solid $quinary-content;
|
||||
|
||||
.mx_BetaCard_betaPill {
|
||||
margin-right: 12px;
|
||||
height: min-content;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
align-self: center;
|
||||
|
||||
&::before {
|
||||
background-color: $secondary-content;
|
||||
content: "";
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
.mx_AccessibleButton_kind_link_inline {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_UserSettingsDialog_labsIcon::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings/lab-flags.svg');
|
||||
mask-image: url('$(res)/img/element-icons/settings/flask.svg');
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog_mjolnirIcon::before {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 887 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 3C7.32843 3 8 2.32843 8 1.5C8 0.671573 7.32843 0 6.5 0C5.67157 0 5 0.671573 5 1.5C5 2.32843 5.67157 3 6.5 3ZM15.2041 14.7404L11.598 8.73148L11.624 6.96296H12.1176C12.2554 6.96296 12.3876 6.91224 12.485 6.82194C12.5825 6.73164 12.6372 6.60918 12.6372 6.48148C12.6372 6.35378 12.5825 6.23132 12.485 6.14102C12.3876 6.05073 12.2554 6 12.1176 6H5.88237C5.74456 6 5.6124 6.05073 5.51495 6.14102C5.41751 6.23132 5.36277 6.35378 5.36277 6.48148C5.36277 6.60918 5.41751 6.73164 5.51495 6.82194C5.6124 6.91224 5.74456 6.96296 5.88237 6.96296H6.376L6.40198 8.73148L2.79592 14.7404C2.59646 15.0747 2.49454 15.4515 2.50023 15.8335C2.50591 16.2155 2.619 16.5895 2.82833 16.9186C3.03766 17.2477 3.336 17.5206 3.69389 17.7102C4.05179 17.8999 4.45686 17.9997 4.86914 18H13.1309C13.5431 17.9997 13.9482 17.8999 14.3061 17.7102C14.664 17.5206 14.9623 17.2477 15.1717 16.9186C15.381 16.5895 15.4941 16.2155 15.4998 15.8335C15.5055 15.4515 15.4035 15.0747 15.2041 14.7404ZM7.37364 9.08778C7.41831 9.01432 7.44159 8.93136 7.44118 8.84704L7.4152 6.96296H10.5848L10.5588 8.84704C10.5584 8.93136 10.5817 9.01432 10.6264 9.08778L12.4814 12.1837C11.4354 12.1572 10.3977 12.0034 9.3949 11.7263C8.31816 11.4355 7.20603 11.2721 6.08502 11.24L7.37364 9.08778ZM13 3.5C13 4.32843 12.3284 5 11.5 5C10.6716 5 10 4.32843 10 3.5C10 2.67157 10.6716 2 11.5 2C12.3284 2 13 2.67157 13 3.5Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -1,4 +0,0 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.9722 17C2.43527 17 2 16.5647 2 16.0278V9.29493C2 9.12977 2.21388 9 2.4861 9H3.45831C3.73052 9 3.94441 9.12977 3.94441 9.29493V16.0278C3.94441 16.5647 3.50914 17 2.9722 17Z" fill="black"/>
|
||||
<path d="M4.67853 1C2.71201 1 2.00037 2.32352 2.00037 3.14492V11.7828H3.46227C6.54174 10.3832 9.54358 13.0302 12.7136 12.9997C15.8837 12.9693 16.0001 11.2655 16.0001 10.4136V3.57086C16.0001 0.786973 14.0724 3.46432 10.7987 2.38431C7.95667 1.44674 7.44433 1 4.67853 1Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 587 B |
14
src/Rooms.ts
14
src/Rooms.ts
|
@ -158,6 +158,20 @@ function guessDMRoomTargetId(room: Room, myUserId: string): string {
|
|||
return oldestUser.userId;
|
||||
}
|
||||
|
||||
export function spaceContextDetailsText(space: Room): string {
|
||||
if (!space.isSpaceRoom()) return undefined;
|
||||
|
||||
const [parent, ...otherParents] = SpaceStore.instance.getKnownParents(space.roomId);
|
||||
if (parent) {
|
||||
return _t("%(spaceName)s and %(count)s others", {
|
||||
spaceName: space.client.getRoom(parent).name,
|
||||
count: otherParents.length,
|
||||
});
|
||||
}
|
||||
|
||||
return space.getCanonicalAlias();
|
||||
}
|
||||
|
||||
export function roomContextDetailsText(room: Room): string {
|
||||
if (room.isSpaceRoom()) return undefined;
|
||||
|
||||
|
|
|
@ -91,7 +91,7 @@ import { RoomUpdateCause } from "../../stores/room-list/models";
|
|||
import SecurityCustomisations from "../../customisations/Security";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import QuestionDialog from "../views/dialogs/QuestionDialog";
|
||||
import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
|
||||
import UserSettingsDialog, { UserTab } from '../views/dialogs/UserSettingsDialog';
|
||||
import CreateGroupDialog from '../views/dialogs/CreateGroupDialog';
|
||||
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
|
||||
import RoomDirectory from './RoomDirectory';
|
||||
|
@ -117,6 +117,7 @@ import { showSpaceInvite } from "../../utils/space";
|
|||
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import { SummarizedNotificationState } from "../../stores/notifications/SummarizedNotificationState";
|
||||
import GenericToast from '../views/toasts/GenericToast';
|
||||
|
||||
/** constants for MatrixChat.state.view */
|
||||
export enum Views {
|
||||
|
@ -1551,6 +1552,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
showNotificationsToast(false);
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("mx_seen_feature_spotlight_toast")) {
|
||||
setTimeout(() => {
|
||||
// Skip the toast if the beta is already enabled or the user has changed the setting from default
|
||||
if (SettingsStore.getValue("feature_spotlight") ||
|
||||
SettingsStore.getValue("feature_spotlight", null, true) !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = "BETA_SPOTLIGHT_TOAST";
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key,
|
||||
title: _t("New search beta available"),
|
||||
props: {
|
||||
description: _t("We're testing a new search to make finding what you want quicker.\n"),
|
||||
acceptLabel: _t("Learn more"),
|
||||
onAccept: () => {
|
||||
dis.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
localStorage.setItem("mx_seen_feature_spotlight_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
rejectLabel: _t("Dismiss"),
|
||||
onReject: () => {
|
||||
localStorage.setItem("mx_seen_feature_spotlight_toast", "true");
|
||||
ToastStore.sharedInstance().dismissToast(key);
|
||||
},
|
||||
},
|
||||
icon: "labs",
|
||||
component: GenericToast,
|
||||
priority: 9,
|
||||
});
|
||||
}, 5 * 60 * 1000); // show after 5 minutes to not overload user with toasts on launch
|
||||
}
|
||||
|
||||
dis.fire(Action.FocusSendMessageComposer);
|
||||
this.setState({
|
||||
ready: true,
|
||||
|
|
|
@ -34,6 +34,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||
import Modal from "../../Modal";
|
||||
import SpotlightDialog from "../views/dialogs/SpotlightDialog";
|
||||
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -46,11 +47,13 @@ interface IProps {
|
|||
interface IState {
|
||||
query: string;
|
||||
focused: boolean;
|
||||
spotlightBetaEnabled: boolean;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.RoomSearch")
|
||||
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
||||
private readonly dispatcherRef: string;
|
||||
private readonly betaRef: string;
|
||||
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
||||
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
||||
|
||||
|
@ -60,11 +63,13 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
this.state = {
|
||||
query: "",
|
||||
focused: false,
|
||||
spotlightBetaEnabled: SettingsStore.getValue("feature_spotlight"),
|
||||
};
|
||||
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
this.betaRef = SettingsStore.watchSetting("feature_spotlight", null, this.onSpotlightChange);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
||||
|
@ -85,8 +90,18 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
||||
SettingsStore.unwatchSetting(this.betaRef);
|
||||
}
|
||||
|
||||
private onSpotlightChange = () => {
|
||||
const spotlightBetaEnabled = SettingsStore.getValue("feature_spotlight");
|
||||
if (this.state.spotlightBetaEnabled !== spotlightBetaEnabled) {
|
||||
this.setState({ spotlightBetaEnabled });
|
||||
}
|
||||
// in case the user was in settings at the 5-minute mark, dismiss the toast
|
||||
ToastStore.sharedInstance().dismissToast("BETA_SPOTLIGHT_TOAST");
|
||||
};
|
||||
|
||||
private openSpotlight() {
|
||||
Modal.createTrackedDialog("Spotlight", "", SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true);
|
||||
}
|
||||
|
@ -95,11 +110,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
if (payload.action === Action.ViewRoom && payload.clear_search) {
|
||||
this.clearInput();
|
||||
} else if (payload.action === 'focus_room_filter') {
|
||||
if (SettingsStore.getValue("feature_spotlight")) {
|
||||
this.openSpotlight();
|
||||
} else {
|
||||
this.inputRef.current?.focus();
|
||||
}
|
||||
this.focus();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -110,10 +121,10 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private openSearch = () => {
|
||||
if (SettingsStore.getValue("feature_spotlight")) {
|
||||
if (this.state.spotlightBetaEnabled) {
|
||||
this.openSpotlight();
|
||||
} else {
|
||||
defaultDispatcher.dispatch({ action: "show_left_panel" });
|
||||
// dispatched as it needs handling by MatrixChat too
|
||||
defaultDispatcher.dispatch({ action: "focus_room_filter" });
|
||||
}
|
||||
};
|
||||
|
@ -124,14 +135,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
if (SettingsStore.getValue("feature_spotlight")) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.openSpotlight();
|
||||
} else {
|
||||
this.setState({ focused: true });
|
||||
ev.target.select();
|
||||
}
|
||||
this.setState({ focused: true });
|
||||
ev.target.select();
|
||||
};
|
||||
|
||||
private onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
|
||||
|
@ -159,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
};
|
||||
|
||||
public focus = (): void => {
|
||||
if (SettingsStore.getValue("feature_spotlight")) {
|
||||
if (this.state.spotlightBetaEnabled) {
|
||||
this.openSpotlight();
|
||||
} else {
|
||||
this.inputRef.current?.focus();
|
||||
|
@ -172,6 +177,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
'mx_RoomSearch_hasQuery': this.state.query,
|
||||
'mx_RoomSearch_focused': this.state.focused,
|
||||
'mx_RoomSearch_minimized': this.props.isMinimized,
|
||||
'mx_RoomSearch_spotlightTrigger': this.state.spotlightBetaEnabled,
|
||||
});
|
||||
|
||||
const inputClasses = classNames({
|
||||
|
@ -180,8 +186,9 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
let icon = (
|
||||
<div className="mx_RoomSearch_icon" onClick={this.focus} />
|
||||
<div className="mx_RoomSearch_icon" />
|
||||
);
|
||||
|
||||
let input = (
|
||||
<input
|
||||
type="text"
|
||||
|
@ -192,10 +199,11 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={SettingsStore.getValue("feature_spotlight") ? _t("Search") : _t("Filter")}
|
||||
placeholder={_t("Filter")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
||||
let clearButton = (
|
||||
<AccessibleButton
|
||||
tabIndex={-1}
|
||||
|
@ -204,7 +212,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onClick={this.clearInput}
|
||||
/>
|
||||
);
|
||||
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt" onClick={this.focus}>
|
||||
|
||||
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
|
||||
{ isMac ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K" }
|
||||
</div>;
|
||||
|
||||
|
@ -221,8 +230,18 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
shortcutPrompt = null;
|
||||
}
|
||||
|
||||
if (this.state.spotlightBetaEnabled) {
|
||||
return <AccessibleButton onClick={this.openSpotlight} className={classes}>
|
||||
{ icon }
|
||||
{ input && <div className="mx_RoomSearch_spotlightTriggerText">
|
||||
{ _t("Search") }
|
||||
</div> }
|
||||
{ shortcutPrompt }
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className={classes} onClick={this.focus}>
|
||||
{ icon }
|
||||
{ input }
|
||||
{ shortcutPrompt }
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 }
|
||||
|
||||
{ _t("Your platform and username will be noted to help us use your feedback as much as we can.") }
|
||||
|
||||
|
||||
{ children }
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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>
|
||||
</>;
|
||||
|
|
|
@ -885,7 +885,15 @@
|
|||
"Show extensible event representation of events": "Show extensible event representation of events",
|
||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||
"Use new room breadcrumbs": "Use new room breadcrumbs",
|
||||
"New spotlight search experience": "New spotlight search experience",
|
||||
"New search experience": "New search experience",
|
||||
"The new search": "The new search",
|
||||
"A new, quick way to search spaces and rooms you're in.": "A new, quick way to search spaces and rooms you're in.",
|
||||
"This feature is a work in progress, we'd love to hear your feedback.": "This feature is a work in progress, we'd love to hear your feedback.",
|
||||
"How can I give feedback?": "How can I give feedback?",
|
||||
"To feedback, join the beta, start a search and click on feedback.": "To feedback, join the beta, start a search and click on feedback.",
|
||||
"How can I leave the beta?": "How can I leave the beta?",
|
||||
"To leave, just return to this page or click on the beta badge when you search.": "To leave, just return to this page or click on the beta badge when you search.",
|
||||
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
||||
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||
"Don't send read receipts": "Don't send read receipts",
|
||||
|
@ -2570,8 +2578,7 @@
|
|||
"Forward message": "Forward message",
|
||||
"Message preview": "Message preview",
|
||||
"Search for rooms or people": "Search for rooms or people",
|
||||
"Thank you for your feedback, we really appreciate it.": "Thank you for your feedback, we really appreciate it.",
|
||||
"Done": "Done",
|
||||
"Feedback sent! Thanks, we appreciate it!": "Feedback sent! Thanks, we appreciate it!",
|
||||
"You may contact me if you have any follow up questions": "You may contact me if you have any follow up questions",
|
||||
"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.",
|
||||
|
@ -2779,11 +2786,8 @@
|
|||
"To search messages, look for this icon at the top of a room <icon/>": "To search messages, look for this icon at the top of a room <icon/>",
|
||||
"Recent searches": "Recent searches",
|
||||
"Clear": "Clear",
|
||||
"Use <arrows/> to scroll results": "Use <arrows/> to scroll results",
|
||||
"Searching rooms and chats you're in and %(spaceName)s": "Searching rooms and chats you're in and %(spaceName)s",
|
||||
"Searching rooms and chats you're in": "Searching rooms and chats you're in",
|
||||
"Spotlight search feedback": "Spotlight search feedback",
|
||||
"Thank you for trying Spotlight search. Your feedback will help inform the next versions.": "Thank you for trying Spotlight search. Your feedback will help inform the next versions.",
|
||||
"Use <arrows/> to scroll": "Use <arrows/> to scroll",
|
||||
"Results not as expected? Please <a>give feedback</a>.": "Results not as expected? Please <a>give feedback</a>.",
|
||||
"To help us prevent this in future, please <a>send us logs</a>.": "To help us prevent this in future, please <a>send us logs</a>.",
|
||||
"Missing session data": "Missing session data",
|
||||
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
|
||||
|
@ -2803,6 +2807,7 @@
|
|||
"Not Trusted": "Not Trusted",
|
||||
"Manually Verify by Text": "Manually Verify by Text",
|
||||
"Interactively verify by Emoji": "Interactively verify by Emoji",
|
||||
"Done": "Done",
|
||||
"Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)",
|
||||
"Upload files": "Upload files",
|
||||
"Upload all": "Upload all",
|
||||
|
@ -2900,8 +2905,8 @@
|
|||
"Revoke permissions": "Revoke permissions",
|
||||
"Move left": "Move left",
|
||||
"Move right": "Move right",
|
||||
"Spaces is a beta feature": "Spaces is a beta feature",
|
||||
"Tap for more info": "Tap for more info",
|
||||
"This is a beta feature": "This is a beta feature",
|
||||
"Click for more info": "Click for more info",
|
||||
"Beta": "Beta",
|
||||
"Leave the beta": "Leave the beta",
|
||||
"Join the beta": "Join the beta",
|
||||
|
@ -3037,6 +3042,8 @@
|
|||
"Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s",
|
||||
"Unable to copy room link": "Unable to copy room link",
|
||||
"Unable to copy a link to the room to the clipboard.": "Unable to copy a link to the room to the clipboard.",
|
||||
"New search beta available": "New search beta available",
|
||||
"We're testing a new search to make finding what you want quicker.\n": "We're testing a new search to make finding what you want quicker.\n",
|
||||
"Signed Out": "Signed Out",
|
||||
"For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.",
|
||||
"Terms and Conditions": "Terms and Conditions",
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { ReactNode } from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
|
||||
import { _td } from '../languageHandler';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import {
|
||||
NotificationBodyEnabledController,
|
||||
NotificationsEnabledController,
|
||||
|
@ -41,6 +41,7 @@ import ReducedMotionController from './controllers/ReducedMotionController';
|
|||
import IncompatibleController from "./controllers/IncompatibleController";
|
||||
import { ImageSize } from "./enums/ImageSize";
|
||||
import { MetaSpace } from "../stores/spaces";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
|
||||
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
|
||||
const LEVELS_ROOM_SETTINGS = [
|
||||
|
@ -155,12 +156,13 @@ interface IBaseSetting {
|
|||
// XXX: Keep this around for re-use in future Betas
|
||||
betaInfo?: {
|
||||
title: string; // _td
|
||||
caption: string; // _td
|
||||
caption: () => ReactNode;
|
||||
disclaimer?: (enabled: boolean) => ReactNode;
|
||||
image: string; // require(...)
|
||||
feedbackSubheading?: string;
|
||||
feedbackLabel?: string;
|
||||
extraSettings?: string[];
|
||||
requiresRefresh?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -336,8 +338,27 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
isFeature: true,
|
||||
labsGroup: LabGroup.Rooms,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
displayName: _td("New spotlight search experience"),
|
||||
displayName: _td("New search experience"),
|
||||
default: false,
|
||||
betaInfo: {
|
||||
title: _td("The new search"),
|
||||
caption: () => <>
|
||||
<p>{ _t("A new, quick way to search spaces and rooms you're in.") }</p>
|
||||
<p>{ _t("This feature is a work in progress, we'd love to hear your feedback.") }</p>
|
||||
</>,
|
||||
disclaimer: () => <>
|
||||
{ SdkConfig.get().bug_report_endpoint_url && <>
|
||||
<h4>{ _t("How can I give feedback?") }</h4>
|
||||
<p>{ _t("To feedback, join the beta, start a search and click on feedback.") }</p>
|
||||
</> }
|
||||
<h4>{ _t("How can I leave the beta?") }</h4>
|
||||
<p>{ _t("To leave, just return to this page or click on the beta badge when you search.") }</p>
|
||||
</>,
|
||||
feedbackLabel: "spotlight-feedback",
|
||||
feedbackSubheading: _td("Thank you for trying the beta, " +
|
||||
"please go into as much detail as you can so we can improve it."),
|
||||
image: require("../../res/img/betas/new_search_experience.gif"),
|
||||
},
|
||||
},
|
||||
"feature_right_panel_default_open": {
|
||||
isFeature: true,
|
||||
|
|
|
@ -95,7 +95,7 @@ const getRoomFn: FetchRoomFn = (room: Room) => {
|
|||
export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||
// The spaces representing the roots of the various tree-like hierarchies
|
||||
private rootSpaces: Room[] = [];
|
||||
// Map from room ID to set of spaces which list it as a child
|
||||
// Map from room/space ID to set of spaces which list it as a child
|
||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||
// Map from SpaceKey to SpaceNotificationState instance representing that space
|
||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
||||
|
|
Loading…
Reference in New Issue