Merge pull request #4687 from matrix-org/travis/split-left-panel

Split the left panel into new and old for new room list designs
This commit is contained in:
Travis Ralston 2020-06-05 10:07:13 -06:00 committed by GitHub
commit efa12b8c2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 216 additions and 44 deletions

View file

@ -23,6 +23,14 @@ limitations under the License.
flex: 0 0 auto;
}
// TODO: Remove temporary indicator of new room list implementation.
// This border is meant to visually distinguish between the two components when the
// user has turned on the new room list implementation, at least until the designs
// themselves give it away.
.mx_LeftPanel2 .mx_LeftPanel {
border-left: 5px #e26dff solid;
}
.mx_LeftPanel_container.collapsed {
min-width: unset;
/* Collapsed LeftPanel 50px */

View file

@ -31,6 +31,7 @@ import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions";
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
@ -529,7 +530,7 @@ export default class ContentMessages {
dis.dispatch({action: 'upload_started'});
// Focus the composer view
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
function onProgress(ev) {
upload.total = ev.total;

View file

@ -26,7 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import RoomList2 from "../views/rooms/RoomList2";
import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({
@ -198,7 +198,7 @@ const LeftPanel = createReactClass({
onSearchCleared: function(source) {
if (source === "keyboard") {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
},
@ -274,28 +274,15 @@ const LeftPanel = createReactClass({
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
let roomList = null;
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
roomList = <RoomList2
onKeyDown={this._onKeyDown}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ref={this.collectRoomList}
onFocus={this._onFocus}
onBlur={this._onBlur}
/>;
} else {
roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
}
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
return (
<div className={containerClasses}>

View file

@ -0,0 +1,154 @@
/*
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 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";
/*******************************************************************
* 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 {
// TODO: Support collapsed state
}
interface IState {
searchExpanded: boolean;
searchFilter: string; // TODO: Move search into room list?
}
export default class LeftPanel2 extends React.Component<IProps, IState> {
// TODO: Properly support TagPanel
// TODO: Properly support searching/filtering
// TODO: Properly support breadcrumbs
// TODO: Properly support TopLeftMenu (User Settings)
// TODO: a11y
// TODO: actually make this useful in general (match design proposals)
// TODO: Fadable support (is this still needed?)
constructor(props: IProps) {
super(props);
this.state = {
searchExpanded: false,
searchFilter: "",
};
}
private onSearch = (term: string): void => {
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 onSearchBlur = (event: FocusEvent): void => {
const target = event.target as HTMLInputElement;
if (target.value.length === 0) {
this.setState({searchExpanded: false});
}
}
public render(): React.ReactNode {
const tagPanel = (
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
</div>
);
const 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>
</div>
);
const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms"
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*/}}
resizeNotifier={null}
collapsed={false}
searchFilter={this.state.searchFilter}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
/>;
// TODO: Breadcrumbs
// TODO: Conference handling / calls
const containerClasses = classNames({
"mx_LeftPanel_container": true,
"mx_fadable": true,
"collapsed": false, // TODO: Collapsed support
"mx_LeftPanel_container_hasTagPanel": true, // TODO: TagPanel support
"mx_fadable_faded": false,
"mx_LeftPanel2": true, // TODO: Remove flag when RoomList2 ships (used as an indicator)
});
return (
<div className={containerClasses}>
{tagPanel}
<aside className="mx_LeftPanel dark-panel">
<TopLeftMenuButton collapsed={false}/>
<div
className="mx_LeftPanel_exploreAndFilterRow"
onKeyDown={() => {/*TODO*/}}
onFocus={() => {/*TODO*/}}
onBlur={() => {/*TODO*/}}
>
{exploreButton}
{searchBox}
</div>
{roomList}
</aside>
</div>
);
}
}

View file

@ -51,6 +51,8 @@ import {
showToast as showServerLimitToast,
hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2";
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@ -358,7 +360,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
// refocusing during a paste event will make the
// paste end up in the newly focused element,
// so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
}
};
@ -508,7 +510,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
if (!isClickShortcut && ev.key !== Key.TAB && !canElementReceiveInput(ev.target)) {
// synchronous dispatch so we focus before key generates input
dis.dispatch({action: 'focus_composer'}, true);
dis.fire(Action.FocusComposer, true);
ev.stopPropagation();
// we should *not* preventDefault() here as
// that would prevent typing in the now-focussed composer
@ -667,6 +669,20 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
let leftPanel = (
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
);
if (SettingsStore.isFeatureEnabled("feature_new_room_list")) {
// TODO: Supply props like collapsed and disabled to LeftPanel2
leftPanel = (
<LeftPanel2 />
);
}
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
@ -680,11 +696,7 @@ class LoggedInView extends React.PureComponent<IProps, IState> {
<ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
{ leftPanel }
<ResizeHandle />
{ pageElement }
</div>

View file

@ -347,7 +347,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Analytics.trackPageChange(durationMs);
}
if (this.focusComposer) {
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
this.focusComposer = false;
}
}
@ -1363,7 +1363,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
showNotificationsToast();
}
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
this.setState({
ready: true,
});

View file

@ -26,6 +26,7 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
@ -127,12 +128,12 @@ export default createReactClass({
_onResendAllClick: function() {
Resend.resendUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
_onCancelAllClick: function() {
Resend.cancelUnsentEvents(this.props.room);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
_onRoomLocalEchoUpdated: function(event, room, oldEventId, oldStatus) {

View file

@ -55,6 +55,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile";
import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { shieldStatusForRoom } from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
const DEBUG = false;
let debuglog = function() {};
@ -1162,7 +1163,7 @@ export default createReactClass({
ev.dataTransfer.files, this.state.room.roomId, this.context,
);
this.setState({ draggingFile: false });
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onDragLeaveOrEnd: function(ev) {
@ -1368,7 +1369,7 @@ export default createReactClass({
event: null,
});
}
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
onLeaveClick: function() {
@ -1479,7 +1480,7 @@ export default createReactClass({
// jump down to the bottom of this room, where new events are arriving
jumpToLiveTimeline: function() {
this._messagePanel.jumpToLiveTimeline();
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
},
// jump up to wherever our read marker is

View file

@ -26,6 +26,7 @@ import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks
import SettingsStore from "../../../settings/SettingsStore";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
@ -290,7 +291,7 @@ export default class ReplyThread extends React.Component {
events,
}, this.loadNextEvent);
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
render() {

View file

@ -31,6 +31,7 @@ import {EventStatus} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer";
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
function _isReply(mxEvent) {
const relatesTo = mxEvent.getContent()["m.relates_to"];
@ -157,7 +158,7 @@ export default class EditMessageComposer extends React.Component {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
}
@ -165,7 +166,7 @@ export default class EditMessageComposer extends React.Component {
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
_isContentModified(newContent) {
@ -195,7 +196,7 @@ export default class EditMessageComposer extends React.Component {
// close the event editing and focus composer
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
};
_cancelPreviousPendingEdit() {

View file

@ -44,6 +44,7 @@ import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -364,7 +365,7 @@ export default class SendMessageComposer extends React.Component {
onAction = (payload) => {
switch (payload.action) {
case 'reply_to_event':
case 'focus_composer':
case Action.FocusComposer:
this._editorRef && this._editorRef.focus();
break;
case 'insert_mention':

View file

@ -53,4 +53,9 @@ export enum Action {
* Provide status information for an ongoing update check. Should be used with a CheckUpdatesPayload.
*/
CheckUpdates = "check_updates",
/**
* Focuses the user's cursor to the composer. No additional payload information required.
*/
FocusComposer = "focus_composer",
}