diff --git a/res/css/_components.scss b/res/css/_components.scss index de4c1c677c..66af2ba00f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -19,6 +19,7 @@ @import "./structures/_NotificationPanel.scss"; @import "./structures/_RightPanel.scss"; @import "./structures/_RoomDirectory.scss"; +@import "./structures/_RoomSearch.scss"; @import "./structures/_RoomStatusBar.scss"; @import "./structures/_RoomSubList.scss"; @import "./structures/_RoomView.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index d335df305f..d9a2b1dd5c 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -88,7 +88,44 @@ $roomListMinimizedWidth: 50px; } .mx_LeftPanel2_filterContainer { - // TODO: Improve CSS for filtering and its input + margin-left: 12px; + margin-right: 12px; + + // Create a flexbox to organize the inputs + display: flex; + align-items: center; + + .mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton { + // Cheaty way to return the occupied space to the filter input + margin: 0; + width: 0; + + // Don't forget to hide the masked ::before icon + visibility: hidden; + } + + .mx_LeftPanel2_exploreButton { + width: 28px; + height: 28px; + border-radius: 20px; + background-color: #fff; // TODO: Variable and theme + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/compass.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } } .mx_LeftPanel2_actualRoomListContainer { diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss new file mode 100644 index 0000000000..d078031090 --- /dev/null +++ b/res/css/structures/_RoomSearch.scss @@ -0,0 +1,70 @@ +/* +Copyright 2020 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. +*/ + +// Note: this component expects to be contained within a flexbox +.mx_RoomSearch { + flex: 1; + border-radius: 20px; + background-color: #fff; // TODO: Variable & theme + height: 26px; + padding: 2px; + + // Create a flexbox for the icons (easier to manage) + display: flex; + align-items: center; + + .mx_RoomSearch_icon { + width: 16px; + height: 16px; + mask: url('$(res)/img/feather-customised/search-input.svg'); + mask-repeat: no-repeat; + background: $primary-fg-color; + margin-left: 7px; + } + + .mx_RoomSearch_input { + border: none !important; // !important to override default app-wide styles + flex: 1 !important; // !important to override default app-wide styles + color: $primary-fg-color !important; // !important to override default app-wide styles + padding: 0; + height: 100%; + width: 100%; + font-size: $font-12px; + line-height: $font-16px; + + &:not(.mx_RoomSearch_inputExpanded)::placeholder { + color: $primary-fg-color !important; // !important to override default app-wide styles + } + } + + &.mx_RoomSearch_expanded { + .mx_RoomSearch_clearButton { + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/x.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + margin-right: 8px; + } + } + + .mx_RoomSearch_clearButton { + width: 0; + height: 0; + } +} diff --git a/res/img/feather-customised/compass.svg b/res/img/feather-customised/compass.svg new file mode 100644 index 0000000000..3296260803 --- /dev/null +++ b/res/img/feather-customised/compass.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-compass"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg> \ No newline at end of file diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index ff8d35a114..209f219598 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -22,9 +22,10 @@ import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); -const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); +const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); const HomePage = () => { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 05cd97df2a..bae69b5631 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -252,7 +252,7 @@ const LeftPanel = createReactClass({ if (!this.props.collapsed) { exploreButton = ( <div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}> - <AccessibleButton onClick={() => dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")}</AccessibleButton> + <AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton> </div> ); } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 2fd8612ff5..f417dc99b1 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -18,16 +18,16 @@ import * as React from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; -import AccessibleButton from "../views/elements/AccessibleButton"; import { _t } from "../../languageHandler"; import SearchBox from "./SearchBox"; import RoomList2 from "../views/rooms/RoomList2"; -import TopLeftMenuButton from "./TopLeftMenuButton"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseAvatar from '../views/avatars/BaseAvatar'; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; import UserMenuButton from "./UserMenuButton"; +import RoomSearch from "./RoomSearch"; +import AccessibleButton from "../views/elements/AccessibleButton"; /******************************************************************* * CAUTION * @@ -42,7 +42,6 @@ interface IProps { } interface IState { - searchExpanded: boolean; searchFilter: string; // TODO: Move search into room list? } @@ -58,7 +57,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { super(props); this.state = { - searchExpanded: false, searchFilter: "", }; } @@ -67,24 +65,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { this.setState({searchFilter: term}); }; - private onSearchCleared = (source: string): void => { - if (source === "keyboard") { - dis.fire(Action.FocusComposer); - } - this.setState({searchExpanded: false}); - } - - private onSearchFocus = (): void => { - this.setState({searchExpanded: true}); + private onExplore = () => { + dis.fire(Action.ViewRoomDirectory); }; - private onSearchBlur = (event: FocusEvent): void => { - const target = event.target as HTMLInputElement; - if (target.value.length === 0) { - this.setState({searchExpanded: false}); - } - } - private renderHeader(): React.ReactNode { // TODO: Update when profile info changes // TODO: Presence @@ -126,6 +110,22 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { ); } + private renderSearchExplore(): React.ReactNode { + // TODO: Collapsed support + + return ( + <div className="mx_LeftPanel2_filterContainer"> + <RoomSearch onQueryUpdate={this.onSearch} /> + <AccessibleButton + tabIndex={-1} + className='mx_LeftPanel2_exploreButton' + onClick={this.onExplore} + alt={_t("Explore rooms")} + /> + </div> + ); + } + public render(): React.ReactNode { const tagPanel = ( <div className="mx_LeftPanel2_tagPanelContainer"> @@ -133,18 +133,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { </div> ); - const searchBox = (<SearchBox - className="mx_LeftPanel2_filterRoomsSearch" - enableRoomSearchFocus={true} - blurredPlaceholder={_t('Filter')} - placeholder={_t('Filter rooms…')} - onKeyDown={() => {/*TODO*/}} - onSearch={this.onSearch} - onCleared={this.onSearchCleared} - onFocus={this.onSearchFocus} - onBlur={this.onSearchBlur} - collapsed={false}/>); // TODO: Collapsed support - // TODO: Improve props for RoomList2 const roomList = <RoomList2 onKeyDown={() => {/*TODO*/}} @@ -167,14 +155,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> { {tagPanel} <aside className="mx_LeftPanel2_roomListContainer"> {this.renderHeader()} - <div - className="mx_LeftPanel2_filterContainer" - onKeyDown={() => {/*TODO*/}} - onFocus={() => {/*TODO*/}} - onBlur={() => {/*TODO*/}} - > - {searchBox} - </div> + {this.renderSearchExplore()} <div className="mx_LeftPanel2_actualRoomListContainer"> {roomList} </div> diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index e08381d8fa..634e13b103 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -624,7 +624,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { Modal.createTrackedDialog('Create Community', '', CreateGroupDialog); break; } - case 'view_room_directory': { + case Action.ViewRoomDirectory: { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, 'mx_RoomDirectory_dialogWrapper', false, true); @@ -1611,9 +1611,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> { action: 'require_registration', }); } else if (screen === 'directory') { - dis.dispatch({ - action: 'view_room_directory', - }); + dis.fire(Action.ViewRoomDirectory); } else if (screen === 'groups') { dis.dispatch({ action: 'view_my_groups', diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx new file mode 100644 index 0000000000..671cd89e08 --- /dev/null +++ b/src/components/structures/RoomSearch.tsx @@ -0,0 +1,143 @@ +/* +Copyright 2020 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 * as React from "react"; +import { createRef } from "react"; +import classNames from "classnames"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { _t } from "../../languageHandler"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { throttle } from 'lodash'; +import { Key } from "../../Keyboard"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import { Action } from "../../dispatcher/actions"; + +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + *******************************************************************/ + +interface IProps { + onQueryUpdate: (newQuery: string) => void; +} + +interface IState { + query: string; + focused: boolean; +} + +export default class RoomSearch extends React.PureComponent<IProps, IState> { + private dispatcherRef: string; + private inputRef: React.RefObject<HTMLInputElement> = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + query: "", + focused: false, + }; + + this.dispatcherRef = defaultDispatcher.register(this.onAction); + } + + public componentWillUnmount() { + defaultDispatcher.unregister(this.dispatcherRef); + } + + 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(); + }; + + private onChange = () => { + if (!this.inputRef.current) return; + this.setState({query: this.inputRef.current.value}); + this.onSearchUpdated(); + }; + + // it wants this at the top of the file, but we know better + // tslint:disable-next-line + private onSearchUpdated = throttle(() => { + // We can't use the state variable because it can lag behind the input. + // The lag is most obvious when deleting/clearing text with the keyboard. + this.props.onQueryUpdate(this.inputRef.current.value); + }, 200, {trailing: true, leading: true}, + ); + + private onFocus = (ev: React.FocusEvent<HTMLInputElement>) => { + this.setState({focused: true}); + ev.target.select(); + }; + + private onBlur = () => { + this.setState({focused: false}); + }; + + private onKeyDown = (ev: React.KeyboardEvent) => { + if (ev.key === Key.ESCAPE) { + this.clearInput(); + defaultDispatcher.fire(Action.FocusComposer); + } + }; + + public render(): React.ReactNode { + const classes = classNames({ + 'mx_RoomSearch': true, + 'mx_RoomSearch_expanded': this.state.query || this.state.focused, + }); + + const inputClasses = classNames({ + 'mx_RoomSearch_input': true, + 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, + }); + + return ( + <div className={classes}> + <div className='mx_RoomSearch_icon'/> + <input + type="text" + ref={this.inputRef} + className={inputClasses} + value={this.state.query} + onFocus={this.onFocus} + onBlur={this.onBlur} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + placeholder={_t("Search")} + autoComplete="off" + /> + <AccessibleButton + tabIndex={-1} + className='mx_RoomSearch_clearButton' + onClick={this.clearInput} + /> + </div> + ); + } +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 0ff997ee09..ab3da035c4 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1458,9 +1458,7 @@ export default createReactClass({ // using /leave rather than /join. In the short term though, we // just ignore them. // https://github.com/vector-im/vector-web/issues/1134 - dis.dispatch({ - action: 'view_room_directory', - }); + dis.fire(Action.ViewRoomDirectory); }, onSearchClick: function() { diff --git a/src/components/views/elements/RoomDirectoryButton.js b/src/components/views/elements/RoomDirectoryButton.js index d0bff4beeb..e9de6f8d15 100644 --- a/src/components/views/elements/RoomDirectoryButton.js +++ b/src/components/views/elements/RoomDirectoryButton.js @@ -18,11 +18,12 @@ import React from 'react'; import * as sdk from '../../../index'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; +import {Action} from "../../../dispatcher/actions"; const RoomDirectoryButton = function(props) { const ActionButton = sdk.getComponent('elements.ActionButton'); return ( - <ActionButton action="view_room_directory" + <ActionButton action={Action.ViewRoomDirectory} mouseOverAction={props.callout ? "callout_room_directory" : null} label={_t("Room directory")} iconPath={require("../../../../res/img/icons-directory.svg")} diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index c9b5d9e3ad..5f7ca1293c 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -40,6 +40,11 @@ export enum Action { */ ViewUserSettings = "view_user_settings", + /** + * Opens the room directory. No additional payload information required. + */ + ViewRoomDirectory = "view_room_directory", + /** * Sets the current tooltip. Should be use with ViewTooltipPayload. */