diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 35d9f0e7da..899824bc57 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -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 */ 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..05cd97df2a 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -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 = (); } - 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 7fcaadf7a5..0504e3a76a 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -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 { // 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 { 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 { 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 }
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 7aaedcfb09..69f91047b7 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 dd4b9759d6..65d062cfaa 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -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) { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 49d7e3c238..0ff997ee09 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() {}; @@ -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 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/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", }