Move New Search Experience out of beta (#8859)

This commit is contained in:
Janne Mareike Koschinski 2022-06-28 12:02:08 +02:00 committed by GitHub
parent e1d6356927
commit 8b841951db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 18 additions and 386 deletions

View file

@ -31,7 +31,7 @@ import SpaceStore from "../../stores/spaces/SpaceStore";
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import UIStore from "../../stores/UIStore";
import { findSiblingElement, IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import { IState as IRovingTabIndexState } from "../../accessibility/RovingTabIndex";
import RoomListHeader from "../views/rooms/RoomListHeader";
import RecentlyViewedButton from "../views/rooms/RecentlyViewedButton";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
@ -64,7 +64,6 @@ interface IState {
export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef = createRef<HTMLDivElement>();
private roomSearchRef = createRef<RoomSearch>();
private roomListRef = createRef<RoomList>();
private focusedElement = null;
private isDoingStickyHeaders = false;
@ -302,32 +301,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.roomListRef.current?.focus();
}
break;
case KeyBindingAction.PrevRoom:
if (state && state.activeRef === findSiblingElement(state.refs, 0)) {
ev.stopPropagation();
ev.preventDefault();
this.roomSearchRef.current?.focus();
}
break;
}
};
private onRoomListKeydown = (ev: React.KeyboardEvent) => {
if (ev.altKey || ev.ctrlKey || ev.metaKey) return;
if (SettingsStore.getValue("feature_spotlight")) return;
const action = getKeyBindingsManager().getAccessibilityAction(ev);
// we cannot handle Space as that is an activation key for all focusable elements in this widget
if (ev.key.length === 1) {
ev.preventDefault();
ev.stopPropagation();
this.roomSearchRef.current?.appendChar(ev.key);
} else if (action === KeyBindingAction.Backspace) {
ev.preventDefault();
ev.stopPropagation();
this.roomSearchRef.current?.backspace();
}
};
@ -386,7 +359,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
>
<RoomSearch
isMinimized={this.props.isMinimized}
ref={this.roomSearchRef}
onSelectRoom={this.selectRoom}
/>
@ -436,7 +408,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
onKeyDown={this.onRoomListKeydown}
>
{ roomList }
</div>

View file

@ -93,7 +93,6 @@ import SecurityCustomisations from "../../customisations/Security";
import Spinner from "../views/elements/Spinner";
import QuestionDialog from "../views/dialogs/QuestionDialog";
import UserSettingsDialog from '../views/dialogs/UserSettingsDialog';
import { UserTab } from "../views/dialogs/UserTab";
import CreateRoomDialog from '../views/dialogs/CreateRoomDialog';
import RoomDirectory from './RoomDirectory';
import KeySignatureUploadFailedDialog from "../views/dialogs/KeySignatureUploadFailedDialog";
@ -118,7 +117,6 @@ 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';
import Views from '../../Views';
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { ViewHomePagePayload } from '../../dispatcher/payloads/ViewHomePagePayload';
@ -738,9 +736,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.resizeNotifier.notifyLeftHandleResized();
});
break;
case 'focus_room_filter': // for CtrlOrCmd+K to work by expanding the left panel first
if (SettingsStore.getValue("feature_spotlight")) break; // don't expand if spotlight enabled
// fallthrough
case 'show_left_panel':
this.setState({
collapseLhs: false,
@ -1398,42 +1393,6 @@ 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,

View file

@ -14,26 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import * as React from "react";
import { createRef, RefObject } from "react";
import classNames from "classnames";
import * as React from "react";
import { ALTERNATE_KEY_NAME } from "../../accessibility/KeyboardShortcuts";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads";
import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import SpaceStore from "../../stores/spaces/SpaceStore";
import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { IS_MAC, Key } from "../../Keyboard";
import SettingsStore from "../../settings/SettingsStore";
import { _t } from "../../languageHandler";
import Modal from "../../Modal";
import SpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import ToastStore from "../../stores/ToastStore";
import AccessibleButton from "../views/elements/AccessibleButton";
interface IProps {
isMinimized: boolean;
@ -43,220 +34,49 @@ interface IProps {
onSelectRoom(): boolean;
}
interface IState {
query: string;
focused: boolean;
spotlightBetaEnabled: boolean;
}
export default class RoomSearch extends React.PureComponent<IProps, IState> {
export default class RoomSearch extends React.PureComponent<IProps> {
private readonly dispatcherRef: string;
private readonly betaRef: string;
private elementRef: React.RefObject<HTMLInputElement | HTMLDivElement> = createRef();
private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) {
super(props);
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 {
if (prevState.query !== this.state.query) {
const hadSearch = !!this.searchFilter.search.trim();
const haveSearch = !!this.state.query.trim();
this.searchFilter.search = this.state.query;
if (!hadSearch && haveSearch) {
// started a new filter - add the condition
RoomListStore.instance.addFilter(this.searchFilter);
} else if (hadSearch && !haveSearch) {
// cleared a filter - remove the condition
RoomListStore.instance.removeFilter(this.searchFilter);
} // else the filter hasn't changed enough for us to care here
}
}
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,
query: "",
});
}
// in case the user was in settings at the 5-minute mark, dismiss the toast
ToastStore.sharedInstance().dismissToast("BETA_SPOTLIGHT_TOAST");
};
private openSpotlight() {
Modal.createDialog(SpotlightDialog, {}, "mx_SpotlightDialog_wrapper", false, true);
}
private onAction = (payload: ActionPayload) => {
if (payload.action === Action.ViewRoom && payload.clear_search) {
this.clearInput();
} else if (payload.action === 'focus_room_filter') {
if (this.state.spotlightBetaEnabled) {
this.openSpotlight();
} else {
this.focus();
}
}
};
private clearInput = () => {
if (this.elementRef.current?.tagName !== "INPUT") return;
(this.elementRef.current as HTMLInputElement).value = "";
this.onChange();
};
private openSearch = () => {
if (this.state.spotlightBetaEnabled) {
if (payload.action === 'focus_room_filter') {
this.openSpotlight();
} else {
// dispatched as it needs handling by MatrixChat too
defaultDispatcher.dispatch({ action: "focus_room_filter" });
}
};
private onChange = () => {
if (this.elementRef.current?.tagName !== "INPUT") return;
this.setState({ query: (this.elementRef.current as HTMLInputElement).value });
};
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({ focused: true });
ev.target.select();
};
private onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
this.setState({ focused: false });
};
private onKeyDown = (ev: React.KeyboardEvent) => {
const action = getKeyBindingsManager().getRoomListAction(ev);
switch (action) {
case KeyBindingAction.ClearRoomFilter:
this.clearInput();
defaultDispatcher.fire(Action.FocusSendMessageComposer);
break;
case KeyBindingAction.SelectRoomInRoomList: {
const shouldClear = this.props.onSelectRoom();
if (shouldClear) {
// wrap in set immediate to delay it so that we don't clear the filter & then change room
setImmediate(() => {
this.clearInput();
});
}
break;
}
}
};
public focus = (): void => {
this.elementRef.current?.focus();
};
public render(): React.ReactNode {
const classes = classNames({
'mx_RoomSearch': true,
'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({
'mx_RoomSearch_input': true,
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
});
}, 'mx_RoomSearch_spotlightTrigger');
const icon = (
<div className="mx_RoomSearch_icon" />
);
let input = (
<input
type="text"
ref={this.elementRef as RefObject<HTMLInputElement>}
className={inputClasses}
value={this.state.query}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
placeholder={_t("Filter")}
autoComplete="off"
/>
);
let shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
const shortcutPrompt = <div className="mx_RoomSearch_shortcutPrompt">
{ IS_MAC ? "⌘ K" : _t(ALTERNATE_KEY_NAME[Key.CONTROL]) + " K" }
</div>;
if (this.props.isMinimized) {
input = null;
shortcutPrompt = null;
}
if (this.state.spotlightBetaEnabled) {
return <AccessibleButton onClick={this.openSpotlight} className={classes} inputRef={this.elementRef}>
{ icon }
{ input && <div className="mx_RoomSearch_spotlightTriggerText">
{ _t("Search") }
</div> }
{ shortcutPrompt }
</AccessibleButton>;
} else if (this.props.isMinimized) {
return <AccessibleButton
onClick={this.openSearch}
className="mx_RoomSearch mx_RoomSearch_minimized"
title={_t("Filter rooms and people")}
inputRef={this.elementRef}
>
{ icon }
</AccessibleButton>;
}
return (
<div className={classes} onClick={this.focus}>
{ icon }
{ input }
{ shortcutPrompt }
<AccessibleButton
tabIndex={-1}
title={_t("Clear filter")}
className="mx_RoomSearch_clearButton"
onClick={this.clearInput}
/>
</div>
);
}
public appendChar(char: string): void {
this.setState({
query: this.state.query + char,
});
}
public backspace(): void {
this.setState({
query: this.state.query.substring(0, this.state.query.length - 1),
});
return <AccessibleButton onClick={this.openSpotlight} className={classes}>
{ icon }
{ (!this.props.isMinimized) && <div className="mx_RoomSearch_spotlightTriggerText">
{ _t("Search") }
</div> }
{ shortcutPrompt }
</AccessibleButton>;
}
}