diff --git a/res/css/_components.scss b/res/css/_components.scss index b047519d99..de4c1c677c 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"; @@ -28,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"; @@ -177,10 +179,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..d335df305f --- /dev/null +++ b/res/css/structures/_LeftPanel2.scss @@ -0,0 +1,99 @@ +/* +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. +*/ + +// 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; + +.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; + flex: 1; + } + + .mx_LeftPanel2_headerButtons { + // No special styles: the rest of the layout happens to make it work. + } + + .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/structures/_UserMenuButton.scss b/res/css/structures/_UserMenuButton.scss new file mode 100644 index 0000000000..1f4183f8d6 --- /dev/null +++ b/res/css/structures/_UserMenuButton.scss @@ -0,0 +1,162 @@ +/* +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; + + &: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; + 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; + + .mx_AccessibleButton { + 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/css/views/rooms/_RoomList2.scss b/res/css/views/rooms/_RoomList2.scss new file mode 100644 index 0000000000..89760958f9 --- /dev/null +++ b/res/css/views/rooms/_RoomList2.scss @@ -0,0 +1,25 @@ +/* +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. +*/ + +// 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. + 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 9ab1785566..e6e5af3b48 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -14,8 +14,42 @@ See the License for the specific language governing permissions and 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_RoomList2 .mx_RoomSubList_labelContainer { - z-index: 12; +.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; + + .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/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss new file mode 100644 index 0000000000..3151bb8716 --- /dev/null +++ b/res/css/views/rooms/_RoomTile2.scss @@ -0,0 +1,103 @@ +/* +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. +*/ + +// 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: 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; + } + + .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/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 78fe2a74c5..a50c34cf03 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -172,6 +172,13 @@ $header-divider-color: #91A1C0; // ******************** +// TODO: Update variables for new room list +// TODO: Dark theme +$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; $roomtile-selected-color: #212121; 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 c9a4948539..2fd8612ff5 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -24,6 +24,10 @@ import SearchBox from "./SearchBox"; import RoomList2 from "../views/rooms/RoomList2"; import TopLeftMenuButton from "./TopLeftMenuButton"; import { Action } from "../../dispatcher/actions"; +import { MatrixClientPeg } from "../../MatrixClientPeg"; +import BaseAvatar from '../views/avatars/BaseAvatar'; +import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; +import UserMenuButton from "./UserMenuButton"; /******************************************************************* * CAUTION * @@ -46,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?) @@ -82,24 +85,56 @@ export default class LeftPanel2 extends React.Component { } } + private renderHeader(): React.ReactNode { + // 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 ( +
+
+ + + + {displayName} + + + +
+
+ +
+
+ ); + } + 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/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/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/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..d8f96d4a91 --- /dev/null +++ b/src/components/structures/UserMenuButton.tsx @@ -0,0 +1,270 @@ +/* +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"; +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"; +import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; +import {getCustomTheme} from "../../theme"; +import {getHostingLink} from "../../utils/HostingLink"; +import AccessibleButton from "../views/elements/AccessibleButton"; + +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) { + super(props); + + this.state = { + menuDisplayed: false, + user: MatrixClientPeg.get().getUser(MatrixClientPeg.get().getUserId()), + isDarkTheme: this.isUserOnDarkTheme(), + }; + } + + 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); + 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 + + // 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 = () => { + // 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) => { + ev.preventDefault(); + ev.stopPropagation(); + + const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId}; + defaultDispatcher.dispatch(payload); + this.setState({menuDisplayed: false}); // also close the menu + }; + + private onShowArchived = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + // TODO: Archived room view (deferred) + console.log("TODO: Show archived rooms"); + }; + + private onProvideFeedback = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + this.setState({menuDisplayed: false}); // also close the menu + }; + + private onSignOutClick = (ev: React.MouseEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + this.setState({menuDisplayed: false}); // also close the menu + }; + + 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 = ( + +
+
+
+ + {this.displayName} + + + {MatrixClientPeg.get().getUserId()} + +
+
+ {_t("Switch +
+
+ {hostingLink} +
+
    +
  • + this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}> + + {_t("Notification settings")} + +
  • +
  • + this.onSettingsOpen(e, USER_SECURITY_TAB)}> + + {_t("Security & privacy")} + +
  • +
  • + this.onSettingsOpen(e, null)}> + + {_t("All settings")} + +
  • +
  • + + + {_t("Archived rooms")} + +
  • +
  • + + + {_t("Feedback")} + +
  • +
+
+
+
    +
  • + + + {_t("Sign out")} + +
  • +
+
+
+
+ ); + } + + return ( + + + ... + + {contextMenu} + + ) + } +} 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/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 15aa880109..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} /> ); } @@ -216,7 +217,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 d3bb19729d..650a3ae645 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_RoomSubList_chevron': true, - 'mx_RoomSubList_chevronRight': false, // isCollapsed - 'mx_RoomSubList_chevronDown': true, // !isCollapsed - }); - chevron = (
); - } - return ( {({onFocus, isActive, ref}) => { @@ -127,68 +123,68 @@ export default class RoomSublist2 extends React.Component { const tabIndex = isActive ? 0 : -1; // TODO: Collapsed state - let badge; - if (true) { // !isCollapsed - const badgeClasses = classNames({ - 'mx_RoomSubList_badge': true, - 'mx_RoomSubList_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 ( -
+
- {chevron} {this.props.label} - {badge} - {addRoomButton}
); }} @@ -204,9 +200,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; @@ -244,7 +239,7 @@ export default class RoomSublist2 extends React.Component { visibleTiles.splice(visibleTiles.length - 1, 1, (
{_t("Show %(n)s more", {n: numMissing})} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c95cd108dc..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 * @@ -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? @@ -65,6 +66,7 @@ interface INotificationState { interface IState { hover: boolean; notificationState: INotificationState; + selected: boolean; } export default class RoomTile2 extends React.Component { @@ -87,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() { @@ -100,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); @@ -186,39 +191,33 @@ export default class RoomTile2 extends React.Component { }); }; + private onActiveRoomUpdate = (isActive: boolean) => { + this.setState({selected: isActive}); + }; + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites // 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_RoomTile': 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_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile_noBadges': !hasBadge, - // 'mx_RoomTile_transparent': this.props.transparent, - // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, + 'mx_RoomTile2': true, + 'mx_RoomTile2_selected': this.state.selected, }); - const avatarClasses = classNames({ - 'mx_RoomTile_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_RoomTile_badge': true, - 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + 'mx_RoomTile2_badge': true, + '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 +225,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_RoomTile_name': true, - 'mx_RoomTile_invite': this.roomIsInvite, - 'mx_RoomTile_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 +254,18 @@ export default class RoomTile2 extends React.Component { onClick={this.onTileClick} role="treeitem" > -
-
- -
+
+
-
-
-
- {name} -
+
+
+ {name}
+ {messagePreview} +
+
{badge}
- {tooltip} } diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts index 71493d6e44..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", @@ -58,4 +59,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/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; +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0aa4c3779e..3520446c2b 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,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.", @@ -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", @@ -2041,6 +2042,14 @@ "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", + "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", diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index b41d56be3e..a6abb7d37a 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -16,7 +16,7 @@ limitations under the License. import { TagID } from "./models"; -const TILE_HEIGHT_PX = 34; +const TILE_HEIGHT_PX = 44; interface ISerializedListLayout { numTiles: number; diff --git a/src/theme.js b/src/theme.js index ccb753d601..6ed0657bbc 100644 --- a/src/theme.js +++ b/src/theme.js @@ -62,7 +62,7 @@ function setCustomThemeVars(customTheme) { } } -function getCustomTheme(themeName) { +export function getCustomTheme(themeName) { // set css variables const customThemes = SettingsStore.getValue("custom_themes"); if (!customThemes) {