From 1f1f61377775014dfbe8303eb7d89d3ed6f5c520 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 2 Jun 2020 19:07:46 -0600 Subject: [PATCH 1/2] Add a focus_composer dispatcher action and use it --- src/ContentMessages.tsx | 3 ++- src/components/structures/LeftPanel.js | 3 ++- src/components/structures/LoggedInView.tsx | 5 +++-- src/components/structures/MatrixChat.tsx | 4 ++-- src/components/structures/RoomStatusBar.js | 5 +++-- src/components/structures/RoomView.js | 7 ++++--- src/components/views/elements/ReplyThread.js | 3 ++- src/components/views/rooms/EditMessageComposer.js | 7 ++++--- src/components/views/rooms/SendMessageComposer.js | 3 ++- src/cryptodevices.js | 3 ++- src/dispatcher/actions.ts | 5 +++++ 11 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx index 249ad8381c..25445b1c74 100644 --- a/src/ContentMessages.tsx +++ b/src/ContentMessages.tsx @@ -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; diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a1b4f49c56..837064e822 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -27,6 +27,7 @@ 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 +199,7 @@ const LeftPanel = createReactClass({ onSearchCleared: function(source) { if (source === "keyboard") { - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } this.setState({searchExpanded: false}); }, diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 1ad38c6f04..cf985187e3 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -51,6 +51,7 @@ import { showToast as showServerLimitToast, hideToast as hideServerLimitToast } from "../../toasts/ServerLimitToast"; +import { Action } from "../../dispatcher/actions"; // 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. @@ -346,7 +347,7 @@ class LoggedInView extends React.PureComponent { // 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); } }; @@ -496,7 +497,7 @@ class LoggedInView extends React.PureComponent { 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 diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 058a7ba50b..97fdfa262e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -347,7 +347,7 @@ export default class MatrixChat extends React.PureComponent { 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 { showNotificationsToast(); } - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); this.setState({ ready: true, }); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index ae628fd06a..b550b4d756 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -27,6 +27,7 @@ import Resend from '../../Resend'; import * as cryptodevices from '../../cryptodevices'; 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; @@ -135,12 +136,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); }, _onShowDevicesClick: function() { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index c87f4cc4dd..81983b9708 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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() {}; @@ -1171,7 +1172,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) { @@ -1377,7 +1378,7 @@ export default createReactClass({ event: null, }); } - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); }, onLeaveClick: function() { @@ -1488,7 +1489,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 diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index e7f7196ac6..e96d9ced11 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -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() { diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index b70ef6255c..78c7de887d 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -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() { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 3098c62433..25ad192ea4 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -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': diff --git a/src/cryptodevices.js b/src/cryptodevices.js index 86b97364f9..0b3dac5434 100644 --- a/src/cryptodevices.js +++ b/src/cryptodevices.js @@ -19,6 +19,7 @@ import * as sdk from './index'; import dis from './dispatcher/dispatcher'; import Modal from './Modal'; import { _t } from './languageHandler'; +import {Action} from "./dispatcher/actions"; /** * Mark all given devices as 'known' @@ -66,7 +67,7 @@ export async function getUnknownDevicesForRoom(matrixClient, room) { } function focusComposer() { - dis.dispatch({action: 'focus_composer'}); + dis.fire(Action.FocusComposer); } /** diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 7e76ea5ccb..71493d6e44 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -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", } From 6d96d66c031d2cb9753653a079ad3ed9694a7abf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 2 Jun 2020 19:26:07 -0600 Subject: [PATCH 2/2] Split the left panel into new and old for new room list designs Though we consider the "room list" to mean the RoomList component specifically, the room list is actually the entire left panel as far as the user is concerned. The new proposed designs for the room list modify the whole left panel, so we had might as well break it into new and old now instead of later. This "new" left panel is a bare-bones implementation and meant to only provide the absolute basic feature set to function for those who enable the experimental room list, for whatever reason. This is not intended to be a final implementation, or even remotely close to what it could be. An example of this is the lack of breadcrumbs. Given they are likely to change, they are excluded from this temporary skeleton completely. This also includes a purple/pink bar between the tag panel and left panel. This is so we can, if needed, differentiate between people who made the mistake of turning on the experimental room list while the overall aesthetic makes it indistinguishable. Once the designs are moderately approved, we can (and definitely should) remove the hideous indicator. --- res/css/structures/_LeftPanel.scss | 8 ++ src/components/structures/LeftPanel.js | 32 ++--- src/components/structures/LeftPanel2.tsx | 154 +++++++++++++++++++++ src/components/structures/LoggedInView.tsx | 21 ++- 4 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 src/components/structures/LeftPanel2.tsx diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 7d57425f6f..6edee5eed2 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -22,6 +22,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 */ diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 837064e822..05cd97df2a 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -26,7 +26,6 @@ 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"; @@ -275,28 +274,15 @@ const LeftPanel = createReactClass({ breadcrumbs = (); } - let roomList = null; - if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { - roomList = ; - } else { - roomList = ; - } + const roomList = ; return (
diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx new file mode 100644 index 0000000000..c9a4948539 --- /dev/null +++ b/src/components/structures/LeftPanel2.tsx @@ -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 { + // 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 = ( +
+ +
+ ); + + const exploreButton = ( +
+ dis.dispatch({action: 'view_room_directory'})}> + {_t("Explore")} + +
+ ); + + const searchBox = ( {/*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 = {/*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 ( +
+ {tagPanel} + +
+ ); + } +} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index cf985187e3..d09706402a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -52,6 +52,7 @@ import { 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. @@ -656,6 +657,20 @@ class LoggedInView extends React.PureComponent { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + let leftPanel = ( + + ); + if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { + // TODO: Supply props like collapsed and disabled to LeftPanel2 + leftPanel = ( + + ); + } + return (
{
- + { leftPanel } { pageElement }