From 4c1bc50649afcf31f403b7b887ecbdc95ba31772 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Jun 2020 16:34:04 -0600 Subject: [PATCH 01/17] Initial styling for new room list This is a work in progress, but covers the coarse areas. This uses all-new classes to better describe what everything is, and to reduce the number of selectors we keep track of. This is primarily layout for the list and not actually the final structure. For example, some buttons are missing and other areas are not styled correctly - the idea in this commit was to get things roughly in the right place and work on it. --- res/css/_components.scss | 3 + res/css/structures/_LeftPanel.scss | 8 -- res/css/structures/_LeftPanel2.scss | 91 +++++++++++++++++++++ res/css/structures/_MatrixChat.scss | 2 +- res/css/views/rooms/_RoomList2.scss | 23 ++++++ res/css/views/rooms/_RoomSublist2.scss | 2 +- res/css/views/rooms/_RoomTile2.scss | 18 ++++ src/components/structures/LeftPanel2.tsx | 63 +++++++++----- src/components/views/rooms/RoomList2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 21 +++-- src/components/views/rooms/RoomTile2.tsx | 32 ++++---- 11 files changed, 205 insertions(+), 60 deletions(-) create mode 100644 res/css/structures/_LeftPanel2.scss create mode 100644 res/css/views/rooms/_RoomList2.scss create mode 100644 res/css/views/rooms/_RoomTile2.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index b047519d99..62bec5ad62 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -12,6 +12,7 @@ @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; @import "./structures/_LeftPanel.scss"; +@import "./structures/_LeftPanel2.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; @@ -177,10 +178,12 @@ @import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; +@import "./views/rooms/_RoomList2.scss"; @import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomTile.scss"; +@import "./views/rooms/_RoomTile2.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SendMessageComposer.scss"; diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss index 899824bc57..35d9f0e7da 100644 --- a/res/css/structures/_LeftPanel.scss +++ b/res/css/structures/_LeftPanel.scss @@ -23,14 +23,6 @@ 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/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss new file mode 100644 index 0000000000..52ee4f16ac --- /dev/null +++ b/res/css/structures/_LeftPanel2.scss @@ -0,0 +1,91 @@ +/* +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. +*/ + +$tagPanelWidth: 70px; +$roomListMinimizedWidth: 50px; + +.mx_LeftPanel2 { + background-color: $header-panel-bg-color; + min-width: 260px; + max-width: 50%; + + // Create a row-based flexbox for the TagPanel and the room list + display: flex; + + .mx_LeftPanel2_tagPanelContainer { + flex-grow: 0; + flex-shrink: 0; + flex-basis: $tagPanelWidth; + height: 100%; + + // Create another flexbox so the TagPanel fills the container + display: flex; + + // TagPanel handles its own CSS + } + + // Note: The 'room list' in this context is actually everything that isn't the tag + // panel, such as the menu options, breadcrumbs, filtering, etc + .mx_LeftPanel2_roomListContainer { + width: calc(100% - $tagPanelWidth); + + // Create another flexbox (this time a column) for the room list components + display: flex; + flex-direction: column; + + .mx_LeftPanel2_userHeader { + padding: 14px 12px 20px; // 14px top, 12px sides, 20px bottom + + // Create another flexbox column for the rows to stack within + display: flex; + flex-direction: column; + + // There's 2 rows when breadcrumbs are present: the top bit and the breadcrumbs + .mx_LeftPanel2_headerRow { + // Create yet another flexbox, this time within the row, to ensure items stay + // aligned correctly. This is also a row-based flexbox. + display: flex; + align-items: center; + } + + .mx_LeftPanel2_userAvatarContainer { + position: relative; // to make default avatars work + margin-right: 8px; + } + + .mx_LeftPanel2_userName { + font-weight: 600; + font-size: $font-15px; + line-height: $font-20px; + } + + .mx_LeftPanel2_breadcrumbsContainer { + // TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed) + width: 100%; + overflow: hidden; + } + } + + .mx_LeftPanel2_filterContainer { + // TODO: Improve CSS for filtering and its input + } + + .mx_LeftPanel2_actualRoomListContainer { + flex-grow: 1; // fill the available space + overflow-y: auto; + } + } +} diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index 05c703ab6d..08ed9e5559 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -66,7 +66,7 @@ limitations under the License. } /* not the left panel, and not the resize handle, so the roomview/groupview/... */ -.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) { +.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) { background-color: $primary-bg-color; flex: 1 1 0; diff --git a/res/css/views/rooms/_RoomList2.scss b/res/css/views/rooms/_RoomList2.scss new file mode 100644 index 0000000000..add7214468 --- /dev/null +++ b/res/css/views/rooms/_RoomList2.scss @@ -0,0 +1,23 @@ +/* +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. +*/ + +.mx_RoomList2 { + // Create a column-based flexbox for the sublists. That's pretty much all we have to + // worry about in this stylesheet. + display: flex; + flex-direction: column; + flex-wrap: wrap; +} diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index abc3133fc1..55b16843b3 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -16,7 +16,7 @@ limitations under the License. @import "../../../../node_modules/react-resizable/css/styles.css"; -.mx_RoomList2 .mx_RoomSubList_labelContainer { +.mx_RoomList2 .mx_RoomSubList2_labelContainer { z-index: 12; background-color: purple; } diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss new file mode 100644 index 0000000000..6577f1ce25 --- /dev/null +++ b/res/css/views/rooms/_RoomTile2.scss @@ -0,0 +1,18 @@ +/* +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. +*/ + +.mx_RoomTile2 { +} diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index c9a4948539..bf0e72beeb 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -24,6 +24,9 @@ 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"; /******************************************************************* * CAUTION * @@ -82,24 +85,44 @@ export default class LeftPanel2 extends React.Component { } } + private renderHeader(): React.ReactNode { + // TODO: Use real profile info + // TODO: Presence + // TODO: Breadcrumbs toggle + // TODO: Menu button + const avatarSize = 32; + return ( +
+
+ + + + Irene +
+
+ +
+
+ ); + } + public render(): React.ReactNode { const tagPanel = ( -
+
); - const exploreButton = ( -
- dis.dispatch({action: 'view_room_directory'})}> - {_t("Explore")} - -
- ); - const searchBox = ( { // 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) + "mx_LeftPanel2": true, }); return (
{tagPanel} -
); diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 15aa880109..af366e9685 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -216,7 +216,7 @@ export default class RoomList2 extends React.Component { onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyDown={onKeyDownHandler} - className="mx_RoomList mx_RoomList2" + className="mx_RoomList2" role="tree" aria-label={_t("Rooms")} // Firefox sometimes makes this element focusable due to diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index cc56f92769..e7682a9bb4 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -113,9 +113,9 @@ export default class RoomSublist2 extends React.Component { let chevron = null; if (this.hasTiles()) { const chevronClasses = classNames({ - 'mx_RoomSubList_chevron': true, - 'mx_RoomSubList_chevronRight': false, // isCollapsed - 'mx_RoomSubList_chevronDown': true, // !isCollapsed + 'mx_RoomSublist2_chevron': true, + 'mx_RoomSublist2_chevronRight': false, // isCollapsed + 'mx_RoomSublist2_chevronDown': true, // !isCollapsed }); chevron = (
); } @@ -130,8 +130,8 @@ export default class RoomSublist2 extends React.Component { let badge; if (true) { // !isCollapsed const badgeClasses = classNames({ - 'mx_RoomSubList_badge': true, - 'mx_RoomSubList_badgeHighlight': notifHighlight, + 'mx_RoomSublist2_badge': true, + 'mx_RoomSublist2_badgeHighlight': notifHighlight, }); // Wrap the contents in a div and apply styles to the child div so that the browser default outline works if (notifCount > 0) { @@ -168,7 +168,7 @@ export default class RoomSublist2 extends React.Component { ); @@ -176,11 +176,11 @@ export default class RoomSublist2 extends React.Component { // TODO: a11y (see old component) return ( -
+
@@ -204,9 +204,8 @@ export default class RoomSublist2 extends React.Component { const classes = classNames({ // TODO: Proper collapse support - 'mx_RoomSubList': true, - 'mx_RoomSubList_hidden': false, // len && isCollapsed - 'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed + 'mx_RoomSublist2': true, + 'mx_RoomSublist2_collapsed': false, // len && isCollapsed }); let content = null; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c95cd108dc..40c7bcbc12 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -195,28 +195,28 @@ export default class RoomTile2 extends React.Component { const hasBadge = this.state.notificationState.color > NotificationColor.Bold; const isUnread = this.state.notificationState.color > NotificationColor.None; const classes = classNames({ - 'mx_RoomTile': true, + 'mx_RoomTile2': true, // 'mx_RoomTile_selected': this.state.selected, - 'mx_RoomTile_unread': isUnread, - 'mx_RoomTile_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey, - 'mx_RoomTile_highlight': this.state.notificationState.color >= NotificationColor.Red, - 'mx_RoomTile_invited': this.roomIsInvite, + 'mx_RoomTile2_unread': isUnread, + 'mx_RoomTile2_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey, + 'mx_RoomTile2_highlight': this.state.notificationState.color >= NotificationColor.Red, + 'mx_RoomTile2_invited': this.roomIsInvite, // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile_noBadges': !hasBadge, + 'mx_RoomTile2_noBadges': !hasBadge, // 'mx_RoomTile_transparent': this.props.transparent, // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); const avatarClasses = classNames({ - 'mx_RoomTile_avatar': true, + 'mx_RoomTile2_avatar': true, }); let badge; if (hasBadge) { const badgeClasses = classNames({ - 'mx_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + 'mx_RoomTile2_badge': true, + 'mx_RoomTile2_badgeButton': false, // this.state.badgeHover || isMenuDisplayed }); badge =
{this.state.notificationState.symbol}
; } @@ -227,16 +227,16 @@ export default class RoomTile2 extends React.Component { name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon const nameClasses = classNames({ - 'mx_RoomTile_name': true, - 'mx_RoomTile_invite': this.roomIsInvite, - 'mx_RoomTile_badgeShown': hasBadge, + 'mx_RoomTile2_name': true, + 'mx_RoomTile2_invite': this.roomIsInvite, + 'mx_RoomTile2_badgeShown': hasBadge, }); // TODO: Support collapsed state properly let tooltip = null; if (false) { // isCollapsed if (this.state.hover) { - tooltip = + tooltip = } } @@ -255,12 +255,12 @@ export default class RoomTile2 extends React.Component { role="treeitem" >
-
+
-
-
+
+
{name}
From 0c15b2bdb6c332713b8e95cb907c6b50adeaeeda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Jun 2020 21:21:04 -0600 Subject: [PATCH 02/17] Simple structuring of the room list itself This covers the larger parts of the design, but doesn't deal with the nuances of hover states, badge sizing, etc. --- res/css/views/rooms/_RoomSublist2.scss | 26 +++- res/css/views/rooms/_RoomTile2.scss | 76 +++++++++++ res/themes/light/css/_light.scss | 5 + src/components/views/rooms/RoomList2.tsx | 3 +- src/components/views/rooms/RoomSublist2.tsx | 135 ++++++++++---------- src/components/views/rooms/RoomTile2.tsx | 65 ++++------ src/i18n/strings/en_EN.json | 9 +- src/stores/room-list/ListLayout.ts | 2 +- 8 files changed, 205 insertions(+), 116 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 55b16843b3..4d8c3fffff 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -16,7 +16,27 @@ limitations under the License. @import "../../../../node_modules/react-resizable/css/styles.css"; -.mx_RoomList2 .mx_RoomSubList2_labelContainer { - z-index: 12; - background-color: purple; +.mx_RoomSublist2 { + // The sublist is a column of rows, essentially + display: flex; + flex-direction: column; + + margin-left: 8px; + margin-top: 12px; + margin-bottom: 12px; + + .mx_RoomSublist2_headerContainer { + text-transform: uppercase; + opacity: 0.5; + line-height: $font-16px; + font-size: $font-12px; + padding-bottom: 8px; + } + + .mx_RoomSublist2_resizeBox { + // Create another flexbox column for the tiles + display: flex; + flex-direction: column; + overflow: hidden; + } } diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 6577f1ce25..bb27942f81 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -14,5 +14,81 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Note: the room tile expects to be in a flexbox column container .mx_RoomTile2 { + width: 100%; + padding-bottom: 12px; + + // The tile is also a flexbox row itself + display: flex; + flex-wrap: wrap; + + .mx_RoomTile2_avatarContainer { + margin-right: 8px; + } + + .mx_RoomTile2_nameContainer { + // Create a new column layout flexbox for the name parts + display: flex; + flex-direction: column; + justify-content: center; + + .mx_RoomTile2_name, + .mx_RoomTile2_messagePreview { + margin: 0 2px; + } + + // TODO: Ellipsis on the name and preview + + .mx_RoomTile2_name { + font-weight: 600; + font-size: $font-14px; + line-height: $font-19px; + } + + .mx_RoomTile2_messagePreview { + font-size: $font-13px; + line-height: $font-18px; + color: $roomtile2-preview-color; + } + } + + .mx_RoomTile2_badgeContainer { + flex-grow: 1; + + // Create another flexbox row because it's super easy to position the badge at + // the end this way. + display: flex; + align-items: center; + justify-content: flex-end; + + .mx_RoomTile2_badge { + background-color: $roomtile2-badge-color; + + &:not(.mx_RoomTile2_badgeEmpty) { + border-radius: 16px; + font-size: $font-10px; + line-height: $font-14px; + text-align: center; + font-weight: bold; + margin-right: 14px; + color: #fff; // TODO: Variable + + // TODO: Confirm padding on counted badges + padding: 2px 5px; + } + + &.mx_RoomTile2_badgeEmpty { + width: 6px; + height: 6px; + border-radius: 6px; + margin-right: 18px; + } + + &.mx_RoomTile2_badgeHighlight { + // TODO: Use a more specific variable + background-color: $warning-color; + } + } + } } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 78fe2a74c5..683c02528d 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -172,6 +172,11 @@ $header-divider-color: #91A1C0; // ******************** +// TODO: Update variables for new room list +// TODO: Dark theme +$roomtile2-preview-color: #9e9e9e; +$roomtile2-badge-color: #61708b; + $roomtile-name-color: #61708b; $roomtile-badge-fg-color: $accent-fg-color; $roomtile-selected-color: #212121; diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index af366e9685..ce1956f68d 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -96,7 +96,7 @@ const TAG_AESTHETICS: { defaultHidden: false, }, [DefaultTagID.DM]: { - sectionLabel: _td("Direct Messages"), + sectionLabel: _td("People"), isInvite: false, defaultHidden: false, addRoomLabel: _td("Start chat"), @@ -200,6 +200,7 @@ export default class RoomList2 extends React.Component { addRoomLabel={aesthetics.addRoomLabel} isInvite={aesthetics.isInvite} layout={this.state.layouts.get(orderedTagId)} + showMessagePreviews={orderedTagId === DefaultTagID.DM} /> ); } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index e7682a9bb4..84bcb1ea4d 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -20,15 +20,13 @@ import * as React from "react"; import { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from 'classnames'; -import * as RoomNotifs from '../../../RoomNotifs'; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../../views/elements/AccessibleButton"; -import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton"; -import * as FormattingUtils from '../../../utils/FormattingUtils'; import RoomTile2 from "./RoomTile2"; import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ListLayout } from "../../../stores/room-list/ListLayout"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; /******************************************************************* * CAUTION * @@ -43,6 +41,7 @@ interface IProps { rooms?: Room[]; startAsHidden: boolean; label: string; + showMessagePreviews: boolean; onAddRoom?: () => void; addRoomLabel: string; isInvite: boolean; @@ -93,7 +92,13 @@ export default class RoomSublist2 extends React.Component { if (this.props.rooms) { for (const room of this.props.rooms) { - tiles.push(); + tiles.push( + + ); } } @@ -101,25 +106,16 @@ export default class RoomSublist2 extends React.Component { } private renderHeader(): React.ReactElement { - const notifications = !this.props.isInvite - ? RoomNotifs.aggregateNotificationCount(this.props.rooms) - : {count: 0, highlight: true}; - const notifCount = notifications.count; - const notifHighlight = notifications.highlight; + // TODO: Handle badge count + // const notifications = !this.props.isInvite + // ? RoomNotifs.aggregateNotificationCount(this.props.rooms) + // : {count: 0, highlight: true}; + // const notifCount = notifications.count; + // const notifHighlight = notifications.highlight; // TODO: Title on collapsed // TODO: Incoming call box - let chevron = null; - if (this.hasTiles()) { - const chevronClasses = classNames({ - 'mx_RoomSublist2_chevron': true, - 'mx_RoomSublist2_chevronRight': false, // isCollapsed - 'mx_RoomSublist2_chevronDown': true, // !isCollapsed - }); - chevron = (
); - } - return ( {({onFocus, isActive, ref}) => { @@ -127,52 +123,55 @@ export default class RoomSublist2 extends React.Component { const tabIndex = isActive ? 0 : -1; // TODO: Collapsed state - let badge; - if (true) { // !isCollapsed - const badgeClasses = classNames({ - 'mx_RoomSublist2_badge': true, - 'mx_RoomSublist2_badgeHighlight': notifHighlight, - }); - // Wrap the contents in a div and apply styles to the child div so that the browser default outline works - if (notifCount > 0) { - badge = ( - -
- {FormattingUtils.formatCount(notifCount)} -
-
- ); - } else if (this.props.isInvite && this.hasTiles()) { - // Render the `!` badge for invites - badge = ( - -
- {FormattingUtils.formatCount(this.numTiles)} -
-
- ); - } - } + // TODO: Handle badge count + // let badge; + // if (true) { // !isCollapsed + // const showCount = localStorage.getItem("mx_rls_count") || notifHighlight; + // const badgeClasses = classNames({ + // 'mx_RoomSublist2_badge': true, + // 'mx_RoomSublist2_badgeHighlight': notifHighlight, + // 'mx_RoomSublist2_badgeEmpty': !showCount, + // }); + // // Wrap the contents in a div and apply styles to the child div so that the browser default outline works + // if (notifCount > 0) { + // const count =
{FormattingUtils.formatCount(notifCount)}
; + // badge = ( + // + // {showCount ? count : null} + // + // ); + // } else if (this.props.isInvite && this.hasTiles()) { + // // Render the `!` badge for invites + // badge = ( + // + //
+ // {FormattingUtils.formatCount(this.numTiles)} + //
+ //
+ // ); + // } + // } - let addRoomButton = null; - if (!!this.props.onAddRoom) { - addRoomButton = ( - - ); - } + // TODO: Aux button + // let addRoomButton = null; + // if (!!this.props.onAddRoom) { + // addRoomButton = ( + // + // ); + // } // TODO: a11y (see old component) return ( @@ -184,11 +183,8 @@ export default class RoomSublist2 extends React.Component { role="treeitem" aria-level="1" > - {chevron} {this.props.label} - {badge} - {addRoomButton}
); }} @@ -243,13 +239,14 @@ export default class RoomSublist2 extends React.Component { // TODO: CSS TBD // TODO: Show N more instead of infinity more? // TODO: Safely use the same height of a tile, not hardcoded hacks + const moreTileHeightPx = `${layout.tileHeight}px`; visibleTiles.splice(visibleTiles.length - 1, 1, (
- {_t("Show %(n)s more rooms", {n: numMissing})} + {_t("Show %(n)s more", {n: numMissing})}
)); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 40c7bcbc12..8a51327ae2 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -51,6 +51,7 @@ enum NotificationColor { interface IProps { room: Room; + showMessagePreview: boolean; // TODO: Allow falsifying counts (for invites and stuff) // TODO: Transparency? Was this ever used? @@ -192,33 +193,22 @@ export default class RoomTile2 extends React.Component { // TODO: a11y proper // TODO: Render more than bare minimum - const hasBadge = this.state.notificationState.color > NotificationColor.Bold; - const isUnread = this.state.notificationState.color > NotificationColor.None; const classes = classNames({ 'mx_RoomTile2': true, - // 'mx_RoomTile_selected': this.state.selected, - 'mx_RoomTile2_unread': isUnread, - 'mx_RoomTile2_unreadNotify': this.state.notificationState.color >= NotificationColor.Grey, - 'mx_RoomTile2_highlight': this.state.notificationState.color >= NotificationColor.Red, - 'mx_RoomTile2_invited': this.roomIsInvite, - // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile2_noBadges': !hasBadge, - // 'mx_RoomTile_transparent': this.props.transparent, - // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); - const avatarClasses = classNames({ - 'mx_RoomTile2_avatar': true, - }); - - let badge; + const hasBadge = this.state.notificationState.color > NotificationColor.Bold; if (hasBadge) { + const hasNotif = this.state.notificationState.color >= NotificationColor.Red; + const isEmptyBadge = !localStorage.getItem("mx_rl_rt_badgeCount"); const badgeClasses = classNames({ 'mx_RoomTile2_badge': true, - 'mx_RoomTile2_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + 'mx_RoomTile2_badgeHighlight': hasNotif, + 'mx_RoomTile2_badgeEmpty': isEmptyBadge, }); - badge =
{this.state.notificationState.symbol}
; + const symbol = this.state.notificationState.symbol; + badge =
{isEmptyBadge ? null : symbol}
; } // TODO: the original RoomTile uses state for the room name. Do we need to? @@ -226,20 +216,21 @@ export default class RoomTile2 extends React.Component { if (typeof name !== 'string') name = ''; name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon - const nameClasses = classNames({ - 'mx_RoomTile2_name': true, - 'mx_RoomTile2_invite': this.roomIsInvite, - 'mx_RoomTile2_badgeShown': hasBadge, - }); - // TODO: Support collapsed state properly - let tooltip = null; - if (false) { // isCollapsed - if (this.state.hover) { - tooltip = - } + // TODO: Tooltip? + + let messagePreview = null; + if (this.props.showMessagePreview) { + // TODO: Actually get the real message preview from state + messagePreview =
I just ate a pie.
; } + const nameClasses = classNames({ + "mx_RoomTile2_name": true, + "mx_RoomTile2_nameWithPreview": !!messagePreview, + }); + + const avatarSize = 32; return ( @@ -254,20 +245,18 @@ export default class RoomTile2 extends React.Component { onClick={this.onTileClick} role="treeitem" > -
-
- -
+
+
-
-
- {name} -
+
+ {name}
+ {messagePreview} +
+
{badge}
- {tooltip} } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 884192b22a..cf6dc2431a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1090,6 +1090,7 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", + "People": "People", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", @@ -1133,10 +1134,7 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", - "Add room": "Add room", - "Show %(n)s more rooms": "Show %(n)s more rooms", + "Show %(n)s more": "Show %(n)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.", @@ -2017,6 +2015,9 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", + "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index fd57a03ca1..ee51230a61 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -const TILE_HEIGHT_PX = 34; +const TILE_HEIGHT_PX = 44; interface ISerializedListLayout { numTiles: number; From 211ad66fea90e3c488a890fb9c7c71e3d24cf1ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Jun 2020 08:39:38 -0600 Subject: [PATCH 03/17] Fix i18n --- src/i18n/strings/en_EN.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1f70edc5d1..cf6dc2431a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1134,9 +1134,6 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", - "Add room": "Add room", "Show %(n)s more": "Show %(n)s more", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", From 6752c2832ee8a21e95ce105888b2257ec5bfce2a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Jun 2020 08:40:32 -0600 Subject: [PATCH 04/17] Add missing var --- src/components/views/rooms/RoomSublist2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 15a0ff801b..1a58b67377 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -237,6 +237,7 @@ export default class RoomSublist2 extends React.Component { // TODO: CSS TBD // TODO: Make this an actual tile + const moreTileHeightPx = layout.tileHeight; visibleTiles.splice(visibleTiles.length - 1, 1, (
Date: Fri, 5 Jun 2020 08:48:23 -0600 Subject: [PATCH 05/17] Give the show more button some real CSS This is still somewhat placeholder. --- res/css/views/rooms/_RoomSublist2.scss | 9 +++++++++ src/components/views/rooms/RoomSublist2.tsx | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 4d8c3fffff..da1c23b664 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -38,5 +38,14 @@ limitations under the License. display: flex; flex-direction: column; overflow: hidden; + + .mx_RoomSublist2_showMoreButton { + height: 44px; // 1 room tile high + cursor: pointer; + + // We create a flexbox to cheat at alignment + display: flex; + align-items: center; + } } } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 1a58b67377..90231596fe 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -237,11 +237,10 @@ export default class RoomSublist2 extends React.Component { // TODO: CSS TBD // TODO: Make this an actual tile - const moreTileHeightPx = layout.tileHeight; visibleTiles.splice(visibleTiles.length - 1, 1, (
{_t("Show %(n)s more", {n: numMissing})} From 1d8833e9f83a50e9223b04c59b8e216b86c56f04 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Jun 2020 14:08:20 -0600 Subject: [PATCH 06/17] Selected state, cleanup, and profile display --- res/css/structures/_LeftPanel2.scss | 3 +++ res/css/views/rooms/_RoomList2.scss | 2 ++ res/css/views/rooms/_RoomSublist2.scss | 2 ++ res/css/views/rooms/_RoomTile2.scss | 13 +++++++++++-- res/themes/light/css/_light.scss | 1 + src/ActiveRoomObserver.js | 14 +++++++++----- src/components/structures/LeftPanel2.tsx | 17 +++++++++++++---- src/components/views/rooms/RoomTile2.tsx | 11 ++++++++++- 8 files changed, 51 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 52ee4f16ac..822a5ac399 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -14,6 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Rename to mx_LeftPanel during replacement of old component + +// TODO: Put these variables in the right place, or namespace them. $tagPanelWidth: 70px; $roomListMinimizedWidth: 50px; diff --git a/res/css/views/rooms/_RoomList2.scss b/res/css/views/rooms/_RoomList2.scss index add7214468..89760958f9 100644 --- a/res/css/views/rooms/_RoomList2.scss +++ b/res/css/views/rooms/_RoomList2.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Rename to mx_RoomList during replacement of old component + .mx_RoomList2 { // Create a column-based flexbox for the sublists. That's pretty much all we have to // worry about in this stylesheet. diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index da1c23b664..3b3eccfd60 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Rename to mx_RoomSublist during replacement of old component + @import "../../../../node_modules/react-resizable/css/styles.css"; .mx_RoomSublist2 { diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index bb27942f81..3151bb8716 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -14,15 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ +// TODO: Rename to mx_RoomTile during replacement of old component + // Note: the room tile expects to be in a flexbox column container .mx_RoomTile2 { - width: 100%; - padding-bottom: 12px; + width: calc(100% - 11px); // 8px for padding (4px on either side), 3px for margin + margin-bottom: 4px; + margin-right: 3px; + padding: 4px; // The tile is also a flexbox row itself display: flex; flex-wrap: wrap; + &.mx_RoomTile2_selected { + background-color: $roomtile2-selected-bg-color; + border-radius: 32px; + } + .mx_RoomTile2_avatarContainer { margin-right: 8px; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 683c02528d..5aeb125774 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -176,6 +176,7 @@ $header-divider-color: #91A1C0; // TODO: Dark theme $roomtile2-preview-color: #9e9e9e; $roomtile2-badge-color: #61708b; +$roomtile2-selected-bg-color: #FFF; $roomtile-name-color: #61708b; $roomtile-badge-fg-color: $accent-fg-color; diff --git a/src/ActiveRoomObserver.js b/src/ActiveRoomObserver.js index d6fbb460b5..b7695d401d 100644 --- a/src/ActiveRoomObserver.js +++ b/src/ActiveRoomObserver.js @@ -27,7 +27,7 @@ import RoomViewStore from './stores/RoomViewStore'; */ class ActiveRoomObserver { constructor() { - this._listeners = {}; + this._listeners = {}; // key=roomId, value=function(isActive:boolean) this._activeRoomId = RoomViewStore.getRoomId(); // TODO: We could self-destruct when the last listener goes away, or at least @@ -35,6 +35,10 @@ class ActiveRoomObserver { this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this)); } + get activeRoomId(): string { + return this._activeRoomId; + } + addListener(roomId, listener) { if (!this._listeners[roomId]) this._listeners[roomId] = []; this._listeners[roomId].push(listener); @@ -51,23 +55,23 @@ class ActiveRoomObserver { } } - _emit(roomId) { + _emit(roomId, isActive: boolean) { if (!this._listeners[roomId]) return; for (const l of this._listeners[roomId]) { - l.call(); + l.call(null, isActive); } } _onRoomViewStoreUpdate() { // emit for the old room ID - if (this._activeRoomId) this._emit(this._activeRoomId); + if (this._activeRoomId) this._emit(this._activeRoomId, false); // update our cache this._activeRoomId = RoomViewStore.getRoomId(); // and emit for the new one - if (this._activeRoomId) this._emit(this._activeRoomId); + if (this._activeRoomId) this._emit(this._activeRoomId, true); } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index bf0e72beeb..00419465eb 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -86,26 +86,35 @@ export default class LeftPanel2 extends React.Component { } private renderHeader(): React.ReactNode { - // TODO: Use real profile info + // TODO: Update when profile info changes // TODO: Presence // TODO: Breadcrumbs toggle // TODO: Menu button const avatarSize = 32; + // TODO: Don't do this profile lookup in render() + const client = MatrixClientPeg.get(); + let displayName = client.getUserId(); + let avatarUrl: string = null; + const myUser = client.getUser(client.getUserId()); + if (myUser) { + displayName = myUser.rawDisplayName; + avatarUrl = myUser.avatarUrl; + } return (
- Irene + {displayName}
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8a51327ae2..09d7b46ba5 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -23,7 +23,6 @@ import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; -import Tooltip from "../../views/elements/Tooltip"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import * as RoomNotifs from '../../../RoomNotifs'; @@ -32,6 +31,7 @@ import * as Unread from '../../../Unread'; import * as FormattingUtils from "../../../utils/FormattingUtils"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import ActiveRoomObserver from "../../../ActiveRoomObserver"; /******************************************************************* * CAUTION * @@ -66,6 +66,7 @@ interface INotificationState { interface IState { hover: boolean; notificationState: INotificationState; + selected: boolean; } export default class RoomTile2 extends React.Component { @@ -88,12 +89,14 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, notificationState: this.getNotificationState(), + selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId, }; this.props.room.on("Room.receipt", this.handleRoomEventUpdate); this.props.room.on("Room.timeline", this.handleRoomEventUpdate); this.props.room.on("Room.redaction", this.handleRoomEventUpdate); MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); + ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); } public componentWillUnmount() { @@ -101,6 +104,7 @@ export default class RoomTile2 extends React.Component { this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate); this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate); this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate); + ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); } if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); @@ -187,6 +191,10 @@ export default class RoomTile2 extends React.Component { }); }; + private onActiveRoomUpdate = (isActive: boolean) => { + this.setState({selected: isActive}); + }; + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites @@ -195,6 +203,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile2': true, + 'mx_RoomTile2_selected': this.state.selected, }); let badge; From 829bf3c774498bacad2aadbaecc582821457ccab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Jun 2020 14:11:04 -0600 Subject: [PATCH 07/17] Add another TODO comment --- res/css/views/rooms/_RoomSublist2.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 3b3eccfd60..e6e5af3b48 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -16,6 +16,8 @@ limitations under the License. // TODO: Rename to mx_RoomSublist during replacement of old component +// TODO: Just use the 3 selectors we need from this instead of importing it. +// We're going to end up with heavy modifications anyways. @import "../../../../node_modules/react-resizable/css/styles.css"; .mx_RoomSublist2 { From 2806c8c18ba4f8bf864b981b54011c5211c333ab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 5 Jun 2020 14:13:28 -0600 Subject: [PATCH 08/17] Fix temporary class --- src/components/structures/LeftPanel2.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 00419465eb..c66c0a6799 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -131,7 +131,7 @@ export default class LeftPanel2 extends React.Component { ); const searchBox = ( Date: Sun, 7 Jun 2020 22:06:41 -0600 Subject: [PATCH 09/17] Add most of the UI for the new room list's menu button Incomplete implementation: buttons don't work, some text is missing, etc --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 5 + res/css/structures/_UserMenuButton.scss | 157 +++++++++++++ res/img/feather-customised/archive.svg | 1 + .../feather-customised/more-horizontal.svg | 1 + res/img/feather-customised/sun.svg | 1 + res/themes/light/css/_light.scss | 1 + src/components/structures/LeftPanel2.tsx | 5 +- src/components/structures/LoggedInView.tsx | 4 +- .../structures/TopLeftMenuButton.js | 3 +- src/components/structures/UserMenuButton.tsx | 211 ++++++++++++++++++ src/dispatcher/actions.ts | 5 + src/i18n/strings/en_EN.json | 7 + 13 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 res/css/structures/_UserMenuButton.scss create mode 100644 res/img/feather-customised/archive.svg create mode 100644 res/img/feather-customised/more-horizontal.svg create mode 100644 res/img/feather-customised/sun.svg create mode 100644 src/components/structures/UserMenuButton.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 62bec5ad62..de4c1c677c 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -29,6 +29,7 @@ @import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; +@import "./structures/_UserMenuButton.scss"; @import "./structures/_ViewSource.scss"; @import "./structures/auth/_CompleteSecurity.scss"; @import "./structures/auth/_Login.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 822a5ac399..d335df305f 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -73,6 +73,11 @@ $roomListMinimizedWidth: 50px; font-weight: 600; font-size: $font-15px; line-height: $font-20px; + flex: 1; + } + + .mx_LeftPanel2_headerButtons { + // No special styles: the rest of the layout happens to make it work. } .mx_LeftPanel2_breadcrumbsContainer { diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss new file mode 100644 index 0000000000..f7097311c9 --- /dev/null +++ b/res/css/structures/_UserMenuButton.scss @@ -0,0 +1,157 @@ +/* +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. +*/ + +.mx_UserMenuButton { + // No special styles on the button itself +} + +.mx_UserMenuButton_contextMenu { + width: 231px; + + // Put 20px of padding around the whole menu. We do this instead of a + // simple `padding: 20px` rule so the horizontal rules added by the + // optionLists is rendered correctly (full width). + >* { + padding-left: 20px; + padding-right: 20px; + + &:first-child { + padding-top: 20px; + } + + &:last-child { + padding-bottom: 20px; + } + } + + .mx_UserMenuButton_contextMenu_header { + // Create a flexbox to organize the header a bit easier + display: flex; + align-items: center; + + .mx_UserMenuButton_contextMenu_name { + // Create another flexbox of columns to handle large user IDs + display: flex; + flex-direction: column; + + // fit the container + flex: 1; + width: calc(100% - 40px); // 40px = 32px theme button + 8px margin to theme button + + * { + // Automatically grow all subelements to fit the container + flex: 1; + width: 100%; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .mx_UserMenuButton_contextMenu_displayName { + font-weight: bold; + font-size: $font-15px; + line-height: $font-20px; + } + + .mx_UserMenuButton_contextMenu_userId { + font-size: $font-15px; + line-height: $font-24px; + } + } + + .mx_UserMenuButton_contextMenu_themeButton { + min-width: 32px; + max-width: 32px; + width: 32px; + height: 32px; + margin-left: 8px; + border-radius: 32px; + background-color: $theme-button-bg-color; + cursor: pointer; + + // to make alignment easier, create flexbox for the image + display: flex; + align-items: center; + justify-content: center; + } + } + + .mx_UserMenuButton_contextMenu_optionList { + margin-top: 20px; + + // This is a bit of a hack when we could just use a simple border-top property, + // however we have a (kinda) good reason for doing it this way: we need opacity. + // To get the right color, we need an opacity modifier which means we have to work + // around the problem. PostCSS doesn't support the opacity() function, and if we + // use something like postcss-functions we quickly run into an issue where the + // function we would define gets passed a CSS variable for custom themes, which + // can't be converted easily even when considering https://stackoverflow.com/a/41265350/7037379 + // + // Therefore, we just hack in a line and border the thing ourselves + &::before { + border-top: 1px solid $primary-fg-color; + opacity: 0.1; + content: ''; + + // Counteract the padding problems (width: 100% ignores the 40px padding, + // unless we position it absolutely then it does the right thing). + width: 100%; + position: absolute; + left: 0; + } + + ul { + list-style: none; + margin: 0; + padding: 0; + + li { + margin: 0; + padding: 20px 0 0; + + a { + text-decoration: none; + color: $primary-fg-color; + font-size: $font-15px; + line-height: $font-24px; + + // Create a flexbox to more easily define the list items + display: flex; + align-items: center; + + img { // icons + width: 16px; + min-width: 16px; + max-width: 16px; + } + + span { // labels + padding-left: 14px; + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + } + } + } +} diff --git a/res/img/feather-customised/archive.svg b/res/img/feather-customised/archive.svg new file mode 100644 index 0000000000..428882c87b --- /dev/null +++ b/res/img/feather-customised/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/more-horizontal.svg b/res/img/feather-customised/more-horizontal.svg new file mode 100644 index 0000000000..dc6a85564e --- /dev/null +++ b/res/img/feather-customised/more-horizontal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/feather-customised/sun.svg b/res/img/feather-customised/sun.svg new file mode 100644 index 0000000000..7f51b94d1c --- /dev/null +++ b/res/img/feather-customised/sun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 5aeb125774..a50c34cf03 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -177,6 +177,7 @@ $header-divider-color: #91A1C0; $roomtile2-preview-color: #9e9e9e; $roomtile2-badge-color: #61708b; $roomtile2-selected-bg-color: #FFF; +$theme-button-bg-color: #e3e8f0; $roomtile-name-color: #61708b; $roomtile-badge-fg-color: $accent-fg-color; diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index c66c0a6799..2fd8612ff5 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -27,6 +27,7 @@ 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"; /******************************************************************* * CAUTION * @@ -49,7 +50,6 @@ 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?) @@ -115,6 +115,9 @@ export default class LeftPanel2 extends React.Component { /> {displayName} + + +
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 0504e3a76a..19edf505e0 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -452,9 +452,7 @@ class LoggedInView extends React.PureComponent { // composer, so CTRL+` it is if (ctrlCmdOnly) { - dis.dispatch({ - action: 'toggle_top_left_menu', - }); + dis.fire(Action.ToggleUserMenu); handled = true; } break; diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index 234dc661f9..71e7e61406 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -24,6 +24,7 @@ import * as Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; import dis from "../../dispatcher/dispatcher"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; +import {Action} from "../../dispatcher/actions"; const AVATAR_SIZE = 28; @@ -75,7 +76,7 @@ export default class TopLeftMenuButton extends React.Component { onAction = (payload) => { // For accessibility - if (payload.action === "toggle_top_left_menu") { + if (payload.action === Action.ToggleUserMenu) { if (this._buttonRef) this._buttonRef.click(); } }; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx new file mode 100644 index 0000000000..0968e0bdb6 --- /dev/null +++ b/src/components/structures/UserMenuButton.tsx @@ -0,0 +1,211 @@ +/* +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 {User} from "matrix-js-sdk/src/models/user"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { Action } from "../../dispatcher/actions"; +import { createRef } from "react"; +import { _t } from "../../languageHandler"; +import {ContextMenu, ContextMenuButton} from "./ContextMenu"; + +interface IProps { +} + +interface IState { + user: User; + menuDisplayed: boolean; +} + +export default class UserMenuButton extends React.Component { + private dispatcherRef: string; + private buttonRef: React.RefObject = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + menuDisplayed: false, + user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), + }; + } + + private get displayName(): string { + if (MatrixClientPeg.get().isGuest()) { + return _t("Guest"); + } else if (this.state.user) { + return this.state.user.displayName; + } else { + return MatrixClientPeg.get().getUserId(); + } + } + + public componentDidMount() { + this.dispatcherRef = defaultDispatcher.register(this.onAction); + } + + private onAction = (ev: ActionPayload) => { + if (ev.action !== Action.ToggleUserMenu) return; // not interested + + // For accessibility + if (this.buttonRef.current) this.buttonRef.current.click(); + }; + + private onOpenMenuClick = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + this.setState({menuDisplayed: true}); + }; + + private onCloseMenu = () => { + this.setState({menuDisplayed: false}); + }; + + private onSwitchThemeClick = () => { + console.log("TODO: Switch theme"); + }; + + private onSettingsOpen = (ev: React.MouseEvent, tabRef: string) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Open settings", tabRef); + }; + + private onShowArchived = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Show archived rooms"); + }; + + private onProvideFeedback = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Show feedback"); + }; + + private onSignOutClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + console.log("TODO: Sign out"); + }; + + public render() { + let contextMenu; + if (this.state.menuDisplayed) { + const elementRect = this.buttonRef.current.getBoundingClientRect(); + contextMenu = ( + + + + ); + } + + return ( + + + ... + + {contextMenu} + + ) + } +} diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 71493d6e44..60ef61a6e9 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -58,4 +58,9 @@ export enum Action { * Focuses the user's cursor to the composer. No additional payload information required. */ FocusComposer = "focus_composer", + + /** + * Opens the user menu (previously known as the top left menu). No additional payload information required. + */ + ToggleUserMenu = "toggle_user_menu", } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cf6dc2431a..8575b3a258 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2042,6 +2042,13 @@ "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", + "Switch to dark mode": "Switch to dark mode", + "Switch theme": "Switch theme", + "Security & privacy": "Security & privacy", + "All settings": "All settings", + "Archived rooms": "Archived rooms", + "Feedback": "Feedback", + "Account settings": "Account settings", "Could not load user profile": "Could not load user profile", "Verify this login": "Verify this login", "Session verified": "Session verified", From 8b6b117fbfe95f3f896972d30ca9fa08a410807a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 7 Jun 2020 22:15:54 -0600 Subject: [PATCH 10/17] Appease the linter --- res/css/structures/_UserMenuButton.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index f7097311c9..aca5f4253a 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -24,7 +24,7 @@ limitations under the License. // Put 20px of padding around the whole menu. We do this instead of a // simple `padding: 20px` rule so the horizontal rules added by the // optionLists is rendered correctly (full width). - >* { + > * { padding-left: 20px; padding-right: 20px; From f05a1e532b5652d0fe2236b6a2794b889c43149d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 7 Jun 2020 22:17:02 -0600 Subject: [PATCH 11/17] Point buttons at the right functions --- src/components/structures/UserMenuButton.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 0968e0bdb6..abf4689aa1 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -171,7 +171,7 @@ export default class UserMenuButton extends React.Component {
  • - + {_t("Feedback")} @@ -181,7 +181,7 @@ export default class UserMenuButton extends React.Component {
    • - this.onSettingsOpen(e, 'notifications')}> + {_t("Sign out")} From de18af35ff58b8ffd4f323a47130ff4f05b331ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 08:20:15 -0600 Subject: [PATCH 12/17] Support minimum to open user settings to a particular tab Tabs now have IDs, and we use those IDs to open things. This doesn't do any conversion to typescript, and doesn't add the same feature to the room settings out of concern for the size of diff. --- src/components/structures/MatrixChat.tsx | 8 +++-- src/components/structures/TabbedView.tsx | 30 ++++++++----------- src/components/structures/UserMenuButton.tsx | 14 +++++---- .../views/dialogs/RoomSettingsDialog.js | 13 ++++++++ .../views/dialogs/UserSettingsDialog.js | 24 ++++++++++++++- src/dispatcher/actions.ts | 1 + src/dispatcher/payloads/OpenToTabPayload.ts | 27 +++++++++++++++++ 7 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 src/dispatcher/payloads/OpenToTabPayload.ts diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 69f91047b7..e08381d8fa 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -72,6 +72,7 @@ import { hideToast as hideAnalyticsToast } from "../../toasts/AnalyticsToast"; import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; /** constants for MatrixChat.state.view */ export enum Views { @@ -604,9 +605,12 @@ export default class MatrixChat extends React.PureComponent { this.viewIndexedRoom(payload.roomIndex); break; case Action.ViewUserSettings: { + const tabPayload = payload as OpenToTabPayload; const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); - Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, - /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + Modal.createTrackedDialog('User settings', '', UserSettingsDialog, + {initialTabId: tabPayload.initialTabId}, + /*className=*/null, /*isPriority=*/false, /*isStatic=*/true + ); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index c0e0e58db8..704dbf8832 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -27,25 +27,20 @@ import { ReactNode } from "react"; * Represents a tab for the TabbedView. */ export class Tab { - public label: string; - public icon: string; - public body: React.ReactNode; - /** * Creates a new tab. - * @param {string} tabLabel The untranslated tab label. - * @param {string} tabIconClass The class for the tab icon. This should be a simple mask. - * @param {React.ReactNode} tabJsx The JSX for the tab container. + * @param {string} id The tab's ID. + * @param {string} label The untranslated tab label. + * @param {string} icon The class for the tab icon. This should be a simple mask. + * @param {React.ReactNode} body The JSX for the tab container. */ - constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) { - this.label = tabLabel; - this.icon = tabIconClass; - this.body = tabJsx; + constructor(public id: string, public label: string, public icon: string, public body: React.ReactNode) { } } interface IProps { tabs: Tab[]; + initialTabId?: string; } interface IState { @@ -53,16 +48,17 @@ interface IState { } export default class TabbedView extends React.Component { - static propTypes = { - // The tabs to show - tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired, - }; - constructor(props: IProps) { super(props); + let activeTabIndex = 0; + if (props.initialTabId) { + const tabIndex = props.tabs.findIndex(t => t.id === props.initialTabId); + if (tabIndex >= 0) activeTabIndex = tabIndex; + } + this.state = { - activeTabIndex: 0, + activeTabIndex, }; } diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index abf4689aa1..827a279d98 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -23,6 +23,8 @@ import { Action } from "../../dispatcher/actions"; import { createRef } from "react"; import { _t } from "../../languageHandler"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; +import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; +import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; interface IProps { } @@ -80,11 +82,13 @@ export default class UserMenuButton extends React.Component { console.log("TODO: Switch theme"); }; - private onSettingsOpen = (ev: React.MouseEvent, tabRef: string) => { + private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Open settings", tabRef); + const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; + defaultDispatcher.dispatch(payload); + this.setState({menuDisplayed: false}); // also close the menu }; private onShowArchived = (ev: React.MouseEvent) => { @@ -147,19 +151,19 @@ export default class UserMenuButton extends React.Component {
      • - this.onSettingsOpen(e, 'notifications')}> + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> {_t("Notification settings")}
      • - this.onSettingsOpen(e, 'security')}> + this.onSettingsOpen(e, USER_SECURITY_TAB)}> {_t("Security & privacy")}
      • - this.onSettingsOpen(e, 'all')}> + this.onSettingsOpen(e, null)}> {_t("All settings")} diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c2b98cd9f3..7ad1001f75 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -30,6 +30,13 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import SettingsStore from "../../../settings/SettingsStore"; +export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; +export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB"; +export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB"; +export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB"; +export const ROOM_BRIDGES_TAB = "ROOM_BRIDGES_TAB"; +export const ROOM_ADVANCED_TAB = "ROOM_ADVANCED_TAB"; + export default class RoomSettingsDialog extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -56,21 +63,25 @@ export default class RoomSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + ROOM_GENERAL_TAB, _td("General"), "mx_RoomSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + ROOM_SECURITY_TAB, _td("Security & Privacy"), "mx_RoomSettingsDialog_securityIcon", , )); tabs.push(new Tab( + ROOM_ROLES_TAB, _td("Roles & Permissions"), "mx_RoomSettingsDialog_rolesIcon", , )); tabs.push(new Tab( + ROOM_NOTIFICATIONS_TAB, _td("Notifications"), "mx_RoomSettingsDialog_notificationsIcon", , @@ -78,6 +89,7 @@ export default class RoomSettingsDialog extends React.Component { if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { tabs.push(new Tab( + ROOM_BRIDGES_TAB, _td("Bridges"), "mx_RoomSettingsDialog_bridgesIcon", , @@ -85,6 +97,7 @@ export default class RoomSettingsDialog extends React.Component { } tabs.push(new Tab( + ROOM_ADVANCED_TAB, _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", , diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 4592d921a9..1f1a8d1523 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -33,9 +33,21 @@ import * as sdk from "../../../index"; import SdkConfig from "../../../SdkConfig"; import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab"; +export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; +export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; +export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; +export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; +export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; +export const USER_VOICE_TAB = "USER_VOICE_TAB"; +export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; +export const USER_LABS_TAB = "USER_LABS_TAB"; +export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; +export const USER_HELP_TAB = "USER_HELP_TAB"; + export default class UserSettingsDialog extends React.Component { static propTypes = { onFinished: PropTypes.func.isRequired, + initialTabId: PropTypes.string, }; constructor() { @@ -63,42 +75,50 @@ export default class UserSettingsDialog extends React.Component { const tabs = []; tabs.push(new Tab( + USER_GENERAL_TAB, _td("General"), "mx_UserSettingsDialog_settingsIcon", , )); tabs.push(new Tab( + USER_APPEARANCE_TAB, _td("Appearance"), "mx_UserSettingsDialog_appearanceIcon", , )); tabs.push(new Tab( + USER_FLAIR_TAB, _td("Flair"), "mx_UserSettingsDialog_flairIcon", , )); tabs.push(new Tab( + USER_NOTIFICATIONS_TAB, _td("Notifications"), "mx_UserSettingsDialog_bellIcon", , )); tabs.push(new Tab( + USER_PREFERENCES_TAB, _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", , )); tabs.push(new Tab( + USER_VOICE_TAB, _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", , )); tabs.push(new Tab( + USER_SECURITY_TAB, _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", , )); if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( + USER_LABS_TAB, _td("Labs"), "mx_UserSettingsDialog_labsIcon", , @@ -106,12 +126,14 @@ export default class UserSettingsDialog extends React.Component { } if (this.state.mjolnirEnabled) { tabs.push(new Tab( + USER_MJOLNIR_TAB, _td("Ignored users"), "mx_UserSettingsDialog_mjolnirIcon", , )); } tabs.push(new Tab( + USER_HELP_TAB, _td("Help & About"), "mx_UserSettingsDialog_helpIcon", , @@ -127,7 +149,7 @@ export default class UserSettingsDialog extends React.Component {
        - +
        ); diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 60ef61a6e9..c9b5d9e3ad 100644 --- a/src/dispatcher/actions.ts +++ b/src/dispatcher/actions.ts @@ -36,6 +36,7 @@ export enum Action { /** * Open the user settings. No additional payload information required. + * Optionally can include an OpenToTabPayload. */ ViewUserSettings = "view_user_settings", diff --git a/src/dispatcher/payloads/OpenToTabPayload.ts b/src/dispatcher/payloads/OpenToTabPayload.ts new file mode 100644 index 0000000000..2877ee053e --- /dev/null +++ b/src/dispatcher/payloads/OpenToTabPayload.ts @@ -0,0 +1,27 @@ +/* +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 { ActionPayload } from "../payloads"; +import { Action } from "../actions"; + +export interface OpenToTabPayload extends ActionPayload { + action: Action.ViewUserSettings | string, // TODO: Add room settings action + + /** + * The tab ID to open in the settings view to start, if possible. + */ + initialTabId?: string; +} From acf78ae475799d955eac3b03017a0278c29ce600 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:04:43 -0600 Subject: [PATCH 13/17] Wire up the remaining dialogs --- src/components/structures/UserMenuButton.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 827a279d98..35b5cc4d4e 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -25,6 +25,9 @@ import { _t } from "../../languageHandler"; import {ContextMenu, ContextMenuButton} from "./ContextMenu"; import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; +import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; +import Modal from "../../Modal"; +import LogoutDialog from "../views/dialogs/LogoutDialog"; interface IProps { } @@ -95,6 +98,7 @@ export default class UserMenuButton extends React.Component { ev.preventDefault(); ev.stopPropagation(); + // TODO: Archived room view (deferred) console.log("TODO: Show archived rooms"); }; @@ -102,14 +106,16 @@ export default class UserMenuButton extends React.Component { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Show feedback"); + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + this.setState({menuDisplayed: false}); // also close the menu }; private onSignOutClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); - console.log("TODO: Sign out"); + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + this.setState({menuDisplayed: false}); // also close the menu }; public render() { From 94ce23aa4b9e835be45df49d4889f9639592b3b4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:24:08 -0600 Subject: [PATCH 14/17] Wire up theme changer --- src/components/structures/UserMenuButton.tsx | 31 ++++++++++++++++++-- src/theme.js | 2 +- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 35b5cc4d4e..40b53a35fb 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -28,6 +28,8 @@ import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; +import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import {getCustomTheme} from "../../theme"; interface IProps { } @@ -35,10 +37,12 @@ interface IProps { interface IState { user: User; menuDisplayed: boolean; + isDarkTheme: boolean; } export default class UserMenuButton extends React.Component { private dispatcherRef: string; + private themeWatcherRef: string; private buttonRef: React.RefObject = createRef(); constructor(props: IProps) { @@ -47,6 +51,7 @@ export default class UserMenuButton extends React.Component { this.state = { menuDisplayed: false, user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), + isDarkTheme: this.isUserOnDarkTheme(), }; } @@ -62,8 +67,26 @@ export default class UserMenuButton extends React.Component { public componentDidMount() { this.dispatcherRef = defaultDispatcher.register(this.onAction); + this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); } + public componentWillUnmount() { + if (this.themeWatcherRef) SettingsStore.unwatchSetting(this.themeWatcherRef); + if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); + } + + private isUserOnDarkTheme(): boolean { + const theme = SettingsStore.getValue("theme"); + if (theme.startsWith("custom-")) { + return getCustomTheme(theme.substring(0, 7)).is_dark; + } + return theme === "dark"; + } + + private onThemeChanged = () => { + this.setState({isDarkTheme: this.isUserOnDarkTheme()}); + }; + private onAction = (ev: ActionPayload) => { if (ev.action !== Action.ToggleUserMenu) return; // not interested @@ -82,7 +105,11 @@ export default class UserMenuButton extends React.Component { }; private onSwitchThemeClick = () => { - console.log("TODO: Switch theme"); + // Disable system theme matching if the user hits this button + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, false); + + const newTheme = this.state.isDarkTheme ? "light" : "dark"; + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); }; private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { @@ -142,7 +169,7 @@ export default class UserMenuButton extends React.Component {
        Date: Mon, 8 Jun 2020 09:32:16 -0600 Subject: [PATCH 15/17] Add hosting link --- res/css/structures/_UserMenuButton.scss | 5 ++++ src/components/structures/UserMenuButton.tsx | 27 +++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index aca5f4253a..0d35c1cbb2 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -42,6 +42,11 @@ limitations under the License. display: flex; align-items: center; + &:nth-child(n + 1) { + // The first header will have appropriate padding, subsequent ones need a margin. + margin-top: 10px; + } + .mx_UserMenuButton_contextMenu_name { // Create another flexbox of columns to handle large user IDs display: flex; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index 40b53a35fb..dc4415ea54 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -30,6 +30,7 @@ import Modal from "../../Modal"; import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; +import {getHostingLink} from "../../utils/HostingLink"; interface IProps { } @@ -148,6 +149,28 @@ export default class UserMenuButton extends React.Component { public render() { let contextMenu; if (this.state.menuDisplayed) { + let hostingLink; + const signupLink = getHostingLink("user-context-menu"); + if (signupLink) { + hostingLink = ( +
        + {_t( + "Upgrade to your own domain", {}, + { + a: sub => ( + {sub} + ), + }, + )} +
        + ); + } + const elementRect = this.buttonRef.current.getBoundingClientRect(); contextMenu = ( { />
      -
      - TODO: Upgrade prompt -
      + {hostingLink}
      • From fd8c056200723a3a9e1aa82e5436e93aa1e16b09 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 09:40:03 -0600 Subject: [PATCH 16/17] Fix i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8575b3a258..3520446c2b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2042,6 +2042,7 @@ "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", + "Switch to light mode": "Switch to light mode", "Switch to dark mode": "Switch to dark mode", "Switch theme": "Switch theme", "Security & privacy": "Security & privacy", From de4c2fe3d9b2fb09e07daf0ff9190d7f838a98a6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 11:06:21 -0600 Subject: [PATCH 17/17] Use real buttons in user menu --- res/css/structures/_UserMenuButton.scss | 2 +- src/components/structures/UserMenuButton.tsx | 25 ++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/res/css/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss index 0d35c1cbb2..1f4183f8d6 100644 --- a/res/css/structures/_UserMenuButton.scss +++ b/res/css/structures/_UserMenuButton.scss @@ -129,7 +129,7 @@ limitations under the License. margin: 0; padding: 20px 0 0; - a { + .mx_AccessibleButton { text-decoration: none; color: $primary-fg-color; font-size: $font-15px; diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index dc4415ea54..d8f96d4a91 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -31,6 +31,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; +import AccessibleButton from "../views/elements/AccessibleButton"; interface IProps { } @@ -205,44 +206,44 @@ export default class UserMenuButton extends React.Component {