Merge pull request #4736 from matrix-org/travis/room-list/filter-explore

Add filtering and exploring to the new room list
This commit is contained in:
Travis Ralston 2020-06-09 07:59:13 -06:00 committed by GitHub
commit 182dd41947
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 289 additions and 52 deletions

View file

@ -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";

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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>

After

Width:  |  Height:  |  Size: 342 B

View file

@ -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 = () => {

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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',

View file

@ -0,0 +1,144 @@
/*
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>
);
}
}

View file

@ -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() {

View file

@ -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")}

View file

@ -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.
*/

View file

@ -1958,6 +1958,7 @@
"Explore": "Explore",
"Filter": "Filter",
"Filter rooms…": "Filter rooms…",
"Explore rooms": "Explore rooms",
"Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
"Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?",
@ -2003,7 +2004,6 @@
"Find a room…": "Find a room…",
"Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)",
"If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>Create a new room</a>.",
"Explore rooms": "Explore rooms",
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.",
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please <a>contact your service administrator</a> to continue using the service.",
"Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please <a>contact your service administrator</a> to continue using the service.",