2020-06-09 05:33:21 +03:00
|
|
|
/*
|
2021-03-25 16:29:17 +03:00
|
|
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
2020-06-09 05:33:21 +03:00
|
|
|
|
|
|
|
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 { createRef } from "react";
|
|
|
|
import classNames from "classnames";
|
2021-05-07 12:39:35 +03:00
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2021-04-29 18:40:08 +03:00
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
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";
|
2020-07-24 23:42:53 +03:00
|
|
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
|
|
|
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
2021-03-01 11:43:00 +03:00
|
|
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
2021-05-04 20:06:43 +03:00
|
|
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
2021-05-07 12:39:35 +03:00
|
|
|
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
|
2020-06-09 05:33:21 +03:00
|
|
|
|
|
|
|
interface IProps {
|
2020-06-11 23:39:28 +03:00
|
|
|
isMinimized: boolean;
|
2021-03-28 09:28:48 +03:00
|
|
|
onKeyDown(ev: React.KeyboardEvent): void;
|
|
|
|
/**
|
|
|
|
* @returns true if a room has been selected and the search field should be cleared
|
|
|
|
*/
|
|
|
|
onSelectRoom(): boolean;
|
2020-06-09 05:33:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
query: string;
|
|
|
|
focused: boolean;
|
2021-05-07 12:39:35 +03:00
|
|
|
inSpaces: boolean;
|
2020-06-09 05:33:21 +03:00
|
|
|
}
|
|
|
|
|
2021-03-09 05:35:10 +03:00
|
|
|
@replaceableComponent("structures.RoomSearch")
|
2020-06-09 05:33:21 +03:00
|
|
|
export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|
|
|
private dispatcherRef: string;
|
|
|
|
private inputRef: React.RefObject<HTMLInputElement> = createRef();
|
2020-07-24 23:42:53 +03:00
|
|
|
private searchFilter: NameFilterCondition = new NameFilterCondition();
|
2020-06-09 05:33:21 +03:00
|
|
|
|
|
|
|
constructor(props: IProps) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
query: "",
|
|
|
|
focused: false,
|
2021-05-07 12:39:35 +03:00
|
|
|
inSpaces: false,
|
2020-06-09 05:33:21 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
2021-03-25 16:29:17 +03:00
|
|
|
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
|
|
|
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
|
2021-05-07 12:39:35 +03:00
|
|
|
SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
2020-06-09 05:33:21 +03:00
|
|
|
}
|
|
|
|
|
2020-07-24 23:42:53 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
public componentWillUnmount() {
|
|
|
|
defaultDispatcher.unregister(this.dispatcherRef);
|
2021-03-25 16:29:17 +03:00
|
|
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
|
2021-05-07 12:39:35 +03:00
|
|
|
SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces);
|
2020-06-09 05:33:21 +03:00
|
|
|
}
|
|
|
|
|
2021-05-07 12:39:35 +03:00
|
|
|
private onSpaces = (spaces: Room[]) => {
|
|
|
|
this.setState({
|
|
|
|
inSpaces: spaces.length > 0,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
private onAction = (payload: ActionPayload) => {
|
|
|
|
if (payload.action === 'view_room' && payload.clear_search) {
|
|
|
|
this.clearInput();
|
|
|
|
} else if (payload.action === 'focus_room_filter' && this.inputRef.current) {
|
|
|
|
this.inputRef.current.focus();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private clearInput = () => {
|
|
|
|
if (!this.inputRef.current) return;
|
|
|
|
this.inputRef.current.value = "";
|
|
|
|
this.onChange();
|
|
|
|
};
|
|
|
|
|
2020-06-11 23:39:28 +03:00
|
|
|
private openSearch = () => {
|
2021-06-29 15:11:58 +03:00
|
|
|
defaultDispatcher.dispatch({ action: "show_left_panel" });
|
|
|
|
defaultDispatcher.dispatch({ action: "focus_room_filter" });
|
2020-06-11 23:39:28 +03:00
|
|
|
};
|
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
private onChange = () => {
|
|
|
|
if (!this.inputRef.current) return;
|
2021-06-29 15:11:58 +03:00
|
|
|
this.setState({ query: this.inputRef.current.value });
|
2020-06-09 05:33:21 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
|
2021-06-29 15:11:58 +03:00
|
|
|
this.setState({ focused: true });
|
2020-06-09 05:33:21 +03:00
|
|
|
ev.target.select();
|
|
|
|
};
|
|
|
|
|
2020-07-08 19:28:15 +03:00
|
|
|
private onBlur = (ev: React.FocusEvent<HTMLInputElement>) => {
|
2021-06-29 15:11:58 +03:00
|
|
|
this.setState({ focused: false });
|
2020-06-09 05:33:21 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
2021-03-01 11:43:00 +03:00
|
|
|
const action = getKeyBindingsManager().getRoomListAction(ev);
|
2021-02-28 10:13:34 +03:00
|
|
|
switch (action) {
|
2021-03-01 11:43:00 +03:00
|
|
|
case RoomListAction.ClearSearch:
|
2021-02-28 10:13:34 +03:00
|
|
|
this.clearInput();
|
2021-07-08 18:36:31 +03:00
|
|
|
defaultDispatcher.fire(Action.FocusSendMessageComposer);
|
2021-02-28 10:13:34 +03:00
|
|
|
break;
|
2021-03-01 11:43:00 +03:00
|
|
|
case RoomListAction.NextRoom:
|
|
|
|
case RoomListAction.PrevRoom:
|
2021-03-28 09:28:48 +03:00
|
|
|
// we don't handle these actions here put pass the event on to the interested party (LeftPanel)
|
|
|
|
this.props.onKeyDown(ev);
|
2021-02-28 10:13:34 +03:00
|
|
|
break;
|
2021-03-01 11:43:00 +03:00
|
|
|
case RoomListAction.SelectRoom: {
|
2021-03-28 09:28:48 +03:00
|
|
|
const shouldClear = this.props.onSelectRoom();
|
2021-02-28 10:13:34 +03:00
|
|
|
if (shouldClear) {
|
|
|
|
// wrap in set immediate to delay it so that we don't clear the filter & then change room
|
|
|
|
setImmediate(() => {
|
|
|
|
this.clearInput();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
break;
|
2020-07-16 08:31:06 +03:00
|
|
|
}
|
2020-06-09 05:33:21 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
public render(): React.ReactNode {
|
|
|
|
const classes = classNames({
|
|
|
|
'mx_RoomSearch': true,
|
2020-08-17 18:59:29 +03:00
|
|
|
'mx_RoomSearch_hasQuery': this.state.query,
|
|
|
|
'mx_RoomSearch_focused': this.state.focused,
|
2020-06-11 23:39:28 +03:00
|
|
|
'mx_RoomSearch_minimized': this.props.isMinimized,
|
2020-06-09 05:33:21 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
const inputClasses = classNames({
|
|
|
|
'mx_RoomSearch_input': true,
|
|
|
|
'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused,
|
|
|
|
});
|
|
|
|
|
2021-05-07 12:39:35 +03:00
|
|
|
let placeholder = _t("Filter");
|
|
|
|
if (this.state.inSpaces) {
|
|
|
|
placeholder = _t("Filter all spaces");
|
|
|
|
}
|
|
|
|
|
2020-06-11 23:39:28 +03:00
|
|
|
let icon = (
|
2020-08-29 03:11:08 +03:00
|
|
|
<div className='mx_RoomSearch_icon' />
|
2020-06-11 23:39:28 +03:00
|
|
|
);
|
|
|
|
let input = (
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
ref={this.inputRef}
|
|
|
|
className={inputClasses}
|
|
|
|
value={this.state.query}
|
|
|
|
onFocus={this.onFocus}
|
|
|
|
onBlur={this.onBlur}
|
|
|
|
onChange={this.onChange}
|
|
|
|
onKeyDown={this.onKeyDown}
|
2021-05-07 12:39:35 +03:00
|
|
|
placeholder={placeholder}
|
2020-06-11 23:39:28 +03:00
|
|
|
autoComplete="off"
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
let clearButton = (
|
|
|
|
<AccessibleButton
|
|
|
|
tabIndex={-1}
|
2020-07-05 03:07:46 +03:00
|
|
|
title={_t("Clear filter")}
|
|
|
|
className="mx_RoomSearch_clearButton"
|
2020-06-11 23:39:28 +03:00
|
|
|
onClick={this.clearInput}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
if (this.props.isMinimized) {
|
|
|
|
icon = (
|
2020-06-09 05:33:21 +03:00
|
|
|
<AccessibleButton
|
2020-11-11 16:08:09 +03:00
|
|
|
title={_t("Filter rooms and people")}
|
2020-09-18 19:13:45 +03:00
|
|
|
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
|
2020-06-11 23:39:28 +03:00
|
|
|
onClick={this.openSearch}
|
2020-06-09 05:33:21 +03:00
|
|
|
/>
|
2020-06-11 23:39:28 +03:00
|
|
|
);
|
|
|
|
input = null;
|
|
|
|
clearButton = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={classes}>
|
|
|
|
{icon}
|
|
|
|
{input}
|
|
|
|
{clearButton}
|
2020-06-09 05:33:21 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|