From c0ac44e4711e55f32501365045e14576665a0aec Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 2 Jun 2020 17:10:22 +0100 Subject: [PATCH 01/76] Change internal font size from from 15 to 20. --- res/css/_common.scss | 2 +- res/css/_font-sizes.scss | 112 +++++++++++++-------------- src/settings/Settings.js | 2 +- src/settings/watchers/FontWatcher.ts | 3 +- 4 files changed, 60 insertions(+), 59 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index ebeeb381e6..c562c3e95f 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -19,7 +19,7 @@ limitations under the License. @import "./_font-sizes.scss"; :root { - font-size: 15px; + font-size: 20px; } html { diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index 2d7ab67e40..c4a223d66c 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,59 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-1px: 0.067rem; -$font-1-5px: 0.100rem; -$font-2px: 0.133rem; -$font-3px: 0.200rem; -$font-4px: 0.267rem; -$font-5px: 0.333rem; -$font-6px: 0.400rem; -$font-7px: 0.467rem; -$font-8px: 0.533rem; -$font-9px: 0.600rem; -$font-10px: 0.667rem; -$font-10-4px: 0.693rem; -$font-11px: 0.733rem; -$font-12px: 0.800rem; -$font-13px: 0.867rem; -$font-14px: 0.933rem; -$font-15px: 1.000rem; -$font-16px: 1.067rem; -$font-17px: 1.133rem; -$font-18px: 1.200rem; -$font-19px: 1.267rem; -$font-20px: 1.3333333rem; -$font-21px: 1.400rem; -$font-22px: 1.467rem; -$font-23px: 1.533rem; -$font-24px: 1.600rem; -$font-25px: 1.667rem; -$font-26px: 1.733rem; -$font-27px: 1.800rem; -$font-28px: 1.867rem; -$font-29px: 1.933rem; -$font-30px: 2.000rem; -$font-31px: 2.067rem; -$font-32px: 2.133rem; -$font-33px: 2.200rem; -$font-34px: 2.267rem; -$font-35px: 2.333rem; -$font-36px: 2.400rem; -$font-37px: 2.467rem; -$font-38px: 2.533rem; -$font-39px: 2.600rem; -$font-40px: 2.667rem; -$font-41px: 2.733rem; -$font-42px: 2.800rem; -$font-43px: 2.867rem; -$font-44px: 2.933rem; -$font-45px: 3.000rem; -$font-46px: 3.067rem; -$font-47px: 3.133rem; -$font-48px: 3.200rem; -$font-49px: 3.267rem; -$font-50px: 3.333rem; -$font-51px: 3.400rem; -$font-52px: 3.467rem; -$font-88px: 5.887rem; -$font-400px: 26.667rem; +$font-1px: 0.05rem; +$font-1-5px: 0.075rem; +$font-2px: 0.1rem; +$font-3px: 0.15rem; +$font-4px: 0.2rem; +$font-5px: 0.25rem; +$font-6px: 0.3rem; +$font-7px: 0.35rem; +$font-8px: 0.4rem; +$font-9px: 0.45rem; +$font-10px: 0.5rem; +$font-10-4px: 0.52rem; +$font-11px: 0.55rem; +$font-12px: 0.6rem; +$font-13px: 0.65rem; +$font-14px: 0.7rem; +$font-15px: 0.75rem; +$font-16px: 0.8rem; +$font-17px: 0.85rem; +$font-18px: 0.9rem; +$font-19px: 0.95rem; +$font-20px: 1.0rem; +$font-21px: 1.05rem; +$font-22px: 1.1rem; +$font-23px: 1.15rem; +$font-24px: 1.2rem; +$font-25px: 1.25rem; +$font-26px: 1.3rem; +$font-27px: 1.35rem; +$font-28px: 1.4rem; +$font-29px: 1.45rem; +$font-30px: 1.5rem; +$font-31px: 1.55rem; +$font-32px: 1.6rem; +$font-33px: 1.65rem; +$font-34px: 1.7rem; +$font-35px: 1.75rem; +$font-36px: 1.8rem; +$font-37px: 1.85rem; +$font-38px: 1.9rem; +$font-39px: 1.95rem; +$font-40px: 2.0rem; +$font-41px: 2.05rem; +$font-42px: 2.1rem; +$font-43px: 2.15rem; +$font-44px: 2.2rem; +$font-45px: 2.25rem; +$font-46px: 2.3rem; +$font-47px: 2.35rem; +$font-48px: 2.4rem; +$font-49px: 2.45rem; +$font-50px: 2.5rem; +$font-51px: 2.55rem; +$font-52px: 2.6rem; +$font-88px: 4.4rem; +$font-400px: 20rem; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index e6aa112c5f..7cb4421a4f 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -180,7 +180,7 @@ export const SETTINGS = { "fontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 15, + default: 20, controller: new FontSizeController(), }, "useCustomFontSize": { diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index dce9e77e9e..08a809f869 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -45,7 +45,8 @@ export class FontWatcher implements IWatcher { }; private setRootFontSize = (size) => { - const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); + // Externally we tell the user the font is size 15. Internally we use 20. + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE) + 5; if (fontSize !== size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); From f6504d67ba0a09379c1291a2000dac1dae9697aa Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 4 Jun 2020 16:23:28 +0100 Subject: [PATCH 02/76] Use 10px instead --- res/css/_common.scss | 2 +- res/css/_font-sizes.scss | 112 +++++++++++++-------------- src/settings/watchers/FontWatcher.ts | 4 +- 3 files changed, 59 insertions(+), 59 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index c562c3e95f..77a8ff9f4a 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -19,7 +19,7 @@ limitations under the License. @import "./_font-sizes.scss"; :root { - font-size: 20px; + font-size: 10px; } html { diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index c4a223d66c..5b876ab11d 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,59 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ -$font-1px: 0.05rem; -$font-1-5px: 0.075rem; -$font-2px: 0.1rem; -$font-3px: 0.15rem; -$font-4px: 0.2rem; -$font-5px: 0.25rem; -$font-6px: 0.3rem; -$font-7px: 0.35rem; -$font-8px: 0.4rem; -$font-9px: 0.45rem; -$font-10px: 0.5rem; -$font-10-4px: 0.52rem; -$font-11px: 0.55rem; -$font-12px: 0.6rem; -$font-13px: 0.65rem; -$font-14px: 0.7rem; -$font-15px: 0.75rem; -$font-16px: 0.8rem; -$font-17px: 0.85rem; -$font-18px: 0.9rem; -$font-19px: 0.95rem; -$font-20px: 1.0rem; -$font-21px: 1.05rem; -$font-22px: 1.1rem; -$font-23px: 1.15rem; -$font-24px: 1.2rem; -$font-25px: 1.25rem; -$font-26px: 1.3rem; -$font-27px: 1.35rem; -$font-28px: 1.4rem; -$font-29px: 1.45rem; -$font-30px: 1.5rem; -$font-31px: 1.55rem; -$font-32px: 1.6rem; -$font-33px: 1.65rem; -$font-34px: 1.7rem; -$font-35px: 1.75rem; -$font-36px: 1.8rem; -$font-37px: 1.85rem; -$font-38px: 1.9rem; -$font-39px: 1.95rem; -$font-40px: 2.0rem; -$font-41px: 2.05rem; -$font-42px: 2.1rem; -$font-43px: 2.15rem; -$font-44px: 2.2rem; -$font-45px: 2.25rem; -$font-46px: 2.3rem; -$font-47px: 2.35rem; -$font-48px: 2.4rem; -$font-49px: 2.45rem; -$font-50px: 2.5rem; -$font-51px: 2.55rem; -$font-52px: 2.6rem; -$font-88px: 4.4rem; -$font-400px: 20rem; +$font-1px: 0.1rem; +$font-1-5px: 0.15rem; +$font-2px: 0.2rem; +$font-3px: 0.3rem; +$font-4px: 0.4rem; +$font-5px: 0.5rem; +$font-6px: 0.6rem; +$font-7px: 0.7rem; +$font-8px: 0.8rem; +$font-9px: 0.9rem; +$font-10px: 1.0rem; +$font-10-4px: 1.04rem; +$font-11px: 1.1rem; +$font-12px: 1.2rem; +$font-13px: 1.3rem; +$font-14px: 1.4rem; +$font-15px: 1.5rem; +$font-16px: 1.6rem; +$font-17px: 1.7rem; +$font-18px: 1.8rem; +$font-19px: 1.9rem; +$font-20px: 2.0rem; +$font-21px: 2.1rem; +$font-22px: 2.2rem; +$font-23px: 2.3rem; +$font-24px: 2.4rem; +$font-25px: 2.5rem; +$font-26px: 2.6rem; +$font-27px: 2.7rem; +$font-28px: 2.8rem; +$font-29px: 2.9rem; +$font-30px: 3.0rem; +$font-31px: 3.1rem; +$font-32px: 3.2rem; +$font-33px: 3.3rem; +$font-34px: 3.4rem; +$font-35px: 3.5rem; +$font-36px: 3.6rem; +$font-37px: 3.7rem; +$font-38px: 3.8rem; +$font-39px: 3.9rem; +$font-40px: 4.0rem; +$font-41px: 4.1rem; +$font-42px: 4.2rem; +$font-43px: 4.3rem; +$font-44px: 4.4rem; +$font-45px: 4.5rem; +$font-46px: 4.6rem; +$font-47px: 4.7rem; +$font-48px: 4.8rem; +$font-49px: 4.9rem; +$font-50px: 5.0rem; +$font-51px: 5.1rem; +$font-52px: 5.2rem; +$font-88px: 8.8rem; +$font-400px: 40rem; diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 08a809f869..5d7c601fa1 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -45,8 +45,8 @@ export class FontWatcher implements IWatcher { }; private setRootFontSize = (size) => { - // Externally we tell the user the font is size 15. Internally we use 20. - const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE) + 5; + // Externally we tell the user the font is size 15. Internally we use 10. + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE) - 5; if (fontSize !== size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); From 2ec47ecc743593ef816b1b7e7f7e903428b065f3 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 4 Jun 2020 17:50:56 +0100 Subject: [PATCH 03/76] Use different setting flag print expected values --- .../tabs/user/AppearanceUserSettingsTab.tsx | 12 +++++++++--- src/settings/Settings.js | 4 ++-- src/settings/watchers/FontWatcher.ts | 14 ++++++++------ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index bcd87b290a..9b77493c4f 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -62,7 +62,7 @@ export default class AppearanceUserSettingsTab extends React.Component { this.setState({fontSize: size.toString()}); - SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); + SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, size - FontWatcher.SIZE_DIFF); }; private onValidateFontSize = async ({value}: Pick): Promise => { @@ -151,7 +151,13 @@ export default class AppearanceUserSettingsTab extends React.Component { - // Externally we tell the user the font is size 15. Internally we use 10. - const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE) - 5; + console.log({size}) + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); if (fontSize !== size) { - SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); + SettingsStore.setValue("baseFontSize", null, SettingLevel.DEVICE, fontSize); } (document.querySelector(":root")).style.fontSize = toPx(fontSize); }; From bba6767608cea9444d927b02195794f48111c3f5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 4 Jun 2020 18:00:22 +0100 Subject: [PATCH 04/76] Update displayed min and max --- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 9b77493c4f..2b58e0e28e 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -137,8 +137,8 @@ export default class AppearanceUserSettingsTab extends React.Component): Promise => { const parsedSize = parseFloat(value); - const min = FontWatcher.MIN_SIZE; - const max = FontWatcher.MAX_SIZE; + const min = FontWatcher.MIN_SIZE + FontWatcher.SIZE_DIFF; + const max = FontWatcher.MAX_SIZE + FontWatcher.SIZE_DIFF; if (isNaN(parsedSize)) { return {valid: false, feedback: _t("Size must be a number")}; From f3011f00f78a3de7a34868fc506b462472154b7c Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 4 Jun 2020 19:43:35 +0100 Subject: [PATCH 05/76] Remove debug --- src/settings/watchers/FontWatcher.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 1eb0af9f3e..05c707a57b 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -47,7 +47,6 @@ export class FontWatcher implements IWatcher { }; private setRootFontSize = (size) => { - console.log({size}) const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); if (fontSize !== size) { From 4c1bc50649afcf31f403b7b887ecbdc95ba31772 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Jun 2020 16:34:04 -0600 Subject: [PATCH 06/76] 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 07/76] 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 08/76] 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 09/76] 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 10/76] 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 11/76] 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 12/76] 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 13/76] 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 14/76] 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 15/76] 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 16/76] 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 6fe5196a193ca3b1c814cf27463a86db0563186b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 8 Jun 2020 11:43:50 +0100 Subject: [PATCH 17/76] send state of lowBandwidth in rageshakes Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/submit-rageshake.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 9f9d7898cb..64a1ea0c33 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -145,6 +145,10 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp if (enabledLabs.length) { body.append('enabled_labs', enabledLabs.join(', ')); } + // if low bandwidth mode is enabled, say so over rageshake, it causes many issues + if (SettingsStore.getValue("lowBandwidth")) { + body.append("lowBandwidth", "enabled"); + } // add storage persistence/quota information if (navigator.storage && navigator.storage.persisted) { From 458bea20bebdbff420643306b0f1217f38964e3a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 8 Jun 2020 14:31:53 +0100 Subject: [PATCH 18/76] Load correct fonstSize default value --- src/settings/watchers/FontWatcher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/watchers/FontWatcher.ts b/src/settings/watchers/FontWatcher.ts index 05c707a57b..5527284cd0 100644 --- a/src/settings/watchers/FontWatcher.ts +++ b/src/settings/watchers/FontWatcher.ts @@ -32,7 +32,7 @@ export class FontWatcher implements IWatcher { } public start() { - this.setRootFontSize(SettingsStore.getValue("baseFontSize") + FontWatcher.SIZE_DIFF); + this.setRootFontSize(SettingsStore.getValue("baseFontSize")); this.dispatcherRef = dis.register(this.onAction); } From a7bc722b3fb15fc87abcfb41d0b5be1d1ba5ccc4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 8 Jun 2020 14:45:12 +0100 Subject: [PATCH 19/76] Use px where images use px width --- src/components/views/rooms/ReadReceiptMarker.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 2fe577377d..1c490f019e 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; import {formatDate} from '../../../DateUtils'; import Velociraptor from "../../../Velociraptor"; import * as sdk from "../../../index"; -import {toRem} from "../../../utils/units"; +import {toPx} from "../../../utils/units"; let bounce = false; try { @@ -149,7 +149,7 @@ export default createReactClass({ // start at the old height and in the old h pos startStyles.push({ top: startTopOffset+"px", - left: toRem(oldInfo.left) }); + left: toPx(oldInfo.left) }); const reorderTransitionOpts = { duration: 100, @@ -182,7 +182,7 @@ export default createReactClass({ } const style = { - left: toRem(this.props.leftOffset), + left: toPx(this.props.leftOffset), top: '0px', visibility: this.props.hidden ? 'hidden' : 'visible', }; From de18af35ff58b8ffd4f323a47130ff4f05b331ed Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 08:20:15 -0600 Subject: [PATCH 20/76] 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 21/76] 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 22/76] 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 23/76] 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 24/76] 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 bd58f6ea7bfe77decb86f20bcfa5b696a6240cdc Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 8 Jun 2020 17:38:07 +0100 Subject: [PATCH 25/76] Hide checkbox on dark backgrounds --- res/css/views/elements/_StyledCheckbox.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/res/css/views/elements/_StyledCheckbox.scss b/res/css/views/elements/_StyledCheckbox.scss index 14081f1e99..9cb82349ca 100644 --- a/res/css/views/elements/_StyledCheckbox.scss +++ b/res/css/views/elements/_StyledCheckbox.scss @@ -48,6 +48,8 @@ limitations under the License. border-radius: $border-radius; img { + display: none; + height: 100%; width: 100%; filter: invert(100%); @@ -57,6 +59,10 @@ limitations under the License. &:checked + label > .mx_Checkbox_background { background: $accent-color; border-color: $accent-color; + + img { + display: block; + } } & + label > *:not(.mx_Checkbox_background) { From de4c2fe3d9b2fb09e07daf0ff9190d7f838a98a6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 11:06:21 -0600 Subject: [PATCH 26/76] 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 { From b4bdb23f5f7bf88205c7f2a50508f9840ff81b0b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 8 Jun 2020 19:02:36 +0100 Subject: [PATCH 27/76] Clean up font scaling appearance --- .../settings/tabs/user/_AppearanceUserSettingsTab.scss | 4 ++++ .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index e82ae3c575..7308bb7177 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -43,3 +43,7 @@ limitations under the License. padding-left: 20px; padding-right: 5px; } + +.mx_SettingsTab_customFontSizeField { + margin-left: calc($font-16px + 10px); +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 2b58e0e28e..e7f22d5ea2 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -281,7 +281,7 @@ export default class AppearanceUserSettingsTab extends React.Component ""} + displayFunc={_ => ""} disabled={this.state.useCustomFontSize} />
        Aa
        @@ -290,9 +290,10 @@ export default class AppearanceUserSettingsTab extends React.Component this.setState({useCustomFontSize: checked})} + useCheckbox={true} /> this.setState({fontSize: value.target.value})} disabled={!this.state.useCustomFontSize} + className="mx_SettingsTab_customFontSizeField" />
      ; } From 21c8611300e27ef6d731b6f60a4b5b21115165d9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 12:14:10 -0600 Subject: [PATCH 28/76] Convert FormattingUtils to TypeScript and add badge utility function The new function is to be used in the new room list. --- ...{FormattingUtils.js => FormattingUtils.ts} | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) rename src/utils/{FormattingUtils.js => FormattingUtils.ts} (81%) diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.ts similarity index 81% rename from src/utils/FormattingUtils.js rename to src/utils/FormattingUtils.ts index b932214530..ed1727d190 100644 --- a/src/utils/FormattingUtils.js +++ b/src/utils/FormattingUtils.ts @@ -1,6 +1,6 @@ /* Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 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. @@ -21,8 +21,8 @@ import { _t } from '../languageHandler'; * formats numbers to fit into ~3 characters, suitable for badge counts * e.g: 999, 9.9K, 99K, 0.9M, 9.9M, 99M, 0.9B, 9.9B */ -export function formatCount(count) { - if (count < 1000) return count; +export function formatCount(count: number): string { + if (count < 1000) return count.toString(); if (count < 10000) return (count / 1000).toFixed(1) + "K"; if (count < 100000) return (count / 1000).toFixed(0) + "K"; if (count < 10000000) return (count / 1000000).toFixed(1) + "M"; @@ -34,7 +34,7 @@ export function formatCount(count) { * Format a count showing the whole number but making it a bit more readable. * e.g: 1000 => 1,000 */ -export function formatCountLong(count) { +export function formatCountLong(count: number): string { const formatter = new Intl.NumberFormat(); return formatter.format(count) } @@ -43,7 +43,7 @@ export function formatCountLong(count) { * format a size in bytes into a human readable form * e.g: 1024 -> 1.00 KB */ -export function formatBytes(bytes, decimals = 2) { +export function formatBytes(bytes: number, decimals = 2): string { if (bytes === 0) return '0 Bytes'; const k = 1024; @@ -62,7 +62,7 @@ export function formatBytes(bytes, decimals = 2) { * * @return {string} */ -export function formatCryptoKey(key) { +export function formatCryptoKey(key: string): string { return key.match(/.{1,4}/g).join(" "); } /** @@ -72,7 +72,7 @@ export function formatCryptoKey(key) { * * @return {number} */ -export function hashCode(str) { +export function hashCode(str: string): number { let hash = 0; let i; let chr; @@ -87,7 +87,7 @@ export function hashCode(str) { return Math.abs(hash); } -export function getUserNameColorClass(userId) { +export function getUserNameColorClass(userId: string): string { const colorNumber = (hashCode(userId) % 8) + 1; return `mx_Username_color${colorNumber}`; } @@ -103,7 +103,7 @@ export function getUserNameColorClass(userId) { * @returns {string} a string constructed by joining `items` with a comma * between each item, but with the last item appended as " and [lastItem]". */ -export function formatCommaSeparatedList(items, itemLimit) { +export function formatCommaSeparatedList(items: string[], itemLimit?: number): string { const remaining = itemLimit === undefined ? 0 : Math.max( items.length - itemLimit, 0, ); @@ -119,3 +119,13 @@ export function formatCommaSeparatedList(items, itemLimit) { return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); } } + +/** + * Formats a number into a 'minimal' badge count (9, 99, 99+). + * @param count The number to convert + * @returns The badge count, stringified. + */ +export function formatMinimalBadgeCount(count: number): string { + if (count < 100) return count.toString(); + return "99+"; +} From 086b9101fa8720f698644b934e6886ad1563e567 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 13:42:18 -0600 Subject: [PATCH 29/76] Add sublist badge counts to new room list Also add IDLE state to rooms --- res/css/_components.scss | 1 + res/css/views/rooms/_NotificationBadge.scss | 72 +++++ res/css/views/rooms/_RoomSublist2.scss | 35 ++- res/css/views/rooms/_RoomTile2.scss | 34 +-- .../views/rooms/NotificationBadge.tsx | 279 ++++++++++++++++++ src/components/views/rooms/RoomSublist2.tsx | 64 ++-- src/components/views/rooms/RoomTile2.tsx | 109 +------ 7 files changed, 408 insertions(+), 186 deletions(-) create mode 100644 res/css/views/rooms/_NotificationBadge.scss create mode 100644 src/components/views/rooms/NotificationBadge.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 62bec5ad62..61e3018725 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -170,6 +170,7 @@ @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss"; +@import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; diff --git a/res/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss new file mode 100644 index 0000000000..609e41c583 --- /dev/null +++ b/res/css/views/rooms/_NotificationBadge.scss @@ -0,0 +1,72 @@ +/* +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_NotificationBadge { + &:not(.mx_NotificationBadge_visible) { + display: none; + } + + // Badges are structured a bit weirdly to work around issues with non-monospace + // font styles. The badge pill is actually a background div and the count floats + // within that. For example: + // + // ( 99+ ) <-- Rounded pill is a _bg class. + // ^- The count is an element floating within that. + + &.mx_NotificationBadge_visible { + background-color: $roomtile2-badge-color; + margin-right: 14px; + + // Create a flexbox to order the count a bit easier + display: flex; + align-items: center; + justify-content: center; + + &.mx_NotificationBadge_highlighted { + // TODO: Use a more specific variable + background-color: $warning-color; + } + + // These are the 3 background types + + &.mx_NotificationBadge_dot { + width: 6px; + height: 6px; + border-radius: 6px; + margin-right: 18px; + } + + &.mx_NotificationBadge_2char { + width: 16px; + height: 16px; + border-radius: 16px; + } + + &.mx_NotificationBadge_3char { + width: 26px; + height: 16px; + border-radius: 16px; + } + + // The following is the floating badge + + .mx_NotificationBadge_count { + font-size: $font-10px; + line-height: $font-14px; + color: #fff; // TODO: Variable + } + } +} diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index e6e5af3b48..cfb9bc3b6d 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -30,11 +30,36 @@ limitations under the License. margin-bottom: 12px; .mx_RoomSublist2_headerContainer { - text-transform: uppercase; - opacity: 0.5; - line-height: $font-16px; - font-size: $font-12px; - padding-bottom: 8px; + // Create a flexbox to make ordering easy + display: flex; + align-items: center; + + .mx_RoomSublist2_badgeContainer { + opacity: 0.8; + padding-right: 7px; + + // 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_RoomSublist2_headerText { + text-transform: uppercase; + opacity: 0.5; + line-height: $font-16px; + font-size: $font-12px; + padding-bottom: 8px; + + width: 100%; + flex: 1; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } } .mx_RoomSublist2_resizeBox { diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 3151bb8716..41c9469bc1 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -50,11 +50,14 @@ limitations under the License. // TODO: Ellipsis on the name and preview .mx_RoomTile2_name { - font-weight: 600; font-size: $font-14px; line-height: $font-19px; } + .mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents { + font-weight: 600; + } + .mx_RoomTile2_messagePreview { font-size: $font-13px; line-height: $font-18px; @@ -70,34 +73,5 @@ limitations under the License. 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/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx new file mode 100644 index 0000000000..a77d2fc8d0 --- /dev/null +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -0,0 +1,279 @@ +/* +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 React from "react"; +import classNames from "classnames"; +import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import RoomAvatar from "../../views/avatars/RoomAvatar"; +import dis from '../../../dispatcher/dispatcher'; +import { Key } from "../../../Keyboard"; +import * as RoomNotifs from '../../../RoomNotifs'; +import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; +import * as Unread from '../../../Unread'; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import ActiveRoomObserver from "../../../ActiveRoomObserver"; +import { EventEmitter } from "events"; +import { arrayDiff } from "../../../utils/arrays"; + +export const NOTIFICATION_STATE_UPDATE = "update"; + +export enum NotificationColor { + // Inverted (None -> Red) because we do integer comparisons on this + None, // nothing special + Bold, // no badge, show as unread + Grey, // unread notified messages + Red, // unread pings +} + +export interface INotificationState extends EventEmitter { + symbol?: string; + count: number; + color: NotificationColor; +} + +interface IProps { + notification: INotificationState; + + /** + * If true, the badge will conditionally display a badge without count for the user. + */ + allowNoCount: boolean; +} + +interface IState { +} + +export default class NotificationBadge extends React.PureComponent { + constructor(props: IProps) { + super(props); + this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + } + + public componentDidUpdate(prevProps: Readonly) { + if (prevProps.notification) { + prevProps.notification.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + } + + this.props.notification.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate); + } + + private onNotificationUpdate = () => { + this.forceUpdate(); // notification state changed - update + }; + + public render(): React.ReactElement { + // Don't show a badge if we don't need to + if (this.props.notification.color <= NotificationColor.Bold) return null; + + const hasNotif = this.props.notification.color >= NotificationColor.Red; + const hasCount = this.props.notification.color >= NotificationColor.Grey; + const isEmptyBadge = this.props.allowNoCount && !localStorage.getItem("mx_rl_rt_badgeCount"); + + let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count); + if (isEmptyBadge) symbol = ""; + + const classes = classNames({ + 'mx_NotificationBadge': true, + 'mx_NotificationBadge_visible': hasCount, + 'mx_NotificationBadge_highlighted': hasNotif, + 'mx_NotificationBadge_dot': isEmptyBadge, + 'mx_NotificationBadge_2char': symbol.length > 0 && symbol.length < 3, + 'mx_NotificationBadge_3char': symbol.length > 2, + }); + + return ( +
      + {symbol} +
      + ); + } +} + +export class RoomNotificationState extends EventEmitter { + private _symbol: string; + private _count: number; + private _color: NotificationColor; + + constructor(private room: Room) { + super(); + this.room.on("Room.receipt", this.handleRoomEventUpdate); + this.room.on("Room.timeline", this.handleRoomEventUpdate); + this.room.on("Room.redaction", this.handleRoomEventUpdate); + MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate); + this.updateNotificationState(); + } + + public get symbol(): string { + return this._symbol; + } + + public get count(): number { + return this._count; + } + + public get color(): NotificationColor { + return this._color; + } + + private get roomIsInvite(): boolean { + return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite; + } + + public dispose(): void { + this.room.removeListener("Room.receipt", this.handleRoomEventUpdate); + this.room.removeListener("Room.timeline", this.handleRoomEventUpdate); + this.room.removeListener("Room.redaction", this.handleRoomEventUpdate); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); + } + } + + private handleRoomEventUpdate = (event: MatrixEvent) => { + const roomId = event.getRoomId(); + + if (roomId !== this.room.roomId) return; // ignore - not for us + this.updateNotificationState(); + }; + + private updateNotificationState() { + const before = {count: this.count, symbol: this.symbol, color: this.color}; + + if (this.roomIsInvite) { + this._color = NotificationColor.Red; + this._symbol = "!"; + this._count = 1; // not used, technically + } else { + const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight'); + const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total'); + + // For a 'true count' we pick the grey notifications first because they include the + // red notifications. If we don't have a grey count for some reason we use the red + // count. If that count is broken for some reason, assume zero. This avoids us showing + // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). + const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0); + + // Note: we only set the symbol if we have an actual count. We don't want to show + // zero on badges. + + if (redNotifs > 0) { + this._color = NotificationColor.Red; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } else if (greyNotifs > 0) { + this._color = NotificationColor.Grey; + this._count = trueCount; + this._symbol = null; // symbol calculated by component + } else { + // We don't have any notified messages, but we might have unread messages. Let's + // find out. + const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room); + if (hasUnread) { + this._color = NotificationColor.Bold; + } else { + this._color = NotificationColor.None; + } + + // no symbol or count for this state + this._count = 0; + this._symbol = null; + } + } + + // finally, publish an update if needed + const after = {count: this.count, symbol: this.symbol, color: this.color}; + if (JSON.stringify(before) !== JSON.stringify(after)) { + this.emit(NOTIFICATION_STATE_UPDATE); + } + } +} + +export class ListNotificationState extends EventEmitter { + private _count: number; + private _color: NotificationColor; + private rooms: Room[] = []; + private states: { [roomId: string]: RoomNotificationState } = {}; + + constructor(private byTileCount = false) { + super(); + } + + public get symbol(): string { + return null; // This notification state doesn't support symbols + } + + public get count(): number { + return this._count; + } + + public get color(): NotificationColor { + return this._color; + } + + public setRooms(rooms: Room[]) { + // If we're only concerned about the tile count, don't bother setting up listeners. + if (this.byTileCount) { + this.rooms = rooms; + this.calculateTotalState(); + return; + } + + const oldRooms = this.rooms; + const diff = arrayDiff(oldRooms, rooms); + for (const oldRoom of diff.removed) { + const state = this.states[oldRoom.roomId]; + delete this.states[oldRoom.roomId]; + state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); + state.dispose(); + } + for (const newRoom of diff.added) { + const state = new RoomNotificationState(newRoom); + state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate); + this.states[newRoom.roomId] = state; + } + + this.calculateTotalState(); + } + + private onRoomNotificationStateUpdate = () => { + this.calculateTotalState(); + }; + + private calculateTotalState() { + const before = {count: this.count, symbol: this.symbol, color: this.color}; + + if (this.byTileCount) { + this._color = NotificationColor.Red; + this._count = this.rooms.length; + } else { + this._count = 0; + this._color = NotificationColor.None; + for (const state of Object.values(this.states)) { + this._count += state.count; + this._color = Math.max(this.color, state.color); + } + } + + // finally, publish an update if needed + const after = {count: this.count, symbol: this.symbol, color: this.color}; + if (JSON.stringify(before) !== JSON.stringify(after)) { + this.emit(NOTIFICATION_STATE_UPDATE); + } + } +} diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 650a3ae645..cd27156cbd 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -26,7 +26,7 @@ import AccessibleButton from "../../views/elements/AccessibleButton"; 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"; +import NotificationBadge, { ListNotificationState } from "./NotificationBadge"; /******************************************************************* * CAUTION * @@ -56,13 +56,19 @@ interface IProps { } interface IState { + notificationState: ListNotificationState; } export default class RoomSublist2 extends React.Component { private headerButton = createRef(); - private hasTiles(): boolean { - return this.numTiles > 0; + constructor(props: IProps) { + super(props); + + this.state = { + notificationState: new ListNotificationState(this.props.isInvite), + }; + this.state.notificationState.setRooms(this.props.rooms); } private get numTiles(): number { @@ -70,6 +76,10 @@ export default class RoomSublist2 extends React.Component { return (this.props.rooms || []).length; } + public componentDidUpdate() { + this.state.notificationState.setRooms(this.props.rooms); + } + private onAddRoom = (e) => { e.stopPropagation(); if (this.props.onAddRoom) this.props.onAddRoom(); @@ -106,13 +116,6 @@ export default class RoomSublist2 extends React.Component { } private renderHeader(): React.ReactElement { - // 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 @@ -123,42 +126,8 @@ export default class RoomSublist2 extends React.Component { const tabIndex = isActive ? 0 : -1; // TODO: Collapsed state - // 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)} - //
      - //
      - // ); - // } - // } + + const badge = ; // TODO: Aux button // let addRoomButton = null; @@ -185,6 +154,9 @@ export default class RoomSublist2 extends React.Component { > {this.props.label} +
      + {badge} +
    ); }} diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 09d7b46ba5..d4f64e4571 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -25,13 +25,8 @@ import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; -import * as RoomNotifs from '../../../RoomNotifs'; -import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; -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"; +import NotificationBadge, { INotificationState, NotificationColor, RoomNotificationState } from "./NotificationBadge"; /******************************************************************* * CAUTION * @@ -41,14 +36,6 @@ import ActiveRoomObserver from "../../../ActiveRoomObserver"; * warning disappears. * *******************************************************************/ -enum NotificationColor { - // Inverted (None -> Red) because we do integer comparisons on this - None, // nothing special - Bold, // no badge, show as unread - Grey, // unread notified messages - Red, // unread pings -} - interface IProps { room: Room; showMessagePreview: boolean; @@ -58,11 +45,6 @@ interface IProps { // TODO: Incoming call boxes? } -interface INotificationState { - symbol: string; - color: NotificationColor; -} - interface IState { hover: boolean; notificationState: INotificationState; @@ -88,89 +70,17 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, - notificationState: this.getNotificationState(), + notificationState: new RoomNotificationState(this.props.room), 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() { if (this.props.room) { - 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); - } - } - - // XXX: This is a bit of an awful-looking hack. We should probably be using state for - // this, but instead we're kinda forced to either duplicate the code or thread a variable - // through the code paths. This feels like the least evil option. - private get roomIsInvite(): boolean { - return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite; - } - - private handleRoomEventUpdate = (event: MatrixEvent) => { - const roomId = event.getRoomId(); - - // Sanity check: should never happen - if (roomId !== this.props.room.roomId) return; - - this.updateNotificationState(); - }; - - private updateNotificationState() { - this.setState({notificationState: this.getNotificationState()}); - } - - private getNotificationState(): INotificationState { - const state: INotificationState = { - color: NotificationColor.None, - symbol: null, - }; - - if (this.roomIsInvite) { - state.color = NotificationColor.Red; - state.symbol = "!"; - } else { - const redNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight'); - const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.props.room, 'total'); - - // For a 'true count' we pick the grey notifications first because they include the - // red notifications. If we don't have a grey count for some reason we use the red - // count. If that count is broken for some reason, assume zero. This avoids us showing - // a badge for 'NaN' (which formats as 'NaNB' for NaN Billion). - const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0); - - // Note: we only set the symbol if we have an actual count. We don't want to show - // zero on badges. - - if (redNotifs > 0) { - state.color = NotificationColor.Red; - state.symbol = FormattingUtils.formatCount(trueCount); - } else if (greyNotifs > 0) { - state.color = NotificationColor.Grey; - state.symbol = FormattingUtils.formatCount(trueCount); - } else { - // We don't have any notified messages, but we might have unread messages. Let's - // find out. - const hasUnread = Unread.doesRoomHaveUnreadMessages(this.props.room); - if (hasUnread) { - state.color = NotificationColor.Bold; - // no symbol for this state - } - } - } - - return state; } private onTileMouseEnter = () => { @@ -206,19 +116,7 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_selected': this.state.selected, }); - 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_badgeHighlight': hasNotif, - 'mx_RoomTile2_badgeEmpty': isEmptyBadge, - }); - const symbol = this.state.notificationState.symbol; - badge =
    {isEmptyBadge ? null : symbol}
    ; - } + const badge = ; // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; @@ -237,6 +135,7 @@ export default class RoomTile2 extends React.Component { const nameClasses = classNames({ "mx_RoomTile2_name": true, "mx_RoomTile2_nameWithPreview": !!messagePreview, + "mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.color >= NotificationColor.Bold, }); const avatarSize = 32; From 0354bf9b6d432354c4c11f376a1c82b4a9705e0e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 17:11:58 -0600 Subject: [PATCH 30/76] Reimplement breadcrumbs for new room list This all-new component handles breadcrumbs a bit more smoothly for the app by always listening to changes even if the component isn't present. This allows the breadcrumbs to remain up to date for when the user re-enables breadcrumbs. The new behaviour is that we turn breadcrumbs on once the user has a room, and we don't turn it back off for them. This also introduces a new animation which is more stable and not laggy, though instead of sliding the breadcrumbs pop. This might be undesirable - to be reviewed. --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 2 +- res/css/views/rooms/_RoomBreadcrumbs2.scss | 53 ++++++ src/components/structures/LeftPanel2.tsx | 34 +++- .../views/rooms/RoomBreadcrumbs2.tsx | 90 ++++++++++ src/i18n/strings/en_EN.json | 1 + src/settings/SettingsStore.js | 2 + src/stores/AsyncStoreWithClient.ts | 53 ++++++ src/stores/BreadcrumbsStore.ts | 154 ++++++++++++++++++ 9 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 res/css/views/rooms/_RoomBreadcrumbs2.scss create mode 100644 src/components/views/rooms/RoomBreadcrumbs2.tsx create mode 100644 src/stores/AsyncStoreWithClient.ts create mode 100644 src/stores/BreadcrumbsStore.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 62bec5ad62..8958aee2fc 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -175,6 +175,7 @@ @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; +@import "./views/rooms/_RoomBreadcrumbs2.scss"; @import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomList.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 822a5ac399..502ed18a87 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -76,9 +76,9 @@ $roomListMinimizedWidth: 50px; } .mx_LeftPanel2_breadcrumbsContainer { - // TODO: Improve CSS for breadcrumbs (currently shoved into the view rather than placed) width: 100%; overflow: hidden; + margin-top: 8px; } } diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss new file mode 100644 index 0000000000..aa0b0ecb08 --- /dev/null +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -0,0 +1,53 @@ +/* +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. +*/ + +@keyframes breadcrumb-popin { + 0% { + // Ideally we'd use `width` instead of `opacity`, but we only + // have 16 nanoseconds to render the frame, and width is expensive. + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.mx_RoomBreadcrumbs2 { + // Create a flexbox for the crumbs + display: flex; + flex-direction: row; + align-items: flex-start; + width: 100%; + + .mx_RoomBreadcrumbs2_crumb { + margin-right: 8px; + width: 32px; + + // React loves to add elements, so only target the one we want to animate + &:first-child { + animation: breadcrumb-popin 0.3s; + } + } + + .mx_RoomBreadcrumbs2_placeholder { + font-weight: 600; + font-size: $font-14px; + line-height: 32px; // specifically to match the height this is not scaled + height: 32px; + } +} diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index c66c0a6799..b42da0be09 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -26,7 +26,9 @@ 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 RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2"; +import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; +import { UPDATE_EVENT } from "../../stores/AsyncStore"; /******************************************************************* * CAUTION * @@ -43,6 +45,7 @@ interface IProps { interface IState { searchExpanded: boolean; searchFilter: string; // TODO: Move search into room list? + showBreadcrumbs: boolean; } export default class LeftPanel2 extends React.Component { @@ -60,7 +63,14 @@ export default class LeftPanel2 extends React.Component { this.state = { searchExpanded: false, searchFilter: "", + showBreadcrumbs: BreadcrumbsStore.instance.visible, }; + + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + public componentWillUnmount() { + BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); } private onSearch = (term: string): void => { @@ -85,6 +95,13 @@ export default class LeftPanel2 extends React.Component { } } + private onBreadcrumbsUpdate = () => { + const newVal = BreadcrumbsStore.instance.visible; + if (newVal !== this.state.showBreadcrumbs) { + this.setState({showBreadcrumbs: newVal}); + } + }; + private renderHeader(): React.ReactNode { // TODO: Update when profile info changes // TODO: Presence @@ -100,6 +117,16 @@ export default class LeftPanel2 extends React.Component { displayName = myUser.rawDisplayName; avatarUrl = myUser.avatarUrl; } + + let breadcrumbs; + if (this.state.showBreadcrumbs) { + breadcrumbs = ( +
    + +
    + ); + } + return (
    @@ -116,9 +143,7 @@ export default class LeftPanel2 extends React.Component { {displayName}
    -
    - -
    + {breadcrumbs}
    ); } @@ -152,7 +177,6 @@ export default class LeftPanel2 extends React.Component { onBlur={() => {/*TODO*/}} />; - // TODO: Breadcrumbs // TODO: Conference handling / calls const containerClasses = classNames({ diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx new file mode 100644 index 0000000000..195757ccf0 --- /dev/null +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -0,0 +1,90 @@ +/* +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 React from "react"; +import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; +import AccessibleButton from "../elements/AccessibleButton"; +import RoomAvatar from "../avatars/RoomAvatar"; +import { _t } from "../../../languageHandler"; +import { Room } from "matrix-js-sdk/src/models/room"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import Analytics from "../../../Analytics"; +import { UPDATE_EVENT } from "../../../stores/AsyncStore"; + +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + *******************************************************************/ + +interface IProps { +} + +interface IState { +} + +export default class RoomBreadcrumbs2 extends React.PureComponent { + private isMounted = true; + + constructor(props: IProps) { + super(props); + + BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + public componentWillUnmount() { + this.isMounted = false; + BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + } + + private onBreadcrumbsUpdate = () => { + if (!this.isMounted) return; + this.forceUpdate(); // we have no state, so this is the best we can do + }; + + private viewRoom = (room: Room, index: number) => { + Analytics.trackEvent("Breadcrumbs", "click_node", index); + defaultDispatcher.dispatch({action: "view_room", room_id: room.roomId}); + }; + + public render(): React.ReactElement { + // TODO: Decorate crumbs with icons + const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => { + return ( + this.viewRoom(r, i)} + aria-label={_t("Room %(name)s", {name: r.name})} + > + + + ) + }); + + if (tiles.length === 0) { + tiles.push( +
    + {_t("No recently visited rooms")} +
    + ); + } + + return
    {tiles}
    ; + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cf6dc2431a..75caf5b593 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1069,6 +1069,7 @@ "Replying": "Replying", "Room %(name)s": "Room %(name)s", "Recent rooms": "Recent rooms", + "No recently visited rooms": "No recently visited rooms", "No rooms to show": "No rooms to show", "Unnamed room": "Unnamed room", "World readable": "World readable", diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 4b18a27c6c..dcdde46631 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -181,6 +181,8 @@ export default class SettingsStore { * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms. */ static monitorSetting(settingName, roomId) { + roomId = roomId || null; // the thing wants null specifically to work, so appease it. + if (!this._monitors[settingName]) this._monitors[settingName] = {}; const registerWatcher = () => { diff --git a/src/stores/AsyncStoreWithClient.ts b/src/stores/AsyncStoreWithClient.ts new file mode 100644 index 0000000000..ce7fd45eec --- /dev/null +++ b/src/stores/AsyncStoreWithClient.ts @@ -0,0 +1,53 @@ +/* +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { AsyncStore } from "./AsyncStore"; +import { ActionPayload } from "../dispatcher/payloads"; + + +export abstract class AsyncStoreWithClient extends AsyncStore { + protected matrixClient: MatrixClient; + + protected abstract async onAction(payload: ActionPayload); + + protected async onReady() { + // Default implementation is to do nothing. + } + + protected async onNotReady() { + // Default implementation is to do nothing. + } + + protected async onDispatch(payload: ActionPayload) { + await this.onAction(payload); + + if (payload.action === 'MatrixActions.sync') { + // Filter out anything that isn't the first PREPARED sync. + if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { + return; + } + + this.matrixClient = payload.matrixClient; + await this.onReady(); + } else if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { + if (this.matrixClient) { + await this.onNotReady(); + this.matrixClient = null; + } + } + } +} diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts new file mode 100644 index 0000000000..783b38e62f --- /dev/null +++ b/src/stores/BreadcrumbsStore.ts @@ -0,0 +1,154 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingsStore, { SettingLevel } from "../settings/SettingsStore"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { ActionPayload } from "../dispatcher/payloads"; +import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { arrayHasDiff } from "../utils/arrays"; + +const MAX_ROOMS = 20; // arbitrary +const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up + +interface IState { + enabled?: boolean; + rooms?: Room[]; +} + +export class BreadcrumbsStore extends AsyncStoreWithClient { + private static internalInstance = new BreadcrumbsStore(); + + private waitingRooms: { roomId: string, addedTs: number }[] = []; + + private constructor() { + super(defaultDispatcher); + + SettingsStore.monitorSetting("breadcrumb_rooms", null); + SettingsStore.monitorSetting("breadcrumbs", null); + } + + public static get instance(): BreadcrumbsStore { + return BreadcrumbsStore.internalInstance; + } + + public get rooms(): Room[] { + return this.state.rooms || []; + } + + public get visible(): boolean { + return this.state.enabled; + } + + protected async onAction(payload: ActionPayload) { + if (!this.matrixClient) return; + + if (payload.action === 'setting_updated') { + if (payload.settingName === 'breadcrumb_rooms') { + await this.updateRooms(); + } else if (payload.settingName === 'breadcrumbs') { + await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); + } + } else if (payload.action === 'view_room') { + if (payload.auto_join && !this.matrixClient.getRoom(payload.room_id)) { + // Queue the room instead of pushing it immediately. We're probably just + // waiting for a room join to complete. + this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()}); + } else { + await this.appendRoom(this.matrixClient.getRoom(payload.room_id)); + } + } + } + + protected async onReady() { + await this.updateRooms(); + await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); + + this.matrixClient.on("Room.myMembership", this.onMyMembership); + this.matrixClient.on("Room", this.onRoom); + } + + protected async onNotReady() { + this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); + this.matrixClient.removeListener("Room", this.onRoom); + } + + private onMyMembership = async (room: Room) => { + // We turn on breadcrumbs by default once the user has at least 1 room to show. + if (!this.state.enabled) { + await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true); + } + }; + + private onRoom = async (room: Room) => { + const waitingRoom = this.waitingRooms.find(r => r.roomId === room.roomId); + if (!waitingRoom) return; + this.waitingRooms.splice(this.waitingRooms.indexOf(waitingRoom), 1); + + if ((Date.now() - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago. + await this.appendRoom(room); + }; + + private async updateRooms() { + let roomIds = SettingsStore.getValue("breadcrumb_rooms"); + if (!roomIds || roomIds.length === 0) roomIds = []; + + const rooms = roomIds.map(r => this.matrixClient.getRoom(r)).filter(r => !!r); + const currentRooms = this.state.rooms || []; + if (!arrayHasDiff(rooms, currentRooms)) return; // no change (probably echo) + await this.updateState({rooms}); + } + + private async appendRoom(room: Room) { + const rooms = this.state.rooms.slice(); // cheap clone + + // If the room is upgraded, use that room instead. We'll also splice out + // any children of the room. + const history = this.matrixClient.getRoomUpgradeHistory(room.roomId); + if (history.length > 1) { + room = history[history.length - 1]; // Last room is most recent in history + + // Take out any room that isn't the most recent room + for (let i = 0; i < history.length - 1; i++) { + const idx = rooms.findIndex(r => r.roomId === history[i].roomId); + if (idx !== -1) rooms.splice(idx, 1); + } + } + + // Remove the existing room, if it is present + const existingIdx = rooms.findIndex(r => r.roomId === room.roomId); + if (existingIdx !== -1) { + rooms.splice(existingIdx, 1); + } + + // Splice the room to the start of the list + rooms.splice(0, 0, room); + + if (rooms.length > MAX_ROOMS) { + // This looks weird, but it's saying to start at the MAX_ROOMS point in the + // list and delete everything after it. + rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS); + } + + // Update the breadcrumbs + await this.updateState({rooms}); + const roomIds = rooms.map(r => r.roomId); + if (roomIds.length > 0) { + await SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds); + } + } + +} From 04566e12b202c50d10045fda65bdaf0e288bfecc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 17:14:40 -0600 Subject: [PATCH 31/76] Fix indentation in styles --- res/css/views/rooms/_RoomBreadcrumbs2.scss | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index aa0b0ecb08..2db0fdca08 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -15,16 +15,16 @@ limitations under the License. */ @keyframes breadcrumb-popin { - 0% { - // Ideally we'd use `width` instead of `opacity`, but we only - // have 16 nanoseconds to render the frame, and width is expensive. - opacity: 0; - transform: scale(0); - } - 100% { - opacity: 1; - transform: scale(1); - } + 0% { + // Ideally we'd use `width` instead of `opacity`, but we only + // have 16 nanoseconds to render the frame, and width is expensive. + opacity: 0; + transform: scale(0); + } + 100% { + opacity: 1; + transform: scale(1); + } } .mx_RoomBreadcrumbs2 { From eff97e6c205ecdcce0bc40714fe51ca6f4e6d960 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 18:18:34 -0600 Subject: [PATCH 32/76] Fix the tests --- src/stores/BreadcrumbsStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 783b38e62f..5944091d00 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -113,7 +113,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } private async appendRoom(room: Room) { - const rooms = this.state.rooms.slice(); // cheap clone + const rooms = (this.state.rooms || []).slice(); // cheap clone // If the room is upgraded, use that room instead. We'll also splice out // any children of the room. From 5083811deb8a2a1fc53acd331c667a0926bc5bce Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 18:26:43 -0600 Subject: [PATCH 33/76] Appease the tests --- src/stores/BreadcrumbsStore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 5944091d00..f0f2dad91b 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -68,7 +68,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { // waiting for a room join to complete. this.waitingRooms.push({roomId: payload.room_id, addedTs: Date.now()}); } else { - await this.appendRoom(this.matrixClient.getRoom(payload.room_id)); + // The tests might not result in a valid room object. + const room = this.matrixClient.getRoom(payload.room_id); + if (room) await this.appendRoom(room); } } } From 708c65cd965f9da3fd8ada635a71a116d710b292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 19:08:18 -0600 Subject: [PATCH 34/76] Disable new breadcrumb store when old room list is in use --- src/stores/BreadcrumbsStore.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index f0f2dad91b..332fa7fe2e 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -20,6 +20,7 @@ import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; +import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -56,6 +57,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload) { if (!this.matrixClient) return; + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + if (payload.action === 'setting_updated') { if (payload.settingName === 'breadcrumb_rooms') { await this.updateRooms(); @@ -76,6 +80,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onReady() { + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + await this.updateRooms(); await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); @@ -84,6 +91,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } protected async onNotReady() { + // TODO: Remove when new room list is made the default + if (!RoomListStoreTempProxy.isUsingNewStore()) return; + this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); this.matrixClient.removeListener("Room", this.onRoom); } From 5114c37b82374429c7a7cb1f5889efbd2b1651d7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 8 Jun 2020 20:33:21 -0600 Subject: [PATCH 35/76] Add filtering and exploring to the new room list This is per the designs. Animation doesn't feel required here. Like the rest of this series, this rewrites a component to be more purpose-built to help match the designs and to solve the smallest possible problem. --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 39 ++++- res/css/structures/_RoomSearch.scss | 70 +++++++++ res/img/feather-customised/compass.svg | 1 + src/components/structures/HomePage.tsx | 3 +- src/components/structures/LeftPanel.js | 2 +- src/components/structures/LeftPanel2.tsx | 61 +++----- src/components/structures/MatrixChat.tsx | 6 +- src/components/structures/RoomSearch.tsx | 143 ++++++++++++++++++ src/components/structures/RoomView.js | 4 +- .../views/elements/RoomDirectoryButton.js | 3 +- src/dispatcher/actions.ts | 5 + 12 files changed, 287 insertions(+), 51 deletions(-) create mode 100644 res/css/structures/_RoomSearch.scss create mode 100644 res/img/feather-customised/compass.svg create mode 100644 src/components/structures/RoomSearch.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index de4c1c677c..66af2ba00f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -19,6 +19,7 @@ @import "./structures/_NotificationPanel.scss"; @import "./structures/_RightPanel.scss"; @import "./structures/_RoomDirectory.scss"; +@import "./structures/_RoomSearch.scss"; @import "./structures/_RoomStatusBar.scss"; @import "./structures/_RoomSubList.scss"; @import "./structures/_RoomView.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index d335df305f..d9a2b1dd5c 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -88,7 +88,44 @@ $roomListMinimizedWidth: 50px; } .mx_LeftPanel2_filterContainer { - // TODO: Improve CSS for filtering and its input + margin-left: 12px; + margin-right: 12px; + + // Create a flexbox to organize the inputs + display: flex; + align-items: center; + + .mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton { + // Cheaty way to return the occupied space to the filter input + margin: 0; + width: 0; + + // Don't forget to hide the masked ::before icon + visibility: hidden; + } + + .mx_LeftPanel2_exploreButton { + width: 28px; + height: 28px; + border-radius: 20px; + background-color: #fff; // TODO: Variable and theme + position: relative; + margin-left: 8px; + + &::before { + content: ''; + position: absolute; + top: 6px; + left: 6px; + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/compass.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + } + } } .mx_LeftPanel2_actualRoomListContainer { diff --git a/res/css/structures/_RoomSearch.scss b/res/css/structures/_RoomSearch.scss new file mode 100644 index 0000000000..d078031090 --- /dev/null +++ b/res/css/structures/_RoomSearch.scss @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Note: this component expects to be contained within a flexbox +.mx_RoomSearch { + flex: 1; + border-radius: 20px; + background-color: #fff; // TODO: Variable & theme + height: 26px; + padding: 2px; + + // Create a flexbox for the icons (easier to manage) + display: flex; + align-items: center; + + .mx_RoomSearch_icon { + width: 16px; + height: 16px; + mask: url('$(res)/img/feather-customised/search-input.svg'); + mask-repeat: no-repeat; + background: $primary-fg-color; + margin-left: 7px; + } + + .mx_RoomSearch_input { + border: none !important; // !important to override default app-wide styles + flex: 1 !important; // !important to override default app-wide styles + color: $primary-fg-color !important; // !important to override default app-wide styles + padding: 0; + height: 100%; + width: 100%; + font-size: $font-12px; + line-height: $font-16px; + + &:not(.mx_RoomSearch_inputExpanded)::placeholder { + color: $primary-fg-color !important; // !important to override default app-wide styles + } + } + + &.mx_RoomSearch_expanded { + .mx_RoomSearch_clearButton { + width: 16px; + height: 16px; + mask-image: url('$(res)/img/feather-customised/x.svg'); + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background: $primary-fg-color; + margin-right: 8px; + } + } + + .mx_RoomSearch_clearButton { + width: 0; + height: 0; + } +} diff --git a/res/img/feather-customised/compass.svg b/res/img/feather-customised/compass.svg new file mode 100644 index 0000000000..3296260803 --- /dev/null +++ b/res/img/feather-customised/compass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index ff8d35a114..209f219598 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -22,9 +22,10 @@ import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; +import { Action } from "../../dispatcher/actions"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); -const onClickExplore = () => dis.dispatch({action: 'view_room_directory'}); +const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); const HomePage = () => { diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index 05cd97df2a..bae69b5631 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -252,7 +252,7 @@ const LeftPanel = createReactClass({ if (!this.props.collapsed) { exploreButton = (
    - dis.dispatch({action: 'view_room_directory'})}>{_t("Explore")} + dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}
    ); } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 2fd8612ff5..f417dc99b1 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -18,16 +18,16 @@ import * as React from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; import dis from "../../dispatcher/dispatcher"; -import AccessibleButton from "../views/elements/AccessibleButton"; import { _t } from "../../languageHandler"; import SearchBox from "./SearchBox"; import RoomList2 from "../views/rooms/RoomList2"; -import TopLeftMenuButton from "./TopLeftMenuButton"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import BaseAvatar from '../views/avatars/BaseAvatar'; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs"; import UserMenuButton from "./UserMenuButton"; +import RoomSearch from "./RoomSearch"; +import AccessibleButton from "../views/elements/AccessibleButton"; /******************************************************************* * CAUTION * @@ -42,7 +42,6 @@ interface IProps { } interface IState { - searchExpanded: boolean; searchFilter: string; // TODO: Move search into room list? } @@ -58,7 +57,6 @@ export default class LeftPanel2 extends React.Component { super(props); this.state = { - searchExpanded: false, searchFilter: "", }; } @@ -67,24 +65,10 @@ export default class LeftPanel2 extends React.Component { this.setState({searchFilter: term}); }; - private onSearchCleared = (source: string): void => { - if (source === "keyboard") { - dis.fire(Action.FocusComposer); - } - this.setState({searchExpanded: false}); - } - - private onSearchFocus = (): void => { - this.setState({searchExpanded: true}); + private onExplore = () => { + dis.fire(Action.ViewRoomDirectory); }; - private onSearchBlur = (event: FocusEvent): void => { - const target = event.target as HTMLInputElement; - if (target.value.length === 0) { - this.setState({searchExpanded: false}); - } - } - private renderHeader(): React.ReactNode { // TODO: Update when profile info changes // TODO: Presence @@ -126,6 +110,22 @@ export default class LeftPanel2 extends React.Component { ); } + private renderSearchExplore(): React.ReactNode { + // TODO: Collapsed support + + return ( +
    + + +
    + ); + } + public render(): React.ReactNode { const tagPanel = (
    @@ -133,18 +133,6 @@ export default class LeftPanel2 extends React.Component {
    ); - const searchBox = ( {/*TODO*/}} - onSearch={this.onSearch} - onCleared={this.onSearchCleared} - onFocus={this.onSearchFocus} - onBlur={this.onSearchBlur} - collapsed={false}/>); // TODO: Collapsed support - // TODO: Improve props for RoomList2 const roomList = {/*TODO*/}} @@ -167,14 +155,7 @@ export default class LeftPanel2 extends React.Component { {tagPanel}