From f10d9e47e7c9e4d4ee20175256af3a2616940877 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 31 Mar 2020 16:02:23 +0100 Subject: [PATCH 001/241] Fix event tiles to smoothly resize with font. --- res/css/views/rooms/_EventTile.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 0dc60226b8..a6cff9cfb0 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -114,10 +114,9 @@ limitations under the License. clear: both; position: relative; padding-left: 65px; /* left gutter */ - padding-top: 4px; - padding-bottom: 2px; + padding-top: 3px; + padding-bottom: 3px; border-radius: 4px; - min-height: 24px; line-height: $font-22px; } From 1ff0f3445a52806888d82a2f25d95a120b9114cb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Fri, 3 Apr 2020 17:53:49 +0100 Subject: [PATCH 002/241] Fix pills. This was a hard pill to swallow --- res/css/views/elements/_RichText.scss | 35 +++++++++---------- res/css/views/rooms/_Autocomplete.scss | 9 ++--- .../views/rooms/_BasicMessageComposer.scss | 13 +++---- src/components/views/avatars/BaseAvatar.js | 22 ++++++++---- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index e01b1f8938..cb0c2eacc1 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -6,11 +6,14 @@ .mx_RoomPill, .mx_GroupPill, .mx_AtRoomPill { - border-radius: 16px; - display: inline-block; - height: 20px; + display: inline-flex; + align-items: center; + vertical-align: middle; + border-radius: $font-16px; + height: $font-16px; line-height: $font-20px; - padding-left: 5px; + padding-left: 0; + padding-right: 0.5em; } a.mx_Pill { @@ -19,6 +22,11 @@ a.mx_Pill { overflow: hidden; vertical-align: text-bottom; max-width: calc(100% - 1ch); + height: $font-24px; +} + +.mx_Pill { + padding: 0.3rem; } /* More specific to override `.markdown-body a` text-decoration */ @@ -31,8 +39,7 @@ a.mx_Pill { .mx_UserPill { color: $primary-fg-color; background-color: $other-user-pill-bg-color; - padding-right: 5px; -} + } .mx_UserPill_selected { background-color: $accent-color !important; @@ -45,7 +52,6 @@ a.mx_Pill { .mx_MessageComposer_input .mx_AtRoomPill { color: $accent-fg-color; background-color: $mention-user-pill-bg-color; - padding-right: 5px; } /* More specific to override `.markdown-body a` color */ @@ -55,15 +61,6 @@ a.mx_Pill { .mx_GroupPill { color: $accent-fg-color; background-color: $rte-room-pill-color; - padding-right: 5px; -} - -/* More specific to override `.markdown-body a` color */ -.mx_EventTile_content .markdown-body a.mx_GroupPill, -.mx_GroupPill { - color: $accent-fg-color; - background-color: $rte-group-pill-color; - padding-right: 5px; } .mx_EventTile_body .mx_UserPill, @@ -77,8 +74,10 @@ a.mx_Pill { .mx_GroupPill .mx_BaseAvatar, .mx_AtRoomPill .mx_BaseAvatar { position: relative; - left: -3px; - top: 2px; + display: inline-flex; + align-items: center; + border-radius: 10rem; + margin-right: 0.24rem; } .mx_Markdown_BOLD { diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index e5316f5a46..a4aebdb708 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -31,9 +31,10 @@ } .mx_Autocomplete_Completion_pill { - border-radius: 17px; - height: 34px; - padding: 0px 5px; + box-sizing: border-box; + border-radius: 2rem; + height: $font-34px; + padding: 0.4rem; display: flex; user-select: none; cursor: pointer; @@ -42,7 +43,7 @@ } .mx_Autocomplete_Completion_pill > * { - margin: 0 3px; + margin-right: 0.3rem; } /* styling for common completion elements */ diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e9013eb7b7..e126e523a6 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -46,22 +46,19 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { - padding-left: 21px; position: relative; // avatar psuedo element &::before { - position: absolute; - left: 2px; - top: 2px; content: var(--avatar-letter); - width: 16px; - height: 16px; + width: $font-16px; + height: $font-16px; + margin-right: 0.24rem; background: var(--avatar-background), $avatar-bg-color; color: $avatar-initial-color; background-repeat: no-repeat; - background-size: 16px; - border-radius: 8px; + background-size: $font-16px; + border-radius: $font-16px; text-align: center; font-weight: normal; line-height: $font-16px; diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 3e3a2e6bd9..d9336a81e7 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -164,9 +164,9 @@ export default createReactClass({ const initialLetter = AvatarLogic.getInitialLetter(name); const textNode = ( @@ -174,7 +174,11 @@ export default createReactClass({ const imgNode = ( + aria-hidden="true" + style={{ + width: width/15 + "rem", + height: height/15 + "rem" + }} /> ); if (onClick != null) { return ( @@ -202,7 +206,10 @@ export default createReactClass({ src={imageUrl} onClick={onClick} onError={this.onError} - width={width} height={height} + style={{ + width: width/15 + "rem", + height: height/15 + "rem" + }} title={title} alt="" inputRef={inputRef} {...otherProps} /> @@ -213,7 +220,10 @@ export default createReactClass({ className="mx_BaseAvatar mx_BaseAvatar_image" src={imageUrl} onError={this.onError} - width={width} height={height} + style={{ + width: width/15 + "rem", + height: height/15 + "rem" + }} title={title} alt="" ref={inputRef} {...otherProps} /> From 27b1ce841f4397a0bf6c362c7bc718efdd4571dc Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 16:23:16 +0100 Subject: [PATCH 003/241] Scale room tile heights. --- res/css/views/rooms/_RoomTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index de018bf178..5bc7d5624d 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -20,7 +20,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: 34px; + height: $font-34px; margin: 0; padding: 0 8px 0 10px; position: relative; From f54ca219cf01a8c88f74abfb76b0f0cad7fae10c Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 16:23:52 +0100 Subject: [PATCH 004/241] Scale read receipt images --- res/css/views/rooms/_EventTile.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a6cff9cfb0..77bfa85862 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -310,6 +310,8 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { .mx_EventTile_readAvatars .mx_BaseAvatar { position: absolute; display: inline-block; + height: $font-14px; + width: $font-14px; } .mx_EventTile_readAvatarRemainder { From d12ed333c1040d0bcd6fb4b00b7a40baeac433f4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 16:48:35 +0100 Subject: [PATCH 005/241] Fix avatar alignment for room state events --- res/css/views/rooms/_EventTile.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 77bfa85862..1c781eec20 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -45,7 +45,7 @@ limitations under the License. } .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: 8px; + top: $font-8px; left: 65px; } From 05d11fea6900b18c7d4063668188bfdf1c648772 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 16:49:44 +0100 Subject: [PATCH 006/241] Use a function to convert to rem. --- src/components/views/avatars/BaseAvatar.js | 19 ++++++++++--------- src/utils/rem.js | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 src/utils/rem.js diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index d9336a81e7..a17124b9ad 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -24,6 +24,7 @@ import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import toRem from "../../../utils/rem"; export default createReactClass({ displayName: 'BaseAvatar', @@ -164,9 +165,9 @@ export default createReactClass({ const initialLetter = AvatarLogic.getInitialLetter(name); const textNode = ( @@ -176,8 +177,8 @@ export default createReactClass({ alt="" title={title} onError={this.onError} aria-hidden="true" style={{ - width: width/15 + "rem", - height: height/15 + "rem" + width: toRem(width), + height: toRem(height) }} /> ); if (onClick != null) { @@ -207,8 +208,8 @@ export default createReactClass({ onClick={onClick} onError={this.onError} style={{ - width: width/15 + "rem", - height: height/15 + "rem" + width: toRem(width), + height: toRem(height) }} title={title} alt="" inputRef={inputRef} @@ -221,8 +222,8 @@ export default createReactClass({ src={imageUrl} onError={this.onError} style={{ - width: width/15 + "rem", - height: height/15 + "rem" + width: toRem(width), + height: toRem(height) }} title={title} alt="" ref={inputRef} diff --git a/src/utils/rem.js b/src/utils/rem.js new file mode 100644 index 0000000000..0ba430950c --- /dev/null +++ b/src/utils/rem.js @@ -0,0 +1,20 @@ +/* +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. +*/ + +// converts a pixel value to rem. +export default function(pixel_val) { + return pixel_val / 15 + "rem" +} From c6bc0c79147b51c737709a202a90ebb3033e5eed Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 16:51:07 +0100 Subject: [PATCH 007/241] Fix read receipt horizontal spacing at scale --- src/components/views/rooms/ReadReceiptMarker.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index be0ca1f8d6..85d443d55a 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -23,6 +23,7 @@ import { _t } from '../../../languageHandler'; import {formatDate} from '../../../DateUtils'; import Velociraptor from "../../../Velociraptor"; import * as sdk from "../../../index"; +import toRem from "../../../utils/rem"; let bounce = false; try { @@ -148,7 +149,7 @@ export default createReactClass({ // start at the old height and in the old h pos startStyles.push({ top: startTopOffset+"px", - left: oldInfo.left+"px" }); + left: toRem(oldInfo.left) }); const reorderTransitionOpts = { duration: 100, @@ -181,7 +182,7 @@ export default createReactClass({ } const style = { - left: this.props.leftOffset+'px', + left: toRem(this.props.leftOffset), top: '0px', visibility: this.props.hidden ? 'hidden' : 'visible', }; From f29717ee62aceb982afd573fc0f473efad8c4488 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 17:47:12 +0100 Subject: [PATCH 008/241] Fix room header vertical alignment --- res/css/views/rooms/_RoomHeader.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index 969106c9ea..80f6c40f39 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -173,8 +173,6 @@ limitations under the License. .mx_RoomHeader_avatar { flex: 0; - width: 28px; - height: 28px; margin: 0 7px; position: relative; } From 38919518da7bef490df539b08c2d72e4f23c5c22 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 6 Apr 2020 20:52:36 +0100 Subject: [PATCH 009/241] Resize room list scroll section. This fix isn't perfect. Currently the scroll view is slightly smaller than the list of rooms. I think it has something to do with the how the heigh is calculate in js, considering it has some assumptions about the height of each bar and the padding. However room items are the only things which change with respect to the root value. Therefore the item list is actually taller than the computed pixel value of the list converted to rems. I'll look into it. --- src/components/structures/RoomSubList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 2ae2d71100..1e3e15b4ec 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -32,6 +32,7 @@ import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; +import toRem from "../../utils/rem"; // turn this on for drop & drag console debugging galore const debug = false; @@ -383,7 +384,7 @@ export default class RoomSubList extends React.PureComponent { setHeight = (height) => { if (this._subList.current) { - this._subList.current.style.height = `${height}px`; + this._subList.current.style.height = toRem(height); } this._updateLazyRenderHeight(height); }; From cc601e1595e5bd4e571e74066e3b5cc1d3a8013f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 14:21:21 +0100 Subject: [PATCH 010/241] Resize toggle switches with font --- res/css/views/elements/_ToggleSwitch.scss | 34 +++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss index 1f4445b88c..9a07e01978 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -16,12 +16,19 @@ limitations under the License. .mx_ToggleSwitch { transition: background-color 0.20s ease-out 0.1s; - width: 48px; - height: 24px; - border-radius: 14px; + + width: $font-48px; + height: $font-20px; + border-radius: 1.5rem; + padding: 2px; + background-color: $togglesw-off-color; - position: relative; opacity: 0.5; + + display: flex; + flex-direction: row; + flex: 0 0 auto; + align-items: center; } .mx_ToggleSwitch_enabled { @@ -35,19 +42,12 @@ limitations under the License. .mx_ToggleSwitch_ball { transition: left 0.15s ease-out 0.1s; - margin: 2px; - width: 20px; - height: 20px; - border-radius: 20px; + width: $font-20px; + height: $font-20px; + border-radius: $font-20px; background-color: $togglesw-ball-color; - position: absolute; - top: 0; } -.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball { - left: 23px; // 48px switch - 20px ball - 5px padding = 23px -} - -.mx_ToggleSwitch:not(.mx_ToggleSwitch_on) > .mx_ToggleSwitch_ball { - left: 2px; -} +.mx_ToggleSwitch_on { + flex-direction: row-reverse; +} \ No newline at end of file From 0182128189e433cd775f78a44f90ed0d523a961a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 15:07:34 +0100 Subject: [PATCH 011/241] Fix settings when scaling up --- res/css/views/elements/_ToggleSwitch.scss | 2 +- res/css/views/settings/tabs/_SettingsTab.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss index 9a07e01978..073bf5c00b 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_ToggleSwitch { transition: background-color 0.20s ease-out 0.1s; - width: $font-48px; + width: $font-44px; height: $font-20px; border-radius: 1.5rem; padding: 2px; diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 1fbfb35927..e3a61e6825 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -63,7 +63,7 @@ limitations under the License. display: inline-block; font-size: $font-14px; color: $primary-fg-color; - max-width: calc(100% - 48px); // Force word wrap instead of colliding with the switch + max-width: calc(100% - $font-48px); // Force word wrap instead of colliding with the switch box-sizing: border-box; padding-right: 10px; } From 5caec2a289a2f2b1bfc75cfeae1b05a9d3479b34 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 15:23:19 +0100 Subject: [PATCH 012/241] lint --- res/css/views/elements/_RichText.scss | 2 +- res/css/views/elements/_ToggleSwitch.scss | 4 ++-- src/utils/rem.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index cb0c2eacc1..21d01fdd51 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -39,7 +39,7 @@ a.mx_Pill { .mx_UserPill { color: $primary-fg-color; background-color: $other-user-pill-bg-color; - } +} .mx_UserPill_selected { background-color: $accent-color !important; diff --git a/res/css/views/elements/_ToggleSwitch.scss b/res/css/views/elements/_ToggleSwitch.scss index 073bf5c00b..7c65fbc90d 100644 --- a/res/css/views/elements/_ToggleSwitch.scss +++ b/res/css/views/elements/_ToggleSwitch.scss @@ -21,7 +21,7 @@ limitations under the License. height: $font-20px; border-radius: 1.5rem; padding: 2px; - + background-color: $togglesw-off-color; opacity: 0.5; @@ -50,4 +50,4 @@ limitations under the License. .mx_ToggleSwitch_on { flex-direction: row-reverse; -} \ No newline at end of file +} diff --git a/src/utils/rem.js b/src/utils/rem.js index 0ba430950c..1f18c9de05 100644 --- a/src/utils/rem.js +++ b/src/utils/rem.js @@ -15,6 +15,6 @@ limitations under the License. */ // converts a pixel value to rem. -export default function(pixel_val) { - return pixel_val / 15 + "rem" +export default function(pixelVal) { + return pixelVal / 15 + "rem"; } From a58721047e6345ccd1b722c648314da7fbacac07 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 15:41:41 +0100 Subject: [PATCH 013/241] Room memebers avatars scale --- res/css/views/rooms/_EntityTile.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_EntityTile.scss b/res/css/views/rooms/_EntityTile.scss index 966d2c4e70..8db71f297c 100644 --- a/res/css/views/rooms/_EntityTile.scss +++ b/res/css/views/rooms/_EntityTile.scss @@ -69,8 +69,6 @@ limitations under the License. padding-right: 12px; padding-top: 4px; padding-bottom: 4px; - width: 36px; - height: 36px; position: relative; } From e49c041df79cb8169b39c76de21c087c5e21caec Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 16:08:59 +0100 Subject: [PATCH 014/241] Commuity seleciton bar vertical alignment scale fix --- res/css/structures/_TagPanel.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 0065ffa502..f5c4c4c698 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -110,13 +110,13 @@ limitations under the License. .mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; - height: 56px; + height: calc(100% + 16px); background-color: $accent-color; width: 5px; position: absolute; left: -15px; border-radius: 0 3px 3px 0; - top: -8px; // (56 - 40)/2 + top: -8px; // (16px / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { From 121b53f99a5f1d17decdd296ff4b960ffc026c3a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 16:27:26 +0100 Subject: [PATCH 015/241] FIx lanugage selection alignment at scale. --- res/css/views/elements/_Dropdown.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 32a68d5252..dd8511cc42 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -67,6 +67,8 @@ limitations under the License. text-overflow: ellipsis; white-space: nowrap; flex: 1; + display: inline-flex; + align-items: center; } .mx_Dropdown_option div { From 3966e45b8fccf884850e68469a9443795b0e830e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 17:13:09 +0100 Subject: [PATCH 016/241] Fix test --- test/components/views/messages/TextualBody-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 4ad46586ae..7801995254 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -206,7 +206,8 @@ describe("", () => { 'Hey ' + '' + 'Member' + + 'style=\"width: 1.0666666666666667rem; height: 1.0666666666666667rem;\" ' + + 'title="@member:domain.bla" alt="" aria-hidden="true">Member' + ''); }); }); From 2d2b39892a2ad349514754f42725e86f33b58a54 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 17:23:07 +0100 Subject: [PATCH 017/241] lint test --- test/components/views/messages/TextualBody-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 7801995254..59671327ce 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -206,7 +206,7 @@ describe("", () => { 'Hey ' + '' + 'Member' + ''); }); From c2a29087460969fd7368d84ff5112fb5b3b25d06 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 7 Apr 2020 17:41:18 +0100 Subject: [PATCH 018/241] Fix community line height --- res/css/structures/_TagPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index f5c4c4c698..4a78c8df92 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -69,7 +69,7 @@ limitations under the License. height: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: 40px; + height: $font-40px; padding: 10px 0 9px 0; } From c921859067b25b4c86d33ec93f3d06d6a0889ef0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:35:38 +0100 Subject: [PATCH 019/241] Use rem instead of em. --- res/css/views/elements/_RichText.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 21d01fdd51..d46d07e82b 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -13,7 +13,7 @@ height: $font-16px; line-height: $font-20px; padding-left: 0; - padding-right: 0.5em; + padding-right: 0.5rem; } a.mx_Pill { From 07853f48bb882a55ec2fdab9255bf2a8366d4efd Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:45:21 +0100 Subject: [PATCH 020/241] Use font-variable instead of rem --- res/css/_font-sizes.scss | 7 +++++++ res/css/views/elements/_RichText.scss | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index ad9e2e7103..b37b2eb3c2 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -14,6 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +$font-1px: 0.067rem; +$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; diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index d46d07e82b..5b5759b794 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -26,7 +26,7 @@ a.mx_Pill { } .mx_Pill { - padding: 0.3rem; + padding: $font-5px; } /* More specific to override `.markdown-body a` text-decoration */ From 18efbcccebece7ebd4cf17d5e6e9073261b3cb2a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 17:07:59 +0100 Subject: [PATCH 021/241] Undo superfluous delete --- res/css/views/elements/_RichText.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 5b5759b794..b933b1b828 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -29,6 +29,13 @@ a.mx_Pill { padding: $font-5px; } +/* More specific to override `.markdown-body a` color */ +.mx_EventTile_content .markdown-body a.mx_GroupPill, +.mx_GroupPill { + color: $accent-fg-color; + background-color: $rte-group-pill-color; +} + /* More specific to override `.markdown-body a` text-decoration */ .mx_EventTile_content .markdown-body a.mx_Pill { text-decoration: none; From fc04266b4239fe7f1135e81bf8d4a38b771d095d Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 17:32:24 +0100 Subject: [PATCH 022/241] Skinnier pills are easier to swallow --- res/css/views/elements/_RichText.scss | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index b933b1b828..1fa04595f9 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -10,10 +10,9 @@ align-items: center; vertical-align: middle; border-radius: $font-16px; - height: $font-16px; - line-height: $font-20px; + line-height: $font-15px; padding-left: 0; - padding-right: 0.5rem; + padding-right: $font-5px; } a.mx_Pill { @@ -22,11 +21,10 @@ a.mx_Pill { overflow: hidden; vertical-align: text-bottom; max-width: calc(100% - 1ch); - height: $font-24px; } .mx_Pill { - padding: $font-5px; + padding: $font-1px; } /* More specific to override `.markdown-body a` color */ From 7a0caafb77fadeabba717a78ae5ddd09e8fdaf33 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 19:15:34 +0100 Subject: [PATCH 023/241] FIx unread count --- res/css/structures/_RoomSubList.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_RoomSubList.scss b/res/css/structures/_RoomSubList.scss index 2e0c94263e..2c53258b08 100644 --- a/res/css/structures/_RoomSubList.scss +++ b/res/css/structures/_RoomSubList.scss @@ -74,7 +74,7 @@ limitations under the License. .mx_RoomSubList_badge > div { flex: 0 0 auto; - border-radius: 8px; + border-radius: $font-16px; font-weight: 600; font-size: $font-12px; padding: 0 5px; From 83609f0ab2bb1b75d0e0282eba5baee014e6f569 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 20:12:53 +0100 Subject: [PATCH 024/241] Scale user photo upload with font size --- res/css/_font-sizes.scss | 1 + res/css/views/settings/_AvatarSetting.scss | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/res/css/_font-sizes.scss b/res/css/_font-sizes.scss index b37b2eb3c2..5cd19ce620 100644 --- a/res/css/_font-sizes.scss +++ b/res/css/_font-sizes.scss @@ -67,4 +67,5 @@ $font-49px: 3.267rem; $font-50px: 3.333rem; $font-51px: 3.400rem; $font-52px: 3.467rem; +$font-88px: 5.887rem; $font-400px: 26.667rem; diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss index 35dba90f85..9fa10907b4 100644 --- a/res/css/views/settings/_AvatarSetting.scss +++ b/res/css/views/settings/_AvatarSetting.scss @@ -15,13 +15,13 @@ limitations under the License. */ .mx_AvatarSetting_avatar { - width: 88px; - height: 88px; + width: $font-88px; + height: $font-88px; margin-left: 13px; position: relative; & > * { - width: 88px; + width: $font-88px; box-sizing: border-box; } @@ -63,7 +63,7 @@ limitations under the License. & > img, .mx_AvatarSetting_avatarPlaceholder { display: block; - height: 88px; + height: $font-88px; border-radius: 4px; } From 42ec21f5bb306c51ae21e4e2b3995bc757139db8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 20:13:13 +0100 Subject: [PATCH 025/241] Tweak read receipt remainder text --- src/components/views/rooms/EventTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f67877373e..af14f6922c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -34,6 +34,7 @@ import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import * as ObjectUtils from "../../../ObjectUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {E2E_STATE} from "./E2EIcon"; +import torem from "../../../utils/rem"; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -473,7 +474,7 @@ export default createReactClass({ if (remainder > 0) { remText = { remainder }+ + style={{ right: "calc(" + torem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+ ; } } From e10a511997ca6667bfb96b74938735c8c81447d5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 15:49:19 +0100 Subject: [PATCH 026/241] Extra right padding in user pills --- res/css/views/elements/_RichText.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 1fa04595f9..b45b567db7 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -25,6 +25,7 @@ a.mx_Pill { .mx_Pill { padding: $font-1px; + padding-right: $font-6px; } /* More specific to override `.markdown-body a` color */ From 61f2e19d95b8f7ef7589de0461d1877b9e607a11 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:18:57 +0100 Subject: [PATCH 027/241] Basic font settings. Include a default value getter in the store in order to make the default value easy to work with. --- src/settings/Settings.js | 7 ++++ src/settings/SettingsStore.js | 14 ++++++++ .../controllers/FontSizeController.js | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/settings/controllers/FontSizeController.js diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 928b1985fa..3b316e39d0 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -29,6 +29,7 @@ import ThemeController from './controllers/ThemeController'; import PushToMatrixClientController from './controllers/PushToMatrixClientController'; import ReloadOnChangeController from "./controllers/ReloadOnChangeController"; import {RIGHT_PANEL_PHASES} from "../stores/RightPanelStorePhases"; +import FontSizeController from './controllers/FontSizeController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -94,6 +95,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "font_size": { + displayName: _td("Font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 16, + controller: new FontSizeController(), + }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 0122916bc3..70ea5ac57c 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -370,6 +370,20 @@ export default class SettingsStore { return SettingsStore._getFinalValue(setting, level, roomId, null, null); } + /** + * Gets the default value of a setting. + * @param {string} settingName The name of the setting to read the value of. + * @return {*} The value, or null if not found + */ + static getDefaultValue(settingName, roomId = null, excludeDefault = false) { + // Verify that the setting is actually a setting + if (!SETTINGS[settingName]) { + throw new Error("Setting '" + settingName + "' does not appear to be a setting."); + } + + return SETTINGS[settingName].default; + } + static _getFinalValue(setting, level, roomId, calculatedValue, calculatedAtLevel) { let resultingValue = calculatedValue; diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js new file mode 100644 index 0000000000..4ab2f331f1 --- /dev/null +++ b/src/settings/controllers/FontSizeController.js @@ -0,0 +1,33 @@ +/* +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 SettingController from "./SettingController"; +import dis from "../../dispatcher"; + +export default class FontSizeController extends SettingController { + + constructor() { + super() + } + + onChange(level, roomId, newValue) { + // Dispatch font size change so that everything open responds to the change. + dis.dispatch({ + action: "update-font-size", + size: newValue, + }); + } +} From 269621ad2436f2181d44a10829cad1447249d8fd Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:19:50 +0100 Subject: [PATCH 028/241] Move theme settings to a new tab --- .../tabs/user/GeneralUserSettingsTab.js | 172 ------------- .../tabs/user/StyleUserSettingsTab.js | 233 ++++++++++++++++++ src/theme.js | 2 +- 3 files changed, 234 insertions(+), 173 deletions(-) create mode 100644 src/components/views/settings/tabs/user/StyleUserSettingsTab.js diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 801062bff4..4e57203ca2 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -19,7 +19,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import ProfileSettings from "../../ProfileSettings"; -import Field from "../../../elements/Field"; import * as languageHandler from "../../../../../languageHandler"; import {SettingLevel} from "../../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore"; @@ -27,7 +26,6 @@ import LanguageDropdown from "../../../elements/LanguageDropdown"; import AccessibleButton from "../../../elements/AccessibleButton"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; import PropTypes from "prop-types"; -import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import PlatformPeg from "../../../../../PlatformPeg"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -60,9 +58,6 @@ export default class GeneralUserSettingsTab extends React.Component { }, emails: [], msisdns: [], - ...this._calculateThemeState(), - customThemeUrl: "", - customThemeMessage: {isError: false, text: ""}, }; this.dispatcherRef = dis.register(this._onAction); @@ -91,39 +86,6 @@ export default class GeneralUserSettingsTab extends React.Component { dis.unregister(this.dispatcherRef); } - _calculateThemeState() { - // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we - // show the right values for things. - - const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); - const systemThemeExplicit = SettingsStore.getValueAt( - SettingLevel.DEVICE, "use_system_theme", null, false, true); - const themeExplicit = SettingsStore.getValueAt( - SettingLevel.DEVICE, "theme", null, false, true); - - // If the user has enabled system theme matching, use that. - if (systemThemeExplicit) { - return { - theme: themeChoice, - useSystemTheme: true, - }; - } - - // If the user has set a theme explicitly, use that (no system theme matching) - if (themeExplicit) { - return { - theme: themeChoice, - useSystemTheme: false, - }; - } - - // Otherwise assume the defaults for the settings - return { - theme: themeChoice, - useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), - }; - } - _onAction = (payload) => { if (payload.action === 'id_server_changed') { this.setState({haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl())}); @@ -214,33 +176,6 @@ export default class GeneralUserSettingsTab extends React.Component { PlatformPeg.get().reload(); }; - _onThemeChange = (e) => { - const newTheme = e.target.value; - if (this.state.theme === newTheme) return; - - // doing getValue in the .catch will still return the value we failed to set, - // so remember what the value was before we tried to set it so we can revert - const oldTheme = SettingsStore.getValue('theme'); - SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { - dis.dispatch({action: 'recheck_theme'}); - this.setState({theme: oldTheme}); - }); - this.setState({theme: newTheme}); - // The settings watcher doesn't fire until the echo comes back from the - // server, so to make the theme change immediately we need to manually - // do the dispatch now - // XXX: The local echoed value appears to be unreliable, in particular - // when settings custom themes(!) so adding forceTheme to override - // the value from settings. - dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); - }; - - _onUseSystemThemeChanged = (checked) => { - this.setState({useSystemTheme: checked}); - SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); - dis.dispatch({action: 'recheck_theme'}); - }; - _onPasswordChangeError = (err) => { // TODO: Figure out a design that doesn't involve replacing the current dialog let errMsg = err.error || ""; @@ -277,41 +212,6 @@ export default class GeneralUserSettingsTab extends React.Component { }); }; - _onAddCustomTheme = async () => { - let currentThemes = SettingsStore.getValue("custom_themes"); - if (!currentThemes) currentThemes = []; - currentThemes = currentThemes.map(c => c); // cheap clone - - if (this._themeTimer) { - clearTimeout(this._themeTimer); - } - - try { - const r = await fetch(this.state.customThemeUrl); - const themeInfo = await r.json(); - if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') { - this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}}); - return; - } - currentThemes.push(themeInfo); - } catch (e) { - console.error(e); - this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}}); - return; // Don't continue on error - } - - await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); - this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); - - this._themeTimer = setTimeout(() => { - this.setState({customThemeMessage: {text: "", isError: false}}); - }, 3000); - }; - - _onCustomThemeChange = (e) => { - this.setState({customThemeUrl: e.target.value}); - }; - _renderProfileSection() { return (
@@ -391,77 +291,6 @@ export default class GeneralUserSettingsTab extends React.Component { ); } - _renderThemeSection() { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); - const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); - - const themeWatcher = new ThemeWatcher(); - let systemThemeSection; - if (themeWatcher.isSystemThemeSupported()) { - systemThemeSection =
- -
; - } - - let customThemeForm; - if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { - let messageElement = null; - if (this.state.customThemeMessage.text) { - if (this.state.customThemeMessage.isError) { - messageElement =
{this.state.customThemeMessage.text}
; - } else { - messageElement =
{this.state.customThemeMessage.text}
; - } - } - customThemeForm = ( -
-
- - {_t("Add theme")} - {messageElement} - -
- ); - } - - const themes = Object.entries(enumerateThemes()) - .map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability - const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); - const customThemes = themes.filter(p => !builtInThemes.includes(p)) - .sort((a, b) => a.name.localeCompare(b.name)); - const orderedThemes = [...builtInThemes, ...customThemes]; - return ( -
- {_t("Theme")} - {systemThemeSection} - - {orderedThemes.map(theme => { - return ; - })} - - {customThemeForm} - -
- ); - } - _renderDiscoverySection() { const SetIdServer = sdk.getComponent("views.settings.SetIdServer"); @@ -547,7 +376,6 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderProfileSection()} {this._renderAccountSection()} {this._renderLanguageSection()} - {this._renderThemeSection()}
{discoWarning} {_t("Discovery")}
{this._renderDiscoverySection()} {this._renderIntegrationManagerSection() /* Has its own title */} diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js new file mode 100644 index 0000000000..5c68f214d4 --- /dev/null +++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js @@ -0,0 +1,233 @@ +/* +Copyright 2019 New Vector Ltd + +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 {_t} from "../../../../../languageHandler"; +import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; +import * as sdk from "../../../../../index"; +import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; +import Field from "../../../elements/Field"; + +export default class StyleUserSettingsTab extends React.Component { + constructor() { + super(); + + this.state = { + fontSize: SettingsStore.getValue("font_size", null), + ...this._calculateThemeState(), + customThemeUrl: "", + customThemeMessage: {isError: false, text: ""}, + } + } + + _calculateThemeState() { + // We have to mirror the logic from ThemeWatcher.getEffectiveTheme so we + // show the right values for things. + + const themeChoice = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "theme"); + const systemThemeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "use_system_theme", null, false, true); + const themeExplicit = SettingsStore.getValueAt( + SettingLevel.DEVICE, "theme", null, false, true); + + // If the user has enabled system theme matching, use that. + if (systemThemeExplicit) { + return { + theme: themeChoice, + useSystemTheme: true, + }; + } + + // If the user has set a theme explicitly, use that (no system theme matching) + if (themeExplicit) { + return { + theme: themeChoice, + useSystemTheme: false, + }; + } + + // Otherwise assume the defaults for the settings + return { + theme: themeChoice, + useSystemTheme: SettingsStore.getValueAt(SettingLevel.DEVICE, "use_system_theme"), + }; + } + + _onThemeChange = (e) => { + const newTheme = e.target.value; + if (this.state.theme === newTheme) return; + + // doing getValue in the .catch will still return the value we failed to set, + // so remember what the value was before we tried to set it so we can revert + const oldTheme = SettingsStore.getValue('theme'); + SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme).catch(() => { + dis.dispatch({action: 'recheck_theme'}); + this.setState({theme: oldTheme}); + }); + this.setState({theme: newTheme}); + // The settings watcher doesn't fire until the echo comes back from the + // server, so to make the theme change immediately we need to manually + // do the dispatch now + // XXX: The local echoed value appears to be unreliable, in particular + // when settings custom themes(!) so adding forceTheme to override + // the value from settings. + dis.dispatch({action: 'recheck_theme', forceTheme: newTheme}); + }; + + _onUseSystemThemeChanged = (checked) => { + this.setState({useSystemTheme: checked}); + SettingsStore.setValue("use_system_theme", null, SettingLevel.DEVICE, checked); + dis.dispatch({action: 'recheck_theme'}); + }; + + _onFontSizeChanged = (size) => { + let parsed_size = isNaN(parseInt(size))?SettingsStore.getDefaultValue("font_size"):parseFloat(size); + this.setState({fontSize: parsed_size}) + SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsed_size) + }; + + _onAddCustomTheme = async () => { + let currentThemes = SettingsStore.getValue("custom_themes"); + if (!currentThemes) currentThemes = []; + currentThemes = currentThemes.map(c => c); // cheap clone + + if (this._themeTimer) { + clearTimeout(this._themeTimer); + } + + try { + const r = await fetch(this.state.customThemeUrl); + const themeInfo = await r.json(); + if (!themeInfo || typeof(themeInfo['name']) !== 'string' || typeof(themeInfo['colors']) !== 'object') { + this.setState({customThemeMessage: {text: _t("Invalid theme schema."), isError: true}}); + return; + } + currentThemes.push(themeInfo); + } catch (e) { + console.error(e); + this.setState({customThemeMessage: {text: _t("Error downloading theme information."), isError: true}}); + return; // Don't continue on error + } + + await SettingsStore.setValue("custom_themes", null, SettingLevel.ACCOUNT, currentThemes); + this.setState({customThemeUrl: "", customThemeMessage: {text: _t("Theme added!"), isError: false}}); + + this._themeTimer = setTimeout(() => { + this.setState({customThemeMessage: {text: "", isError: false}}); + }, 3000); + }; + + _onCustomThemeChange = (e) => { + this.setState({customThemeUrl: e.target.value}); + }; + + render() { + return ( +
+
{_t("Style")}
+ {this._renderThemeSection()} + {this._renderFontSection()} +
+ ); + } + + _renderThemeSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); + const LabelledToggleSwitch = sdk.getComponent("views.elements.LabelledToggleSwitch"); + + const themeWatcher = new ThemeWatcher(); + let systemThemeSection; + if (themeWatcher.isSystemThemeSupported()) { + systemThemeSection =
+ +
; + } + + let customThemeForm; + if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { + let messageElement = null; + if (this.state.customThemeMessage.text) { + if (this.state.customThemeMessage.isError) { + messageElement =
{this.state.customThemeMessage.text}
; + } else { + messageElement =
{this.state.customThemeMessage.text}
; + } + } + customThemeForm = ( +
+
+ + {_t("Add theme")} + {messageElement} + +
+ ); + } + + const themes = Object.entries(enumerateThemes()) + .map(p => ({id: p[0], name: p[1]})); // convert pairs to objects for code readability + const builtInThemes = themes.filter(p => !p.id.startsWith("custom-")); + const customThemes = themes.filter(p => !builtInThemes.includes(p)) + .sort((a, b) => a.name.localeCompare(b.name)); + const orderedThemes = [...builtInThemes, ...customThemes]; + return ( +
+ {_t("Theme")} + {systemThemeSection} + + {orderedThemes.map(theme => { + return ; + })} + + {customThemeForm} + +
+ ); + } + + _renderFontSection() { + return
+ {_t("Font size")} + this._onFontSizeChanged(ev.target.value)} + /> +
+ } + +} diff --git a/src/theme.js b/src/theme.js index 442a89e25f..3309acdd01 100644 --- a/src/theme.js +++ b/src/theme.js @@ -81,7 +81,7 @@ export class ThemeWatcher { } getEffectiveTheme() { - // Dev note: Much of this logic is replicated in the GeneralUserSettingsTab + // Dev note: Much of this logic is replicated in the StyleUserSettingsTab // XXX: checking the isLight flag here makes checking it in the ThemeController // itself completely redundant since we just override the result here and we're From bce1fa0c1a5277e87b46470055017f7f8a49d32e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:20:08 +0100 Subject: [PATCH 029/241] Simple translations --- src/i18n/strings/en_EN.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3eac055054..f10da54729 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2293,5 +2293,7 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End" + "End": "End", + "Font size": "Font size", + "Style": "Style" } From c1827925dad16eaac32c784dbbb13d7530dc7dd2 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:20:41 +0100 Subject: [PATCH 030/241] Use new style tab --- src/components/views/dialogs/UserSettingsDialog.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index b135d5f5f6..ee6f7e13ec 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -22,6 +22,7 @@ import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import SettingsStore from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; +import StyleUserSettingsTab from "../settings/tabs/user/StyleUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; @@ -66,6 +67,11 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_settingsIcon", , )); + tabs.push(new Tab( + _td("Style"), + "mx_userSettingsDialog_styleIcon", + , + )); tabs.push(new Tab( _td("Flair"), "mx_UserSettingsDialog_flairIcon", From af4dd2770c4cc3da7ef20617c2bb4fdb3e174a52 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 14 Apr 2020 16:21:04 +0100 Subject: [PATCH 031/241] Respond to font size changes --- src/components/structures/MatrixChat.js | 4 ++ src/fontSize.js | 50 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/fontSize.js diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1293ccc7e9..1845e0011d 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -66,6 +66,7 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; +import { FontWatcher } from '../../fontSize'; /** constants for MatrixChat.state.view */ export const VIEWS = { @@ -265,7 +266,9 @@ export default createReactClass({ this.dispatcherRef = dis.register(this.onAction); this._themeWatcher = new ThemeWatcher(); + this._fontWatcher = new FontWatcher(10, 20); this._themeWatcher.start(); + this._fontWatcher.start(); this.focusComposer = false; @@ -353,6 +356,7 @@ export default createReactClass({ Lifecycle.stopMatrixClient(); dis.unregister(this.dispatcherRef); this._themeWatcher.stop(); + this._fontWatcher.stop(); window.removeEventListener("focus", this.onFocus); window.removeEventListener('resize', this.handleResize); this.state.resizeNotifier.removeListener("middlePanelResized", this._dispatchTimelineResize); diff --git a/src/fontSize.js b/src/fontSize.js new file mode 100644 index 0000000000..c242fcc743 --- /dev/null +++ b/src/fontSize.js @@ -0,0 +1,50 @@ +/* +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 dis from './dispatcher'; +import SettingsStore from './settings/SettingsStore'; + +export class FontWatcher { + constructor(min_size, max_size) { + this._min_size = min_size; + this._max_size = max_size; + this._dispatcherRef = null; + } + + start() { + this._setRootFontSize(SettingsStore.getValue("font_size")); + this._dispatcherRef = dis.register(this._onAction); + } + + stop() { + dis.unregister(this._dispatcherRef); + } + + _onAction = (payload) => { + if (payload.action === 'update-font-size') { + this._setRootFontSize(payload.size); + } + }; + + _setRootFontSize = size => { + let fontSize = this._min_size < size?size:this._min_size; + fontSize = fontSize < this._max_size?fontSize:this._max_size; + if (fontSize != size) { + SettingsStore.setValue("font_size", null, fontSize) + } + document.querySelector(":root").style.fontSize = fontSize + "px"; + } +} \ No newline at end of file From 14551b1885bc0d309e284d0293b7ad0f042e9927 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 10:28:07 +0100 Subject: [PATCH 032/241] Hide font scaling behind labs --- .../views/settings/tabs/user/StyleUserSettingsTab.js | 2 +- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js index 5c68f214d4..ab9107eac0 100644 --- a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js @@ -139,7 +139,7 @@ export default class StyleUserSettingsTab extends React.Component {
{_t("Style")}
{this._renderThemeSection()} - {this._renderFontSection()} + {SettingsStore.getValue("feature_font_scaling") ? this._renderFontSection() : null}
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f10da54729..bf2eaa4652 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2295,5 +2295,6 @@ "Space": "Space", "End": "End", "Font size": "Font size", + "Font scaling": "Font scaling", "Style": "Style" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3b316e39d0..23b73f740b 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -95,6 +95,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "feature_font_scaling": { + isFeature: true, + displayName: _td("Font scaling"), + supportedLevels: LEVELS_FEATURE, + default: false + }, "font_size": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 0faf7b865f47a4fc28a0339d999f6592c501e045 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 10:31:40 +0100 Subject: [PATCH 033/241] Set font option width --- .../tabs/user/_GeneralUserSettingsTab.scss | 3 ++- .../tabs/user/_StyleUserSettingsTab.scss | 20 +++++++++++++++++++ .../tabs/user/StyleUserSettingsTab.js | 4 ++-- 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 62d230e752..45aecd032f 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -15,7 +15,8 @@ limitations under the License. */ .mx_GeneralUserSettingsTab_changePassword .mx_Field, -.mx_GeneralUserSettingsTab_themeSection .mx_Field { +.mx_StyleUserSettingsTab_themeSection .mx_Field, +.mx_StyleUserSettingsTab_fontScaling .mx_Field { @mixin mx_Settings_fullWidthField; } diff --git a/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss b/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss new file mode 100644 index 0000000000..dd9646bd5a --- /dev/null +++ b/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss @@ -0,0 +1,20 @@ +/* +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_StyleUserSettingsTab_themeSection .mx_Field, +.mx_StyleUserSettingsTab_fontScaling .mx_Field { + @mixin mx_Settings_fullWidthField; +} \ No newline at end of file diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js index ab9107eac0..e7b7385f5a 100644 --- a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js @@ -199,7 +199,7 @@ export default class StyleUserSettingsTab extends React.Component { .sort((a, b) => a.name.localeCompare(b.name)); const orderedThemes = [...builtInThemes, ...customThemes]; return ( -
+
{_t("Theme")} {systemThemeSection} + return
{_t("Font size")} Date: Thu, 16 Apr 2020 10:33:23 +0100 Subject: [PATCH 034/241] Make a font slider --- res/css/_components.scss | 1 + res/css/structures/_FontSlider.scss | 88 +++++++++++++++++++++++++ res/themes/light/css/_light.scss | 4 ++ src/components/structures/FontSlider.js | 62 +++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 res/css/structures/_FontSlider.scss create mode 100644 src/components/structures/FontSlider.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 0ba2b609e8..9d6629e703 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -7,6 +7,7 @@ @import "./structures/_CreateRoom.scss"; @import "./structures/_CustomRoomTagPanel.scss"; @import "./structures/_FilePanel.scss"; +@import "./structures/_FontSlider.scss"; @import "./structures/_GenericErrorPage.scss"; @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; diff --git a/res/css/structures/_FontSlider.scss b/res/css/structures/_FontSlider.scss new file mode 100644 index 0000000000..94902f6fd9 --- /dev/null +++ b/res/css/structures/_FontSlider.scss @@ -0,0 +1,88 @@ +/* +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_fontSlider { + position: relative; + margin: 0px; + @mixin mx_Settings_fullWidthField; +} + +.mx_fontSlider_dotContainer { + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.mx_fontSlider_bar { + display: flex; + box-sizing: border-box; + position: absolute; + height: 1rem; + width: 100%; + padding: 0 1.1rem; + align-items: center; +} + +.mx_fontSlider_bar > hr { + width: 100%; + border: 0.2rem solid $fontSlider-background-color; +} + +.mx_fontSlider_selection { + display: flex; + align-items: center; + width: calc(100% - 2.2rem); + height: 1rem; + position: absolute; +} + +.mx_fontSlider_selectionDot { + transition: left 0.25s; + position: absolute; + width: 1.1rem; + height: 1.1rem; + background-color: $fontSlider-selection-color; + border-radius: 50%; + box-shadow: 0 0 6px lightgrey; + z-index: 10; +} + +.mx_fontSlider_selection > hr { + transition: width 0.25s; + margin: 0; + border: 0.2rem solid $fontSlider-selection-color; +} + +.mx_fontSlider_dot { + transition: background-color 0.2s ease-in; + height: 1rem; + width: 1rem; + border-radius: 50%; + background-color: $fontSlider-background-color; + margin-bottom: 5px; + z-index: 0; +} + +.mx_fontSlider_dotActive { + background-color: $fontSlider-selection-color; +} + +.mx_fontSlider_dotValue { + display: flex; + flex-direction: column; + align-items: center; + color: $fontSlider-background-color; +} \ No newline at end of file diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index f5f3013354..b457a8bccb 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -262,6 +262,10 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; +// FontSlider +$fontSlider-selection-color: $accent-color; +$fontSlider-background-color: #c1c9d6; + $progressbar-color: #000; $room-warning-bg-color: $yellow-background; diff --git a/src/components/structures/FontSlider.js b/src/components/structures/FontSlider.js new file mode 100644 index 0000000000..e2e36c2c9f --- /dev/null +++ b/src/components/structures/FontSlider.js @@ -0,0 +1,62 @@ +/* +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'; + +export default class Slider extends React.Component { + render() { + let dots = this.props.values.map(v => + this.props.updateFontSize(v)} + key={v} + />); + + let offset = this.offset(this.props.values, this.props.value); + + return
+
+
+
+
+
+
+
+
+
+ {dots} +
+
+
+ } + + offset(values, value) { + return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; + } +} + +class Dot extends React.Component { + render () { + let className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); + + return +
+
+ {this.props.value} +
+ + } +} \ No newline at end of file From 8b72756b8d987bb7218867dcf690ead46c0dea14 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 10:33:44 +0100 Subject: [PATCH 035/241] Use the font slider --- .../views/settings/tabs/user/StyleUserSettingsTab.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js index e7b7385f5a..d77822ceb7 100644 --- a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js @@ -20,6 +20,7 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; +import FontSlider from "../../../../structures/FontSlider"; export default class StyleUserSettingsTab extends React.Component { constructor() { @@ -219,11 +220,18 @@ export default class StyleUserSettingsTab extends React.Component { _renderFontSection() { return
{_t("Font size")} + value + 'px'} + /> this._onFontSizeChanged(ev.target.value)} /> From 647d99a17a6ff66816caee01f0258138a7a15493 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 10:33:59 +0100 Subject: [PATCH 036/241] Smooth font-size change transition --- res/css/_common.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/_common.scss b/res/css/_common.scss index 03442ca510..687a238c8e 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -19,6 +19,7 @@ limitations under the License. @import "./_font-sizes.scss"; :root { + transition: font-size 0.25s; font-size: 15px; } From 66fd0f707fc290db64ba3a729c96ceedd402258d Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 10:53:23 +0100 Subject: [PATCH 037/241] Type enforcement and comments --- src/components/structures/FontSlider.js | 35 ++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/components/structures/FontSlider.js b/src/components/structures/FontSlider.js index e2e36c2c9f..0ae0092443 100644 --- a/src/components/structures/FontSlider.js +++ b/src/components/structures/FontSlider.js @@ -15,12 +15,29 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; export default class Slider extends React.Component { + + static propTypes = { + // A callback for the new value onclick + updateFontSize: PropTypes.func, + + // The current value of the slider + value: PropTypes.number, + + // The range and values of the slider + // Currently only supports an ascending, constant interval range + values: PropTypes.arrayOf(PropTypes.number), + + // A function for formatting the the values + displayFunc: PropTypes.func, + }; + render() { let dots = this.props.values.map(v => this.props.updateFontSize(v)} key={v} />); @@ -42,20 +59,32 @@ export default class Slider extends React.Component {
} - + offset(values, value) { return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; } } class Dot extends React.Component { + + static propTypes = { + // Callback for behaviour onclick + onClick: PropTypes.func, + + // Whether the dot should appear active + active: PropTypes.bool, + + // The label on the dot + label: PropTypes.string, + } + render () { let className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); return
- {this.props.value} + {this.props.label}
} From f1130ecba11be12bbc1e1efbf2823106501727e6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 11:56:43 +0100 Subject: [PATCH 038/241] Linting. Finally set up my linter properly --- src/components/structures/FontSlider.js | 28 +++++++++---------- .../tabs/user/StyleUserSettingsTab.js | 16 ++++++----- src/fontSize.js | 10 +++---- .../controllers/FontSizeController.js | 3 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/components/structures/FontSlider.js b/src/components/structures/FontSlider.js index 0ae0092443..68173345ff 100644 --- a/src/components/structures/FontSlider.js +++ b/src/components/structures/FontSlider.js @@ -18,8 +18,8 @@ import React from 'react'; import PropTypes from 'prop-types'; export default class Slider extends React.Component { - static propTypes = { + // A callback for the new value onclick updateFontSize: PropTypes.func, @@ -32,41 +32,41 @@ export default class Slider extends React.Component { // A function for formatting the the values displayFunc: PropTypes.func, + }; render() { - let dots = this.props.values.map(v => - + this.props.updateFontSize(v)} key={v} />); - let offset = this.offset(this.props.values, this.props.value); + const offset = this.offset(this.props.values, this.props.value); return

-
-
+
+
{dots}
-
+
; } offset(values, value) { - return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; + return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; } } class Dot extends React.Component { - static propTypes = { // Callback for behaviour onclick onClick: PropTypes.func, @@ -78,14 +78,14 @@ class Dot extends React.Component { label: PropTypes.string, } - render () { - let className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); + render() { + const className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); return
{this.props.label}
- + ; } -} \ No newline at end of file +} diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js index d77822ceb7..9cca0a2ce9 100644 --- a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/StyleUserSettingsTab.js @@ -21,6 +21,8 @@ import * as sdk from "../../../../../index"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; import FontSlider from "../../../../structures/FontSlider"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import dis from "../../../../../dispatcher"; export default class StyleUserSettingsTab extends React.Component { constructor() { @@ -31,7 +33,8 @@ export default class StyleUserSettingsTab extends React.Component { ...this._calculateThemeState(), customThemeUrl: "", customThemeMessage: {isError: false, text: ""}, - } + + }; } _calculateThemeState() { @@ -95,9 +98,9 @@ export default class StyleUserSettingsTab extends React.Component { }; _onFontSizeChanged = (size) => { - let parsed_size = isNaN(parseInt(size))?SettingsStore.getDefaultValue("font_size"):parseFloat(size); - this.setState({fontSize: parsed_size}) - SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsed_size) + const parsedSize = isNaN(parseInt(size))?SettingsStore.getDefaultValue("font_size"):parseFloat(size); + this.setState({fontSize: parsedSize}); + SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsedSize); }; _onAddCustomTheme = async () => { @@ -221,7 +224,7 @@ export default class StyleUserSettingsTab extends React.Component { return
{_t("Font size")} value + 'px'} @@ -235,7 +238,6 @@ export default class StyleUserSettingsTab extends React.Component { id="font_size_field" onChange={(ev) => this._onFontSizeChanged(ev.target.value)} /> -
+
; } - } diff --git a/src/fontSize.js b/src/fontSize.js index c242fcc743..8dbdb29102 100644 --- a/src/fontSize.js +++ b/src/fontSize.js @@ -18,9 +18,9 @@ import dis from './dispatcher'; import SettingsStore from './settings/SettingsStore'; export class FontWatcher { - constructor(min_size, max_size) { - this._min_size = min_size; - this._max_size = max_size; + constructor(minSize, maxSize) { + this._min_size = minSize; + this._max_size = maxSize; this._dispatcherRef = null; } @@ -43,8 +43,8 @@ export class FontWatcher { let fontSize = this._min_size < size?size:this._min_size; fontSize = fontSize < this._max_size?fontSize:this._max_size; if (fontSize != size) { - SettingsStore.setValue("font_size", null, fontSize) + SettingsStore.setValue("font_size", null, fontSize); } document.querySelector(":root").style.fontSize = fontSize + "px"; } -} \ No newline at end of file +} diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js index 4ab2f331f1..8e855e31ec 100644 --- a/src/settings/controllers/FontSizeController.js +++ b/src/settings/controllers/FontSizeController.js @@ -18,9 +18,8 @@ import SettingController from "./SettingController"; import dis from "../../dispatcher"; export default class FontSizeController extends SettingController { - constructor() { - super() + super(); } onChange(level, roomId, newValue) { From af3858fa98ccc5ff93e2e63b372b095626126390 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 12:09:36 +0100 Subject: [PATCH 039/241] Style linting --- res/css/structures/_FontSlider.scss | 3 ++- res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/structures/_FontSlider.scss b/res/css/structures/_FontSlider.scss index 94902f6fd9..fc83bd91bc 100644 --- a/res/css/structures/_FontSlider.scss +++ b/res/css/structures/_FontSlider.scss @@ -17,6 +17,7 @@ limitations under the License. .mx_fontSlider { position: relative; margin: 0px; + @mixin mx_Settings_fullWidthField; } @@ -85,4 +86,4 @@ limitations under the License. flex-direction: column; align-items: center; color: $fontSlider-background-color; -} \ No newline at end of file +} diff --git a/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss b/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss index dd9646bd5a..f2a98ac426 100644 --- a/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss @@ -17,4 +17,4 @@ limitations under the License. .mx_StyleUserSettingsTab_themeSection .mx_Field, .mx_StyleUserSettingsTab_fontScaling .mx_Field { @mixin mx_Settings_fullWidthField; -} \ No newline at end of file +} From 7c9df04d427d419c8df747eaef69e11e988e27a6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 20:26:04 +0100 Subject: [PATCH 040/241] Use "Appearance" instead of "Style" --- src/components/views/dialogs/UserSettingsDialog.js | 6 +++--- ...StyleUserSettingsTab.js => AppearanceUserSettingsTab.js} | 2 +- src/i18n/strings/en_EN.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/components/views/settings/tabs/user/{StyleUserSettingsTab.js => AppearanceUserSettingsTab.js} (99%) diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index ee6f7e13ec..91ab203753 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -22,7 +22,7 @@ import {_t, _td} from "../../../languageHandler"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import SettingsStore from "../../../settings/SettingsStore"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; -import StyleUserSettingsTab from "../settings/tabs/user/StyleUserSettingsTab"; +import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; @@ -68,9 +68,9 @@ export default class UserSettingsDialog extends React.Component { , )); tabs.push(new Tab( - _td("Style"), + _td("Appearance"), "mx_userSettingsDialog_styleIcon", - , + , )); tabs.push(new Tab( _td("Flair"), diff --git a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js similarity index 99% rename from src/components/views/settings/tabs/user/StyleUserSettingsTab.js rename to src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 9cca0a2ce9..d09f4b3e6a 100644 --- a/src/components/views/settings/tabs/user/StyleUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -141,7 +141,7 @@ export default class StyleUserSettingsTab extends React.Component { render() { return (
-
{_t("Style")}
+
{_t("Appearance")}
{this._renderThemeSection()} {SettingsStore.getValue("feature_font_scaling") ? this._renderFontSection() : null}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bf2eaa4652..56798ff932 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2296,5 +2296,5 @@ "End": "End", "Font size": "Font size", "Font scaling": "Font scaling", - "Style": "Style" + "Appearance": "Appearance" } From b1452b5aa3bdd971aa2fa24c3bbd672ecd5d5255 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 20:47:29 +0100 Subject: [PATCH 041/241] Lint lint lint --- src/settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 23b73f740b..dd0103aa78 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -99,7 +99,7 @@ export const SETTINGS = { isFeature: true, displayName: _td("Font scaling"), supportedLevels: LEVELS_FEATURE, - default: false + default: false, }, "font_size": { displayName: _td("Font size"), From e455473d8fe1fba82c7c00603d746f8930b013e2 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 16 Apr 2020 20:54:11 +0100 Subject: [PATCH 042/241] i18n happy --- src/i18n/strings/en_EN.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 56798ff932..cff2b8cda4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -395,6 +395,8 @@ "Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.", "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", + "Font scaling": "Font scaling", + "Font size": "Font size", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", @@ -742,22 +744,23 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "Invalid theme schema.": "Invalid theme schema.", + "Error downloading theme information.": "Error downloading theme information.", + "Theme added!": "Theme added!", + "Appearance": "Appearance", + "Custom theme URL": "Custom theme URL", + "Add theme": "Add theme", + "Theme": "Theme", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other sessions until you log back in to them", - "Invalid theme schema.": "Invalid theme schema.", - "Error downloading theme information.": "Error downloading theme information.", - "Theme added!": "Theme added!", "Profile": "Profile", "Email addresses": "Email addresses", "Phone numbers": "Phone numbers", "Set a new account password...": "Set a new account password...", "Account": "Account", "Language and region": "Language and region", - "Custom theme URL": "Custom theme URL", - "Add theme": "Add theme", - "Theme": "Theme", "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.": "Agree to the identity server (%(serverName)s) Terms of Service to allow yourself to be discoverable by email address or phone number.", "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", @@ -2293,8 +2296,5 @@ "Esc": "Esc", "Enter": "Enter", "Space": "Space", - "End": "End", - "Font size": "Font size", - "Font scaling": "Font scaling", - "Appearance": "Appearance" + "End": "End" } From 787e408016e53a3293d1df336e0e2a5fc1ac50c5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 10:08:40 +0100 Subject: [PATCH 043/241] Explain origin of magic number Co-Authored-By: Travis Ralston --- res/css/structures/_TagPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 4a78c8df92..536c88be63 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -116,7 +116,7 @@ limitations under the License. position: absolute; left: -15px; border-radius: 0 3px 3px 0; - top: -8px; // (16px / 2) + top: -8px; // (16px from height / 2) } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { From 4d0cac1260af9dd9f4cdd930de47a202f0bee65b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 10:15:37 +0100 Subject: [PATCH 044/241] Render should be last method declared --- src/components/structures/FontSlider.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/structures/FontSlider.js b/src/components/structures/FontSlider.js index 68173345ff..aa4bfe42f5 100644 --- a/src/components/structures/FontSlider.js +++ b/src/components/structures/FontSlider.js @@ -35,6 +35,10 @@ export default class Slider extends React.Component { }; + _offset(values, value) { + return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; + } + render() { const dots = this.props.values.map(v => ); - const offset = this.offset(this.props.values, this.props.value); + const offset = this._offset(this.props.values, this.props.value); return
@@ -60,10 +64,6 @@ export default class Slider extends React.Component {
; } - - offset(values, value) { - return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; - } } class Dot extends React.Component { From db1141b162e441ddf324c4e708f7c7c3c55297d9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 10:46:33 +0100 Subject: [PATCH 045/241] Move to typescript --- .../{FontSlider.js => FontSlider.tsx} | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) rename src/components/structures/{FontSlider.js => FontSlider.tsx} (73%) diff --git a/src/components/structures/FontSlider.js b/src/components/structures/FontSlider.tsx similarity index 73% rename from src/components/structures/FontSlider.js rename to src/components/structures/FontSlider.tsx index aa4bfe42f5..9048e7b37b 100644 --- a/src/components/structures/FontSlider.js +++ b/src/components/structures/FontSlider.tsx @@ -14,10 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import * as React from 'react'; +import * as PropTypes from 'prop-types'; -export default class Slider extends React.Component { +type SliderProps = { + // A callback for the new value onclick + updateFontSize: (size: number) => null; + + // The current value of the slider + value: number; + + // The range and values of the slider + // Currently only supports an ascending, constant interval range + values: number[]; + + // A function for formatting the the values + displayFunc: (value: number) => string; + +} + +export default class Slider extends React.Component { static propTypes = { // A callback for the new value onclick @@ -35,11 +51,11 @@ export default class Slider extends React.Component { }; - _offset(values, value) { + _offset(values: number[], value: number): number { return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; } - render() { + render(): React.ReactNode { const dots = this.props.values.map(v => null, + + // Whether the dot should appear active + active: boolean, + + // The label on the dot + label: string, +} + +class Dot extends React.Component { static propTypes = { // Callback for behaviour onclick onClick: PropTypes.func, @@ -78,7 +105,7 @@ class Dot extends React.Component { label: PropTypes.string, } - render() { + render(): React.ReactNode { const className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); return From dd841fcde92f097a0cc056fa8f856d65c938620e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 11:01:52 +0100 Subject: [PATCH 046/241] Remove references to font --- res/css/structures/_FontSlider.scss | 20 ++++++++++---------- src/components/structures/FontSlider.tsx | 22 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/res/css/structures/_FontSlider.scss b/res/css/structures/_FontSlider.scss index fc83bd91bc..2112ac9a88 100644 --- a/res/css/structures/_FontSlider.scss +++ b/res/css/structures/_FontSlider.scss @@ -14,20 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_fontSlider { +.mx_Slider { position: relative; margin: 0px; @mixin mx_Settings_fullWidthField; } -.mx_fontSlider_dotContainer { +.mx_Slider_dotContainer { display: flex; flex-direction: row; justify-content: space-between; } -.mx_fontSlider_bar { +.mx_Slider_bar { display: flex; box-sizing: border-box; position: absolute; @@ -37,12 +37,12 @@ limitations under the License. align-items: center; } -.mx_fontSlider_bar > hr { +.mx_Slider_bar > hr { width: 100%; border: 0.2rem solid $fontSlider-background-color; } -.mx_fontSlider_selection { +.mx_Slider_selection { display: flex; align-items: center; width: calc(100% - 2.2rem); @@ -50,7 +50,7 @@ limitations under the License. position: absolute; } -.mx_fontSlider_selectionDot { +.mx_Slider_selectionDot { transition: left 0.25s; position: absolute; width: 1.1rem; @@ -61,13 +61,13 @@ limitations under the License. z-index: 10; } -.mx_fontSlider_selection > hr { +.mx_Slider_selection > hr { transition: width 0.25s; margin: 0; border: 0.2rem solid $fontSlider-selection-color; } -.mx_fontSlider_dot { +.mx_Slider_dot { transition: background-color 0.2s ease-in; height: 1rem; width: 1rem; @@ -77,11 +77,11 @@ limitations under the License. z-index: 0; } -.mx_fontSlider_dotActive { +.mx_Slider_dotActive { background-color: $fontSlider-selection-color; } -.mx_fontSlider_dotValue { +.mx_Slider_dotValue { display: flex; flex-direction: column; align-items: center; diff --git a/src/components/structures/FontSlider.tsx b/src/components/structures/FontSlider.tsx index 9048e7b37b..7985fa206a 100644 --- a/src/components/structures/FontSlider.tsx +++ b/src/components/structures/FontSlider.tsx @@ -18,8 +18,8 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; type SliderProps = { - // A callback for the new value onclick - updateFontSize: (size: number) => null; + // A callback for the selected value + onSelectionChange: (value: number) => null; // The current value of the slider value: number; @@ -37,7 +37,7 @@ export default class Slider extends React.Component { static propTypes = { // A callback for the new value onclick - updateFontSize: PropTypes.func, + onSelectionChange: PropTypes.func, // The current value of the slider value: PropTypes.number, @@ -59,22 +59,22 @@ export default class Slider extends React.Component { const dots = this.props.values.map(v => this.props.updateFontSize(v)} + onClick={() => this.props.onSelectionChange(v)} key={v} />); const offset = this._offset(this.props.values, this.props.value); - return
+ return
-
+

-
-
+
+

-
+
{dots}
@@ -106,9 +106,9 @@ class Dot extends React.Component { } render(): React.ReactNode { - const className = "mx_fontSlider_dot" + (this.props.active? " mx_fontSlider_dotActive": ""); + const className = "mx_Slider_dot" + (this.props.active? " mx_Slider_dotActive": ""); - return + return
{this.props.label} From abd94a65bd4f5a8e60478fc52d3475318b63b562 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 11:29:37 +0100 Subject: [PATCH 047/241] Move compoenets/FontSlider to views/Slider --- res/css/_components.scss | 2 +- .../structures/{_FontSlider.scss => _Slider.scss} | 12 ++++++------ res/themes/light/css/_light.scss | 6 +++--- .../FontSlider.tsx => views/elements/Slider.tsx} | 2 ++ .../settings/tabs/user/AppearanceUserSettingsTab.js | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) rename res/css/structures/{_FontSlider.scss => _Slider.scss} (84%) rename src/components/{structures/FontSlider.tsx => views/elements/Slider.tsx} (96%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 9d6629e703..ab602be49e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -7,7 +7,7 @@ @import "./structures/_CreateRoom.scss"; @import "./structures/_CustomRoomTagPanel.scss"; @import "./structures/_FilePanel.scss"; -@import "./structures/_FontSlider.scss"; +@import "./structures/_Slider.scss"; @import "./structures/_GenericErrorPage.scss"; @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; diff --git a/res/css/structures/_FontSlider.scss b/res/css/structures/_Slider.scss similarity index 84% rename from res/css/structures/_FontSlider.scss rename to res/css/structures/_Slider.scss index 2112ac9a88..51f1688f6b 100644 --- a/res/css/structures/_FontSlider.scss +++ b/res/css/structures/_Slider.scss @@ -39,7 +39,7 @@ limitations under the License. .mx_Slider_bar > hr { width: 100%; - border: 0.2rem solid $fontSlider-background-color; + border: 0.2rem solid $Slider-background-color; } .mx_Slider_selection { @@ -55,7 +55,7 @@ limitations under the License. position: absolute; width: 1.1rem; height: 1.1rem; - background-color: $fontSlider-selection-color; + background-color: $Slider-selection-color; border-radius: 50%; box-shadow: 0 0 6px lightgrey; z-index: 10; @@ -64,7 +64,7 @@ limitations under the License. .mx_Slider_selection > hr { transition: width 0.25s; margin: 0; - border: 0.2rem solid $fontSlider-selection-color; + border: 0.2rem solid $Slider-selection-color; } .mx_Slider_dot { @@ -72,18 +72,18 @@ limitations under the License. height: 1rem; width: 1rem; border-radius: 50%; - background-color: $fontSlider-background-color; + background-color: $Slider-background-color; margin-bottom: 5px; z-index: 0; } .mx_Slider_dotActive { - background-color: $fontSlider-selection-color; + background-color: $Slider-selection-color; } .mx_Slider_dotValue { display: flex; flex-direction: column; align-items: center; - color: $fontSlider-background-color; + color: $Slider-background-color; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b457a8bccb..e06ba33594 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -262,9 +262,9 @@ $togglesw-off-color: #c1c9d6; $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; -// FontSlider -$fontSlider-selection-color: $accent-color; -$fontSlider-background-color: #c1c9d6; +// Slider +$Slider-selection-color: $accent-color; +$Slider-background-color: #c1c9d6; $progressbar-color: #000; diff --git a/src/components/structures/FontSlider.tsx b/src/components/views/elements/Slider.tsx similarity index 96% rename from src/components/structures/FontSlider.tsx rename to src/components/views/elements/Slider.tsx index 7985fa206a..2070f3f167 100644 --- a/src/components/structures/FontSlider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -16,6 +16,7 @@ limitations under the License. import * as React from 'react'; import * as PropTypes from 'prop-types'; +import { replaceableComponent } from '../../../utils/replaceableComponent'; type SliderProps = { // A callback for the selected value @@ -93,6 +94,7 @@ type DotProps = { label: string, } +@replaceableComponent("views.elements.Dot") class Dot extends React.Component { static propTypes = { // Callback for behaviour onclick diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index d09f4b3e6a..9e9f134613 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -20,7 +20,7 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; -import FontSlider from "../../../../structures/FontSlider"; +import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; @@ -223,7 +223,7 @@ export default class StyleUserSettingsTab extends React.Component { _renderFontSection() { return
{_t("Font size")} - Date: Tue, 21 Apr 2020 11:34:15 +0100 Subject: [PATCH 048/241] Retain copyright Co-Authored-By: Travis Ralston --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 9e9f134613..7843ccbe10 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +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. From 82974bd98c01fc673a8fb928f7890c7acd16641b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 11:37:22 +0100 Subject: [PATCH 049/241] Space out ternaries Co-Authored-By: Travis Ralston --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 7843ccbe10..8d1fd348d3 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -99,7 +99,7 @@ export default class StyleUserSettingsTab extends React.Component { }; _onFontSizeChanged = (size) => { - const parsedSize = isNaN(parseInt(size))?SettingsStore.getDefaultValue("font_size"):parseFloat(size); + const parsedSize = isNaN(parseInt(size)) ? SettingsStore.getDefaultValue("font_size") : parseFloat(size); this.setState({fontSize: parsedSize}); SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsedSize); }; From 4525f71b1ce7925379fd9a67fab7b59f52d055b5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 11:40:18 +0100 Subject: [PATCH 050/241] Missed an import --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 8d1fd348d3..42c8e6d854 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -21,7 +21,7 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; -import Slider from "../../../elements/Slider"; +import FontSlider from "../../../../structures/FontSlider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; @@ -224,7 +224,7 @@ export default class StyleUserSettingsTab extends React.Component { _renderFontSection() { return
{_t("Font size")} - Date: Tue, 21 Apr 2020 11:41:41 +0100 Subject: [PATCH 051/241] Move setting away from 'feature' settings for clarity --- src/settings/Settings.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index dd0103aa78..a044027baf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -101,12 +101,6 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, - "font_size": { - displayName: _td("Font size"), - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 16, - controller: new FontSizeController(), - }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), @@ -177,6 +171,12 @@ export const SETTINGS = { displayName: _td("Show padlocks on invite only rooms"), default: true, }, + "font_size": { + displayName: _td("Font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 16, + controller: new FontSizeController(), + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From 4397658bb32654b3a0427d4c1b761c43109e5825 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 11:56:12 +0100 Subject: [PATCH 052/241] Update file name in comments Co-Authored-By: Travis Ralston --- src/theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme.js b/src/theme.js index 3309acdd01..aee55b5abb 100644 --- a/src/theme.js +++ b/src/theme.js @@ -81,7 +81,7 @@ export class ThemeWatcher { } getEffectiveTheme() { - // Dev note: Much of this logic is replicated in the StyleUserSettingsTab + // Dev note: Much of this logic is replicated in the AppearanceUserSettingsTab // XXX: checking the isLight flag here makes checking it in the ThemeController // itself completely redundant since we just override the result here and we're From 315a272cb4aa1026f42723e7b1877f12e4ecffbc Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 12:03:32 +0100 Subject: [PATCH 053/241] File rename --- ...rSettingsTab.scss => _AppearanceUserSettingsTab.scss} | 4 ++-- .../settings/tabs/user/AppearanceUserSettingsTab.js | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) rename res/css/views/settings/tabs/user/{_StyleUserSettingsTab.scss => _AppearanceUserSettingsTab.scss} (85%) diff --git a/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss similarity index 85% rename from res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss rename to res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index f2a98ac426..8c80a35e40 100644 --- a/res/css/views/settings/tabs/user/_StyleUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_StyleUserSettingsTab_themeSection .mx_Field, -.mx_StyleUserSettingsTab_fontScaling .mx_Field { +.mx_AppearanceUserSettingsTab_themeSection .mx_Field, +.mx_AppearanceUserSettingsTab_fontScaling .mx_Field { @mixin mx_Settings_fullWidthField; } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 42c8e6d854..738a5f9178 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -2,6 +2,7 @@ Copyright 2019 New Vector Ltd 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. You may obtain a copy of the License at @@ -21,7 +22,7 @@ import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore import * as sdk from "../../../../../index"; import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; -import FontSlider from "../../../../structures/FontSlider"; +import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; @@ -204,7 +205,7 @@ export default class StyleUserSettingsTab extends React.Component { .sort((a, b) => a.name.localeCompare(b.name)); const orderedThemes = [...builtInThemes, ...customThemes]; return ( -
+
{_t("Theme")} {systemThemeSection} + return
{_t("Font size")} - Date: Tue, 21 Apr 2020 12:06:10 +0100 Subject: [PATCH 054/241] Move slider themes --- res/css/_components.scss | 3 ++- res/css/{structures => views/elements}/_Slider.scss | 0 2 files changed, 2 insertions(+), 1 deletion(-) rename res/css/{structures => views/elements}/_Slider.scss (100%) diff --git a/res/css/_components.scss b/res/css/_components.scss index ab602be49e..77a9b9f8cf 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -7,7 +7,6 @@ @import "./structures/_CreateRoom.scss"; @import "./structures/_CustomRoomTagPanel.scss"; @import "./structures/_FilePanel.scss"; -@import "./structures/_Slider.scss"; @import "./structures/_GenericErrorPage.scss"; @import "./structures/_GroupView.scss"; @import "./structures/_HeaderButtons.scss"; @@ -114,6 +113,7 @@ @import "./views/elements/_RichText.scss"; @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_RoomAliasField.scss"; +@import "./views/elements/_Slider.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; @import "./views/elements/_TextWithTooltip.scss"; @@ -202,6 +202,7 @@ @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; +@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; @import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; @import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss"; diff --git a/res/css/structures/_Slider.scss b/res/css/views/elements/_Slider.scss similarity index 100% rename from res/css/structures/_Slider.scss rename to res/css/views/elements/_Slider.scss From dcea1f32b38c1856f05790a2eddcb014a498a486 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 16:18:25 +0100 Subject: [PATCH 055/241] tslint --- src/components/views/elements/Slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 2070f3f167..f6ab121056 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -58,7 +58,7 @@ export default class Slider extends React.Component { render(): React.ReactNode { const dots = this.props.values.map(v => - this.props.onSelectionChange(v)} key={v} @@ -108,7 +108,7 @@ class Dot extends React.Component { } render(): React.ReactNode { - const className = "mx_Slider_dot" + (this.props.active? " mx_Slider_dotActive": ""); + const className = "mx_Slider_dot" + (this.props.active ? " mx_Slider_dotActive" : ""); return
From 715bcb3c96e343b6c2a83d0ba21c16d2257e5be8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 21 Apr 2020 16:28:41 +0100 Subject: [PATCH 056/241] i18n match file moves --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cff2b8cda4..a3051cbb91 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -396,7 +396,6 @@ "Please contact your homeserver administrator.": "Please contact your homeserver administrator.", "Failed to join room": "Failed to join room", "Font scaling": "Font scaling", - "Font size": "Font size", "Message Pinning": "Message Pinning", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", @@ -407,6 +406,7 @@ "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Show padlocks on invite only rooms": "Show padlocks on invite only rooms", + "Font size": "Font size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", From 0d0da6cfdc8d822cdcd8e6c14615aabc41724abf Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 10:19:17 +0100 Subject: [PATCH 057/241] Fix types, abandon propTypes --- src/components/views/elements/Slider.tsx | 45 ++++-------------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index f6ab121056..13f06a4759 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -15,12 +15,10 @@ limitations under the License. */ import * as React from 'react'; -import * as PropTypes from 'prop-types'; -import { replaceableComponent } from '../../../utils/replaceableComponent'; -type SliderProps = { +type IProps = { // A callback for the selected value - onSelectionChange: (value: number) => null; + onSelectionChange: (value: number) => void; // The current value of the slider value: number; @@ -34,24 +32,7 @@ type SliderProps = { } -export default class Slider extends React.Component { - static propTypes = { - - // A callback for the new value onclick - onSelectionChange: PropTypes.func, - - // The current value of the slider - value: PropTypes.number, - - // The range and values of the slider - // Currently only supports an ascending, constant interval range - values: PropTypes.arrayOf(PropTypes.number), - - // A function for formatting the the values - displayFunc: PropTypes.func, - - }; - +export default class Slider extends React.Component { _offset(values: number[], value: number): number { return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; } @@ -83,9 +64,9 @@ export default class Slider extends React.Component { } } -type DotProps = { - // Callback for behaviour onclick - onClick: () => null, +type DotIProps = { + // Callback for behavior onclick + onClick: () => void, // Whether the dot should appear active active: boolean, @@ -94,19 +75,7 @@ type DotProps = { label: string, } -@replaceableComponent("views.elements.Dot") -class Dot extends React.Component { - static propTypes = { - // Callback for behaviour onclick - onClick: PropTypes.func, - - // Whether the dot should appear active - active: PropTypes.bool, - - // The label on the dot - label: PropTypes.string, - } - +class Dot extends React.Component { render(): React.ReactNode { const className = "mx_Slider_dot" + (this.props.active ? " mx_Slider_dotActive" : ""); From ba362b727c5bdf7b6a76e8fdb3b5cdf1c3afdbc0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 10:19:37 +0100 Subject: [PATCH 058/241] Use onSelectionChange prop --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 738a5f9178..5bb6dcc0e0 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -228,7 +228,7 @@ export default class StyleUserSettingsTab extends React.Component { value + 'px'} /> Date: Wed, 22 Apr 2020 10:24:29 +0100 Subject: [PATCH 059/241] Clamp indicated value within value range --- src/components/views/elements/Slider.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 13f06a4759..9f9e1fdef8 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -34,6 +34,17 @@ type IProps = { export default class Slider extends React.Component { _offset(values: number[], value: number): number { + const lowest = values[0]; + const highest = values[values.length - 1]; + + if (value < lowest) { + return 0; + } + + if (value > highest) { + return 100; + } + return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; } From 54a65441a54da04f6c4834fe089ac932778d0952 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 10:29:48 +0100 Subject: [PATCH 060/241] Lint ternary statement --- src/fontSize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontSize.js b/src/fontSize.js index 8dbdb29102..d6b5b020f7 100644 --- a/src/fontSize.js +++ b/src/fontSize.js @@ -40,8 +40,8 @@ export class FontWatcher { }; _setRootFontSize = size => { - let fontSize = this._min_size < size?size:this._min_size; - fontSize = fontSize < this._max_size?fontSize:this._max_size; + let fontSize = this._min_size < size ? size : this._min_size; + fontSize = fontSize < this._max_size ? fontSize : this._max_size; if (fontSize != size) { SettingsStore.setValue("font_size", null, fontSize); } From 8d5965c33c5af0350da208ac8cfe8706dae049ce Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 10:32:00 +0100 Subject: [PATCH 061/241] Fix incorrect call to setValue --- src/fontSize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontSize.js b/src/fontSize.js index d6b5b020f7..30c69b7428 100644 --- a/src/fontSize.js +++ b/src/fontSize.js @@ -15,7 +15,7 @@ limitations under the License. */ import dis from './dispatcher'; -import SettingsStore from './settings/SettingsStore'; +import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { constructor(minSize, maxSize) { @@ -43,7 +43,7 @@ export class FontWatcher { let fontSize = this._min_size < size ? size : this._min_size; fontSize = fontSize < this._max_size ? fontSize : this._max_size; if (fontSize != size) { - SettingsStore.setValue("font_size", null, fontSize); + SettingsStore.setValue("font_size", null, SettingLevel.Device, fontSize); } document.querySelector(":root").style.fontSize = fontSize + "px"; } From 26ccd6f07dcb598a04643e82412f6f801e8c65e9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 10:42:31 +0100 Subject: [PATCH 062/241] Cleaner clamping of value range --- src/components/views/elements/Slider.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 9f9e1fdef8..7862373c1c 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -34,18 +34,13 @@ type IProps = { export default class Slider extends React.Component { _offset(values: number[], value: number): number { - const lowest = values[0]; - const highest = values[values.length - 1]; + const min = values[0]; + const max = values[values.length - 1]; - if (value < lowest) { - return 0; - } + // Clamp value between min and max + value = Math.min(Math.max(value, min), max); - if (value > highest) { - return 100; - } - - return (value - values[0]) / (values[values.length - 1] - values[0]) * 100; + return (value - min) / (max - min) * 100; } render(): React.ReactNode { From 5f50facfba28291514b258de1d26b4e691d5692a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 11:36:23 +0100 Subject: [PATCH 063/241] Make slider independant of label size --- res/css/views/elements/_Slider.scss | 16 ++++++++++++++-- src/components/views/elements/Slider.tsx | 4 +++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 51f1688f6b..7e9acefa06 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -33,7 +33,7 @@ limitations under the License. position: absolute; height: 1rem; width: 100%; - padding: 0 1.1rem; + padding: 0 0.5rem; // half the width of a dot. align-items: center; } @@ -45,7 +45,7 @@ limitations under the License. .mx_Slider_selection { display: flex; align-items: center; - width: calc(100% - 2.2rem); + width: calc(100% - 1rem); // 2 * half the width of a dot height: 1rem; position: absolute; } @@ -87,3 +87,15 @@ limitations under the License. align-items: center; color: $Slider-background-color; } + +// The following is a hack to center the labels without adding +// any width to the slider's dots. +.mx_Slider_labelContainer { + width: 1rem; +} + +.mx_Slider_label { + position: relative; + width: fit-content; + left: -50%; +} diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 7862373c1c..e341eea317 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -87,9 +87,11 @@ class Dot extends React.Component { return
-
+
+
{this.props.label}
+
; } } From ee33fc1c20e93ee35120cd70d4c0caf156154cd0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 11:37:02 +0100 Subject: [PATCH 064/241] Remove labels --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 5bb6dcc0e0..046184da69 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -229,7 +229,7 @@ export default class StyleUserSettingsTab extends React.Component { values={[12, 14, 16, 18, 20]} value={this.state.fontSize} onSelectionChange={this._onFontSizeChanged} - displayFunc={value => value + 'px'} + displayFunc={value => {}} /> Date: Wed, 22 Apr 2020 11:53:29 +0100 Subject: [PATCH 065/241] Add support to disable slider --- src/components/views/elements/Slider.tsx | 28 +++++++++++++++++------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index e341eea317..ad859bfe82 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -30,6 +30,8 @@ type IProps = { // A function for formatting the the values displayFunc: (value: number) => string; + // Whether the slider is disabled + disabled: boolean; } export default class Slider extends React.Component { @@ -47,8 +49,9 @@ export default class Slider extends React.Component { const dots = this.props.values.map(v => this.props.onSelectionChange(v)} + onClick={this.props.disabled ? () => {} : () => this.props.onSelectionChange(v)} key={v} + disabled={this.props.disabled} />); const offset = this._offset(this.props.values, this.props.value); @@ -57,10 +60,13 @@ export default class Slider extends React.Component {

-
-
-
-
+ { this.props.disabled ? + null : +
+
+
+
+ }
{dots} @@ -79,18 +85,24 @@ type DotIProps = { // The label on the dot label: string, + + // Whether the slider is disabled + disabled: boolean; } class Dot extends React.Component { render(): React.ReactNode { - const className = "mx_Slider_dot" + (this.props.active ? " mx_Slider_dotActive" : ""); + let className = "mx_Slider_dot" + if (!this.props.disabled && this.props.active) { + className += " mx_Slider_dotActive"; + } return
- {this.props.label} -
+ {this.props.label} +
; } From 1486beeaf42903fe75168a16ee3c45e7f516eb39 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 12:02:23 +0100 Subject: [PATCH 066/241] Make slider indpendent of settings styling --- res/css/views/elements/_Slider.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 7e9acefa06..2132381591 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -17,8 +17,6 @@ limitations under the License. .mx_Slider { position: relative; margin: 0px; - - @mixin mx_Settings_fullWidthField; } .mx_Slider_dotContainer { From 98799611cf31b5e2036a61ecac60d9b46d4f29b1 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 15:37:46 +0100 Subject: [PATCH 067/241] Remove padding for alignment reasons --- res/css/views/elements/_Slider.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 2132381591..83f100ff92 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -17,6 +17,7 @@ limitations under the License. .mx_Slider { position: relative; margin: 0px; + flex-grow: 1; } .mx_Slider_dotContainer { @@ -71,7 +72,6 @@ limitations under the License. width: 1rem; border-radius: 50%; background-color: $Slider-background-color; - margin-bottom: 5px; z-index: 0; } From f5d65907512971f4592f9be67d7c6ab2917ff0ef Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 16:11:01 +0100 Subject: [PATCH 068/241] Have max and min font configured in settings --- src/components/structures/MatrixChat.js | 2 +- src/fontSize.js | 11 ++++++----- src/settings/Settings.js | 10 ++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 1845e0011d..602d85f048 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -266,7 +266,7 @@ export default createReactClass({ this.dispatcherRef = dis.register(this.onAction); this._themeWatcher = new ThemeWatcher(); - this._fontWatcher = new FontWatcher(10, 20); + this._fontWatcher = new FontWatcher(); this._themeWatcher.start(); this._fontWatcher.start(); diff --git a/src/fontSize.js b/src/fontSize.js index 30c69b7428..2e37921ee6 100644 --- a/src/fontSize.js +++ b/src/fontSize.js @@ -18,9 +18,7 @@ import dis from './dispatcher'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { - constructor(minSize, maxSize) { - this._min_size = minSize; - this._max_size = maxSize; + constructor() { this._dispatcherRef = null; } @@ -40,8 +38,11 @@ export class FontWatcher { }; _setRootFontSize = size => { - let fontSize = this._min_size < size ? size : this._min_size; - fontSize = fontSize < this._max_size ? fontSize : this._max_size; + const min = SettingsStore.getValue("font_size_min"); + const max = SettingsStore.getValue("font_size_max"); + + const fontSize = Math.max(Math.min(max, size), min); + if (fontSize != size) { SettingsStore.setValue("font_size", null, SettingLevel.Device, fontSize); } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index a044027baf..b144b07e84 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -177,6 +177,16 @@ export const SETTINGS = { default: 16, controller: new FontSizeController(), }, + "font_size_min": { + displayName: _td("Min font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 14, + }, + "font_size_max": { + displayName: _td("Max font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: 24, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From fe175bb9a89d87307805582040e1f3295a2d0475 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 22 Apr 2020 17:31:49 +0100 Subject: [PATCH 069/241] Styling for the font slider --- .../tabs/user/_AppearanceUserSettingsTab.scss | 20 +++++++++++++++++++ .../tabs/user/AppearanceUserSettingsTab.js | 18 +++++++++++------ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 8c80a35e40..e4285e248c 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -14,7 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_AppearanceUserSettingsTab_fontSlider, .mx_AppearanceUserSettingsTab_themeSection .mx_Field, .mx_AppearanceUserSettingsTab_fontScaling .mx_Field { @mixin mx_Settings_fullWidthField; } + +.mx_AppearanceUserSettingsTab_fontSlider { + display: flex; + flex-direction: row; + align-items: center; + padding: 10px; + background: #FFFFFF; + border-radius: 10px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_smallText { + font-size: 15px; + padding-right: 10px; +} + +.mx_AppearanceUserSettingsTab_fontSlider_largeText { + font-size: 18px; + padding-left: 10px; +} diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 046184da69..e1bbaab2cc 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -25,6 +25,7 @@ import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; +import _range from "lodash/range"; export default class StyleUserSettingsTab extends React.Component { constructor() { @@ -225,12 +226,17 @@ export default class StyleUserSettingsTab extends React.Component { _renderFontSection() { return
{_t("Font size")} - {}} - /> +
+
Aa
+ {}} + disabled={false} + /> +
Aa
+
Date: Thu, 23 Apr 2020 10:27:41 +0100 Subject: [PATCH 070/241] Linearly interpolate between value intervals. --- src/components/views/elements/Slider.tsx | 36 ++++++++++++++++--- .../tabs/user/AppearanceUserSettingsTab.js | 2 +- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index ad859bfe82..a9fc41c8cc 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -35,14 +35,40 @@ type IProps = { } export default class Slider extends React.Component { + // offset is a terrible inverse approximation. + // if the values represents some function f(x) = y where x is the + // index of the array and y = values[x] then offset(f, y) = x + // s.t f(x) = y. + // it assumes a monotonic function and interpolates linearly between + // y values. + // Offset is used for finding the location of a value on a + // non linear slider. _offset(values: number[], value: number): number { - const min = values[0]; - const max = values[values.length - 1]; + // the index of the first number greater than value. + let closest = values.reduce((prev, curr) => { + return (value > curr ? prev + 1 : prev); + }, 0); - // Clamp value between min and max - value = Math.min(Math.max(value, min), max); + // Off the left + if (closest == 0) { + return 0; + } + + // Off the right + if (closest == values.length) { + return 100; + } + + // Now + const closestLessValue = values[closest - 1]; + const closestGreaterValue = values[closest]; + + const intervalWidth = 1 / (values.length - 1); + + const linearInterpolation = (value - closestLessValue) / (closestGreaterValue - closestLessValue) + + return 100 * (closest - 1 + linearInterpolation) * intervalWidth - return (value - min) / (max - min) * 100; } render(): React.ReactNode { diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index e1bbaab2cc..949b3bed31 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -229,7 +229,7 @@ export default class StyleUserSettingsTab extends React.Component {
Aa
{}} From a16fe09d4275b64939d34f56b1720cb00ef1e93e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 10:58:00 +0100 Subject: [PATCH 071/241] Use em to detach slider from root font-size --- res/css/views/elements/_Slider.scss | 22 +++++++++---------- .../tabs/user/_AppearanceUserSettingsTab.scss | 1 + src/components/views/elements/Slider.tsx | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 83f100ff92..f6982865db 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -30,30 +30,30 @@ limitations under the License. display: flex; box-sizing: border-box; position: absolute; - height: 1rem; + height: 1em; width: 100%; - padding: 0 0.5rem; // half the width of a dot. + padding: 0 0.5em; // half the width of a dot. align-items: center; } .mx_Slider_bar > hr { width: 100%; - border: 0.2rem solid $Slider-background-color; + border: 0.2em solid $Slider-background-color; } .mx_Slider_selection { display: flex; align-items: center; - width: calc(100% - 1rem); // 2 * half the width of a dot - height: 1rem; + width: calc(100% - 1em); // 2 * half the width of a dot + height: 1em; position: absolute; } .mx_Slider_selectionDot { transition: left 0.25s; position: absolute; - width: 1.1rem; - height: 1.1rem; + width: 1.1em; + height: 1.1em; background-color: $Slider-selection-color; border-radius: 50%; box-shadow: 0 0 6px lightgrey; @@ -63,13 +63,13 @@ limitations under the License. .mx_Slider_selection > hr { transition: width 0.25s; margin: 0; - border: 0.2rem solid $Slider-selection-color; + border: 0.2em solid $Slider-selection-color; } .mx_Slider_dot { transition: background-color 0.2s ease-in; - height: 1rem; - width: 1rem; + height: 1em; + width: 1em; border-radius: 50%; background-color: $Slider-background-color; z-index: 0; @@ -89,7 +89,7 @@ limitations under the License. // The following is a hack to center the labels without adding // any width to the slider's dots. .mx_Slider_labelContainer { - width: 1rem; + width: 1em; } .mx_Slider_label { diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index e4285e248c..28a2510508 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -27,6 +27,7 @@ limitations under the License. padding: 10px; background: #FFFFFF; border-radius: 10px; + font-size: 10px; } .mx_AppearanceUserSettingsTab_fontSlider_smallText { diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index a9fc41c8cc..6ec044da41 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -89,7 +89,7 @@ export default class Slider extends React.Component { { this.props.disabled ? null :
-
+

} From 6375e2526324de37ee170637c494bf08a29c4e59 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 11:22:51 +0100 Subject: [PATCH 072/241] Match padding from figma --- .../settings/tabs/user/_AppearanceUserSettingsTab.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 28a2510508..16a14edf85 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -24,7 +24,7 @@ limitations under the License. display: flex; flex-direction: row; align-items: center; - padding: 10px; + padding: 15px; background: #FFFFFF; border-radius: 10px; font-size: 10px; @@ -32,10 +32,12 @@ limitations under the License. .mx_AppearanceUserSettingsTab_fontSlider_smallText { font-size: 15px; - padding-right: 10px; + padding-right: 20px; + padding-left: 5px; } .mx_AppearanceUserSettingsTab_fontSlider_largeText { font-size: 18px; - padding-left: 10px; + padding-left: 20px; + padding-right: 5px; } From 28dca9e52529b3b8f49ee0e92cf448a4875fa403 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 11:33:28 +0100 Subject: [PATCH 073/241] Match figma color scheme --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 2 +- res/themes/dark/css/_dark.scss | 3 +++ res/themes/light/css/_light.scss | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 16a14edf85..4141fb2fb1 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -25,7 +25,7 @@ limitations under the License. flex-direction: row; align-items: center; padding: 15px; - background: #FFFFFF; + background: $font-slider-bg-color; border-radius: 10px; font-size: 10px; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 5d6ba033c8..cb6e7ccdaa 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -177,6 +177,9 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: $room-highlight-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index e06ba33594..b576b57778 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -306,6 +306,9 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; +// FontSlider colors +$font-slider-bg-color: $input-darker-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { From a83993f1ff18f8c7be1d0949a8c983bbecf2ee9d Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 11:49:54 +0100 Subject: [PATCH 074/241] Match margins in settings --- .../views/settings/tabs/user/_AppearanceUserSettingsTab.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss index 4141fb2fb1..e82ae3c575 100644 --- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss @@ -28,6 +28,8 @@ limitations under the License. background: $font-slider-bg-color; border-radius: 10px; font-size: 10px; + margin-top: 24px; + margin-bottom: 24px; } .mx_AppearanceUserSettingsTab_fontSlider_smallText { From c86638c667d023abccb1250825e5c17e1070991c Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 12:09:08 +0100 Subject: [PATCH 075/241] add toggle between font slider and custom setting --- .../settings/tabs/user/AppearanceUserSettingsTab.js | 11 +++++++++-- src/i18n/strings/en_EN.json | 3 +++ src/settings/Settings.js | 5 +++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 949b3bed31..ceb3241b8b 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -36,7 +36,7 @@ export default class StyleUserSettingsTab extends React.Component { ...this._calculateThemeState(), customThemeUrl: "", customThemeMessage: {isError: false, text: ""}, - + useCustomFontSize: SettingsStore.getValue("useCustomFontSize"), }; } @@ -224,6 +224,7 @@ export default class StyleUserSettingsTab extends React.Component { } _renderFontSection() { + const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return
{_t("Font size")}
@@ -233,10 +234,15 @@ export default class StyleUserSettingsTab extends React.Component { value={this.state.fontSize} onSelectionChange={this._onFontSizeChanged} displayFunc={value => {}} - disabled={false} + disabled={this.state.useCustomFontSize} />
Aa
+ this.setState({useCustomFontSize: checked})} + /> this._onFontSizeChanged(ev.target.value)} + disabled={!this.state.useCustomFontSize} />
; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a3051cbb91..2c3239900d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -407,6 +407,9 @@ "Show info about bridges in room settings": "Show info about bridges in room settings", "Show padlocks on invite only rooms": "Show padlocks on invite only rooms", "Font size": "Font size", + "Min font size": "Min font size", + "Max font size": "Max font size", + "Custom font size": "Custom font size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b144b07e84..e0e34179f3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -187,6 +187,11 @@ export const SETTINGS = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: 24, }, + "useCustomFontSize": { + displayName: _td("Custom font size"), + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From 600a812227acafd8a15732cdabed3b6899415735 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 12:20:10 +0100 Subject: [PATCH 076/241] Add brush icon for appearance setting tab --- res/css/views/dialogs/_UserSettingsDialog.scss | 4 ++++ res/img/feather-customised/brush.svg | 5 +++++ src/components/views/dialogs/UserSettingsDialog.js | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 res/img/feather-customised/brush.svg diff --git a/res/css/views/dialogs/_UserSettingsDialog.scss b/res/css/views/dialogs/_UserSettingsDialog.scss index 4d831d7858..7adcc58c4e 100644 --- a/res/css/views/dialogs/_UserSettingsDialog.scss +++ b/res/css/views/dialogs/_UserSettingsDialog.scss @@ -21,6 +21,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/settings.svg'); } +.mx_UserSettingsDialog_appearanceIcon::before { + mask-image: url('$(res)/img/feather-customised/brush.svg'); +} + .mx_UserSettingsDialog_voiceIcon::before { mask-image: url('$(res)/img/feather-customised/phone.svg'); } diff --git a/res/img/feather-customised/brush.svg b/res/img/feather-customised/brush.svg new file mode 100644 index 0000000000..d7f2738629 --- /dev/null +++ b/res/img/feather-customised/brush.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 91ab203753..bf06b8749f 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -69,7 +69,7 @@ export default class UserSettingsDialog extends React.Component { )); tabs.push(new Tab( _td("Appearance"), - "mx_userSettingsDialog_styleIcon", + "mx_UserSettingsDialog_appearanceIcon", , )); tabs.push(new Tab( From e5cb14929602cdc71887b3e95c54f546d9ccdda0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 13:52:08 +0100 Subject: [PATCH 077/241] Handle fontslider input errors correctly --- .../tabs/user/AppearanceUserSettingsTab.js | 33 ++++++++++++++++--- src/i18n/strings/en_EN.json | 3 ++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index ceb3241b8b..4144605999 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -101,11 +101,33 @@ export default class StyleUserSettingsTab extends React.Component { }; _onFontSizeChanged = (size) => { - const parsedSize = isNaN(parseInt(size)) ? SettingsStore.getDefaultValue("font_size") : parseFloat(size); - this.setState({fontSize: parsedSize}); - SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, parsedSize); + this.setState({fontSize: size}); + SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, size); }; + _onValidateFontSize = ({value}) => { + console.log({value}); + this.setState({fontSize: value}); + + const parsedSize = parseFloat(value); + const min = SettingsStore.getValue("font_size_min"); + const max = SettingsStore.getValue("font_size_max"); + + if (isNaN(parsedSize)) { + return {valid: false, feedback: _t("Size must be a number")}; + } + + console.log({min}); + console.log({max}); + console.log({parsedSize}); + if (!(min <= parsedSize && parsedSize <= max)) { + return {valid: false, feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max})}; + } + + SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, value); + return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; + } + _onAddCustomTheme = async () => { let currentThemes = SettingsStore.getValue("custom_themes"); if (!currentThemes) currentThemes = []; @@ -247,10 +269,11 @@ export default class StyleUserSettingsTab extends React.Component { type="text" label={_t("Font size")} autoComplete="off" - placeholder={SettingsStore.getValue("font_size", null).toString()} + placeholder={this.state.fontSize} value={this.state.fontSize} id="font_size_field" - onChange={(ev) => this._onFontSizeChanged(ev.target.value)} + onValidate={this._onValidateFontSize} + onChange={({value}) => this.setState({fontSize: value})} disabled={!this.state.useCustomFontSize} />
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2c3239900d..fa3d5e3f41 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -747,6 +747,9 @@ "Use an Integration Manager to manage bots, widgets, and sticker packs.": "Use an Integration Manager to manage bots, widgets, and sticker packs.", "Manage integrations": "Manage integrations", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.", + "Size must be a number": "Size must be a number", + "Custom font size can only be between %(min)s pt and %(max)s pt": "Custom font size can only be between %(min)s pt and %(max)s pt", + "Use between %(min)s pt and %(max)s pt": "Use between %(min)s pt and %(max)s pt", "Invalid theme schema.": "Invalid theme schema.", "Error downloading theme information.": "Error downloading theme information.", "Theme added!": "Theme added!", From a087f5ea400fd7a8fc7e6f207fdb15def6e4e2f3 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 13:55:10 +0100 Subject: [PATCH 078/241] Lint --- .../tabs/user/AppearanceUserSettingsTab.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 4144605999..5c285d12e6 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -117,11 +117,11 @@ export default class StyleUserSettingsTab extends React.Component { return {valid: false, feedback: _t("Size must be a number")}; } - console.log({min}); - console.log({max}); - console.log({parsedSize}); if (!(min <= parsedSize && parsedSize <= max)) { - return {valid: false, feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max})}; + return { + valid: false, + feedback: _t('Custom font size can only be between %(min)s pt and %(max)s pt', {min, max}), + }; } SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, value); @@ -252,7 +252,11 @@ export default class StyleUserSettingsTab extends React.Component {
Aa
{}} From 06f4eca05d51aaff8986406296ccb0844a66dfe4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 14:15:33 +0100 Subject: [PATCH 079/241] Background opacity --- res/themes/light/css/_light.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index b576b57778..ed7eae48f7 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -307,7 +307,7 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; // FontSlider colors -$font-slider-bg-color: $input-darker-bg-color; +$font-slider-bg-color: rgba($input-darker-bg-color, 0.2); // ***** Mixins! ***** From 4b4599c1d81b4200f41158cb8febe2fcb9121c3a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 23 Apr 2020 14:39:11 +0100 Subject: [PATCH 080/241] tslint --- src/components/views/elements/Slider.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 6ec044da41..559bdd9ce2 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -36,7 +36,7 @@ type IProps = { export default class Slider extends React.Component { // offset is a terrible inverse approximation. - // if the values represents some function f(x) = y where x is the + // if the values represents some function f(x) = y where x is the // index of the array and y = values[x] then offset(f, y) = x // s.t f(x) = y. // it assumes a monotonic function and interpolates linearly between @@ -50,16 +50,16 @@ export default class Slider extends React.Component { }, 0); // Off the left - if (closest == 0) { + if (closest === 0) { return 0; } // Off the right - if (closest == values.length) { + if (closest === values.length) { return 100; } - // Now + // Now const closestLessValue = values[closest - 1]; const closestGreaterValue = values[closest]; From 3962c98c9beaeee3f1685e63fe3604ed8a1a68eb Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Thu, 23 Apr 2020 22:53:02 +0300 Subject: [PATCH 081/241] Ensure PersistedElements are refreshed when AuxPanel scrolls If the screen is not tall enough, AuxPanel starts scrolling its content. If it contains PersistedElements, they need to be notified about scrolling as they only listen on resize events to move their element. Signed-off-by: Pauli Virtanen --- src/components/views/rooms/AuxPanel.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index e102b0dba4..00bdb1c45a 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -141,6 +141,15 @@ export default createReactClass({ return counters; }, + _onScroll: function(rect) { + if (this.props.onResize) { + this.props.onResize(); + } + + /* Force refresh of PersistedElements which may be partially hidden */ + window.dispatchEvent(new Event('resize')); + }, + render: function() { const CallView = sdk.getComponent("voip.CallView"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); @@ -265,7 +274,7 @@ export default createReactClass({ } return ( - + { stateViews } { appsDrawer } { fileDropTarget } From d690d4bed2cb1562fface956ed0c4e34e4e35c54 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Tue, 21 Apr 2020 01:19:39 +0300 Subject: [PATCH 082/241] Prevent PersistedElements overflowing scrolled areas As the DOM element is not in reality contained inside "the parent", it may overflow the area if the parent gets partially hidden by scrolling etc. To make the effect visually less annoying, emulate this by clipping to the element wrapper. This is not a full general-purpose fix, but improves the current situation. Signed-off-by: Pauli Virtanen --- res/css/views/rooms/_AppsDrawer.scss | 4 ++ .../views/elements/PersistedElement.js | 58 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 1b1bab67bc..e4743f189e 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -96,6 +96,10 @@ $AppsDrawerBodyHeight: 273px; height: $AppsDrawerBodyHeight; } +.mx_AppTile_persistedWrapper > div { + height: 100%; +} + .mx_AppTile_mini .mx_AppTile_persistedWrapper { height: 114px; } diff --git a/src/components/views/elements/PersistedElement.js b/src/components/views/elements/PersistedElement.js index 53f2501f19..18fa2aafef 100644 --- a/src/components/views/elements/PersistedElement.js +++ b/src/components/views/elements/PersistedElement.js @@ -156,16 +156,70 @@ export default class PersistedElement extends React.Component { child.style.display = visible ? 'block' : 'none'; } + /* + * Clip element bounding rectangle to that of the parent elements. + * This is not a full visibility check, but prevents the persisted + * element from overflowing parent containers when inside a scrolled + * area. + */ + _getClippedBoundingClientRect(element) { + let parentElement = element.parentElement; + let rect = element.getBoundingClientRect(); + + rect = new DOMRect(rect.left, rect.top, rect.width, rect.height); + + while (parentElement) { + const parentRect = parentElement.getBoundingClientRect(); + + if (parentRect.left > rect.left) { + rect.width = rect.width - (parentRect.left - rect.left); + rect.x = parentRect.x; + } + + if (parentRect.top > rect.top) { + rect.height = rect.height - (parentRect.top - rect.top); + rect.y = parentRect.y; + } + + if (parentRect.right < rect.right) { + rect.width = rect.width - (rect.right - parentRect.right); + } + + if (parentRect.bottom < rect.bottom) { + rect.height = rect.height - (rect.bottom - parentRect.bottom); + } + + parentElement = parentElement.parentElement; + } + + if (rect.width < 0) rect.width = 0; + if (rect.height < 0) rect.height = 0; + + return rect; + } + updateChildPosition(child, parent) { if (!child || !parent) return; const parentRect = parent.getBoundingClientRect(); + const clipRect = this._getClippedBoundingClientRect(parent); + + Object.assign(child.parentElement.style, { + position: 'absolute', + top: clipRect.top + 'px', + left: clipRect.left + 'px', + width: clipRect.width + 'px', + height: clipRect.height + 'px', + overflow: "hidden", + }); + Object.assign(child.style, { position: 'absolute', - top: parentRect.top + 'px', - left: parentRect.left + 'px', + top: (parentRect.top - clipRect.top) + 'px', + left: (parentRect.left - clipRect.left) + 'px', width: parentRect.width + 'px', height: parentRect.height + 'px', + overflow: "hidden", }); } From bfba5e6cfe8e2be24a135af072a6c0b2a41dbfbb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 27 Apr 2020 16:57:38 +0100 Subject: [PATCH 083/241] Fix member info avatar size --- src/components/views/avatars/BaseAvatar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index a17124b9ad..cbe083f3a2 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -208,8 +208,8 @@ export default createReactClass({ onClick={onClick} onError={this.onError} style={{ - width: toRem(width), - height: toRem(height) + width: {width}, + height: {height}, }} title={title} alt="" inputRef={inputRef} From a8407c9508a38b93465ab030e4d99ab8c86212ce Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:00:15 +0100 Subject: [PATCH 084/241] Use purecomponent Co-Authored-By: Travis Ralston --- src/components/views/elements/Slider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 559bdd9ce2..3dfd0c686e 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -116,7 +116,7 @@ type DotIProps = { disabled: boolean; } -class Dot extends React.Component { +class Dot extends React.PureComponent { render(): React.ReactNode { let className = "mx_Slider_dot" if (!this.props.disabled && this.props.active) { From c268b98ded295f9679ddf7eddef436a67ab86bb3 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:17:50 +0100 Subject: [PATCH 085/241] Use faster lookup method Co-Authored-By: Travis Ralston --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 5c285d12e6..ed7d9ef495 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -168,7 +168,7 @@ export default class StyleUserSettingsTab extends React.Component {
{_t("Appearance")}
{this._renderThemeSection()} - {SettingsStore.getValue("feature_font_scaling") ? this._renderFontSection() : null} + {SettingsStore.isFeatureEnabled("feature_font_scaling") ? this._renderFontSection() : null}
); } From f91613f112d2b33839f32eb5500fd3a95f796b95 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 13:53:16 +0100 Subject: [PATCH 086/241] Remove redundent selectors. Check _AppearanceUserSettingsTab --- res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 45aecd032f..5cc220bd33 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -15,8 +15,6 @@ limitations under the License. */ .mx_GeneralUserSettingsTab_changePassword .mx_Field, -.mx_StyleUserSettingsTab_themeSection .mx_Field, -.mx_StyleUserSettingsTab_fontScaling .mx_Field { @mixin mx_Settings_fullWidthField; } From 137b94703aad9344b5f4ab38d4b6e7e441396ab9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 13:59:00 +0100 Subject: [PATCH 087/241] Lint types --- src/components/views/elements/Slider.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 3dfd0c686e..722401801c 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -16,22 +16,22 @@ limitations under the License. import * as React from 'react'; -type IProps = { - // A callback for the selected value - onSelectionChange: (value: number) => void; +interface IProps { + // A callback for the selected value + onSelectionChange: (value: number) => void; - // The current value of the slider - value: number; + // The current value of the slider + value: number; - // The range and values of the slider - // Currently only supports an ascending, constant interval range - values: number[]; + // The range and values of the slider + // Currently only supports an ascending, constant interval range + values: number[]; - // A function for formatting the the values - displayFunc: (value: number) => string; + // A function for formatting the the values + displayFunc: (value: number) => string; - // Whether the slider is disabled - disabled: boolean; + // Whether the slider is disabled + disabled: boolean; } export default class Slider extends React.Component { From 175b5e70b60f23dc446aba22da4f00d56aa2d624 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:09:54 +0100 Subject: [PATCH 088/241] Lint Slider --- src/components/views/elements/Slider.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 722401801c..6712ddd7fd 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -43,7 +43,7 @@ export default class Slider extends React.Component { // y values. // Offset is used for finding the location of a value on a // non linear slider. - _offset(values: number[], value: number): number { + private offset(values: number[], value: number): number { // the index of the first number greater than value. let closest = values.reduce((prev, curr) => { return (value > curr ? prev + 1 : prev); @@ -80,19 +80,21 @@ export default class Slider extends React.Component { disabled={this.props.disabled} />); - const offset = this._offset(this.props.values, this.props.value); + let selection = null; + + if (this.props.disabled) { + const offset = this.offset(this.props.values, this.props.value); + selection =
+
+
+
+ } return

- { this.props.disabled ? - null : -
-
-
-
- } + { selection }
{dots} @@ -102,7 +104,7 @@ export default class Slider extends React.Component { } } -type DotIProps = { +interface IDotProps { // Callback for behavior onclick onClick: () => void, @@ -116,7 +118,7 @@ type DotIProps = { disabled: boolean; } -class Dot extends React.PureComponent { +class Dot extends React.PureComponent { render(): React.ReactNode { let className = "mx_Slider_dot" if (!this.props.disabled && this.props.active) { From 57d880ca5e2bde20a030f13d3f2257fe23e654b4 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:24:44 +0100 Subject: [PATCH 089/241] Use correct name and indentation --- .../settings/tabs/user/AppearanceUserSettingsTab.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index ed7d9ef495..6c94a82c95 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -27,7 +27,7 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; import _range from "lodash/range"; -export default class StyleUserSettingsTab extends React.Component { +export default class AppearanceUserSettingsTab extends React.Component { constructor() { super(); @@ -231,9 +231,10 @@ export default class StyleUserSettingsTab extends React.Component {
{_t("Theme")} {systemThemeSection} - {orderedThemes.map(theme => { return ; From 93f24f12dcf839f35231e8ca9083f670c3f626d7 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:26:08 +0100 Subject: [PATCH 090/241] Match filename to class --- src/{fontSize.js => FontWatcher.js} | 0 src/components/structures/MatrixChat.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{fontSize.js => FontWatcher.js} (100%) diff --git a/src/fontSize.js b/src/FontWatcher.js similarity index 100% rename from src/fontSize.js rename to src/FontWatcher.js diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 602d85f048..fec37472be 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -66,7 +66,7 @@ import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { defer } from "../../utils/promise"; import ToastStore from "../../stores/ToastStore"; import * as StorageManager from "../../utils/StorageManager"; -import { FontWatcher } from '../../fontSize'; +import { FontWatcher } from '../../FontWatcher'; /** constants for MatrixChat.state.view */ export const VIEWS = { From 9ca843fdcbc9f6d5d12dddac23a11af686cc702e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:27:18 +0100 Subject: [PATCH 091/241] Correct return type in docs Co-Authored-By: Travis Ralston --- src/settings/SettingsStore.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 70ea5ac57c..b6856a5a6a 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -373,7 +373,7 @@ export default class SettingsStore { /** * Gets the default value of a setting. * @param {string} settingName The name of the setting to read the value of. - * @return {*} The value, or null if not found + * @return {*} The default value */ static getDefaultValue(settingName, roomId = null, excludeDefault = false) { // Verify that the setting is actually a setting From fe326b9f08534d70c36b0484e727304e8396ba8c Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:30:56 +0100 Subject: [PATCH 092/241] Enfore function name capitalisation --- src/components/structures/RoomSubList.js | 2 +- src/components/views/avatars/BaseAvatar.js | 2 +- src/components/views/rooms/EventTile.js | 4 ++-- src/components/views/rooms/ReadReceiptMarker.js | 2 +- src/utils/rem.js | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 1e3e15b4ec..b1e0bb9f9b 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; -import toRem from "../../utils/rem"; +import {toRem} from "../../utils/rem"; // turn this on for drop & drag console debugging galore const debug = false; diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index cbe083f3a2..e94a83f70b 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -24,7 +24,7 @@ import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import toRem from "../../../utils/rem"; +import {toRem} from "../../../utils/rem"; export default createReactClass({ displayName: 'BaseAvatar', diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index af14f6922c..0881fb3b67 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -34,7 +34,7 @@ import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; import * as ObjectUtils from "../../../ObjectUtils"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import {E2E_STATE} from "./E2EIcon"; -import torem from "../../../utils/rem"; +import {toRem} from "../../../utils/rem"; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -474,7 +474,7 @@ export default createReactClass({ if (remainder > 0) { remText = { remainder }+ + style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+ ; } } diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 85d443d55a..20d39a7f84 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/rem"; +import {toRem} from "../../../utils/rem"; let bounce = false; try { diff --git a/src/utils/rem.js b/src/utils/rem.js index 1f18c9de05..6278a91aa2 100644 --- a/src/utils/rem.js +++ b/src/utils/rem.js @@ -15,6 +15,6 @@ limitations under the License. */ // converts a pixel value to rem. -export default function(pixelVal) { +export function toRem(pixelVal) { return pixelVal / 15 + "rem"; } From 1289367a6b63f8e04e53da6ae5a2ae7e5a8e5455 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 14:31:24 +0100 Subject: [PATCH 093/241] Fix indentation --- src/utils/rem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/rem.js b/src/utils/rem.js index 6278a91aa2..3729b4d596 100644 --- a/src/utils/rem.js +++ b/src/utils/rem.js @@ -16,5 +16,5 @@ limitations under the License. // converts a pixel value to rem. export function toRem(pixelVal) { - return pixelVal / 15 + "rem"; + return pixelVal / 15 + "rem"; } From eb72245493c1dbe163f85afbf97a26b68078e525 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 15:19:12 +0100 Subject: [PATCH 094/241] fix syntax error --- res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index 5cc220bd33..0af7e30d97 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword .mx_Field, +.mx_GeneralUserSettingsTab_changePassword .mx_Field { @mixin mx_Settings_fullWidthField; } From af8430b98aa5e47116e76ce3547da955ad18b1dd Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 15:48:54 +0100 Subject: [PATCH 095/241] Inverted boolean --- src/components/views/elements/Slider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index 6712ddd7fd..adb2a6063b 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -82,7 +82,7 @@ export default class Slider extends React.Component { let selection = null; - if (this.props.disabled) { + if (!this.props.disabled) { const offset = this.offset(this.props.values, this.props.value); selection =
From 4e6748416c3d68757588e563d24f516dc17880c6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 15:53:12 +0100 Subject: [PATCH 096/241] Fix i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bff1b8f415..58226595f7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -405,7 +405,6 @@ "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", - "Show padlocks on invite only rooms": "Show padlocks on invite only rooms", "Font size": "Font size", "Min font size": "Min font size", "Max font size": "Max font size", From 132a753deb787bb626b2431b7f0434debc3c1b74 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 28 Apr 2020 15:55:26 +0100 Subject: [PATCH 097/241] Lint getDefaultValue --- src/settings/SettingsStore.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index b6856a5a6a..688925de40 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -373,9 +373,10 @@ export default class SettingsStore { /** * Gets the default value of a setting. * @param {string} settingName The name of the setting to read the value of. + * @param {String} roomId The room ID to read the setting value in, may be null. * @return {*} The default value */ - static getDefaultValue(settingName, roomId = null, excludeDefault = false) { + static getDefaultValue(settingName) { // Verify that the setting is actually a setting if (!SETTINGS[settingName]) { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); From 2acb1663eb67473738511c3b6aa22899c9344cb3 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 01:01:56 +0100 Subject: [PATCH 098/241] Appease the prop types --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 6c94a82c95..d089b4f6e0 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -274,8 +274,8 @@ export default class AppearanceUserSettingsTab extends React.Component { type="text" label={_t("Font size")} autoComplete="off" - placeholder={this.state.fontSize} - value={this.state.fontSize} + placeholder={toString(this.state.fontSize)} + value={toString(this.state.fontSize)} id="font_size_field" onValidate={this._onValidateFontSize} onChange={({value}) => this.setState({fontSize: value})} From f7b3662e0b306d66feefe7ac03211165518565f7 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 10:32:05 +0100 Subject: [PATCH 099/241] Fully appease prop types --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index d089b4f6e0..6fd44b691d 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -107,7 +107,6 @@ export default class AppearanceUserSettingsTab extends React.Component { _onValidateFontSize = ({value}) => { console.log({value}); - this.setState({fontSize: value}); const parsedSize = parseFloat(value); const min = SettingsStore.getValue("font_size_min"); @@ -274,11 +273,11 @@ export default class AppearanceUserSettingsTab extends React.Component { type="text" label={_t("Font size")} autoComplete="off" - placeholder={toString(this.state.fontSize)} - value={toString(this.state.fontSize)} + placeholder={this.state.fontSize.toString()} + value={this.state.fontSize.toString()} id="font_size_field" onValidate={this._onValidateFontSize} - onChange={({value}) => this.setState({fontSize: value})} + onChange={(value) => this.setState({fontSize: value.target.value})} disabled={!this.state.useCustomFontSize} />
; From bab7d5f461a6c51d142fe9ff7d5be6cd4cfd9bbb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 6 May 2020 17:25:54 +0100 Subject: [PATCH 100/241] Some lints --- res/css/views/elements/_Slider.scss | 12 ++++++------ res/themes/light/css/_light.scss | 4 ++-- src/FontWatcher.js | 12 ++++++------ src/components/views/elements/Slider.tsx | 2 +- .../tabs/user/AppearanceUserSettingsTab.js | 14 +++++++------- src/settings/Settings.js | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index f6982865db..09afb58b12 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -38,7 +38,7 @@ limitations under the License. .mx_Slider_bar > hr { width: 100%; - border: 0.2em solid $Slider-background-color; + border: 0.2em solid $slider-background-color; } .mx_Slider_selection { @@ -54,7 +54,7 @@ limitations under the License. position: absolute; width: 1.1em; height: 1.1em; - background-color: $Slider-selection-color; + background-color: $slider-selection-color; border-radius: 50%; box-shadow: 0 0 6px lightgrey; z-index: 10; @@ -63,7 +63,7 @@ limitations under the License. .mx_Slider_selection > hr { transition: width 0.25s; margin: 0; - border: 0.2em solid $Slider-selection-color; + border: 0.2em solid $slider-selection-color; } .mx_Slider_dot { @@ -71,19 +71,19 @@ limitations under the License. height: 1em; width: 1em; border-radius: 50%; - background-color: $Slider-background-color; + background-color: $slider-background-color; z-index: 0; } .mx_Slider_dotActive { - background-color: $Slider-selection-color; + background-color: $slider-selection-color; } .mx_Slider_dotValue { display: flex; flex-direction: column; align-items: center; - color: $Slider-background-color; + color: $slider-background-color; } // The following is a hack to center the labels without adding diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index ed7eae48f7..78fe2a74c5 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -263,8 +263,8 @@ $togglesw-on-color: $accent-color; $togglesw-ball-color: #fff; // Slider -$Slider-selection-color: $accent-color; -$Slider-background-color: #c1c9d6; +$slider-selection-color: $accent-color; +$slider-background-color: #c1c9d6; $progressbar-color: #000; diff --git a/src/FontWatcher.js b/src/FontWatcher.js index 2e37921ee6..561edc4662 100644 --- a/src/FontWatcher.js +++ b/src/FontWatcher.js @@ -23,7 +23,7 @@ export class FontWatcher { } start() { - this._setRootFontSize(SettingsStore.getValue("font_size")); + this._setRootFontSize(SettingsStore.getValue("fontSize")); this._dispatcherRef = dis.register(this._onAction); } @@ -37,15 +37,15 @@ export class FontWatcher { } }; - _setRootFontSize = size => { - const min = SettingsStore.getValue("font_size_min"); - const max = SettingsStore.getValue("font_size_max"); + _setRootFontSize = (size) => { + const min = SettingsStore.getValue("fontSizeMin"); + const max = SettingsStore.getValue("fontSizeMax"); const fontSize = Math.max(Math.min(max, size), min); if (fontSize != size) { - SettingsStore.setValue("font_size", null, SettingLevel.Device, fontSize); + SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); } document.querySelector(":root").style.fontSize = fontSize + "px"; - } + }; } diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index adb2a6063b..e181f0d9e3 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -47,7 +47,7 @@ export default class Slider extends React.Component { // the index of the first number greater than value. let closest = values.reduce((prev, curr) => { return (value > curr ? prev + 1 : prev); - }, 0); + }, 0); // Off the left if (closest === 0) { diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 6fd44b691d..ac98664be0 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -32,7 +32,7 @@ export default class AppearanceUserSettingsTab extends React.Component { super(); this.state = { - fontSize: SettingsStore.getValue("font_size", null), + fontSize: SettingsStore.getValue("fontSize", null), ...this._calculateThemeState(), customThemeUrl: "", customThemeMessage: {isError: false, text: ""}, @@ -102,15 +102,15 @@ export default class AppearanceUserSettingsTab extends React.Component { _onFontSizeChanged = (size) => { this.setState({fontSize: size}); - SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, size); + SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, size); }; _onValidateFontSize = ({value}) => { console.log({value}); const parsedSize = parseFloat(value); - const min = SettingsStore.getValue("font_size_min"); - const max = SettingsStore.getValue("font_size_max"); + const min = SettingsStore.getValue("fontSizeMin"); + const max = SettingsStore.getValue("fontSizeMax"); if (isNaN(parsedSize)) { return {valid: false, feedback: _t("Size must be a number")}; @@ -123,7 +123,7 @@ export default class AppearanceUserSettingsTab extends React.Component { }; } - SettingsStore.setValue("font_size", null, SettingLevel.DEVICE, value); + SettingsStore.setValue("fontSize", null, SettingLevel.DEVICE, value); return {valid: true, feedback: _t('Use between %(min)s pt and %(max)s pt', {min, max})}; } @@ -253,8 +253,8 @@ export default class AppearanceUserSettingsTab extends React.Component {
Aa
Date: Wed, 29 Apr 2020 14:57:45 +0100 Subject: [PATCH 101/241] add useIRCLayout setting --- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a24b2bde73..3dcba0f546 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -408,6 +408,7 @@ "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", + "Use IRC layout": "Use IRC layout", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5c6d843349..c51bf44ee5 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -94,6 +94,12 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, + "feature_alternate_message_layouts": { + isFeature: true, + displayName: _td("Alternate message layouts"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), @@ -164,6 +170,11 @@ export const SETTINGS = { default: true, invertedSettingName: 'MessageComposerInput.dontSuggestEmoji', }, + "useIRCLayout": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Use IRC layout'), + default: false, + }, "useCompactLayout": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Use compact timeline layout'), From e0c89f6180331350485ca7331ff9e190a83a3d5f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 15:06:38 +0100 Subject: [PATCH 102/241] Add switch between layout classes --- src/components/structures/MessagePanel.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6fbfdb504b..0123d43920 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -117,6 +117,7 @@ export default class MessagePanel extends React.Component { // display 'ghost' read markers that are animating away ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), + useIRCLayout: SettingsStore.getValue("useIRCLayout"), }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -169,6 +170,8 @@ export default class MessagePanel extends React.Component { this._showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); + + this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); } componentDidMount() { @@ -178,6 +181,7 @@ export default class MessagePanel extends React.Component { componentWillUnmount() { this._isMounted = false; SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); + SettingsStore.unwatchSetting(this._layoutWatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -196,6 +200,12 @@ export default class MessagePanel extends React.Component { }); }; + onLayoutChange = () => { + this.setState({ + useIRCLayout: SettingsStore.getValue("useIRCLayout"), + }); + } + /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -779,6 +789,8 @@ export default class MessagePanel extends React.Component { this.props.className, { "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps, + "mx_IRCLayout": this.state.useIRCLayout, + "mx_GroupLayout": !this.state.useIRCLayout, }, ); From c1e740a59603ae178364cac18c8ecdd5443c9df6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 15:07:17 +0100 Subject: [PATCH 103/241] Break out group layout settings --- res/css/views/rooms/_EventTile.scss | 12 ------- res/css/views/rooms/_GroupLayout.scss | 52 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 res/css/views/rooms/_GroupLayout.scss diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 752cf982f6..b9a41c4310 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -68,11 +68,9 @@ limitations under the License. display: inline-block; /* anti-zalgo, with overflow hidden */ overflow: hidden; cursor: pointer; - padding-left: 65px; /* left gutter */ padding-bottom: 0px; padding-top: 0px; margin: 0px; - line-height: $font-17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; @@ -101,12 +99,9 @@ limitations under the License. .mx_EventTile .mx_MessageTimestamp { display: block; - visibility: hidden; white-space: nowrap; left: 0px; - width: 46px; /* 8 + 30 (avatar) + 8 */ text-align: center; - position: absolute; user-select: none; } @@ -117,10 +112,7 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; padding-left: 65px; /* left gutter */ - padding-top: 3px; - padding-bottom: 3px; border-radius: 4px; - line-height: $font-22px; } .mx_RoomView_timeline_rr_enabled, @@ -151,10 +143,6 @@ limitations under the License. margin-right: 10px; } -.mx_EventTile_info .mx_EventTile_line { - padding-left: 83px; -} - /* HACK to override line-height which is already marked important elsewhere */ .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { font-size: 48px !important; diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss new file mode 100644 index 0000000000..6528d6c6cd --- /dev/null +++ b/res/css/views/rooms/_GroupLayout.scss @@ -0,0 +1,52 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +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. +*/ + +$left-gutter: 65px; + +.mx_GroupLayout { + + .mx_EventTile { + > .mx_SenderProfile { + line-height: $font-17px; + padding-left: $left-gutter; + } + + > .mx_EventTile_line { + padding-left: $left-gutter; + } + + > .mx_EventTile_avatar { + position: absolute; + } + + .mx_MessageTimestamp { + visibility: hidden; + position: absolute; + width: 46px; /* 8 + 30 (avatar) + 8 */ + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 3px; + padding-bottom: 3px; + line-height: $font-22px; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: 83px; + } +} From 10c8d253c86334e11c3a8582db6d63e073bcb79e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 15:07:41 +0100 Subject: [PATCH 104/241] Create irc layout --- res/css/views/rooms/_IRCLayout.scss | 124 ++++++++++++++++++++++ src/components/structures/MessagePanel.js | 1 + src/components/views/rooms/EventTile.js | 30 ++++-- 3 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 res/css/views/rooms/_IRCLayout.scss diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss new file mode 100644 index 0000000000..6152749573 --- /dev/null +++ b/res/css/views/rooms/_IRCLayout.scss @@ -0,0 +1,124 @@ +/* +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. +*/ + +$name-width: 50px; +$icon-width: 14px; +$timestamp-width: 45px; +$right-padding: 5px; + +.mx_IRCLayout { + + line-height: $font-22px !important; + + .mx_EventTile { + display: flex; + flex-direction: row; + align-items: flex-start; + + > * { + margin-right: $right-padding; + } + + > .mx_EventTile_msgOption { + order: 4; + flex-shrink: 0; + } + + > .mx_SenderProfile { + order: 2; + flex-shrink: 0; + width: $name-width; + text-overflow: ellipsis; + text-align: right; + display: flex; + align-items: center; + } + + > .mx_EventTile_line { + order: 3; + flex-grow: 1; + } + + > .mx_EventTile_avatar { + order: 1; + position: relative; + top: 0; + left: 0; + flex-shrink: 0; + height: 22px; + display: flex; + align-items: center; + + > .mx_BaseAvatar { + height: 1rem; + width: 1rem; + } + } + + .mx_MessageTimestamp { + font-size: $font-10px; + width: $timestamp-width; + text-align: right; + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding: 0; + } + + .mx_EventTile_e2eIcon { + position: relative; + right: unset; + left: unset; + top: -2px; + padding: 0; + } + + .mx_EventTile_line > * { + display: inline-block; + } + } + + .mx_EventListSummary { + > .mx_EventTile_line { + padding-left: calc($name-width + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding + } + } + + .mx_EventTile.mx_EventTile_info { + .mx_EventTile_avatar { + left: calc($name-width + 10px + $icon-width); + top: 0; + } + + .mx_EventTile_line { + left: calc($name-width + 10px + $icon-width); + } + + .mx_TextualEvent { + line-height: $font-22px; + } + } + + .mx_EventTile_continuation:not(.mx_EventTile_info) { + .mx_EventTile_avatar { + visibility: hidden; + } + + .mx_SenderProfile { + visibility: hidden; + } + } +} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 0123d43920..80e5d17bf3 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -607,6 +607,7 @@ export default class MessagePanel extends React.Component { isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} + useIRCLayout={this.state.useIRCLayout} /> , diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index a64fd82eb5..2da9677a17 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -206,6 +206,9 @@ export default createReactClass({ // whether to show reactions for this event showReactions: PropTypes.bool, + + // whether to use the irc layout + useIRCLayout: PropTypes.bool, }, getDefaultProps: function() { @@ -653,6 +656,8 @@ export default createReactClass({ const classes = classNames({ mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile: true, + mx_EventTile_irc: this.props.useIRCLayout, + mx_EventTile_group: !this.props.useIRCLayout, mx_EventTile_isEditing: isEditing, mx_EventTile_info: isInfoMessage, mx_EventTile_12hr: this.props.isTwelveHour, @@ -696,6 +701,9 @@ export default createReactClass({ // joins/parts/etc avatarSize = 14; needsSenderProfile = false; + } else if (this.props.useIRCLayout) { + avatarSize = 14; + needsSenderProfile = true; } else if (this.props.continuation && this.props.tileShape !== "file_grid") { // no avatar or sender profile for continuation messages avatarSize = 0; @@ -879,21 +887,29 @@ export default createReactClass({ this.props.permalinkCreator, this._replyThread, ); + + const linkedTimestamp = + { timestamp } + ; + + const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null; + const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null; + + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return (
+ { ircTimestamp }
{ readAvatars }
{ sender }
- - { timestamp } - + { groupTimestamp } { !isBubbleMessage && this._renderE2EPadlock() } { thread } Date: Wed, 29 Apr 2020 15:07:47 +0100 Subject: [PATCH 105/241] Include new css files --- res/css/_components.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/_components.scss b/res/css/_components.scss index 0ba2b609e8..a66d15af18 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -159,6 +159,8 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_EventTile_group.scss"; +@import "./views/rooms/_EventTile_irc.scss"; @import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; From 6c3e3161de314b109810db0987f04877e5f42a10 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 29 Apr 2020 15:29:25 +0100 Subject: [PATCH 106/241] Reduce padding --- res/css/views/rooms/_IRCLayout.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 6152749573..94ff681029 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -27,6 +27,7 @@ $right-padding: 5px; display: flex; flex-direction: row; align-items: flex-start; + padding-top: 0; > * { margin-right: $right-padding; From 54d211a847c8f198c2547c0cc9fc6c9715d142e5 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 4 May 2020 21:40:52 +0100 Subject: [PATCH 107/241] Index file name changes --- res/css/_components.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/_components.scss b/res/css/_components.scss index a66d15af18..5466b785c0 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -159,8 +159,8 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; -@import "./views/rooms/_EventTile_group.scss"; -@import "./views/rooms/_EventTile_irc.scss"; +@import "./views/rooms/_GroupLayout.scss"; +@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_InviteOnlyIcon.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; From 67249e1e9c17c9886812e1e2e07a71e298d2bb9e Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 4 May 2020 21:53:48 +0100 Subject: [PATCH 108/241] Fix hover --- res/css/views/rooms/_IRCLayout.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 94ff681029..10d8c701c3 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -122,4 +122,12 @@ $right-padding: 5px; visibility: hidden; } } + + // Suppress highlight thing from the normal Layout. + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: 0; + border-left: 0; + } } From 027826c2e1ca45cda9588a8d5cee257581048987 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 5 May 2020 10:54:44 +0100 Subject: [PATCH 109/241] Replies have the same layout as messages --- res/css/views/rooms/_IRCLayout.scss | 13 +++++++-- src/components/views/elements/ReplyThread.js | 9 ++++-- src/components/views/rooms/EventTile.js | 29 ++++++++++---------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 10d8c701c3..c7cf2c31c2 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -63,9 +63,12 @@ $right-padding: 5px; display: flex; align-items: center; - > .mx_BaseAvatar { - height: 1rem; - width: 1rem; + // Need to use important to override the js provided height and width values. + > .mx_BaseAvatar, .mx_BaseAvatar > * { + height: $font-14px !important; + width: $font-14px !important; + font-size: $font-10px !important; + line-height: $font-14px !important; } } @@ -90,6 +93,10 @@ $right-padding: 5px; .mx_EventTile_line > * { display: inline-block; } + + .mx_EventTile_reply { + order: 3; + } } .mx_EventListSummary { diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index eae2d13f8a..a8f9599f35 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -37,6 +37,8 @@ export default class ReplyThread extends React.Component { // called when the ReplyThread contents has changed, including EventTiles thereof onHeightChanged: PropTypes.func.isRequired, permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, + // Specifies which layout to use. + useIRCLayout: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -176,12 +178,12 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { if (!ReplyThread.getParentEventId(parentEv)) { return
; } return ; + ref={ref} permalinkCreator={permalinkCreator} useIRCLayout={useIRCLayout} />; } componentDidMount() { @@ -331,7 +333,8 @@ export default class ReplyThread extends React.Component { onHeightChanged={this.props.onHeightChanged} permalinkCreator={this.props.permalinkCreator} isRedacted={ev.isRedacted()} - isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} /> + isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} + useIRCLayout={this.props.useIRCLayout} /> ; }); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 2da9677a17..837c8929b9 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -795,6 +795,17 @@ export default createReactClass({ />; } + const linkedTimestamp = + { timestamp } + ; + + const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null; + const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null; + switch (this.props.tileShape) { case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); @@ -862,12 +873,11 @@ export default createReactClass({ } return (
+ { ircTimestamp } { avatar } { sender }
- - { timestamp } - + { groupTimestamp } { !isBubbleMessage && this._renderE2EPadlock() } { thread } - { timestamp } - ; - - const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null; - const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null; - - // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return (
From 0af265bf93e703c42492c51b41a23f485f4099fe Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 5 May 2020 16:24:50 +0100 Subject: [PATCH 110/241] Fix replies --- res/css/views/rooms/_IRCLayout.scss | 9 ++++++++- src/components/views/elements/ReplyThread.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index c7cf2c31c2..b45d34013c 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -90,10 +90,17 @@ $right-padding: 5px; padding: 0; } - .mx_EventTile_line > * { + .mx_EventTile_line .mx_EventTile_content, + .mx_EventTile_line .mx_EventTile_e2eIcon, + .mx_eventTile_line > div { display: inline-block; } + .mx_EvenTile_line .mx_MessageActionBar, + .mx_EvenTile_line .mx_ReplyThread_wrapper { + display: block; + } + .mx_EventTile_reply { order: 3; } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index a8f9599f35..52a94110ba 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -338,7 +338,7 @@ export default class ReplyThread extends React.Component { ; }); - return
+ return
{ header }
{ evTiles }
; From 07c2d0cb0256ff7ab359416222d6411e0100594a Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 6 May 2020 09:24:33 +0100 Subject: [PATCH 111/241] Composer reply previews have group layout --- src/components/views/rooms/MessageComposer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 4749742a7d..663db5e942 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -370,7 +370,7 @@ export default class MessageComposer extends React.Component { } return ( -
+
{ controls } From 5568e6488d18c2ef8da79514f852ac562cdcf049 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 6 May 2020 09:51:01 +0100 Subject: [PATCH 112/241] Fix encryption badge layout --- res/css/views/rooms/_IRCLayout.scss | 2 +- src/components/views/elements/ReplyThread.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index b45d34013c..968d8ebdea 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -92,7 +92,7 @@ $right-padding: 5px; .mx_EventTile_line .mx_EventTile_content, .mx_EventTile_line .mx_EventTile_e2eIcon, - .mx_eventTile_line > div { + .mx_EventTile_line .mx_ReplyThread_wrapper_empty { display: inline-block; } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 52a94110ba..de242f5632 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -180,7 +180,7 @@ export default class ReplyThread extends React.Component { static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { if (!ReplyThread.getParentEventId(parentEv)) { - return
; + return
; } return ; From 771ae5e18f23b7b7954950b43fa7e5e52b70b0e8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 7 May 2020 13:55:23 +0100 Subject: [PATCH 113/241] Fix encryption badge layouts and replies. Begin removing dependence on slider. Move settings to labs. Username disambiguation. --- res/css/views/rooms/_IRCLayout.scss | 34 +++++++++++++++---- src/components/structures/MessagePanel.js | 16 +++++++-- src/components/views/elements/ReplyThread.js | 10 ++++-- .../views/messages/SenderProfile.js | 3 ++ src/components/views/rooms/EventTile.js | 6 +++- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 17 +++++++--- 7 files changed, 69 insertions(+), 18 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 968d8ebdea..e4536aec20 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -$name-width: 50px; +$name-width: 70px; $icon-width: 14px; $timestamp-width: 45px; $right-padding: 5px; .mx_IRCLayout { - line-height: $font-22px !important; + line-height: $font-20px !important; .mx_EventTile { display: flex; @@ -46,11 +46,13 @@ $right-padding: 5px; text-align: right; display: flex; align-items: center; + overflow: visible; } > .mx_EventTile_line { order: 3; flex-grow: 1; + margin-bottom: -6px; } > .mx_EventTile_avatar { @@ -90,10 +92,13 @@ $right-padding: 5px; padding: 0; } - .mx_EventTile_line .mx_EventTile_content, - .mx_EventTile_line .mx_EventTile_e2eIcon, - .mx_EventTile_line .mx_ReplyThread_wrapper_empty { - display: inline-block; + .mx_EventTile_line { + .mx_EventTile_e2eIcon, + .mx_TextualEvent, + .mx_MTextBody, + .mx_ReplyThread_wrapper_empty { + display: inline-block; + } } .mx_EvenTile_line .mx_MessageActionBar, @@ -104,6 +109,10 @@ $right-padding: 5px; .mx_EventTile_reply { order: 3; } + + .mx_EditMessageComposer_buttons { + position: relative; + } } .mx_EventListSummary { @@ -144,4 +153,17 @@ $right-padding: 5px; padding-left: 0; border-left: 0; } + + .mx_SenderProfile_hover { + background-color: $primary-bg-color; + overflow: hidden; + width: $name-width; + transition: width 2s; + } + + .mx_SenderProfile_hover:hover { + overflow: visible; + width: auto; + z-index: 10; + } } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 80e5d17bf3..66ed5ee81f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -117,7 +117,8 @@ export default class MessagePanel extends React.Component { // display 'ghost' read markers that are animating away ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - useIRCLayout: SettingsStore.getValue("useIRCLayout"), + useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + displayAvatars: SettingsStore.getValue("feature_no_timeline_avatars"), }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -171,7 +172,8 @@ export default class MessagePanel extends React.Component { this._showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); - this._layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); + this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); + this._displayAvatarsWatcherRef = SettingsStore.watchSetting("feature_no_timeline_avatars", null, this.onDisplayAvatarsChange); } componentDidMount() { @@ -182,6 +184,7 @@ export default class MessagePanel extends React.Component { this._isMounted = false; SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._layoutWatcherRef); + SettingsStore.unwatchSetting(this._displayAvatarsWatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -202,7 +205,13 @@ export default class MessagePanel extends React.Component { onLayoutChange = () => { this.setState({ - useIRCLayout: SettingsStore.getValue("useIRCLayout"), + useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + }); + } + + onDisplayAvatarsChange = () => { + this.setState({ + displayAvatars: SettingsStore.getValue("feature_no_timeline_avatars"), }); } @@ -608,6 +617,7 @@ export default class MessagePanel extends React.Component { getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} useIRCLayout={this.state.useIRCLayout} + displayAvatars={this.state.displayAvatars} /> , diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index de242f5632..d1d46e709a 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -39,6 +39,8 @@ export default class ReplyThread extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, // Specifies which layout to use. useIRCLayout: PropTypes.bool, + // Specifies whether to display avatars. + displayAvatars: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -178,12 +180,12 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout, displayAvatars) { if (!ReplyThread.getParentEventId(parentEv)) { return
; } return ; + ref={ref} permalinkCreator={permalinkCreator} useIRCLayout={useIRCLayout} displayAvatars={displayAvatars} />; } componentDidMount() { @@ -334,7 +336,9 @@ export default class ReplyThread extends React.Component { permalinkCreator={this.props.permalinkCreator} isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} - useIRCLayout={this.props.useIRCLayout} /> + useIRCLayout={this.props.useIRCLayout} + displayAvatars={this.props.displayAvatars} + /> ; }); diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index bed93b68c3..d95c9d685a 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -131,7 +131,10 @@ export default createReactClass({ return (
+
+ { content } { content } +
); }, diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 837c8929b9..b2daa5667f 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -209,6 +209,9 @@ export default createReactClass({ // whether to use the irc layout useIRCLayout: PropTypes.bool, + + // whether to display avatars + displayAvatars: PropTypes.bool, }, getDefaultProps: function() { @@ -713,7 +716,7 @@ export default createReactClass({ needsSenderProfile = true; } - if (this.props.mxEvent.sender && avatarSize) { + if (this.props.mxEvent.sender && avatarSize && this.props.displayAvatars) { avatar = (
Date: Thu, 7 May 2020 14:06:40 +0100 Subject: [PATCH 114/241] Remove unused setting --- src/settings/Settings.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 7942aa67fc..032b0ee906 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -94,12 +94,6 @@ export const SETTINGS = { // // not use this for new settings. // invertedSettingName: "my-negative-setting", // }, - "feature_alternate_message_layouts": { - isFeature: true, - displayName: _td("Alternate message layouts"), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_pinning": { isFeature: true, displayName: _td("Message Pinning"), From bc5fc57dd667d89fd774949cb2b940d198518a90 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 7 May 2020 14:22:15 +0100 Subject: [PATCH 115/241] Lint This is why we shouldn't rely on regex --- src/components/structures/MessagePanel.js | 6 +++++- src/components/views/elements/ReplyThread.js | 9 +++++++-- src/i18n/strings/en_EN.json | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 66ed5ee81f..f46df0175a 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -173,7 +173,11 @@ export default class MessagePanel extends React.Component { SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); - this._displayAvatarsWatcherRef = SettingsStore.watchSetting("feature_no_timeline_avatars", null, this.onDisplayAvatarsChange); + this._displayAvatarsWatcherRef = SettingsStore.watchSetting( + "feature_no_timeline_avatars", + null, + this.onDisplayAvatarsChange, + ); } componentDidMount() { diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index d1d46e709a..6bfda5dd94 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -184,8 +184,13 @@ export default class ReplyThread extends React.Component { if (!ReplyThread.getParentEventId(parentEv)) { return
; } - return ; + return ; } componentDidMount() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a3cd88f9ae..ca62eb44fa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -405,11 +405,11 @@ "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Support adding custom themes": "Support adding custom themes", + "Use IRC layout": "Use IRC layout", + "Display user avatars on messages": "Display user avatars on messages", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", - "Use IRC layout": "Use IRC layout", - "Display user avatars on messages": "Display user avatars on messages", "Use compact timeline layout": "Use compact timeline layout", "Show a placeholder for removed messages": "Show a placeholder for removed messages", "Show join/leave messages (invites/kicks/bans unaffected)": "Show join/leave messages (invites/kicks/bans unaffected)", From ac95172ed4e6981bc5615fb982176d4a8c17a866 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 7 May 2020 14:56:45 +0100 Subject: [PATCH 116/241] tighter layout --- res/css/views/rooms/_IRCLayout.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index e4536aec20..fcdeef6590 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -21,7 +21,7 @@ $right-padding: 5px; .mx_IRCLayout { - line-height: $font-20px !important; + line-height: $font-18px !important; .mx_EventTile { display: flex; @@ -52,7 +52,6 @@ $right-padding: 5px; > .mx_EventTile_line { order: 3; flex-grow: 1; - margin-bottom: -6px; } > .mx_EventTile_avatar { From 9b7c63a7116c369d3e3e59c3a643067b6666ebca Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Fri, 8 May 2020 20:53:32 +0100 Subject: [PATCH 117/241] Duplicated names --- src/components/views/messages/SenderProfile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index d95c9d685a..d512b186e9 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -133,7 +133,6 @@ export default createReactClass({
{ content } - { content }
); From 82396661cf86d7c41285cded2d22523e84f3bc23 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Fri, 8 May 2020 22:21:26 +0100 Subject: [PATCH 118/241] Implement nitpicks - usernames are elipsed - icon alignment fixed - replies are more dense - reply messages respond to name widths - fixed between message padding problem (flex ftw) --- res/css/views/rooms/_IRCLayout.scss | 62 +++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index fcdeef6590..f5d8664884 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -18,12 +18,19 @@ $name-width: 70px; $icon-width: 14px; $timestamp-width: 45px; $right-padding: 5px; +$irc-line-height: $font-18px; .mx_IRCLayout { - line-height: $font-18px !important; + line-height: $irc-line-height !important; .mx_EventTile { + + // timestamps are links which shouldn't be underlined + > a { + text-decoration: none; + } + display: flex; flex-direction: row; align-items: flex-start; @@ -49,7 +56,10 @@ $right-padding: 5px; overflow: visible; } - > .mx_EventTile_line { + .mx_EventTile_line, .mx_EventTile_reply { + padding: 0; + display: flex; + flex-direction: column; order: 3; flex-grow: 1; } @@ -60,7 +70,7 @@ $right-padding: 5px; top: 0; left: 0; flex-shrink: 0; - height: 22px; + height: $irc-line-height; display: flex; align-items: center; @@ -79,10 +89,6 @@ $right-padding: 5px; text-align: right; } - .mx_EventTile_line, .mx_EventTile_reply { - padding: 0; - } - .mx_EventTile_e2eIcon { position: relative; right: unset; @@ -98,6 +104,8 @@ $right-padding: 5px; .mx_ReplyThread_wrapper_empty { display: inline-block; } + + } .mx_EvenTile_line .mx_MessageActionBar, @@ -114,10 +122,25 @@ $right-padding: 5px; } } + .mx_EventTile_emote { + > .mx_EventTile_avatar { + margin-left: calc($name-width + $icon-width + $right-padding); + } + } + + blockquote { + margin: 0; + } + .mx_EventListSummary { > .mx_EventTile_line { padding-left: calc($name-width + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding } + + .mx_EventListSummary_avatars { + padding: 0; + margin: 0; + } } .mx_EventTile.mx_EventTile_info { @@ -131,16 +154,16 @@ $right-padding: 5px; } .mx_TextualEvent { - line-height: $font-22px; + line-height: $irc-line-height; } } .mx_EventTile_continuation:not(.mx_EventTile_info) { - .mx_EventTile_avatar { + > .mx_EventTile_avatar { visibility: hidden; } - .mx_SenderProfile { + > .mx_SenderProfile { visibility: hidden; } } @@ -156,8 +179,15 @@ $right-padding: 5px; .mx_SenderProfile_hover { background-color: $primary-bg-color; overflow: hidden; - width: $name-width; - transition: width 2s; + + > span { + display: flex; + + > .mx_SenderProfile_name { + overflow: hidden; + text-overflow: ellipsis; + } + } } .mx_SenderProfile_hover:hover { @@ -165,4 +195,12 @@ $right-padding: 5px; width: auto; z-index: 10; } + + .mx_ReplyThread { + margin: 0; + .mx_SenderProfile { + width: unset; + max-width: $name-width; + } + } } From fef4d882c44dcf93b677bac50cc09097df7f7839 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Fri, 8 May 2020 22:35:40 +0100 Subject: [PATCH 119/241] lint --- res/css/views/rooms/_IRCLayout.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index f5d8664884..301f712ffb 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -104,8 +104,6 @@ $irc-line-height: $font-18px; .mx_ReplyThread_wrapper_empty { display: inline-block; } - - } .mx_EvenTile_line .mx_MessageActionBar, From 5029c3f1434b33fe8b4aae825f5f3e03d4425dc0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 02:16:43 +0100 Subject: [PATCH 120/241] Implement IRC draggable display name width --- res/css/views/rooms/_IRCLayout.scss | 23 +++-- src/components/structures/MessagePanel.js | 7 ++ src/components/structures/ScrollPanel.js | 6 ++ src/components/views/elements/Draggable.tsx | 84 ++++++++++++++++++ .../views/elements/ErrorBoundary.js | 2 +- .../elements/IRCTimelineProfileResizer.tsx | 86 +++++++++++++++++++ src/settings/Settings.js | 7 ++ 7 files changed, 207 insertions(+), 8 deletions(-) create mode 100644 src/components/views/elements/Draggable.tsx create mode 100644 src/components/views/elements/IRCTimelineProfileResizer.tsx diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 301f712ffb..159cfc0aad 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -$name-width: 70px; $icon-width: 14px; $timestamp-width: 45px; $right-padding: 5px; $irc-line-height: $font-18px; .mx_IRCLayout { + --name-width: 70px; line-height: $irc-line-height !important; @@ -48,7 +48,7 @@ $irc-line-height: $font-18px; > .mx_SenderProfile { order: 2; flex-shrink: 0; - width: $name-width; + width: var(--name-width); text-overflow: ellipsis; text-align: right; display: flex; @@ -122,7 +122,7 @@ $irc-line-height: $font-18px; .mx_EventTile_emote { > .mx_EventTile_avatar { - margin-left: calc($name-width + $icon-width + $right-padding); + margin-left: calc(var(--name-width) + $icon-width + $right-padding); } } @@ -132,7 +132,7 @@ $irc-line-height: $font-18px; .mx_EventListSummary { > .mx_EventTile_line { - padding-left: calc($name-width + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding + padding-left: calc(var(--name-width) + $icon-width + $timestamp-width + 3 * $right-padding); // 15 px of padding } .mx_EventListSummary_avatars { @@ -143,12 +143,12 @@ $irc-line-height: $font-18px; .mx_EventTile.mx_EventTile_info { .mx_EventTile_avatar { - left: calc($name-width + 10px + $icon-width); + left: calc(var(--name-width) + 10px + $icon-width); top: 0; } .mx_EventTile_line { - left: calc($name-width + 10px + $icon-width); + left: calc(var(--name-width) + 10px + $icon-width); } .mx_TextualEvent { @@ -198,7 +198,16 @@ $irc-line-height: $font-18px; margin: 0; .mx_SenderProfile { width: unset; - max-width: $name-width; + max-width: var(--name-width); } } + + .mx_ProfileResizer { + position: absolute; + height: 100%; + width: 15px; + left: calc(80px + var(--name-width)); + cursor: col-resize; + z-index: 100; + } } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f46df0175a..1c10efb346 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -29,6 +29,7 @@ import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; +import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -819,6 +820,11 @@ export default class MessagePanel extends React.Component { ); } + let ircResizer = null; + if (this.state.useIRCLayout) { + ircResizer = ; + } + return ( { topSpinner } { this._getEventTiles() } diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 4f44c1a169..cb0114b243 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -144,6 +144,11 @@ export default createReactClass({ /* resizeNotifier: ResizeNotifier to know when middle column has changed size */ resizeNotifier: PropTypes.object, + + /* fixedChildren: allows for children to be passed which are rendered outside + * of the wrapper + */ + fixedChildren: PropTypes.node, }, getDefaultProps: function() { @@ -881,6 +886,7 @@ export default createReactClass({ return ( + { this.props.fixedChildren }
    { this.props.children } diff --git a/src/components/views/elements/Draggable.tsx b/src/components/views/elements/Draggable.tsx new file mode 100644 index 0000000000..98f86fd524 --- /dev/null +++ b/src/components/views/elements/Draggable.tsx @@ -0,0 +1,84 @@ +/* +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'; + +interface IProps { + className: string, + dragFunc: (currentLocation: ILocationState, event: MouseEvent) => ILocationState, + onMouseUp: (event: MouseEvent) => void, +} + +interface IState { + onMouseMove: (event: MouseEvent) => void, + onMouseUp: (event: MouseEvent) => void, + location: ILocationState, +} + +export interface ILocationState { + currentX: number, + currentY: number, +} + +export default class Draggable extends React.Component { + + constructor(props: IProps) { + super(props); + + this.state = { + onMouseMove: this.onMouseMove.bind(this), + onMouseUp: this.onMouseUp.bind(this), + location: { + currentX: 0, + currentY: 0, + }, + }; + } + + private onMouseDown = (event: MouseEvent): void => { + this.setState({ + location: { + currentX: event.clientX, + currentY: event.clientY, + }, + }); + + document.addEventListener("mousemove", this.state.onMouseMove); + document.addEventListener("mouseup", this.state.onMouseUp); + console.log("Mouse down") + } + + private onMouseUp = (event: MouseEvent): void => { + document.removeEventListener("mousemove", this.state.onMouseMove); + document.removeEventListener("mouseup", this.state.onMouseUp); + this.props.onMouseUp(event); + console.log("Mouse up") + } + + private onMouseMove(event: MouseEvent): void { + console.log("Mouse Move") + const newLocation = this.props.dragFunc(this.state.location, event); + + this.setState({ + location: newLocation, + }); + } + + render() { + return
    + } + +} \ No newline at end of file diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index a043b350ab..1abd11f838 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -73,7 +73,7 @@ export default class ErrorBoundary extends React.PureComponent { if (this.state.error) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new"; - return
    + return

    {_t("Something went wrong!")}

    {_t( diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx new file mode 100644 index 0000000000..80a86b2005 --- /dev/null +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -0,0 +1,86 @@ +/* +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 SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; +import Draggable, {ILocationState} from './Draggable'; + +interface IProps { + // Current room + roomId: string, + minWidth: number, + maxWidth: number, +}; + +interface IState { + width: number, + IRCLayoutRoot: HTMLElement, +}; + +export default class IRCTimelineProfileResizer extends React.Component { + constructor(props: IProps) { + super(props); + + this.state = { + width: SettingsStore.getValue("ircDisplayNameWidth", this.props.roomId), + IRCLayoutRoot: null, + } + }; + + componentDidMount() { + this.setState({ + IRCLayoutRoot: document.querySelector(".mx_IRCLayout") as HTMLElement, + }, () => this.updateCSSWidth(this.state.width)) + } + + private dragFunc = (location: ILocationState, event: React.MouseEvent): ILocationState => { + const offset = event.clientX - location.currentX; + const newWidth = this.state.width + offset; + + console.log({offset}) + // If we're trying to go smaller than min width, don't. + if (this.state.width <= this.props.minWidth && offset <= 0) { + return location; + } + + if (this.state.width >= this.props.maxWidth && offset >= 0) { + return location; + } + + this.setState({ + width: newWidth, + }); + + this.updateCSSWidth.bind(this)(newWidth); + + return { + currentX: event.clientX, + currentY: location.currentY, + } + } + + private updateCSSWidth(newWidth: number) { + this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px"); + } + + private onMoueUp(event: MouseEvent) { + SettingsStore.setValue("ircDisplayNameWidth", this.props.roomId, SettingLevel.ROOM_DEVICE, this.state.width); + } + + render() { + return + } +}; \ No newline at end of file diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 032b0ee906..6d741ba3a5 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -531,4 +531,11 @@ export const SETTINGS = { MatrixClient.prototype.setCryptoTrustCrossSignedDevices, true, ), }, + "ircDisplayNameWidth": { + // We specifically want to have room-device > device so that users may set a device default + // with a per-room override. + supportedLevels: ['room-device', 'device'], + displayName: _td("IRC display name width"), + default: 80, + }, }; From 51f59c6c327873bd796eab6bdd91f20c854a66a1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 13 May 2020 11:40:56 +0100 Subject: [PATCH 121/241] UserView, show Welcome page in the mid panel instead of empty space Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js index 493cc136d1..694592af88 100644 --- a/src/components/structures/UserView.js +++ b/src/components/structures/UserView.js @@ -22,6 +22,7 @@ import {MatrixClientPeg} from "../../MatrixClientPeg"; import * as sdk from "../../index"; import Modal from '../../Modal'; import { _t } from '../../languageHandler'; +import HomePage from "./HomePage"; export default class UserView extends React.Component { static get propTypes() { @@ -79,7 +80,7 @@ export default class UserView extends React.Component { const RightPanel = sdk.getComponent('structures.RightPanel'); const MainSplit = sdk.getComponent('structures.MainSplit'); const panel = ; - return (

    ); + return (); } else { return (
    ); } From 3f04f5163a963b9193199abb340719061ae0e921 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 14:04:46 +0100 Subject: [PATCH 122/241] Implement more nitpicks - fix avatar inital aligment - right align names - set flair height to avatar's - fix conditions for resizing to be more stable --- res/css/views/rooms/_IRCLayout.scss | 13 ++++++++++++- .../views/elements/IRCTimelineProfileResizer.tsx | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 159cfc0aad..8d48c72f8a 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -54,6 +54,7 @@ $irc-line-height: $font-18px; display: flex; align-items: center; overflow: visible; + justify-content: flex-end; } .mx_EventTile_line, .mx_EventTile_reply { @@ -79,7 +80,7 @@ $irc-line-height: $font-18px; height: $font-14px !important; width: $font-14px !important; font-size: $font-10px !important; - line-height: $font-14px !important; + line-height: $font-15px !important; } } @@ -188,6 +189,10 @@ $irc-line-height: $font-18px; } } + .mx_SenderProfile:hover { + justify-content: flex-start; + } + .mx_SenderProfile_hover:hover { overflow: visible; width: auto; @@ -210,4 +215,10 @@ $irc-line-height: $font-18px; cursor: col-resize; z-index: 100; } + + // Need to use important to override the js provided height and width values. + .mx_Flair > img { + height: $font-14px !important; + width: $font-14px !important; + } } diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 80a86b2005..44ceeb9b7b 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -52,11 +52,11 @@ export default class IRCTimelineProfileResizer extends React.Component= this.props.maxWidth && offset >= 0) { + if (newWidth > this.props.maxWidth) { return location; } From 312b616d7701e9b9f8460d210bc22a07c1253be9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 14:11:16 +0100 Subject: [PATCH 123/241] 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 ca62eb44fa..37b9c1dfc8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -453,6 +453,7 @@ "Keep recovery passphrase in memory for this session": "Keep recovery passphrase in memory for this session", "How fast should messages be downloaded.": "How fast should messages be downloaded.", "Manually verify all remote sessions": "Manually verify all remote sessions", + "IRC display name width": "IRC display name width", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", From 328bb7bcaf05621443c7a75ab791a9c8eb0ac884 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 15:24:08 +0100 Subject: [PATCH 124/241] Remove all animations --- res/css/_common.scss | 1 - res/css/views/elements/_Slider.scss | 3 --- 2 files changed, 4 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index 687a238c8e..03442ca510 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -19,7 +19,6 @@ limitations under the License. @import "./_font-sizes.scss"; :root { - transition: font-size 0.25s; font-size: 15px; } diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 09afb58b12..06c3c4c98b 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -50,7 +50,6 @@ limitations under the License. } .mx_Slider_selectionDot { - transition: left 0.25s; position: absolute; width: 1.1em; height: 1.1em; @@ -61,13 +60,11 @@ limitations under the License. } .mx_Slider_selection > hr { - transition: width 0.25s; margin: 0; border: 0.2em solid $slider-selection-color; } .mx_Slider_dot { - transition: background-color 0.2s ease-in; height: 1em; width: 1em; border-radius: 50%; From fea219915f4a666d73259406323b185a9d5067f6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 15:26:11 +0100 Subject: [PATCH 125/241] fix code regeression --- .../views/settings/tabs/user/GeneralUserSettingsTab.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 0cee29233f..21e406aa23 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -60,9 +60,6 @@ export default class GeneralUserSettingsTab extends React.Component { emails: [], msisdns: [], loading3pids: true, // whether or not the emails and msisdns have been loaded - ...this._calculateThemeState(), - customThemeUrl: "", - customThemeMessage: {isError: false, text: ""}, }; this.dispatcherRef = dis.register(this._onAction); From 20ec900405721dce6faf130a20223ff4faa01ff6 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 15:36:53 +0100 Subject: [PATCH 126/241] Set font range --- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 6 +----- src/settings/Settings.js | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index ac98664be0..1ccc744dc7 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -252,11 +252,7 @@ export default class AppearanceUserSettingsTab extends React.Component {
    Aa
    {}} diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 34610c0caf..afe8d2cecc 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -174,12 +174,12 @@ export const SETTINGS = { "fontSizeMin": { displayName: _td("Min font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 14, + default: 13, }, "fontSizeMax": { displayName: _td("Max font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 24, + default: 20, }, "useCustomFontSize": { displayName: _td("Custom font size"), From 5c2abcf1a4b7bd9cbc35dd6c297073db56eb8a52 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 17:05:37 +0100 Subject: [PATCH 127/241] Show username on continuations --- res/css/views/rooms/_IRCLayout.scss | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 8d48c72f8a..f2a616f9c9 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -157,16 +157,6 @@ $irc-line-height: $font-18px; } } - .mx_EventTile_continuation:not(.mx_EventTile_info) { - > .mx_EventTile_avatar { - visibility: hidden; - } - - > .mx_SenderProfile { - visibility: hidden; - } - } - // Suppress highlight thing from the normal Layout. .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, From fc6e5227aced6f801bc1f1ffa3c7cfc86d84ef6d Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 13 May 2020 22:08:29 +0100 Subject: [PATCH 128/241] FIx roomsublist heights. - also fiddles the font size numbers --- res/css/structures/_TagPanel.scss | 2 +- res/css/views/rooms/_RoomTile.scss | 3 ++- src/components/structures/RoomSubList.js | 4 ++-- src/components/views/avatars/BaseAvatar.js | 20 +++++++++---------- src/components/views/rooms/EventTile.js | 2 +- .../views/rooms/ReadReceiptMarker.js | 2 +- .../tabs/user/AppearanceUserSettingsTab.js | 2 +- src/utils/{rem.js => units.ts} | 9 ++++++++- 8 files changed, 26 insertions(+), 18 deletions(-) rename src/utils/{rem.js => units.ts} (77%) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 536c88be63..1f8443e395 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -69,7 +69,7 @@ limitations under the License. height: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { - height: $font-40px; + height: 40px; padding: 10px 0 9px 0; } diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 5bc7d5624d..759dce5afa 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -20,7 +20,7 @@ limitations under the License. flex-direction: row; align-items: center; cursor: pointer; - height: $font-34px; + height: 32px; margin: 0; padding: 0 8px 0 10px; position: relative; @@ -81,6 +81,7 @@ limitations under the License. .mx_RoomTile_avatar_container { position: relative; + display: flex; } .mx_RoomTile_avatar { diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index c8f9ba1713..245b5f9bea 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -32,7 +32,7 @@ import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; import {_t} from "../../languageHandler"; import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex"; -import {toRem} from "../../utils/rem"; +import {toPx} from "../../utils/units"; // turn this on for drop & drag console debugging galore const debug = false; @@ -420,7 +420,7 @@ export default class RoomSubList extends React.PureComponent { setHeight = (height) => { if (this._subList.current) { - this._subList.current.style.height = toRem(height); + this._subList.current.style.height = toPx(height); } this._updateLazyRenderHeight(height); }; diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index a9bee9cda0..704e6438c8 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -24,7 +24,7 @@ import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {toRem} from "../../../utils/rem"; +import {toPx} from "../../../utils/units"; export default createReactClass({ displayName: 'BaseAvatar', @@ -166,9 +166,9 @@ export default createReactClass({ const textNode = (
    ; } strengthMeter =
    - +
    ; } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 2a79bb8588..663e30d44d 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -29,6 +29,7 @@ import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import withValidation from '../elements/Validation'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar"; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -274,11 +275,7 @@ export default createReactClass({ description: function() { const complexity = this.state.passwordComplexity; const score = complexity ? complexity.score : 0; - return ; + return ; }, rules: [ { diff --git a/src/components/views/elements/ZxcvbnProgressBar.tsx b/src/components/views/elements/ZxcvbnProgressBar.tsx new file mode 100644 index 0000000000..339149b400 --- /dev/null +++ b/src/components/views/elements/ZxcvbnProgressBar.tsx @@ -0,0 +1,30 @@ +/* +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"; + +interface IProps { + value: 0 | 1 | 2 | 3 | 4; + className?: string; +} + +const ZxcvbnProgressBar: React.FC = ({value, className}) => { + const classes = classNames("mx_ZxcvbnProgressBar", className); + return ; +}; + +export default ZxcvbnProgressBar; From 7ef456230479abd914f6aafbd54716007ad0a14f Mon Sep 17 00:00:00 2001 From: Tirifto Date: Thu, 14 May 2020 16:39:20 +0000 Subject: [PATCH 146/241] Translated using Weblate (Esperanto) Currently translated at 100.0% (2309 of 2309 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 11239303f8..6b3881339b 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -177,7 +177,7 @@ "Current password": "Nuna pasvorto", "Password": "Pasvorto", "New Password": "Nova pasvorto", - "Confirm password": "Konfirmi pasvorton", + "Confirm password": "Konfirmu pasvorton", "Change Password": "Ŝanĝi pasvorton", "Authentication": "Aŭtentikigo", "Device ID": "Aparata identigilo", @@ -298,7 +298,7 @@ "Only people who have been invited": "Nur invititaj uzantoj", "Anyone who knows the room's link, apart from guests": "Iu ajn kun la ligilo, krom gastoj", "Anyone who knows the room's link, including guests": "Iu ajn kun la ligilo, inkluzive gastojn", - "Publish this room to the public in %(domain)s's room directory?": "Ĉu publikigi ĉi tiun ĉambron al la publika ĉambrujo de %(domain)s?", + "Publish this room to the public in %(domain)s's room directory?": "Ĉu publikigi ĉi tiun ĉambron al la publika listo de ĉambroj de %(domain)s?", "Who can read history?": "Kiu povas legi la historion?", "Anyone": "Iu ajn", "Members only (since the point in time of selecting this option)": "Nur ĉambranoj (ekde ĉi tiu elekto)", @@ -441,7 +441,7 @@ "collapse": "maletendi", "expand": "etendi", "Custom level": "Propra nivelo", - "Room directory": "Ĉambra dosierujo", + "Room directory": "Listo de ĉambroj", "Username not available": "Uzantonomo ne disponeblas", "Username invalid: %(errMessage)s": "Uzantonomo ne validas: %(errMessage)s", "Username available": "Uzantonomo disponeblas", @@ -732,7 +732,7 @@ "No update available.": "Neniuj ĝisdatigoj haveblas.", "Resend": "Resendi", "Collecting app version information": "Kolektante informon pri versio de la aplikaĵo", - "Delete the room alias %(alias)s and remove %(name)s from the directory?": "Ĉu forigi la ĉambran kromnomon %(alias)s kaj forigi %(name)s de la ujo?", + "Delete the room alias %(alias)s and remove %(name)s from the directory?": "Ĉu forigi la ĉambran kromnomon %(alias)s kaj forigi %(name)s de la listo de ĉambroj?", "Enable notifications for this account": "Ŝalti sciigojn por tiu ĉi konto", "Invite to this community": "Inviti al tiu ĉi komunumo", "Messages containing keywords": "Mesaĝoj enhavantaj ŝlosilovortojn", @@ -1485,7 +1485,7 @@ "Homeserver URL does not appear to be a valid Matrix homeserver": "URL por hejmservilo ŝajne ne ligas al valida hejmservilo de Matrix", "Invalid identity server discovery response": "Nevalida eltrova respondo de identiga servilo", "Identity server URL does not appear to be a valid identity server": "URL por identiga servilo ŝajne ne ligas al valida identiga servilo", - "Sign in with single sign-on": "Salutu per ununura saluto", + "Sign in with single sign-on": "Saluti per ununura saluto", "Failed to re-authenticate due to a homeserver problem": "Malsukcesis reaŭtentikigi pro hejmservila problemo", "Failed to re-authenticate": "Malsukcesis reaŭtentikigi", "Enter your password to sign in and regain access to your account.": "Enigu vian pasvorton por saluti kaj rehavi aliron al via konto.", @@ -2405,5 +2405,6 @@ "Confirm to continue": "Konfirmu por daŭrigi", "Click the button below to confirm your identity.": "Klaku sube la butonon por konfirmi vian identecon.", "Confirm encryption setup": "Konfirmi agordon de ĉifrado", - "Click the button below to confirm setting up encryption.": "Klaku sube la butonon por konfirmi agordon de ĉifrado." + "Click the button below to confirm setting up encryption.": "Klaku sube la butonon por konfirmi agordon de ĉifrado.", + "QR Code": "Rapidresponda kodo" } From 2565f751839c65ac506c8d2e524030c2bfba1d96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 14 May 2020 16:34:16 +0000 Subject: [PATCH 147/241] Translated using Weblate (Estonian) Currently translated at 48.2% (1113 of 2309 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index b6e4830bf7..2ab308b469 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -1085,5 +1085,36 @@ "To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Selleka et jätkata koduserveri %(homeserverDomain)s kasutamist sa pead üle vaatama ja nõustuma meie kasutamistingimustega.", "Permissions": "Õigused", "Select the roles required to change various parts of the room": "Vali rollid, mis on vajalikud jututoa eri osade muutmiseks", - "Enable encryption?": "Kas võtame krüptimise kasutusele?" + "Enable encryption?": "Kas võtame krüptimise kasutusele?", + "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Kui kord juba kasutusele võetud, siis krüptimist enam hiljem ära lõpetada ei saa. Krüptitud sõnumeid ei saa lugeda ei vaheapealses veebiliikluses ega serveris ja vaid jututoa liikmed saavad neid lugeda. Krüptimise kasutusele võtmine takistada nii robotite kui sõnumisildade tööd. Lisateave krüptimise kohta.", + "Guests cannot join this room even if explicitly invited.": "Külalised ei saa selle jututoaga liituda ka siis, kui neid on otseselt kutsutud.", + "Click here to fix": "Parandamiseks klõpsi siia", + "Server error": "Serveri viga", + "Command error": "Käsu viga", + "Server unavailable, overloaded, or something else went wrong.": "Kas server pole saadaval, on üle koormatud või midagi muud läks viltu.", + "Unknown Command": "Tundmatu käsk", + "Unrecognised command: %(commandText)s": "Tundmatu käsk: %(commandText)s", + "Send as message": "Saada sõnumina", + "Failed to connect to integration manager": "Ühendus integratsioonihalduriga ei õnnestunud", + "You don't currently have any stickerpacks enabled": "Sul pole ühtegi kleepsupakki kasutusel", + "Add some now": "Lisa nüüd mõned", + "Stickerpack": "Kleepsupakk", + "Hide Stickers": "Peida kleepsud", + "Show Stickers": "Näita kleepse", + "Failed to revoke invite": "Kutse tühistamine ei õnnestunud", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Kutse tühistamine ei õnnestunud. Serveri töös võib olla ajutine tõrge või sul pole piisavalt õigusi kutse tühistamiseks.", + "Revoke invite": "Tühista kutse", + "Invited by %(sender)s": "Kutsutud %(sender)s poolt", + "Jump to first unread message.": "Mine esimese lugemata sõnumi juurde.", + "Mark all as read": "Märgi kõik loetuks", + "Error updating main address": "Viga põhiaadressi uuendamisel", + "There was an error updating the room's main address. It may not be allowed by the server or a temporary failure occurred.": "Jututoa põhiaadressi uuendamisel tekkis viga. See kas pole serveris lubatud või tekkis mingi ajutine viga.", + "There was an error updating the room's alternative addresses. It may not be allowed by the server or a temporary failure occurred.": "Jututoa lisaaadressi uuendamisel tekkis viga. See kas pole serveris lubatud või tekkis mingi ajutine viga.", + "Error creating alias": "Viga aliase loomisel", + "There was an error creating that alias. It may not be allowed by the server or a temporary failure occurred.": "Aliase loomisel tekkis viga. See kas pole serveris lubatud või tekkis mingi ajutine viga.", + "You don't have permission to delete the alias.": "Sul pole õigusi aliase kustutamiseks.", + "There was an error removing that alias. It may no longer exist or a temporary error occurred.": "Selle aliase eemaldamisel tekkis viga. Teda kas pole enam olemas või tekkis mingi ajutine viga.", + "Error removing alias": "Viga aliase eemaldamisel", + "Main address": "Põhiaadress", + "not specified": "määratlemata" } From 13922a71d3fd15f44a0d10e4fcd6a232081c8f14 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Thu, 14 May 2020 16:48:38 +0000 Subject: [PATCH 148/241] Translated using Weblate (German) Currently translated at 100.0% (2309 of 2309 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 3600cb5148..1166e7710a 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1029,7 +1029,7 @@ "Please review and accept all of the homeserver's policies": "Bitte prüfen und akzeptieren Sie alle Richtlinien des Heimservers", "Failed to load group members": "Konnte Gruppenmitglieder nicht laden", "That doesn't look like a valid email address": "Sieht nicht nach einer validen E-Mail-Adresse aus", - "Unable to load commit detail: %(msg)s": "Konnte Commit-Details nicht laden: %(msg)", + "Unable to load commit detail: %(msg)s": "Konnte Commit-Details nicht laden: %(msg)s", "Checking...": "Überprüfe...", "Unable to load backup status": "Konnte Backupstatus nicht laden", "Failed to decrypt %(failedCount)s sessions!": "Konnte %(failedCount)s Sitzungen nicht entschlüsseln!", From 93a608a6446c6717a8b88d9f47ac71a096da0c20 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 May 2020 19:31:40 +0100 Subject: [PATCH 149/241] flatten out passwordSafe as it was a derived state value Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/auth/RegistrationForm.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 663e30d44d..4e2860c0cf 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -79,7 +79,6 @@ export default createReactClass({ password: this.props.defaultPassword || "", passwordConfirm: this.props.defaultPassword || "", passwordComplexity: null, - passwordSafe: false, }; }, @@ -291,22 +290,21 @@ export default createReactClass({ } const { scorePassword } = await import('../../../utils/PasswordScorer'); const complexity = scorePassword(value); - const safe = complexity.score >= PASSWORD_MIN_SCORE; - const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; this.setState({ passwordComplexity: complexity, - passwordSafe: safe, }); + const safe = complexity.score >= PASSWORD_MIN_SCORE; + const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; return allowUnsafe || safe; }, valid: function() { // Unsafe passwords that are valid are only possible through a // configuration flag. We'll print some helper text to signal // to the user that their password is allowed, but unsafe. - if (!this.state.passwordSafe) { - return _t("Password is allowed, but unsafe"); + if (this.state.passwordComplexity.score >= PASSWORD_MIN_SCORE) { + return _t("Nice, strong password!"); } - return _t("Nice, strong password!"); + return _t("Password is allowed, but unsafe"); }, invalid: function() { const complexity = this.state.passwordComplexity; From 8dd561d28a8a1fc39663a0bc138ac7e317e5f03b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 May 2020 19:33:17 +0100 Subject: [PATCH 150/241] Convert Validation to TypeScript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../{Validation.js => Validation.tsx} | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) rename src/components/views/elements/{Validation.js => Validation.tsx} (87%) diff --git a/src/components/views/elements/Validation.js b/src/components/views/elements/Validation.tsx similarity index 87% rename from src/components/views/elements/Validation.js rename to src/components/views/elements/Validation.tsx index 2be526a3c3..09d6ec12f7 100644 --- a/src/components/views/elements/Validation.js +++ b/src/components/views/elements/Validation.tsx @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +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. @@ -15,11 +16,34 @@ limitations under the License. */ /* eslint-disable babel/no-invalid-this */ +import React from "react"; +import classNames from "classnames"; -import classNames from 'classnames'; +type Data = Pick; + +interface IRule { + key: string; + final?: boolean; + skip?(this: T, data: Data): boolean; + test(this: T, data: Data): boolean | Promise; + valid?(this: T): string; + invalid?(this: T): string; +} + +interface IArgs { + rules: IRule[]; + description(): React.ReactChild; +} + +interface IValidateArgs { + value: string; + focused: boolean; + allowEmpty: boolean; +} /** * Creates a validation function from a set of rules describing what to validate. + * Generic T is the "this" type passed to the rule methods * * @param {Function} description * Function that returns a string summary of the kind of value that will @@ -37,8 +61,8 @@ import classNames from 'classnames'; * A validation function that takes in the current input value and returns * the overall validity and a feedback UI that can be rendered for more detail. */ -export default function withValidation({ description, rules }) { - return async function onValidate({ value, focused, allowEmpty = true }) { +export default function withValidation({ description, rules }: IArgs) { + return async function onValidate({ value, focused, allowEmpty = true }: IValidateArgs) { if (!value && allowEmpty) { return { valid: null, From 82b55ffd7717b4256690f43eb6891b173fe3cec9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 Mar 2020 13:34:56 -0600 Subject: [PATCH 151/241] Add temporary timing functions to old RoomListStore This is to identify how bad of a state we're in to start with. --- src/stores/RoomListStore.js | 60 ++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 89edc9a8ef..e217f7ea38 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -58,7 +58,27 @@ export const ALGO_RECENT = "recent"; const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE]; -const getListAlgorithm = (listKey, settingAlgorithm) => { +function debugLog(...msg) { + console.log(`[RoomListStore:Debug] `, ...msg); +} + +const timers = {}; +let timerCounter = 0; +function startTimer(fnName) { + const id = `${fnName}_${(new Date()).getTime()}_${timerCounter++}`; + debugLog(`Started timer for ${fnName} with ID ${id}`); + timers[id] = {start: (new Date()).getTime(), fnName}; + return id; +} + +function endTimer(id) { + const timer = timers[id]; + delete timers[id]; + const diff = (new Date()).getTime() - timer.start; + debugLog(`${timer.fnName} took ${diff}ms (ID: ${id})`); +} + +function getListAlgorithm(listKey, settingAlgorithm) { // apply manual sorting only to m.favourite, otherwise respect the global setting // all the known tags are listed explicitly here to simplify future changes switch (listKey) { @@ -73,7 +93,7 @@ const getListAlgorithm = (listKey, settingAlgorithm) => { default: // custom-tags return ALGO_MANUAL; } -}; +} const knownLists = new Set([ "m.favourite", @@ -340,6 +360,7 @@ class RoomListStore extends Store { } _getRecommendedTagsForRoom(room) { + const timerId = startTimer(`_getRecommendedTagsForRoom(room:"${room.roomId}")`); const tags = []; const myMembership = room.getMyMembership(); @@ -365,11 +386,12 @@ class RoomListStore extends Store { tags.push("im.vector.fake.archived"); } - + endTimer(timerId); return tags; } _slotRoomIntoList(room, category, tag, existingEntries, newList, lastTimestampFn) { + const timerId = startTimer(`_slotRoomIntoList(room:"${room.roomId}", "${category}", "${tag}", existingEntries: "${existingEntries.length}", "${newList}", lastTimestampFn:"${lastTimestampFn !== null}")`); const targetCategoryIndex = CATEGORY_ORDER.indexOf(category); let categoryComparator = (a, b) => lastTimestampFn(a.room) >= lastTimestampFn(b.room); @@ -481,11 +503,16 @@ class RoomListStore extends Store { pushedEntry = true; } + endTimer(timerId); return pushedEntry; } _setRoomCategory(room, category) { - if (!room) return; // This should only happen in tests + const timerId = startTimer(`_setRoomCategory(room:"${room.roomId}", "${category}")`); + if (!room) { + endTimer(timerId); + return; // This should only happen in tests + } const listsClone = {}; @@ -582,9 +609,11 @@ class RoomListStore extends Store { } this._setState({lists: listsClone}); + endTimer(timerId); } _generateInitialRoomLists() { + const timerId = startTimer(`_generateInitialRoomLists()`); // Log something to show that we're throwing away the old results. This is for the inevitable // question of "why is 100% of my CPU going towards Riot?" - a quick look at the logs would reveal // that something is wrong with the RoomListStore. @@ -697,6 +726,7 @@ class RoomListStore extends Store { lists, ready: true, // Ready to receive updates to ordering }); + endTimer(timerId); } _eventTriggersRecentReorder(ev) { @@ -709,7 +739,9 @@ class RoomListStore extends Store { _tsOfNewestEvent(room) { // Apparently we can have rooms without timelines, at least under testing // environments. Just return MAX_INT when this happens. - if (!room || !room.timeline) return Number.MAX_SAFE_INTEGER; + if (!room || !room.timeline) { + return Number.MAX_SAFE_INTEGER; + } for (let i = room.timeline.length - 1; i >= 0; --i) { const ev = room.timeline[i]; @@ -729,23 +761,35 @@ class RoomListStore extends Store { } _calculateCategory(room) { + const timerId = startTimer(`_calculateCategory(room:"${room.roomId}")`); if (!this._state.orderImportantFirst) { // Effectively disable the categorization of rooms if we're supposed to // be sorting by more recent messages first. This triggers the timestamp // comparison bit of _setRoomCategory and _recentsComparator instead of // the category ordering. + endTimer(timerId); return CATEGORY_IDLE; } const mentions = room.getUnreadNotificationCount("highlight") > 0; - if (mentions) return CATEGORY_RED; + if (mentions) { + endTimer(timerId); + return CATEGORY_RED; + } let unread = room.getUnreadNotificationCount() > 0; - if (unread) return CATEGORY_GREY; + if (unread) { + endTimer(timerId); + return CATEGORY_GREY; + } unread = Unread.doesRoomHaveUnreadMessages(room); - if (unread) return CATEGORY_BOLD; + if (unread) { + endTimer(timerId); + return CATEGORY_BOLD; + } + endTimer(timerId); return CATEGORY_IDLE; } From 08419d195e365eab57e0ffe0e315a7de9264d041 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 20 Mar 2020 14:38:20 -0600 Subject: [PATCH 152/241] Initial breakout for room list rewrite This does a number of things (sorry): * Estimates the type changes needed to the dispatcher (later to be replaced by https://github.com/matrix-org/matrix-react-sdk/pull/4593) * Sets up the stack for a whole new room list store, and later components for usage. * Create a proxy class to ensure the app still functions as expected when the various stores are enabled/disabled * Demonstrates a possible structure for algorithms --- package.json | 1 + src/actions/RoomListActions.js | 17 +- src/components/structures/LeftPanel.js | 34 ++- src/components/structures/LoggedInView.tsx | 13 +- src/components/views/dialogs/InviteDialog.js | 9 +- src/components/views/rooms/RoomList.js | 15 +- src/components/views/rooms/RoomList2.tsx | 130 +++++++++++ src/dispatcher-types.ts | 28 +++ src/i18n/strings/en_EN.json | 2 + src/settings/Settings.js | 6 + src/stores/CustomRoomTagStore.js | 8 +- src/stores/RoomListStore.js | 17 ++ src/stores/room-list/RoomListStore2.ts | 213 ++++++++++++++++++ .../room-list/RoomListStoreTempProxy.ts | 49 ++++ .../room-list/algorithms/ChaoticAlgorithm.ts | 100 ++++++++ src/stores/room-list/algorithms/IAlgorithm.ts | 95 ++++++++ src/stores/room-list/algorithms/index.ts | 36 +++ src/stores/room-list/models.ts | 36 +++ test/components/views/rooms/RoomList-test.js | 16 +- tsconfig.json | 3 +- yarn.lock | 13 ++ 21 files changed, 794 insertions(+), 47 deletions(-) create mode 100644 src/components/views/rooms/RoomList2.tsx create mode 100644 src/dispatcher-types.ts create mode 100644 src/stores/room-list/RoomListStore2.ts create mode 100644 src/stores/room-list/RoomListStoreTempProxy.ts create mode 100644 src/stores/room-list/algorithms/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/IAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/index.ts create mode 100644 src/stores/room-list/models.ts diff --git a/package.json b/package.json index dda4a5a897..22ff071ba7 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@babel/register": "^7.7.4", "@peculiar/webcrypto": "^1.0.22", "@types/classnames": "^2.2.10", + "@types/flux": "^3.1.9", "@types/modernizr": "^3.5.3", "@types/qrcode": "^1.3.4", "@types/react": "16.9", diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index 10a3848dda..072b1d9a86 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -15,11 +15,12 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; import * as sdk from '../index'; +import {RoomListStoreTempProxy} from "../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../stores/room-list/models"; const RoomListActions = {}; @@ -44,7 +45,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, // Is the tag ordered manually? if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const newList = [...lists[newTag]]; newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); @@ -73,11 +74,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) + if ((oldTag === undefined && newTag === DefaultTagID.DM) || + (oldTag === DefaultTagID.DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, + room, newTag === DefaultTagID.DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -91,10 +92,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const hasChangedSubLists = oldTag !== newTag; // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with TAG_DM. + // but we avoid ever doing a request with DefaultTagID.DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && + if (oldTag && oldTag !== DefaultTagID.DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( @@ -112,7 +113,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && + if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData) ) { // metaData is the body of the PUT to set the tag, so it must diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a9cd12199b..1993e2f419 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -26,6 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; +import RoomList2 from "../views/rooms/RoomList2"; const LeftPanel = createReactClass({ @@ -273,6 +274,29 @@ const LeftPanel = createReactClass({ breadcrumbs = (); } + let roomList = null; + if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { + roomList = ; + } else { + roomList = ; + } + return (
    { tagPanelContainer } @@ -284,15 +308,7 @@ const LeftPanel = createReactClass({ { exploreButton } { searchBox }
    - + {roomList}
    ); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9de2aac8e9..0950e52bba 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -31,7 +31,6 @@ import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStore from "../../stores/RoomListStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; @@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; +import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; +import { DefaultTagID } from "../../stores/room-list/models"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent { }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStore.getRoomLists(); - if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; _updateServerNoticeEvents = async () => { - const roomLists = RoomListStore.getRoomLists(); - if (!roomLists['m.server_notice']) return []; + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (!roomLists[DefaultTagID.ServerNotice]) return []; const pinnedEvents = []; - for (const room of roomLists['m.server_notice']) { + for (const room of roomLists[DefaultTagID.ServerNotice]) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 7cbbf8ba64..e719c45f49 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -34,8 +34,9 @@ import {humanizeTime} from "../../../utils/humanize"; import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; import SettingsStore from '../../../settings/SettingsStore'; -import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore"; import {Key} from "../../../Keyboard"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -343,10 +344,10 @@ export default class InviteDialog extends React.PureComponent { _buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room - // Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the + // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const taggedRooms = RoomListStore.getRoomLists(); - const dmTaggedRooms = taggedRooms[TAG_DM]; + const taggedRooms = RoomListStoreTempProxy.getRoomLists(); + const dmTaggedRooms = taggedRooms[DefaultTagID.DM]; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 289a89a206..dc9c9238cd 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -29,7 +29,6 @@ import rate_limited_func from "../../../ratelimitedfunc"; import * as Rooms from '../../../Rooms'; import DMRoomMap from '../../../utils/DMRoomMap'; import TagOrderStore from '../../../stores/TagOrderStore'; -import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore'; import CustomRoomTagStore from '../../../stores/CustomRoomTagStore'; import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; @@ -41,6 +40,8 @@ import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; import * as Unread from "../../../Unread"; import RoomViewStore from "../../../stores/RoomViewStore"; @@ -161,7 +162,7 @@ export default createReactClass({ this.updateVisibleRooms(); }); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._delayedRefreshRoomList(); }); @@ -521,7 +522,7 @@ export default createReactClass({ }, getTagNameForRoomId: function(roomId) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); for (const tagName of Object.keys(lists)) { for (const room of lists[tagName]) { // Should be impossible, but guard anyways. @@ -541,7 +542,7 @@ export default createReactClass({ }, getRoomLists: function() { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const filteredLists = {}; @@ -773,10 +774,10 @@ export default createReactClass({ incomingCall: incomingCallIfTaggedAs('m.favourite'), }, { - list: this.state.lists[TAG_DM], + list: this.state.lists[DefaultTagID.DM], label: _t('Direct Messages'), - tagName: TAG_DM, - incomingCall: incomingCallIfTaggedAs(TAG_DM), + tagName: DefaultTagID.DM, + incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, addRoomLabel: _t("Start chat"), }, diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx new file mode 100644 index 0000000000..1790fa8cf6 --- /dev/null +++ b/src/components/views/rooms/RoomList2.tsx @@ -0,0 +1,130 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 Vector Creations Ltd +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 { _t } from "../../../languageHandler"; +import { Layout } from '../../../resizer/distributors/roomsublist2'; +import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; +import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import RoomListStore from "../../../stores/room-list/RoomListStore2"; + +interface IProps { + onKeyDown: (ev: React.KeyboardEvent) => void; + onFocus: (ev: React.FocusEvent) => void; + onBlur: (ev: React.FocusEvent) => void; + resizeNotifier: ResizeNotifier; + collapsed: boolean; + searchFilter: string; +} + +interface IState { +} + +// TODO: Actually write stub +export class RoomSublist2 extends React.Component { + public setHeight(size: number) { + } +} + +export default class RoomList2 extends React.Component { + private sublistRefs: { [tagId: string]: React.RefObject } = {}; + private sublistSizes: { [tagId: string]: number } = {}; + private sublistCollapseStates: { [tagId: string]: boolean } = {}; + private unfilteredLayout: Layout; + private filteredLayout: Layout; + + constructor(props: IProps) { + super(props); + + this.loadSublistSizes(); + this.prepareLayouts(); + } + + public componentDidMount(): void { + RoomListStore.instance.addListener(() => { + console.log(RoomListStore.instance.orderedLists); + }); + } + + private loadSublistSizes() { + const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); + if (sizesJson) this.sublistSizes = JSON.parse(sizesJson); + + const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); + if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson); + } + + private saveSublistSizes() { + window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes)); + window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates)); + } + + private prepareLayouts() { + this.unfilteredLayout = new Layout((tagId: string, height: number) => { + const sublist = this.sublistRefs[tagId]; + if (sublist) sublist.current.setHeight(height); + + // TODO: Check overflow + + // Don't store a height for collapsed sublists + if (!this.sublistCollapseStates[tagId]) { + this.sublistSizes[tagId] = height; + this.saveSublistSizes(); + } + }, this.sublistSizes, this.sublistCollapseStates, { + allowWhitespace: false, + handleHeight: 1, + }); + + this.filteredLayout = new Layout((tagId: string, height: number) => { + const sublist = this.sublistRefs[tagId]; + if (sublist) sublist.current.setHeight(height); + }, null, null, { + allowWhitespace: false, + handleHeight: 0, + }); + } + + private collectSublistRef(tagId: string, ref: React.RefObject) { + if (!ref) { + delete this.sublistRefs[tagId]; + } else { + this.sublistRefs[tagId] = ref; + } + } + + public render() { + return ( + + {({onKeyDownHandler}) => ( +
    {_t("TODO")}
    + )} +
    + ); + } +} diff --git a/src/dispatcher-types.ts b/src/dispatcher-types.ts new file mode 100644 index 0000000000..16fac0c849 --- /dev/null +++ b/src/dispatcher-types.ts @@ -0,0 +1,28 @@ +/* +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 flux from "flux"; +import dis from "./dispatcher"; + +// TODO: Merge this with the dispatcher and centralize types + +export interface ActionPayload { + [property: string]: any; // effectively "extends Object" + action: string; +} + +// For ease of reference in TypeScript classes +export const defaultDispatcher: flux.Dispatcher = dis; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f16a0d7755..fd474f378c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,6 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", @@ -1116,6 +1117,7 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", + "TODO": "TODO", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5c6d843349..554cf6b968 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -131,6 +131,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_new_room_list": { + isFeature: true, + displayName: _td("Use the improved room list component (refresh to apply changes)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_custom_themes": { isFeature: true, displayName: _td("Support adding custom themes"), diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 909282c085..bf8e970535 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -15,10 +15,10 @@ limitations under the License. */ import dis from '../dispatcher'; import * as RoomNotifs from '../RoomNotifs'; -import RoomListStore from './RoomListStore'; import EventEmitter from 'events'; import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; +import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -60,7 +60,7 @@ class CustomRoomTagStore extends EventEmitter { trailing: true, }, ); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._setState({tags: this._getUpdatedTags()}); }); dis.register(payload => this._onDispatch(payload)); @@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter { } getSortedTags() { - const roomLists = RoomListStore.getRoomLists(); + const roomLists = RoomListStoreTempProxy.getRoomLists(); const tagNames = Object.keys(this._state.tags).sort(); const prefixes = tagNames.map((name, i) => { @@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter { return; } - const newTagNames = Object.keys(RoomListStore.getRoomLists()) + const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists()) .filter((tagName) => { return !tagName.match(STANDARD_TAGS_REGEX); }).sort(); diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index e217f7ea38..ccccbcc313 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -112,11 +112,19 @@ class RoomListStore extends Store { constructor() { super(dis); + this._checkDisabled(); this._init(); this._getManualComparator = this._getManualComparator.bind(this); this._recentsComparator = this._recentsComparator.bind(this); } + _checkDisabled() { + this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.disabled) { + console.warn("DISABLING LEGACY ROOM LIST STORE"); + } + } + /** * Changes the sorting algorithm used by the RoomListStore. * @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants. @@ -133,6 +141,8 @@ class RoomListStore extends Store { } _init() { + if (this.disabled) return; + // Initialise state const defaultLists = { "m.server_notice": [/* { room: js-sdk room, category: string } */], @@ -160,6 +170,8 @@ class RoomListStore extends Store { } _setState(newState) { + if (this.disabled) return; + // If we're changing the lists, transparently change the presentation lists (which // is given to requesting components). This dramatically simplifies our code elsewhere // while also ensuring we don't need to update all the calling components to support @@ -176,6 +188,8 @@ class RoomListStore extends Store { } __onDispatch(payload) { + if (this.disabled) return; + const logicallyReady = this._matrixClient && this._state.ready; switch (payload.action) { case 'setting_updated': { @@ -202,6 +216,9 @@ class RoomListStore extends Store { break; } + this._checkDisabled(); + if (this.disabled) return; + // Always ensure that we set any state needed for settings here. It is possible that // setting updates trigger on startup before we are ready to sync, so we want to make // sure that the right state is in place before we actually react to those changes. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts new file mode 100644 index 0000000000..70d6d4a598 --- /dev/null +++ b/src/stores/room-list/RoomListStore2.ts @@ -0,0 +1,213 @@ +/* +Copyright 2018, 2019 New Vector Ltd +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 {Store} from 'flux/utils'; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClient} from "matrix-js-sdk/src/client"; +import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; +import SettingsStore from "../../settings/SettingsStore"; +import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models"; +import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; +import TagOrderStore from "../TagOrderStore"; +import { getAlgorithmInstance } from "./algorithms"; + +interface IState { + tagsEnabled?: boolean; + + preferredSort?: SortAlgorithm; + preferredAlgorithm?: ListAlgorithm; +} + +class _RoomListStore extends Store { + private state: IState = {}; + private matrixClient: MatrixClient; + private initialListsGenerated = false; + private enabled = false; + private algorithm: IAlgorithm; + + private readonly watchedSettings = [ + 'RoomList.orderAlphabetically', + 'RoomList.orderByImportance', + 'feature_custom_tags', + ]; + + constructor() { + super(defaultDispatcher); + + this.checkEnabled(); + this.reset(); + for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); + } + + public get orderedLists(): ITagMap { + if (!this.algorithm) return {}; // No tags yet. + return this.algorithm.getOrderedRooms(); + } + + // TODO: Remove enabled flag when the old RoomListStore goes away + private checkEnabled() { + this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.enabled) { + console.log("ENABLING NEW ROOM LIST STORE"); + } + } + + private reset(): void { + // We don't call setState() because it'll cause changes to emitted which could + // crash the app during logout/signin/etc. + this.state = {}; + } + + private readAndCacheSettingsFromStore() { + const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); + const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); + const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); + this.setState({ + tagsEnabled, + preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, + preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, + }); + this.setAlgorithmClass(); + } + + protected __onDispatch(payload: ActionPayload): void { + if (payload.action === 'MatrixActions.sync') { + // Filter out anything that isn't the first PREPARED sync. + if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { + return; + } + + this.checkEnabled(); + if (!this.enabled) return; + + this.matrixClient = payload.matrixClient; + + // Update any settings here, as some may have happened before we were logically ready. + this.readAndCacheSettingsFromStore(); + + // noinspection JSIgnoredPromiseFromCall + this.regenerateAllLists(); + } + + // TODO: Remove this once the RoomListStore becomes default + if (!this.enabled) return; + + if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { + // Reset state without causing updates as the client will have been destroyed + // and downstream code will throw NPE errors. + this.reset(); + this.matrixClient = null; + this.initialListsGenerated = false; // we'll want to regenerate them + } + + // Everything below here requires a MatrixClient or some sort of logical readiness. + const logicallyReady = this.matrixClient && this.initialListsGenerated; + if (!logicallyReady) return; + + if (payload.action === 'setting_updated') { + if (this.watchedSettings.includes(payload.settingName)) { + this.readAndCacheSettingsFromStore(); + + // noinspection JSIgnoredPromiseFromCall + this.regenerateAllLists(); // regenerate the lists now + } + } else if (payload.action === 'MatrixActions.Room.receipt') { + // First see if the receipt event is for our own user. If it was, trigger + // a room update (we probably read the room on a different device). + const myUserId = this.matrixClient.getUserId(); + for (const eventId of Object.keys(payload.event.getContent())) { + const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); + if (receiptUsers.includes(myUserId)) { + // TODO: Update room now that it's been read + return; + } + } + } else if (payload.action === 'MatrixActions.Room.tags') { + // TODO: Update room from tags + } else if (payload.action === 'MatrixActions.room.timeline') { + // TODO: Update room from new events + } else if (payload.action === 'MatrixActions.Event.decrypted') { + // TODO: Update room from decrypted event + } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { + // TODO: Update DMs + } else if (payload.action === 'MatrixActions.Room.myMembership') { + // TODO: Update room from membership change + } else if (payload.action === 'MatrixActions.room') { + // TODO: Update room from creation/join + } else if (payload.action === 'view_room') { + // TODO: Update sticky room + } + } + + private getSortAlgorithmFor(tagId: TagID): SortAlgorithm { + switch (tagId) { + case DefaultTagID.Invite: + case DefaultTagID.Untagged: + case DefaultTagID.Archived: + case DefaultTagID.LowPriority: + case DefaultTagID.DM: + return this.state.preferredSort; + case DefaultTagID.Favourite: + default: + return SortAlgorithm.Manual; + } + } + + private setState(newState: IState) { + if (!this.enabled) return; + + this.state = Object.assign(this.state, newState); + this.__emitChange(); + } + + private setAlgorithmClass() { + this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + } + + private async regenerateAllLists() { + console.log("REGEN"); + const tags: ITagSortingMap = {}; + for (const tagId of OrderedDefaultTagIDs) { + tags[tagId] = this.getSortAlgorithmFor(tagId); + } + + if (this.state.tagsEnabled) { + // TODO: Find a more reliable way to get tags + const roomTags = TagOrderStore.getOrderedTags() || []; + console.log("rtags", roomTags); + } + + await this.algorithm.populateTags(tags); + await this.algorithm.setKnownRooms(this.matrixClient.getRooms()); + + this.initialListsGenerated = true; + + // TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync? + } +} + +export default class RoomListStore { + private static internalInstance: _RoomListStore; + + public static get instance(): _RoomListStore { + if (!RoomListStore.internalInstance) { + RoomListStore.internalInstance = new _RoomListStore(); + } + + return RoomListStore.internalInstance; + } +} diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts new file mode 100644 index 0000000000..7b12602541 --- /dev/null +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -0,0 +1,49 @@ +/* +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 { TagID } from "./models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore from "./RoomListStore2"; +import OldRoomListStore from "../RoomListStore"; + +/** + * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when + * it is available to everyone. + * + * TODO: Remove this when RoomListStore gets fully replaced. + */ +export class RoomListStoreTempProxy { + public static isUsingNewStore(): boolean { + return SettingsStore.isFeatureEnabled("feature_new_room_list"); + } + + public static addListener(handler: () => void) { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.addListener(handler); + } else { + return OldRoomListStore.addListener(handler); + } + } + + public static getRoomLists(): {[tagId in TagID]: Room[]} { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.orderedLists; + } else { + return OldRoomListStore.getRoomLists(); + } + } +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..4fe5125a15 --- /dev/null +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -0,0 +1,100 @@ +/* +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 { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { DefaultTagID } from "../models"; + +/** + * A demonstration/temporary algorithm to verify the API surface works. + * TODO: Remove this before shipping + */ +export class ChaoticAlgorithm implements IAlgorithm { + + private cached: ITagMap = {}; + private sortAlgorithms: ITagSortingMap; + private rooms: Room[] = []; + + constructor(private representativeAlgorithm: ListAlgorithm) { + } + + getOrderedRooms(): ITagMap { + return this.cached; + } + + async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + this.setKnownRooms(this.rooms); // regenerate the room lists + } + + handleRoomUpdate(room): Promise { + return undefined; + } + + setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + this.cached = newTags; + return; + } + + // TODO: Remove logging + console.log('setting known rooms - regen in progress'); + console.log({alg: this.representativeAlgorithm}); + + // Step through each room and determine which tags it should be in. + // We don't care about ordering or sorting here - we're simply organizing things. + for (const room of rooms) { + const tags = room.tags; + let inTag = false; + for (const tagId in tags) { + // noinspection JSUnfilteredForInLoop + if (isNullOrUndefined(newTags[tagId])) { + // skip the tag if we don't know about it + continue; + } + + inTag = true; + + // noinspection JSUnfilteredForInLoop + newTags[tagId].push(room); + } + + // If the room wasn't pushed to a tag, push it to the untagged tag. + if (!inTag) { + newTags[DefaultTagID.Untagged].push(room); + } + } + + // TODO: Do sorting + + // Finally, assign the tags to our cache + this.cached = newTags; + } +} diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts new file mode 100644 index 0000000000..fbe2f7a27d --- /dev/null +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -0,0 +1,95 @@ +/* +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 { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export enum Category { + Red = "RED", + Grey = "GREY", + Bold = "BOLD", + Idle = "IDLE", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} + +// TODO: Convert IAlgorithm to an abstract class? +// TODO: Add locking support to avoid concurrent writes +// TODO: EventEmitter support + +/** + * Represents an algorithm for the RoomListStore to use + */ +export interface IAlgorithm { + /** + * Asks the Algorithm to regenerate all lists, using the tags given + * as reference for which lists to generate and which way to generate + * them. + * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @returns {Promise<*>} A promise which resolves when complete. + */ + populateTags(tagSortingMap: ITagSortingMap): Promise; + + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + getOrderedRooms(): ITagMap; + + /** + * Seeds the Algorithm with a set of rooms. The algorithm will discard all + * previously known information and instead use these rooms instead. + * @param {Room[]} rooms The rooms to force the algorithm to use. + * @returns {Promise<*>} A promise which resolves when complete. + */ + setKnownRooms(rooms: Room[]): Promise; + + /** + * Asks the Algorithm to update its knowledge of a room. For example, when + * a user tags a room, joins/creates a room, or leaves a room the Algorithm + * should be told that the room's info might have changed. The Algorithm + * may no-op this request if no changes are required. + * @param {Room} room The room which might have affected sorting. + * @returns {Promise} A promise which resolve to true or false + * depending on whether or not getOrderedRooms() should be called after + * processing. + */ + handleRoomUpdate(room: Room): Promise; +} diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts new file mode 100644 index 0000000000..cb67d42187 --- /dev/null +++ b/src/stores/room-list/algorithms/index.ts @@ -0,0 +1,36 @@ +/* +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 { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; +import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; + +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { + [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), + [ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance), +}; + +/** + * Gets an instance of the defined algorithm + * @param {ListAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm { + if (!ALGORITHM_FACTORIES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_FACTORIES[algorithm](); +} diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts new file mode 100644 index 0000000000..d1c915e035 --- /dev/null +++ b/src/stores/room-list/models.ts @@ -0,0 +1,36 @@ +/* +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. +*/ + +export enum DefaultTagID { + Invite = "im.vector.fake.invite", + Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms + Archived = "im.vector.fake.archived", + LowPriority = "m.lowpriority", + Favourite = "m.favourite", + DM = "im.vector.fake.direct", + ServerNotice = "m.server_notice", +} +export const OrderedDefaultTagIDs = [ + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; + +export type TagID = string | DefaultTagID; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 8dc4647920..7bcd2a8ae3 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -14,7 +14,7 @@ import DMRoomMap from '../../../../src/utils/DMRoomMap.js'; import GroupStore from '../../../../src/stores/GroupStore.js'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; -import {TAG_DM} from "../../../../src/stores/RoomListStore"; +import {DefaultTagID} from "../../../../src/stores/room-list/models"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -153,7 +153,7 @@ describe('RoomList', () => { // Set up the room that will be moved such that it has the correct state for a room in // the section for oldTag if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}}; - if (oldTag === TAG_DM) { + if (oldTag === DefaultTagID.DM) { // Mock inverse m.direct DMRoomMap.shared().roomToUser = { [movingRoom.roomId]: '@someotheruser:domain', @@ -180,7 +180,7 @@ describe('RoomList', () => { // TODO: Re-enable dragging tests when we support dragging again. describe.skip('does correct optimistic update when dragging from', () => { it('rooms to people', () => { - expectCorrectMove(undefined, TAG_DM); + expectCorrectMove(undefined, DefaultTagID.DM); }); it('rooms to favourites', () => { @@ -195,15 +195,15 @@ describe('RoomList', () => { // Whe running the app live, it updates when some other event occurs (likely the // m.direct arriving) that these tests do not fire. xit('people to rooms', () => { - expectCorrectMove(TAG_DM, undefined); + expectCorrectMove(DefaultTagID.DM, undefined); }); it('people to favourites', () => { - expectCorrectMove(TAG_DM, 'm.favourite'); + expectCorrectMove(DefaultTagID.DM, 'm.favourite'); }); it('people to lowpriority', () => { - expectCorrectMove(TAG_DM, 'm.lowpriority'); + expectCorrectMove(DefaultTagID.DM, 'm.lowpriority'); }); it('low priority to rooms', () => { @@ -211,7 +211,7 @@ describe('RoomList', () => { }); it('low priority to people', () => { - expectCorrectMove('m.lowpriority', TAG_DM); + expectCorrectMove('m.lowpriority', DefaultTagID.DM); }); it('low priority to low priority', () => { @@ -223,7 +223,7 @@ describe('RoomList', () => { }); it('favourites to people', () => { - expectCorrectMove('m.favourite', TAG_DM); + expectCorrectMove('m.favourite', DefaultTagID.DM); }); it('favourites to low priority', () => { diff --git a/tsconfig.json b/tsconfig.json index b87f640734..8a01ca335e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "jsx": "react", "types": [ "node", - "react" + "react", + "flux" ] }, "include": [ diff --git a/yarn.lock b/yarn.lock index b0d3816dc4..6375c745fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,6 +1218,19 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fbemitter@*": + version "2.0.32" + resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw= + +"@types/flux@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711" + integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA== + dependencies: + "@types/fbemitter" "*" + "@types/react" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" From 861268d39f4dd612f1ad2e02a7f1097d1f590be4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Apr 2020 15:25:04 -0600 Subject: [PATCH 153/241] Invent an AsyncStore and use it for room lists This is to get around the problem of a slow dispatch loop. Instead of slowing the whole app down to deal with room lists, we'll just raise events to say we're ready. Based upon the EventEmitter class. --- package.json | 1 + src/components/views/rooms/RoomList2.tsx | 6 +- src/stores/AsyncStore.ts | 105 ++++++++++++++++++ src/stores/room-list/RoomListStore2.ts | 55 +++++---- .../room-list/RoomListStoreTempProxy.ts | 6 +- .../room-list/algorithms/ChaoticAlgorithm.ts | 1 - yarn.lock | 5 + 7 files changed, 144 insertions(+), 35 deletions(-) create mode 100644 src/stores/AsyncStore.ts diff --git a/package.json b/package.json index 22ff071ba7..2dfb366972 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@babel/runtime": "^7.8.3", + "await-lock": "^2.0.1", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 1790fa8cf6..f97f599ae3 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -21,7 +21,7 @@ import { _t } from "../../../languageHandler"; import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -56,8 +56,8 @@ export default class RoomList2 extends React.Component { } public componentDidMount(): void { - RoomListStore.instance.addListener(() => { - console.log(RoomListStore.instance.orderedLists); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { + console.log("new lists", store.orderedLists); }); } diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts new file mode 100644 index 0000000000..5e19e17248 --- /dev/null +++ b/src/stores/AsyncStore.ts @@ -0,0 +1,105 @@ +/* +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 { EventEmitter } from 'events'; +import AwaitLock from 'await-lock'; +import { ActionPayload } from "../dispatcher-types"; +import { Dispatcher } from "flux"; + +/** + * The event/channel to listen for in an AsyncStore. + */ +export const UPDATE_EVENT = "update"; + +/** + * Represents a minimal store which works similar to Flux stores. Instead + * of everything needing to happen in a dispatch cycle, everything can + * happen async to that cycle. + * + * The store's core principle is Object.assign(), therefore it is recommended + * to break out your state to be as safe as possible. The state mutations are + * also locked, preventing concurrent writes. + * + * All updates to the store happen on the UPDATE_EVENT event channel with the + * one argument being the instance of the store. + * + * To update the state, use updateState() and preferably await the result to + * help prevent lock conflicts. + */ +export abstract class AsyncStore extends EventEmitter { + private storeState: T = {}; + private lock = new AwaitLock(); + private readonly dispatcherRef: string; + + /** + * Creates a new AsyncStore using the given dispatcher. + * @param {Dispatcher} dispatcher The dispatcher to rely upon. + */ + protected constructor(private dispatcher: Dispatcher) { + super(); + + this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this)); + } + + /** + * The current state of the store. Cannot be mutated. + */ + protected get state(): T { + return Object.freeze(this.storeState); + } + + /** + * Stops the store's listening functions, such as the listener to the dispatcher. + */ + protected stop() { + if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); + } + + /** + * Updates the state of the store. + * @param {T|*} newState The state to update in the store using Object.assign() + */ + protected async updateState(newState: T | Object) { + await this.lock.acquireAsync(); + try { + this.storeState = Object.assign({}, this.storeState, newState); + this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Resets the store's to the provided state or an empty object. + * @param {T|*} newState The new state of the store. + * @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT. + */ + protected async reset(newState: T | Object = null, quiet = false) { + await this.lock.acquireAsync(); + try { + this.storeState = (newState || {}); + if (!quiet) this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Called when the dispatcher broadcasts a dispatch event. + * @param {ActionPayload} payload The event being dispatched. + */ + protected abstract onDispatch(payload: ActionPayload); +} diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 70d6d4a598..7fab8c7ff9 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -15,15 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Store} from 'flux/utils'; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; -import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; import TagOrderStore from "../TagOrderStore"; import { getAlgorithmInstance } from "./algorithms"; +import { AsyncStore } from "../AsyncStore"; interface IState { tagsEnabled?: boolean; @@ -32,8 +31,13 @@ interface IState { preferredAlgorithm?: ListAlgorithm; } -class _RoomListStore extends Store { - private state: IState = {}; +/** + * The event/channel which is called when the room lists have been changed. Raised + * with one argument: the instance of the store. + */ +export const LISTS_UPDATE_EVENT = "lists_update"; + +class _RoomListStore extends AsyncStore { private matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; @@ -49,7 +53,6 @@ class _RoomListStore extends Store { super(defaultDispatcher); this.checkEnabled(); - this.reset(); for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); } @@ -66,17 +69,11 @@ class _RoomListStore extends Store { } } - private reset(): void { - // We don't call setState() because it'll cause changes to emitted which could - // crash the app during logout/signin/etc. - this.state = {}; - } - - private readAndCacheSettingsFromStore() { + private async readAndCacheSettingsFromStore() { const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); - this.setState({ + await this.updateState({ tagsEnabled, preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, @@ -84,23 +81,23 @@ class _RoomListStore extends Store { this.setAlgorithmClass(); } - protected __onDispatch(payload: ActionPayload): void { + protected async onDispatch(payload: ActionPayload) { if (payload.action === 'MatrixActions.sync') { // Filter out anything that isn't the first PREPARED sync. if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { return; } + // TODO: Remove this once the RoomListStore becomes default this.checkEnabled(); if (!this.enabled) return; this.matrixClient = payload.matrixClient; // Update any settings here, as some may have happened before we were logically ready. - this.readAndCacheSettingsFromStore(); - - // noinspection JSIgnoredPromiseFromCall - this.regenerateAllLists(); + console.log("Regenerating room lists: Startup"); + await this.readAndCacheSettingsFromStore(); + await this.regenerateAllLists(); } // TODO: Remove this once the RoomListStore becomes default @@ -109,7 +106,7 @@ class _RoomListStore extends Store { if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { // Reset state without causing updates as the client will have been destroyed // and downstream code will throw NPE errors. - this.reset(); + this.reset(null, true); this.matrixClient = null; this.initialListsGenerated = false; // we'll want to regenerate them } @@ -120,14 +117,15 @@ class _RoomListStore extends Store { if (payload.action === 'setting_updated') { if (this.watchedSettings.includes(payload.settingName)) { - this.readAndCacheSettingsFromStore(); + console.log("Regenerating room lists: Settings changed"); + await this.readAndCacheSettingsFromStore(); - // noinspection JSIgnoredPromiseFromCall - this.regenerateAllLists(); // regenerate the lists now + await this.regenerateAllLists(); // regenerate the lists now } } else if (payload.action === 'MatrixActions.Room.receipt') { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). + // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle const myUserId = this.matrixClient.getUserId(); for (const eventId of Object.keys(payload.event.getContent())) { const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); @@ -167,11 +165,10 @@ class _RoomListStore extends Store { } } - private setState(newState: IState) { + protected async updateState(newState: IState) { if (!this.enabled) return; - this.state = Object.assign(this.state, newState); - this.__emitChange(); + await super.updateState(newState); } private setAlgorithmClass() { @@ -179,7 +176,7 @@ class _RoomListStore extends Store { } private async regenerateAllLists() { - console.log("REGEN"); + console.warn("Regenerating all room lists"); const tags: ITagSortingMap = {}; for (const tagId of OrderedDefaultTagIDs) { tags[tagId] = this.getSortAlgorithmFor(tagId); @@ -196,7 +193,7 @@ class _RoomListStore extends Store { this.initialListsGenerated = true; - // TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync? + this.emit(LISTS_UPDATE_EVENT, this); } } diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 7b12602541..88171c0809 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,6 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; +import { ITagMap } from "./algorithms/IAlgorithm"; +import { UPDATE_EVENT } from "../AsyncStore"; /** * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when @@ -33,13 +35,13 @@ export class RoomListStoreTempProxy { public static addListener(handler: () => void) { if (RoomListStoreTempProxy.isUsingNewStore()) { - return RoomListStore.instance.addListener(handler); + return RoomListStore.instance.on(UPDATE_EVENT, handler); } else { return OldRoomListStore.addListener(handler); } } - public static getRoomLists(): {[tagId in TagID]: Room[]} { + public static getRoomLists(): ITagMap { if (RoomListStoreTempProxy.isUsingNewStore()) { return RoomListStore.instance.orderedLists; } else { diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 4fe5125a15..f72adb3aa8 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -65,7 +65,6 @@ export class ChaoticAlgorithm implements IAlgorithm { } // TODO: Remove logging - console.log('setting known rooms - regen in progress'); console.log({alg: this.representativeAlgorithm}); // Step through each room and determine which tags it should be in. diff --git a/yarn.lock b/yarn.lock index 6375c745fc..80c7e581a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,6 +1834,11 @@ autoprefixer@^9.0.0: postcss "^7.0.27" postcss-value-parser "^4.0.3" +await-lock@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.0.1.tgz#b3f65fdf66e08f7538260f79b46c15bcfc18cadd" + integrity sha512-ntLi9fzlMT/vWjC1wwVI11/cSRJ3nTS35qVekNc9WnaoMOP2eWH0RvIqwLQkDjX4a4YynsKEv+Ere2VONp9wxg== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" From becaddeb80c54014473604b73d3f99d452a57916 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 14:12:58 -0600 Subject: [PATCH 154/241] Categorize rooms by effective membership --- .../room-list/algorithms/ChaoticAlgorithm.ts | 4 +- src/stores/room-list/membership.ts | 73 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/stores/room-list/membership.ts diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index f72adb3aa8..9dfe6f6205 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -18,6 +18,7 @@ import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { DefaultTagID } from "../models"; +import { splitRoomsByMembership } from "../membership"; /** * A demonstration/temporary algorithm to verify the API surface works. @@ -65,7 +66,8 @@ export class ChaoticAlgorithm implements IAlgorithm { } // TODO: Remove logging - console.log({alg: this.representativeAlgorithm}); + const memberships = splitRoomsByMembership(rooms); + console.log({alg: this.representativeAlgorithm, memberships}); // Step through each room and determine which tags it should be in. // We don't care about ordering or sorting here - we're simply organizing things. diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts new file mode 100644 index 0000000000..884e2a4a04 --- /dev/null +++ b/src/stores/room-list/membership.ts @@ -0,0 +1,73 @@ +/* +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 {Room} from "matrix-js-sdk/src/models/room"; +import {Event} from "matrix-js-sdk/src/models/event"; + +/** + * Approximation of a membership status for a given room. + */ +export enum EffectiveMembership { + /** + * The user is effectively joined to the room. For example, actually joined + * or knocking on the room (when that becomes possible). + */ + Join = "JOIN", + + /** + * The user is effectively invited to the room. Currently this is a direct map + * to the invite membership as no other membership states are effectively + * invites. + */ + Invite = "INVITE", + + /** + * The user is effectively no longer in the room. For example, kicked, + * banned, or voluntarily left. + */ + Leave = "LEAVE", +} + +export interface MembershipSplit { + // @ts-ignore - TS wants this to be a string key, but we know better. + [state: EffectiveMembership]: Room[]; +} + +export function splitRoomsByMembership(rooms: Room[]): MembershipSplit { + const split: MembershipSplit = { + [EffectiveMembership.Invite]: [], + [EffectiveMembership.Join]: [], + [EffectiveMembership.Leave]: [], + }; + + for (const room of rooms) { + split[getEffectiveMembership(room.getMyMembership())].push(room); + } + + return split; +} + +export function getEffectiveMembership(membership: string): EffectiveMembership { + if (membership === 'invite') { + return EffectiveMembership.Invite; + } else if (membership === 'join') { + // TODO: Do the same for knock? Update docs as needed in the enum. + return EffectiveMembership.Join; + } else { + // Probably a leave, kick, or ban + return EffectiveMembership.Leave; + } +} From 00d400b516ef35c2121132ecdee657355783d3a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 20:36:42 -0600 Subject: [PATCH 155/241] Possible framework for a proof of concept This is the fruits of about 3 attempts to write code that works. None of those attempts are here, but how edition 4 could work is at least documented now. --- src/stores/room-list/README.md | 117 +++++++++++ src/stores/room-list/TagManager.ts | 32 +++ .../room-list/algorithms/ChaoticAlgorithm.ts | 1 + src/stores/room-list/algorithms/IAlgorithm.ts | 9 +- .../algorithms/ImportanceAlgorithm.ts | 189 ++++++++++++++++++ src/stores/room-list/algorithms/index.ts | 3 +- 6 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 src/stores/room-list/README.md create mode 100644 src/stores/room-list/TagManager.ts create mode 100644 src/stores/room-list/algorithms/ImportanceAlgorithm.ts diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md new file mode 100644 index 0000000000..ed13210420 --- /dev/null +++ b/src/stores/room-list/README.md @@ -0,0 +1,117 @@ +# Room list sorting + +It's so complicated it needs its own README. + +## Algorithms involved + +There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. +Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting +Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting +algorithm determines how individual tags (lists of rooms, sometimes called sublists) are ordered. + +Behaviour of the room list takes the shape of default sorting on tags in most cases, though it can +override what is happening at the tag level depending on the algorithm used (as is the case with the +importance algorithm, described later). + +Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm +the power to decide when and how to apply the tag sorting, if at all. + +### Tag sorting algorithm: Alphabetical + +When used, rooms in a given tag will be sorted alphabetically, where the alphabet is determined by a +simple string comparison operation (essentially giving the browser the problem of figuring out if A +comes before Z). + +### Tag sorting algorithm: Manual + +Manual sorting makes use of the `order` property present on all tags for a room, per the +[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values +of `order` cause rooms to appear closer to the top of the list. + +### Tag sorting algorithm: Recent + +Rooms are ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +in the room list system which determines whether an event type is capable of bubbling up in the room list. +Normally events like room messages, stickers, and room security changes will be considered useful enough +to cause a shift in time. + +Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually +consistent this means that from time to time a room might plummet or skyrocket across the tag due to the +timestamp contained within the event (generated server-side by the sender's server). + +### List ordering algorithm: Natural + +This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no +behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. +Historically, it's been the only option in Riot and extremely common in most chat applications due to +its relative deterministic behaviour. + +### List ordering algorithm: Importance + +On the other end of the spectrum, this is the most complicated algorithm which exists. There's major +behavioural changes and the tag sorting algorithm is selectively applied depending on circumstances. + +Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags +simply get the manual sorting algorithm applied to them with no further involvement from the importance +algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off +relative (perceived) importance to the user: + +* **Red**: The room has unread mentions waiting for the user. +* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically this is the default as rooms are + set to 'All Messages'. +* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +* **Idle**: No relevant activity has occurred in the room since the user last read it. + +Conveniently, each tag is ordered by those categories as presented: red rooms appear above grey, grey +above idle, etc. + +Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm +is applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other and the grey rooms sorted amongst each other, but +collectively the tag will be sorted into categories with red being at the top. + +The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. +The sticky room will remain in position on the room list regardless of other factors going on as typically +clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms +above the selected room at all times where N is the number of rooms above the selected rooms when it was +selected. + +For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one +room above their selection at all times. If they receive another notification and the tag ordering is set +to Recent, they'll see the new notification go to the top position and the one that was previously there +fall behind the sticky room. + +The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the +tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another +room, the previous sticky room is recalculated to determine which category it needs to be in as the user +could have been scrolled up while new messages were received. + +Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what +kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user +selecting the third room (leaving 2 above it), and then having the rooms above it read on another device. +This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain +2 rooms above the sticky room. + +An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement +exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain +the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +put the sticky room in a position where it's had to decrease N will not increase N. + +## Responsibilities of the store + +The store is responsible for the ordering, upkeep, and tracking of all rooms. The component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +all kinds of filtering. + +## Class breakdowns + +The `RoomListStore` is the major coordinator of various `IAlgorithm` implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. A `TagManager` is responsible for figuring +out which tags get which rooms, as Matrix specifies them as a reverse map: tags are defined on rooms and +are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific +utilities are also included, though they are expected to move somewhere more general when needed. For +example, the `membership` utilities could easily be moved elsewhere as needed. diff --git a/src/stores/room-list/TagManager.ts b/src/stores/room-list/TagManager.ts new file mode 100644 index 0000000000..368c4e2c21 --- /dev/null +++ b/src/stores/room-list/TagManager.ts @@ -0,0 +1,32 @@ +/* +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 {EventEmitter} from "events"; + +// TODO: Docs on what this is +export class TagManager extends EventEmitter { + constructor() { + super(); + } + + // TODO: Implementation. + // This will need to track where rooms belong in tags, and which tags they + // should be tracked within. This is algorithm independent because all the + // algorithms need to know what each tag contains. + // + // This will likely make use of the effective membership to determine the + // invite+historical sections. +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 9dfe6f6205..1b640669c0 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -31,6 +31,7 @@ export class ChaoticAlgorithm implements IAlgorithm { private rooms: Room[] = []; constructor(private representativeAlgorithm: ListAlgorithm) { + console.log("Constructed a ChaoticAlgorithm"); } getOrderedRooms(): ITagMap { diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts index fbe2f7a27d..ab5c4742df 100644 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -32,13 +32,6 @@ export enum ListAlgorithm { Natural = "NATURAL", } -export enum Category { - Red = "RED", - Grey = "GREY", - Bold = "BOLD", - Idle = "IDLE", -} - export interface ITagSortingMap { // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. [tagId: TagID]: SortAlgorithm; @@ -50,7 +43,7 @@ export interface ITagMap { } // TODO: Convert IAlgorithm to an abstract class? -// TODO: Add locking support to avoid concurrent writes +// TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support /** diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts new file mode 100644 index 0000000000..0a2184eb43 --- /dev/null +++ b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts @@ -0,0 +1,189 @@ +/* +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 { IAlgorithm, ITagMap, ITagSortingMap } from "./IAlgorithm"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { DefaultTagID, TagID } from "../models"; +import { splitRoomsByMembership } from "../membership"; + +/** + * The determined category of a room. + */ +export enum Category { + /** + * The room has unread mentions within. + */ + Red = "RED", + /** + * The room has unread notifications within. Note that these are not unread + * mentions - they are simply messages which the user has asked to cause a + * badge count update or push notification. + */ + Grey = "GREY", + /** + * The room has unread messages within (grey without the badge). + */ + Bold = "BOLD", + /** + * The room has no relevant unread messages within. + */ + Idle = "IDLE", +} + +/** + * An implementation of the "importance" algorithm for room list sorting. Where + * the tag sorting algorithm does not interfere, rooms will be ordered into + * categories of varying importance to the user. Alphabetical sorting does not + * interfere with this algorithm, however manual ordering does. + * + * The importance of a room is defined by the kind of notifications, if any, are + * present on the room. These are classified internally as Red, Grey, Bold, and + * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy + * version of grey, and idle means all activity has been seen by the user. + * + * The algorithm works by monitoring all room changes, including new messages in + * tracked rooms, to determine if it needs a new category or different placement + * within the same category. For more information, see the comments contained + * within the class. + */ +export class ImportanceAlgorithm implements IAlgorithm { + + // HOW THIS WORKS + // -------------- + // + // This block of comments assumes you've read the README one level higher. + // You should do that if you haven't already. + // + // Tags are fed into the algorithmic functions from the TagManager changes, + // which cause subsequent updates to the room list itself. Categories within + // those tags are tracked as index numbers within the array (zero = top), with + // each sticky room being tracked separately. Internally, the category index + // can be found from `this.indices[tag][category]` and the sticky room information + // from `this.stickyRooms[tag]`. + // + // Room categories are constantly re-evaluated and tracked in the `this.categorized` + // object. Note that this doesn't track rooms by category but instead by room ID. + // The theory is that by knowing the previous position, new desired position, and + // category indices we can avoid tracking multiple complicated maps in memory. + // + // The room list store is always provided with the `this.cached` results, which are + // updated as needed and not recalculated often. For example, when a room needs to + // move within a tag, the array in `this.cached` will be spliced instead of iterated. + + private cached: ITagMap = {}; + private sortAlgorithms: ITagSortingMap; + private rooms: Room[] = []; + private indices: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + // @ts-ignore - TS wants this to be a string but we know better than it + [category: Category]: number; // integer + }; + } = {}; + private stickyRooms: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + room?: Room; + nAbove: number; // integer + }; + } = {}; + private categorized: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + // TODO: Remove note + // Note: Should in theory be able to only track this by room ID as we'll know + // the indices of each category and can determine if a category needs changing + // in the cached list. Could potentially save a bunch of time if we can figure + // out where a room is supposed to be using offsets, some math, and leaving the + // array generally alone. + [roomId: string]: { + room: Room; + category: Category; + }; + }; + } = {}; + + constructor() { + console.log("Constructed an ImportanceAlgorithm"); + } + + getOrderedRooms(): ITagMap { + return this.cached; + } + + async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + this.setKnownRooms(this.rooms); // regenerate the room lists + } + + handleRoomUpdate(room): Promise { + return undefined; + } + + setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + this.cached = newTags; + return; + } + + // TODO: Remove logging + const memberships = splitRoomsByMembership(rooms); + console.log({memberships}); + + // Step through each room and determine which tags it should be in. + // We don't care about ordering or sorting here - we're simply organizing things. + for (const room of rooms) { + const tags = room.tags; + let inTag = false; + for (const tagId in tags) { + // noinspection JSUnfilteredForInLoop + if (isNullOrUndefined(newTags[tagId])) { + // skip the tag if we don't know about it + continue; + } + + inTag = true; + + // noinspection JSUnfilteredForInLoop + newTags[tagId].push(room); + } + + // If the room wasn't pushed to a tag, push it to the untagged tag. + if (!inTag) { + newTags[DefaultTagID.Untagged].push(room); + } + } + + // TODO: Do sorting + + // Finally, assign the tags to our cache + this.cached = newTags; + } +} diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts index cb67d42187..918b176f48 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/index.ts @@ -16,10 +16,11 @@ limitations under the License. import { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; +import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), - [ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance), + [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; /** From 9c0422691a551a6a92533bf6e34684a09b95568a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 20:44:18 -0600 Subject: [PATCH 156/241] Add another thought Maybe we can speed up the algorithm if we know why we're doing the update. --- src/stores/room-list/algorithms/IAlgorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts index ab5c4742df..b49f0b10b2 100644 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -84,5 +84,5 @@ export interface IAlgorithm { * depending on whether or not getOrderedRooms() should be called after * processing. */ - handleRoomUpdate(room: Room): Promise; + handleRoomUpdate(room: Room): Promise; // TODO: Take a ReasonForChange to better predict the behaviour? } From e7fffee17568fba9b89bbe98bc0b9382e111ba1b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Apr 2020 16:19:10 -0600 Subject: [PATCH 157/241] Remove the need for a tag manager Instead putting the tag handling in the Algorithm class --- src/stores/room-list/README.md | 13 +- src/stores/room-list/RoomListStore2.ts | 4 +- .../room-list/RoomListStoreTempProxy.ts | 2 +- src/stores/room-list/TagManager.ts | 32 ---- src/stores/room-list/algorithms/Algorithm.ts | 178 ++++++++++++++++++ .../room-list/algorithms/ChaoticAlgorithm.ts | 80 +------- src/stores/room-list/algorithms/IAlgorithm.ts | 88 --------- .../algorithms/ImportanceAlgorithm.ts | 76 +------- src/stores/room-list/algorithms/index.ts | 8 +- 9 files changed, 212 insertions(+), 269 deletions(-) delete mode 100644 src/stores/room-list/TagManager.ts create mode 100644 src/stores/room-list/algorithms/Algorithm.ts delete mode 100644 src/stores/room-list/algorithms/IAlgorithm.ts diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index ed13210420..0dd6c104d8 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -109,9 +109,10 @@ all kinds of filtering. ## Class breakdowns -The `RoomListStore` is the major coordinator of various `IAlgorithm` implementations, which take care -of the various `ListAlgorithm` and `SortingAlgorithm` options. A `TagManager` is responsible for figuring -out which tags get which rooms, as Matrix specifies them as a reverse map: tags are defined on rooms and -are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific -utilities are also included, though they are expected to move somewhere more general when needed. For -example, the `membership` utilities could easily be moved elsewhere as needed. +The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also +responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: +tags are defined on rooms and are not defined as a collection of rooms (unlike how they are presented +to the user). Various list-specific utilities are also included, though they are expected to move +somewhere more general when needed. For example, the `membership` utilities could easily be moved +elsewhere as needed. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 7fab8c7ff9..dc1cb49cd6 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -19,7 +19,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; -import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; +import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { getAlgorithmInstance } from "./algorithms"; import { AsyncStore } from "../AsyncStore"; @@ -41,7 +41,7 @@ class _RoomListStore extends AsyncStore { private matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; - private algorithm: IAlgorithm; + private algorithm: Algorithm; private readonly watchedSettings = [ 'RoomList.orderAlphabetically', diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 88171c0809..8ad3c5d35e 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/IAlgorithm"; +import { ITagMap } from "./algorithms/Algorithm"; import { UPDATE_EVENT } from "../AsyncStore"; /** diff --git a/src/stores/room-list/TagManager.ts b/src/stores/room-list/TagManager.ts deleted file mode 100644 index 368c4e2c21..0000000000 --- a/src/stores/room-list/TagManager.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -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 {EventEmitter} from "events"; - -// TODO: Docs on what this is -export class TagManager extends EventEmitter { - constructor() { - super(); - } - - // TODO: Implementation. - // This will need to track where rooms belong in tags, and which tags they - // should be tracked within. This is algorithm independent because all the - // algorithms need to know what each tag contains. - // - // This will likely make use of the effective membership to determine the - // invite+historical sections. -} diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts new file mode 100644 index 0000000000..15fc208b21 --- /dev/null +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -0,0 +1,178 @@ +/* +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 { DefaultTagID, TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { EffectiveMembership, splitRoomsByMembership } from "../membership"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} + +// TODO: Add locking support to avoid concurrent writes? +// TODO: EventEmitter support? Might not be needed. + +export abstract class Algorithm { + protected cached: ITagMap = {}; + protected sortAlgorithms: ITagSortingMap; + protected rooms: Room[] = []; + protected roomsByTag: { + // @ts-ignore - TS wants this to be a string but we know better + [tagId: TagID]: Room[]; + } = {}; + + protected constructor() { + } + + /** + * Asks the Algorithm to regenerate all lists, using the tags given + * as reference for which lists to generate and which way to generate + * them. + * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + return this.setKnownRooms(this.rooms); + } + + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + public getOrderedRooms(): ITagMap { + return this.cached; + } + + /** + * Seeds the Algorithm with a set of rooms. The algorithm will discard all + * previously known information and instead use these rooms instead. + * @param {Room[]} rooms The rooms to force the algorithm to use. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags: ITagMap = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + await this.generateFreshTags(newTags); // just in case it wants to do something + this.cached = newTags; + return; + } + + // Split out the easy rooms first (leave and invite) + const memberships = splitRoomsByMembership(rooms); + for (const room of memberships[EffectiveMembership.Invite]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`); + newTags[DefaultTagID.Invite].push(room); + } + for (const room of memberships[EffectiveMembership.Leave]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`); + newTags[DefaultTagID.Archived].push(room); + } + + // Now process all the joined rooms. This is a bit more complicated + for (const room of memberships[EffectiveMembership.Join]) { + const tags = Object.keys(room.tags || {}); + + let inTag = false; + if (tags.length > 0) { + for (const tag of tags) { + if (!isNullOrUndefined(newTags[tag])) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); + newTags[tag].push(room); + inTag = true; + } + } + } + + if (!inTag) { + // TODO: Determine if DM and push there instead + newTags[DefaultTagID.Untagged].push(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`); + } + } + + await this.generateFreshTags(newTags); + + this.cached = newTags; + } + + /** + * Called when the Algorithm believes a complete regeneration of the existing + * lists is needed. + * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag + * will already have the rooms which belong to it - they just need ordering. Must + * be mutated in place. + * @returns {Promise<*>} A promise which resolves when complete. + */ + protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; + + /** + * Called when the Algorithm wants a whole tag to be reordered. Typically this will + * be done whenever the tag's scope changes (added/removed rooms). + * @param {TagID} tagId The tag ID which changed. + * @param {Room[]} rooms The rooms within the tag, unordered. + * @returns {Promise} Resolves to the ordered rooms in the tag. + */ + protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; + + /** + * Asks the Algorithm to update its knowledge of a room. For example, when + * a user tags a room, joins/creates a room, or leaves a room the Algorithm + * should be told that the room's info might have changed. The Algorithm + * may no-op this request if no changes are required. + * @param {Room} room The room which might have affected sorting. + * @returns {Promise} A promise which resolve to true or false + * depending on whether or not getOrderedRooms() should be called after + * processing. + */ + // TODO: Take a ReasonForChange to better predict the behaviour? + // TODO: Intercept here and handle tag changes automatically + public abstract handleRoomUpdate(room: Room): Promise; +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 1b640669c0..5d4177db8b 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -14,89 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { Algorithm, ITagMap } from "./Algorithm"; import { DefaultTagID } from "../models"; -import { splitRoomsByMembership } from "../membership"; /** * A demonstration/temporary algorithm to verify the API surface works. * TODO: Remove this before shipping */ -export class ChaoticAlgorithm implements IAlgorithm { +export class ChaoticAlgorithm extends Algorithm { - private cached: ITagMap = {}; - private sortAlgorithms: ITagSortingMap; - private rooms: Room[] = []; - - constructor(private representativeAlgorithm: ListAlgorithm) { + constructor() { + super(); console.log("Constructed a ChaoticAlgorithm"); } - getOrderedRooms(): ITagMap { - return this.cached; + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + return Promise.resolve(); } - async populateTags(tagSortingMap: ITagSortingMap): Promise { - if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); - this.sortAlgorithms = tagSortingMap; - this.setKnownRooms(this.rooms); // regenerate the room lists + protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { + return Promise.resolve(rooms); } - handleRoomUpdate(room): Promise { - return undefined; - } - - setKnownRooms(rooms: Room[]): Promise { - if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); - if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - - this.rooms = rooms; - - const newTags = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } - - // If we can avoid doing work, do so. - if (!rooms.length) { - this.cached = newTags; - return; - } - - // TODO: Remove logging - const memberships = splitRoomsByMembership(rooms); - console.log({alg: this.representativeAlgorithm, memberships}); - - // Step through each room and determine which tags it should be in. - // We don't care about ordering or sorting here - we're simply organizing things. - for (const room of rooms) { - const tags = room.tags; - let inTag = false; - for (const tagId in tags) { - // noinspection JSUnfilteredForInLoop - if (isNullOrUndefined(newTags[tagId])) { - // skip the tag if we don't know about it - continue; - } - - inTag = true; - - // noinspection JSUnfilteredForInLoop - newTags[tagId].push(room); - } - - // If the room wasn't pushed to a tag, push it to the untagged tag. - if (!inTag) { - newTags[DefaultTagID.Untagged].push(room); - } - } - - // TODO: Do sorting - - // Finally, assign the tags to our cache - this.cached = newTags; + public async handleRoomUpdate(room): Promise { + return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts deleted file mode 100644 index b49f0b10b2..0000000000 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* -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 { TagID } from "../models"; -import { Room } from "matrix-js-sdk/src/models/room"; - - -export enum SortAlgorithm { - Manual = "MANUAL", - Alphabetic = "ALPHABETIC", - Recent = "RECENT", -} - -export enum ListAlgorithm { - // Orders Red > Grey > Bold > Idle - Importance = "IMPORTANCE", - - // Orders however the SortAlgorithm decides - Natural = "NATURAL", -} - -export interface ITagSortingMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: SortAlgorithm; -} - -export interface ITagMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: Room[]; -} - -// TODO: Convert IAlgorithm to an abstract class? -// TODO: Add locking support to avoid concurrent writes? -// TODO: EventEmitter support - -/** - * Represents an algorithm for the RoomListStore to use - */ -export interface IAlgorithm { - /** - * Asks the Algorithm to regenerate all lists, using the tags given - * as reference for which lists to generate and which way to generate - * them. - * @param {ITagSortingMap} tagSortingMap The tags to generate. - * @returns {Promise<*>} A promise which resolves when complete. - */ - populateTags(tagSortingMap: ITagSortingMap): Promise; - - /** - * Gets an ordered set of rooms for the all known tags. - * @returns {ITagMap} The cached list of rooms, ordered, - * for each tag. May be empty, but never null/undefined. - */ - getOrderedRooms(): ITagMap; - - /** - * Seeds the Algorithm with a set of rooms. The algorithm will discard all - * previously known information and instead use these rooms instead. - * @param {Room[]} rooms The rooms to force the algorithm to use. - * @returns {Promise<*>} A promise which resolves when complete. - */ - setKnownRooms(rooms: Room[]): Promise; - - /** - * Asks the Algorithm to update its knowledge of a room. For example, when - * a user tags a room, joins/creates a room, or leaves a room the Algorithm - * should be told that the room's info might have changed. The Algorithm - * may no-op this request if no changes are required. - * @param {Room} room The room which might have affected sorting. - * @returns {Promise} A promise which resolve to true or false - * depending on whether or not getOrderedRooms() should be called after - * processing. - */ - handleRoomUpdate(room: Room): Promise; // TODO: Take a ReasonForChange to better predict the behaviour? -} diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts index 0a2184eb43..1a7a73a9d5 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ITagMap, ITagSortingMap } from "./IAlgorithm"; +import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { DefaultTagID, TagID } from "../models"; @@ -60,7 +60,7 @@ export enum Category { * within the same category. For more information, see the comments contained * within the class. */ -export class ImportanceAlgorithm implements IAlgorithm { +export class ImportanceAlgorithm extends Algorithm { // HOW THIS WORKS // -------------- @@ -68,7 +68,7 @@ export class ImportanceAlgorithm implements IAlgorithm { // This block of comments assumes you've read the README one level higher. // You should do that if you haven't already. // - // Tags are fed into the algorithmic functions from the TagManager changes, + // Tags are fed into the algorithmic functions from the Algorithm superclass, // which cause subsequent updates to the room list itself. Categories within // those tags are tracked as index numbers within the array (zero = top), with // each sticky room being tracked separately. Internally, the category index @@ -84,9 +84,6 @@ export class ImportanceAlgorithm implements IAlgorithm { // updated as needed and not recalculated often. For example, when a room needs to // move within a tag, the array in `this.cached` will be spliced instead of iterated. - private cached: ITagMap = {}; - private sortAlgorithms: ITagSortingMap; - private rooms: Room[] = []; private indices: { // @ts-ignore - TS wants this to be a string but we know better than it [tag: TagID]: { @@ -118,72 +115,19 @@ export class ImportanceAlgorithm implements IAlgorithm { } = {}; constructor() { + super(); console.log("Constructed an ImportanceAlgorithm"); } - getOrderedRooms(): ITagMap { - return this.cached; + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + return Promise.resolve(); } - async populateTags(tagSortingMap: ITagSortingMap): Promise { - if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); - this.sortAlgorithms = tagSortingMap; - this.setKnownRooms(this.rooms); // regenerate the room lists + protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { + return Promise.resolve(rooms); } - handleRoomUpdate(room): Promise { - return undefined; - } - - setKnownRooms(rooms: Room[]): Promise { - if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); - if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - - this.rooms = rooms; - - const newTags = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } - - // If we can avoid doing work, do so. - if (!rooms.length) { - this.cached = newTags; - return; - } - - // TODO: Remove logging - const memberships = splitRoomsByMembership(rooms); - console.log({memberships}); - - // Step through each room and determine which tags it should be in. - // We don't care about ordering or sorting here - we're simply organizing things. - for (const room of rooms) { - const tags = room.tags; - let inTag = false; - for (const tagId in tags) { - // noinspection JSUnfilteredForInLoop - if (isNullOrUndefined(newTags[tagId])) { - // skip the tag if we don't know about it - continue; - } - - inTag = true; - - // noinspection JSUnfilteredForInLoop - newTags[tagId].push(room); - } - - // If the room wasn't pushed to a tag, push it to the untagged tag. - if (!inTag) { - newTags[DefaultTagID.Untagged].push(room); - } - } - - // TODO: Do sorting - - // Finally, assign the tags to our cache - this.cached = newTags; + public async handleRoomUpdate(room): Promise { + return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts index 918b176f48..1277b66ac9 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/index.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; +import { Algorithm, ListAlgorithm } from "./Algorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; -const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; @@ -26,9 +26,9 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = /** * Gets an instance of the defined algorithm * @param {ListAlgorithm} algorithm The algorithm to get an instance of. - * @returns {IAlgorithm} The algorithm instance. + * @returns {Algorithm} The algorithm instance. */ -export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm { +export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } From d244eeb5d585f3101b7450d220cbd91cba014b33 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Apr 2020 16:57:06 -0600 Subject: [PATCH 158/241] Break up algorithms and use the new layering Sorting and ordering has now been split apart. The ImportanceAlgorithm also finally makes use of the sorting. So far metrics look okay at 3ms for a simple account, though this could potentially get worse due to the multiple loops involved (one for tags, one for categories, one for ordering). We might be able to feed a whole list of rooms into the thing and have it regenerate the lists on demand. --- src/stores/room-list/RoomListStore2.ts | 7 +- .../room-list/RoomListStoreTempProxy.ts | 2 +- .../{ => list_ordering}/Algorithm.ts | 37 +++------ .../{ => list_ordering}/ChaoticAlgorithm.ts | 5 +- .../ImportanceAlgorithm.ts | 81 +++++++++++++++++-- .../algorithms/{ => list_ordering}/index.ts | 7 +- src/stores/room-list/algorithms/models.ts | 42 ++++++++++ .../tag_sorting/ChaoticAlgorithm.ts | 29 +++++++ .../algorithms/tag_sorting/IAlgorithm.ts | 31 +++++++ .../algorithms/tag_sorting/ManualAlgorithm.ts | 31 +++++++ .../room-list/algorithms/tag_sorting/index.ts | 52 ++++++++++++ 11 files changed, 283 insertions(+), 41 deletions(-) rename src/stores/room-list/algorithms/{ => list_ordering}/Algorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ChaoticAlgorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ImportanceAlgorithm.ts (63%) rename src/stores/room-list/algorithms/{ => list_ordering}/index.ts (84%) create mode 100644 src/stores/room-list/algorithms/models.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/index.ts diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index dc1cb49cd6..0b3f61e261 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -19,10 +19,11 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; -import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm"; +import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; -import { getAlgorithmInstance } from "./algorithms"; +import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; interface IState { tagsEnabled?: boolean; @@ -172,7 +173,7 @@ class _RoomListStore extends AsyncStore { } private setAlgorithmClass() { - this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); } private async regenerateAllLists() { diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 8ad3c5d35e..4edca2b9cd 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/Algorithm"; +import { ITagMap } from "./algorithms/models"; import { UPDATE_EVENT } from "../AsyncStore"; /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/Algorithm.ts rename to src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 15fc208b21..4c8c9e9c60 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -14,38 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, TagID } from "../models"; +import { DefaultTagID, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { EffectiveMembership, splitRoomsByMembership } from "../membership"; - -export enum SortAlgorithm { - Manual = "MANUAL", - Alphabetic = "ALPHABETIC", - Recent = "RECENT", -} - -export enum ListAlgorithm { - // Orders Red > Grey > Bold > Idle - Importance = "IMPORTANCE", - - // Orders however the SortAlgorithm decides - Natural = "NATURAL", -} - -export interface ITagSortingMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: SortAlgorithm; -} - -export interface ITagMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: Room[]; -} +import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; +import { ITagMap, ITagSortingMap } from "../models"; // TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support? Might not be needed. +/** + * Represents a list ordering algorithm. This class will take care of tag + * management (which rooms go in which tags) and ask the implementation to + * deal with ordering mechanics. + */ export abstract class Algorithm { protected cached: ITagMap = {}; protected sortAlgorithms: ITagSortingMap; @@ -160,6 +142,7 @@ export abstract class Algorithm { * @param {Room[]} rooms The rooms within the tag, unordered. * @returns {Promise} Resolves to the ordered rooms in the tag. */ + // TODO: Do we need this? protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; /** @@ -173,6 +156,6 @@ export abstract class Algorithm { * processing. */ // TODO: Take a ReasonForChange to better predict the behaviour? - // TODO: Intercept here and handle tag changes automatically + // TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that. public abstract handleRoomUpdate(room: Room): Promise; } diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/ChaoticAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 5d4177db8b..7c1a0b1acc 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap } from "./Algorithm"; -import { DefaultTagID } from "../models"; +import { Algorithm } from "./Algorithm"; +import { DefaultTagID } from "../../models"; +import { ITagMap } from "../models"; /** * A demonstration/temporary algorithm to verify the API surface works. diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts similarity index 63% rename from src/stores/room-list/algorithms/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 1a7a73a9d5..d73fdee930 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -1,4 +1,5 @@ /* +Copyright 2018, 2019 New Vector Ltd Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { DefaultTagID, TagID } from "../models"; -import { splitRoomsByMembership } from "../membership"; +import { DefaultTagID, TagID } from "../../models"; +import { ITagMap, SortAlgorithm } from "../models"; +import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting"; +import * as Unread from '../../../../Unread'; /** * The determined category of a room. @@ -44,6 +46,11 @@ export enum Category { Idle = "IDLE", } +interface ICategorizedRoomMap { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: Room[]; +} + /** * An implementation of the "importance" algorithm for room list sorting. Where * the tag sorting algorithm does not interfere, rooms will be ordered into @@ -119,8 +126,72 @@ export class ImportanceAlgorithm extends Algorithm { console.log("Constructed an ImportanceAlgorithm"); } + // noinspection JSMethodCanBeStatic + private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { + const map: ICategorizedRoomMap = { + [Category.Red]: [], + [Category.Grey]: [], + [Category.Bold]: [], + [Category.Idle]: [], + }; + for (const room of rooms) { + const category = this.getRoomCategory(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`); + map[category].push(room); + } + return map; + } + + // noinspection JSMethodCanBeStatic + private getRoomCategory(room: Room): Category { + // Function implementation borrowed from old RoomListStore + + const mentions = room.getUnreadNotificationCount('highlight') > 0; + if (mentions) { + return Category.Red; + } + + let unread = room.getUnreadNotificationCount() > 0; + if (unread) { + return Category.Grey; + } + + unread = Unread.doesRoomHaveUnreadMessages(room); + if (unread) { + return Category.Bold; + } + + return Category.Idle; + } + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - return Promise.resolve(); + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + if (sortBy === SortAlgorithm.Manual) { + // Manual tags essentially ignore the importance algorithm, so don't do anything + // special about them. + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } else { + // Every other sorting type affects the categories, not the whole tag. + const categorized = this.categorizeRooms(unorderedRooms); + for (const category of Object.keys(categorized)) { + const roomsToOrder = categorized[category]; + categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); + } + + // TODO: Update positions of categories in cache + updatedTagMap[tagId] = [ + ...categorized[Category.Red], + ...categorized[Category.Grey], + ...categorized[Category.Bold], + ...categorized[Category.Idle], + ]; + } + } } protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/list_ordering/index.ts similarity index 84% rename from src/stores/room-list/algorithms/index.ts rename to src/stores/room-list/algorithms/list_ordering/index.ts index 1277b66ac9..35f4af14cf 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/list_ordering/index.ts @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ListAlgorithm } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; +import { ListAlgorithm } from "../models"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { - [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), + [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; @@ -28,7 +29,7 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { * @param {ListAlgorithm} algorithm The algorithm to get an instance of. * @returns {Algorithm} The algorithm instance. */ -export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { +export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts new file mode 100644 index 0000000000..284600a776 --- /dev/null +++ b/src/stores/room-list/algorithms/models.ts @@ -0,0 +1,42 @@ +/* +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 { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..31846d084a --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts @@ -0,0 +1,29 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * A demonstration to test the API surface. + * TODO: Remove this before landing + */ +export class ChaoticAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + return rooms; + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts new file mode 100644 index 0000000000..6c22ee0c9c --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; + +/** + * Represents a tag sorting algorithm. + */ +export interface IAlgorithm { + /** + * Sorts the given rooms according to the sorting rules of the algorithm. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag ID in which the rooms are being sorted. + * @returns {Promise} Resolves to the sorted rooms. + */ + sortRooms(rooms: Room[], tagId: TagID): Promise; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts new file mode 100644 index 0000000000..b8c0357633 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * Sorts rooms according to the tag's `order` property on the room. + */ +export class ManualAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + const getOrderProp = (r: Room) => r.tags[tagId].order || 0; + return rooms.sort((a, b) => { + return getOrderProp(a) - getOrderProp(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts new file mode 100644 index 0000000000..07f8f484d8 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -0,0 +1,52 @@ +/* +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 { ChaoticAlgorithm } from "./ChaoticAlgorithm"; +import { SortAlgorithm } from "../models"; +import { ManualAlgorithm } from "./ManualAlgorithm"; +import { IAlgorithm } from "./IAlgorithm"; +import { TagID } from "../../models"; +import {Room} from "matrix-js-sdk/src/models/room"; + +const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { + [SortAlgorithm.Recent]: new ChaoticAlgorithm(), + [SortAlgorithm.Alphabetic]: new ChaoticAlgorithm(), + [SortAlgorithm.Manual]: new ManualAlgorithm(), +}; + +/** + * Gets an instance of the defined algorithm + * @param {SortAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm { + if (!ALGORITHM_INSTANCES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_INSTANCES[algorithm]; +} + +/** + * Sorts rooms in a given tag according to the algorithm given. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag in which the sorting is occurring. + * @param {SortAlgorithm} algorithm The algorithm to use for sorting. + * @returns {Promise} Resolves to the sorted rooms. + */ +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { + return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); +} From ecf8090b750acf559f0550ba80a09745c0c18839 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Apr 2020 12:29:32 -0600 Subject: [PATCH 159/241] Handle DMs --- .../room-list/algorithms/list_ordering/Algorithm.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 4c8c9e9c60..fd98f34966 100644 --- a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; import { ITagMap, ITagSortingMap } from "../models"; +import DMRoomMap from "../../../../utils/DMRoomMap"; // TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support? Might not be needed. @@ -100,13 +101,21 @@ export abstract class Algorithm { // Now process all the joined rooms. This is a bit more complicated for (const room of memberships[EffectiveMembership.Join]) { - const tags = Object.keys(room.tags || {}); + let tags = Object.keys(room.tags || {}); + + if (tags.length === 0) { + // Check to see if it's a DM if it isn't anything else + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + tags = [DefaultTagID.DM]; + } + } let inTag = false; if (tags.length > 0) { for (const tag of tags) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); if (!isNullOrUndefined(newTags[tag])) { - console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`); newTags[tag].push(room); inTag = true; } From 09b7f39df8118a0c6979b7fc4091c4d583ee2782 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Apr 2020 13:21:50 -0600 Subject: [PATCH 160/241] Simple rendering of the room list for visual aid This is largely meant to prove the algorithm works and nothing more. --- src/components/views/rooms/RoomList2.tsx | 130 +++++++++++++++++++- src/components/views/rooms/RoomSublist2.tsx | 61 +++++++++ 2 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/components/views/rooms/RoomSublist2.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index f97f599ae3..04cb8a4549 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -17,11 +17,18 @@ limitations under the License. */ import * as React from "react"; -import { _t } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; +import { ITagMap } from "../../../stores/room-list/algorithms/models"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { Dispatcher } from "flux"; +import { ActionPayload } from "../../../dispatcher-types"; +import dis from "../../../dispatcher"; +import { RoomSublist2 } from "./RoomSublist2"; +import { isNullOrUndefined } from "matrix-js-sdk/lib/src/utils"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -33,14 +40,83 @@ interface IProps { } interface IState { + sublists: ITagMap; } -// TODO: Actually write stub -export class RoomSublist2 extends React.Component { - public setHeight(size: number) { - } +const TAG_ORDER: TagID[] = [ + // -- Community Invites Placeholder -- + + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + + // -- Custom Tags Placeholder -- + + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; +const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite; +const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority; +const ALWAYS_VISIBLE_TAGS: TagID[] = [ + DefaultTagID.DM, + DefaultTagID.Untagged, +]; + +interface ITagAesthetics { + sectionLabel: string; + addRoomLabel?: string; + onAddRoom?: (dispatcher: Dispatcher) => void; + isInvite: boolean; + defaultHidden: boolean; } +const TAG_AESTHETICS: { + // @ts-ignore - TS wants this to be a string but we know better + [tagId: TagID]: ITagAesthetics; +} = { + [DefaultTagID.Invite]: { + sectionLabel: _td("Invites"), + isInvite: true, + defaultHidden: false, + }, + [DefaultTagID.Favourite]: { + sectionLabel: _td("Favourites"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.DM]: { + sectionLabel: _td("Direct Messages"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Start chat"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_chat'}), + }, + [DefaultTagID.Untagged]: { + sectionLabel: _td("Rooms"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Create room"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_room'}), + }, + [DefaultTagID.LowPriority]: { + sectionLabel: _td("Low priority"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.ServerNotice]: { + sectionLabel: _td("System Alerts"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.Archived]: { + sectionLabel: _td("Historical"), + isInvite: false, + defaultHidden: true, + }, +}; + export default class RoomList2 extends React.Component { private sublistRefs: { [tagId: string]: React.RefObject } = {}; private sublistSizes: { [tagId: string]: number } = {}; @@ -51,6 +127,7 @@ export default class RoomList2 extends React.Component { constructor(props: IProps) { super(props); + this.state = {sublists: {}}; this.loadSublistSizes(); this.prepareLayouts(); } @@ -58,6 +135,7 @@ export default class RoomList2 extends React.Component { public componentDidMount(): void { RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { console.log("new lists", store.orderedLists); + this.setState({sublists: store.orderedLists}); }); } @@ -108,7 +186,47 @@ export default class RoomList2 extends React.Component { } } + private renderSublists(): React.ReactElement[] { + const components: React.ReactElement[] = []; + + for (const orderedTagId of TAG_ORDER) { + if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) { + // Populate community invites if we have the chance + // TODO + } + if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) { + // Populate custom tags if needed + // TODO + } + + const orderedRooms = this.state.sublists[orderedTagId] || []; + if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) { + continue; // skip tag - not needed + } + + const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; + if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); + + const onAddRoomFn = () => { + if (!aesthetics.onAddRoom) return; + aesthetics.onAddRoom(dis); + }; + components.push(); + } + + return components; + } + public render() { + const sublists = this.renderSublists(); return ( {({onKeyDownHandler}) => ( @@ -122,7 +240,7 @@ export default class RoomList2 extends React.Component { // Firefox sometimes makes this element focusable due to // overflow:scroll;, so force it out of tab order. tabIndex={-1} - >{_t("TODO")}
    + >{sublists}
    )} ); diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx new file mode 100644 index 0000000000..a3ca525b14 --- /dev/null +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 Vector Creations Ltd +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 { Room } from "matrix-js-sdk/src/models/room"; + +interface IProps { + forRooms: boolean; + rooms?: Room[]; + startAsHidden: boolean; + label: string; + onAddRoom?: () => void; + addRoomLabel: string; + isInvite: boolean; + + // TODO: Collapsed state + // TODO: Height + // TODO: Group invites + // TODO: Calls + // TODO: forceExpand? + // TODO: Header clicking + // TODO: Spinner support for historical +} + +interface IState { +} + +// TODO: Actually write stub +export class RoomSublist2 extends React.Component { + public setHeight(size: number) { + } + + public render() { + // TODO: Proper rendering + + const rooms = this.props.rooms.map(r => ( +
    {r.name} ({r.roomId})
    + )); + return ( +
    +

    {this.props.label}

    + {rooms} +
    + ); + } +} From 5dda7f02cffb88e0621b68429fcc725e4dab8bb6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 4 May 2020 09:06:34 -0600 Subject: [PATCH 161/241] Early handling of dispatched events A possible approach to handling the various triggers for recategorizing rooms. --- src/components/views/rooms/RoomList2.tsx | 1 - src/stores/room-list/RoomListStore2.ts | 41 ++++++++++++++++--- .../algorithms/list_ordering/Algorithm.ts | 41 +++++++++++-------- .../list_ordering/ChaoticAlgorithm.ts | 7 +--- .../list_ordering/ImportanceAlgorithm.ts | 17 ++++---- src/stores/room-list/models.ts | 5 +++ 6 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 04cb8a4549..f7788f0003 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -28,7 +28,6 @@ import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; import { RoomSublist2 } from "./RoomSublist2"; -import { isNullOrUndefined } from "matrix-js-sdk/lib/src/utils"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 0b3f61e261..06f97ae53e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -18,12 +18,14 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; -import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; interface IState { tagsEnabled?: boolean; @@ -123,7 +125,14 @@ class _RoomListStore extends AsyncStore { await this.regenerateAllLists(); // regenerate the lists now } - } else if (payload.action === 'MatrixActions.Room.receipt') { + } + + if (!this.algorithm) { + // This shouldn't happen because `initialListsGenerated` implies we have an algorithm. + throw new Error("Room list store has no algorithm to process dispatcher update with"); + } + + if (payload.action === 'MatrixActions.Room.receipt') { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle @@ -132,23 +141,45 @@ class _RoomListStore extends AsyncStore { const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); if (receiptUsers.includes(myUserId)) { // TODO: Update room now that it's been read + console.log(payload); return; } } } else if (payload.action === 'MatrixActions.Room.tags') { // TODO: Update room from tags - } else if (payload.action === 'MatrixActions.room.timeline') { - // TODO: Update room from new events + console.log(payload); + } else if (payload.action === 'MatrixActions.Room.timeline') { + const eventPayload = (payload); // TODO: Type out the dispatcher types + + // Ignore non-live events (backfill) + if (!eventPayload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent) return; + + const roomId = eventPayload.event.getRoomId(); + const room = this.matrixClient.getRoom(roomId); + await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.Event.decrypted') { // TODO: Update room from decrypted event + console.log(payload); } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { // TODO: Update DMs + console.log(payload); } else if (payload.action === 'MatrixActions.Room.myMembership') { // TODO: Update room from membership change - } else if (payload.action === 'MatrixActions.room') { + console.log(payload); + } else if (payload.action === 'MatrixActions.Room') { // TODO: Update room from creation/join + console.log(payload); } else if (payload.action === 'view_room') { // TODO: Update sticky room + console.log(payload); + } + } + + private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); + if (shouldUpdate) { + console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`); + this.emit(LISTS_UPDATE_EVENT, this); } } diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts index fd98f34966..e154847847 100644 --- a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, TagID } from "../../models"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; @@ -33,9 +33,8 @@ export abstract class Algorithm { protected cached: ITagMap = {}; protected sortAlgorithms: ITagSortingMap; protected rooms: Room[] = []; - protected roomsByTag: { - // @ts-ignore - TS wants this to be a string but we know better - [tagId: TagID]: Room[]; + protected roomIdsToTags: { + [roomId: string]: TagID[]; } = {}; protected constructor() { @@ -132,6 +131,25 @@ export abstract class Algorithm { await this.generateFreshTags(newTags); this.cached = newTags; + this.updateTagsFromCache(); + } + + /** + * Updates the roomsToTags map + */ + protected updateTagsFromCache() { + const newMap = {}; + + const tags = Object.keys(this.cached); + for (const tagId of tags) { + const rooms = this.cached[tagId]; + for (const room of rooms) { + if (!newMap[room.roomId]) newMap[room.roomId] = []; + newMap[room.roomId].push(tagId); + } + } + + this.roomIdsToTags = newMap; } /** @@ -144,27 +162,16 @@ export abstract class Algorithm { */ protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; - /** - * Called when the Algorithm wants a whole tag to be reordered. Typically this will - * be done whenever the tag's scope changes (added/removed rooms). - * @param {TagID} tagId The tag ID which changed. - * @param {Room[]} rooms The rooms within the tag, unordered. - * @returns {Promise} Resolves to the ordered rooms in the tag. - */ - // TODO: Do we need this? - protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; - /** * Asks the Algorithm to update its knowledge of a room. For example, when * a user tags a room, joins/creates a room, or leaves a room the Algorithm * should be told that the room's info might have changed. The Algorithm * may no-op this request if no changes are required. * @param {Room} room The room which might have affected sorting. + * @param {RoomUpdateCause} cause The reason for the update being triggered. * @returns {Promise} A promise which resolve to true or false * depending on whether or not getOrderedRooms() should be called after * processing. */ - // TODO: Take a ReasonForChange to better predict the behaviour? - // TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that. - public abstract handleRoomUpdate(room: Room): Promise; + public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; } diff --git a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 7c1a0b1acc..185fb606fb 100644 --- a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { Algorithm } from "./Algorithm"; -import { DefaultTagID } from "../../models"; import { ITagMap } from "../models"; /** @@ -33,11 +32,7 @@ export class ChaoticAlgorithm extends Algorithm { return Promise.resolve(); } - protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { - return Promise.resolve(rooms); - } - - public async handleRoomUpdate(room): Promise { + public async handleRoomUpdate(room, cause): Promise { return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index d73fdee930..d66f7cdc37 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -17,9 +17,9 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DefaultTagID, TagID } from "../../models"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; -import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag_sorting"; import * as Unread from '../../../../Unread'; /** @@ -194,11 +194,12 @@ export class ImportanceAlgorithm extends Algorithm { } } - protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { - return Promise.resolve(rooms); - } - - public async handleRoomUpdate(room): Promise { - return Promise.resolve(false); + public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + return false; } } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index d1c915e035..5294680773 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -34,3 +34,8 @@ export const OrderedDefaultTagIDs = [ ]; export type TagID = string | DefaultTagID; + +export enum RoomUpdateCause { + Timeline = "TIMELINE", + RoomRead = "ROOM_READ", +} From ea34bb3022cda77e96a67e6ae1f06ef881db24d2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 4 May 2020 09:13:35 -0600 Subject: [PATCH 162/241] Make component index happy --- src/components/views/rooms/RoomList2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index f7788f0003..7aefc21a49 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -27,7 +27,7 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; -import { RoomSublist2 } from "./RoomSublist2"; +import RoomSublist2 from "./RoomSublist2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index a3ca525b14..ed3740e8b3 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -41,7 +41,7 @@ interface IState { } // TODO: Actually write stub -export class RoomSublist2 extends React.Component { +export default class RoomSublist2 extends React.Component { public setHeight(size: number) { } From e1fab9a5b663a8c15398dfd15d2015a0b242e193 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 May 2020 16:34:22 -0600 Subject: [PATCH 163/241] Work out the new category index for each room update See comments within for details on what this means. --- .../list_ordering/ImportanceAlgorithm.ts | 173 ++++++++++++++---- 1 file changed, 136 insertions(+), 37 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index d66f7cdc37..76cdf4763f 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -17,7 +17,7 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; +import { RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag_sorting"; import * as Unread from '../../../../Unread'; @@ -51,6 +51,16 @@ interface ICategorizedRoomMap { [category: Category]: Room[]; } +interface ICategoryIndex { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: number; // integer +} + +// Caution: changing this means you'll need to update a bunch of assumptions and +// comments! Check the usage of Category carefully to figure out what needs changing +// if you're going to change this array's order. +const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idle]; + /** * An implementation of the "importance" algorithm for room list sorting. Where * the tag sorting algorithm does not interfere, rooms will be ordered into @@ -69,6 +79,7 @@ interface ICategorizedRoomMap { */ export class ImportanceAlgorithm extends Algorithm { + // TODO: Update documentation // HOW THIS WORKS // -------------- // @@ -80,7 +91,7 @@ export class ImportanceAlgorithm extends Algorithm { // those tags are tracked as index numbers within the array (zero = top), with // each sticky room being tracked separately. Internally, the category index // can be found from `this.indices[tag][category]` and the sticky room information - // from `this.stickyRooms[tag]`. + // from `this.stickyRoom`. // // Room categories are constantly re-evaluated and tracked in the `this.categorized` // object. Note that this doesn't track rooms by category but instead by room ID. @@ -93,33 +104,17 @@ export class ImportanceAlgorithm extends Algorithm { private indices: { // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - // @ts-ignore - TS wants this to be a string but we know better than it - [category: Category]: number; // integer - }; - } = {}; - private stickyRooms: { - // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - room?: Room; - nAbove: number; // integer - }; - } = {}; - private categorized: { - // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - // TODO: Remove note - // Note: Should in theory be able to only track this by room ID as we'll know - // the indices of each category and can determine if a category needs changing - // in the cached list. Could potentially save a bunch of time if we can figure - // out where a room is supposed to be using offsets, some math, and leaving the - // array generally alone. - [roomId: string]: { - room: Room; - category: Category; - }; - }; + [tag: TagID]: ICategoryIndex; } = {}; + private stickyRoom: { + roomId: string; + tag: TagID; + fromTop: number; + } = { + roomId: null, + tag: null, + fromTop: 0, + }; constructor() { super(); @@ -136,7 +131,6 @@ export class ImportanceAlgorithm extends Algorithm { }; for (const room of rooms) { const category = this.getRoomCategory(room); - console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`); map[category].push(room); } return map; @@ -183,13 +177,16 @@ export class ImportanceAlgorithm extends Algorithm { categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); } - // TODO: Update positions of categories in cache - updatedTagMap[tagId] = [ - ...categorized[Category.Red], - ...categorized[Category.Grey], - ...categorized[Category.Bold], - ...categorized[Category.Idle], - ]; + const newlyOrganized: Room[] = []; + const newIndicies: ICategoryIndex = {}; + + for (const category of CATEGORY_ORDER) { + newIndicies[category] = newlyOrganized.length; + newlyOrganized.push(...categorized[category]); + } + + this.indices[tagId] = newIndicies; + updatedTagMap[tagId] = newlyOrganized; } } } @@ -200,6 +197,108 @@ export class ImportanceAlgorithm extends Algorithm { console.warn(`No tags known for "${room.name}" (${room.roomId})`); return false; } - return false; + const category = this.getRoomCategory(room); + let changed = false; + for (const tag of tags) { + if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) { + continue; // Nothing to do here. + } + + const taggedRooms = this.cached[tag]; + const indicies = this.indices[tag]; + let roomIdx = taggedRooms.indexOf(room); + let inList = true; + if (roomIdx === -1) { + console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); + roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); + } + if (roomIdx === -1) { + console.warn(`Room ${room.roomId} has no index in ${tag} - assuming end of list`); + roomIdx = taggedRooms.length; + inList = false; // used so we don't splice the dead room out + } + + // Try to avoid doing array operations if we don't have to: only move rooms within + // the categories if we're jumping categories + const oldCategory = this.getCategoryFromIndicies(roomIdx, indicies); + if (oldCategory !== category) { + // Move the room and update the indicies + this.moveRoomIndexes(1, oldCategory, category, indicies); + taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) + taggedRooms.splice(indicies[category], 0, room); // splice in the new room (pre-adjusted) + // Note: if moveRoomIndexes() is called after the splice then the insert operation + // will happen in the wrong place. Because we would have already adjusted the index + // for the category, we don't need to determine how the room is moving in the list. + // If we instead tried to insert before updating the indicies, we'd have to determine + // whether the room was moving later (towards IDLE) or earlier (towards RED) from its + // current position, as it'll affect the category's start index after we remove the + // room from the array. + } + + // The room received an update, so take out the slice and sort it. This should be relatively + // quick because the room is inserted at the top of the category, and most popular sorting + // algorithms will deal with trying to keep the active room at the top/start of the category. + // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room + // for example), the list should already be sorted well enough that it can rip through the + // array and slot the changed room in quickly. + const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] + ? Number.MAX_SAFE_INTEGER + : indicies[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = indicies[category]; + const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine + const unsortedSlice = taggedRooms.splice(startIdx, numSort); + const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); + taggedRooms.splice(startIdx, 0, ...sorted); + + // Finally, flag that we've done something + changed = true; + } + return changed; + } + + private getCategoryFromIndicies(index: number, indicies: ICategoryIndex): Category { + for (let i = 0; i < CATEGORY_ORDER.length; i++) { + const category = CATEGORY_ORDER[i]; + const isLast = i === (CATEGORY_ORDER.length - 1); + const startIdx = indicies[category]; + const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indicies[CATEGORY_ORDER[i + 1]]; + if (index >= startIdx && index < endIdx) { + return category; + } + } + + // "Should never happen" disclaimer goes here + throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); + } + + private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indicies: ICategoryIndex) { + // We have to update the index of the category *after* the from/toCategory variables + // in order to update the indicies correctly. Because the room is moving from/to those + // categories, the next category's index will change - not the category we're modifying. + // We also need to update subsequent categories as they'll all shift by nRooms, so we + // loop over the order to achieve that. + + for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indicies[nextCategory] -= nRooms; + } + + for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indicies[nextCategory] += nRooms; + } + + // Do a quick check to see if we've completely broken the index + for (let i = 1; i <= CATEGORY_ORDER.length; i++) { + const lastCat = CATEGORY_ORDER[i - 1]; + const thisCat = CATEGORY_ORDER[i]; + + if (indicies[lastCat] > indicies[thisCat]) { + // "should never happen" disclaimer goes here + console.warn(`!! Room list index corruption: ${lastCat} (i:${indicies[lastCat]}) is greater than ${thisCat} (i:${indicies[thisCat]}) - category indicies are likely desynced from reality`); + + // TODO: Regenerate index when this happens + } + } } } From 4a0d14e32269fa8e4049d06173e7d9c4035a6499 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 May 2020 16:38:14 -0600 Subject: [PATCH 164/241] Make missing rooms throw instead For now at least. We shouldn't encounter this case until we get around to adding support for newly-joined rooms. --- .../algorithms/list_ordering/ImportanceAlgorithm.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 76cdf4763f..0ebdad1ed1 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -207,15 +207,12 @@ export class ImportanceAlgorithm extends Algorithm { const taggedRooms = this.cached[tag]; const indicies = this.indices[tag]; let roomIdx = taggedRooms.indexOf(room); - let inList = true; if (roomIdx === -1) { console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); } if (roomIdx === -1) { - console.warn(`Room ${room.roomId} has no index in ${tag} - assuming end of list`); - roomIdx = taggedRooms.length; - inList = false; // used so we don't splice the dead room out + throw new Error(`Room ${room.roomId} has no index in ${tag}`); } // Try to avoid doing array operations if we don't have to: only move rooms within From e88788f4e9c7804c9b78bcd670035bcfe14fae18 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 May 2020 11:59:03 -0600 Subject: [PATCH 165/241] Handle event decryption too --- src/stores/room-list/RoomListStore2.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 06f97ae53e..3a6d911dde 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -156,10 +156,21 @@ class _RoomListStore extends AsyncStore { const roomId = eventPayload.event.getRoomId(); const room = this.matrixClient.getRoom(roomId); + console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`); await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.Event.decrypted') { - // TODO: Update room from decrypted event - console.log(payload); + const eventPayload = (payload); // TODO: Type out the dispatcher types + const roomId = eventPayload.event.getRoomId(); + const room = this.matrixClient.getRoom(roomId); + if (!room) { + console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`); + return; + } + console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`); + // TODO: Check that e2e rooms are calculated correctly on initial load. + // It seems like when viewing the room the timeline is decrypted, rather than at startup. This could + // cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :( + await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { // TODO: Update DMs console.log(payload); From cb3d17ee28de8d0e96650b8f1dfb5a6fa698a388 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 May 2020 12:53:05 -0600 Subject: [PATCH 166/241] Bare minimum for rendering a room list This is non-interactive and missing most features users will expect to have --- src/components/views/rooms/RoomList2.tsx | 9 +- src/components/views/rooms/RoomSublist2.tsx | 174 +++++++++++++++++++- src/components/views/rooms/RoomTile2.tsx | 118 +++++++++++++ 3 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 src/components/views/rooms/RoomTile2.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 7aefc21a49..9b0d4579f0 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -156,7 +156,7 @@ export default class RoomList2 extends React.Component { const sublist = this.sublistRefs[tagId]; if (sublist) sublist.current.setHeight(height); - // TODO: Check overflow + // TODO: Check overflow (see old impl) // Don't store a height for collapsed sublists if (!this.sublistCollapseStates[tagId]) { @@ -178,6 +178,7 @@ export default class RoomList2 extends React.Component { } private collectSublistRef(tagId: string, ref: React.RefObject) { + // TODO: Is this needed? if (!ref) { delete this.sublistRefs[tagId]; } else { @@ -206,11 +207,9 @@ export default class RoomList2 extends React.Component { const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); - const onAddRoomFn = () => { - if (!aesthetics.onAddRoom) return; - aesthetics.onAddRoom(dis); - }; + const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; components.push( { + private headerButton = createRef(); + public setHeight(size: number) { + // TODO: Do a thing } - public render() { - // TODO: Proper rendering + private hasTiles(): boolean { + return this.numTiles > 0; + } + + private get numTiles(): number { + // TODO: Account for group invites + return (this.props.rooms || []).length; + } + + private onAddRoom = (e) => { + e.stopPropagation(); + if (this.props.onAddRoom) this.props.onAddRoom(); + }; + + private renderTiles(): React.ReactElement[] { + const tiles: React.ReactElement[] = []; + + if (this.props.rooms) { + for (const room of this.props.rooms) { + tiles.push(); + } + } + + return tiles; + } + + 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: Title on collapsed + // TODO: Incoming call box + + let chevron = null; + if (this.hasTiles()) { + const chevronClasses = classNames({ + 'mx_RoomSubList_chevron': true, + 'mx_RoomSubList_chevronRight': false, // isCollapsed + 'mx_RoomSubList_chevronDown': true, // !isCollapsed + }); + chevron = (
    ); + } - const rooms = this.props.rooms.map(r => ( -
    {r.name} ({r.roomId})
    - )); return ( -
    -

    {this.props.label}

    - {rooms} + + {({onFocus, isActive, ref}) => { + const tabIndex = isActive ? 0 : -1; + + let badge; + if (true) { // !isCollapsed + const badgeClasses = classNames({ + 'mx_RoomSubList_badge': true, + 'mx_RoomSubList_badgeHighlight': notifHighlight, + }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works + if (notifCount > 0) { + badge = ( + +
    + {FormattingUtils.formatCount(notifCount)} +
    +
    + ); + } else if (this.props.isInvite && this.hasTiles()) { + // Render the `!` badge for invites + badge = ( + +
    + {FormattingUtils.formatCount(this.numTiles)} +
    +
    + ); + } + } + + let addRoomButton = null; + if (!!this.props.onAddRoom) { + addRoomButton = ( + + ); + } + + // TODO: a11y + return ( +
    + + {chevron} + {this.props.label} + + {badge} + {addRoomButton} +
    + ); + }} +
    + ); + } + + public render(): React.ReactElement { + // TODO: Proper rendering + // TODO: Error boundary + + const tiles = this.renderTiles(); + + const classes = classNames({ + // TODO: Proper collapse support + 'mx_RoomSubList': true, + 'mx_RoomSubList_hidden': false, // len && isCollapsed + 'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed + }); + + let content = null; + if (tiles.length > 0) { + // TODO: Lazy list rendering + // TODO: Whatever scrolling magic needs to happen here + content = ( + + {tiles} + + ) + } + + // TODO: onKeyDown support + return ( +
    + {this.renderHeader()} + {content}
    ); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx new file mode 100644 index 0000000000..cb9ce5cf1a --- /dev/null +++ b/src/components/views/rooms/RoomTile2.tsx @@ -0,0 +1,118 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +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. +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, { createRef } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import classNames from "classnames"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import RoomAvatar from "../../views/avatars/RoomAvatar"; + +interface IProps { + room: Room; + + // TODO: Allow faslifying counts (for invites and stuff) + // TODO: Transparency? + // TODO: Incoming call? + // TODO: onClick +} + +interface IState { +} + +// TODO: Finish stub +export default class RoomTile2 extends React.Component { + private roomTile = createRef(); + + // TODO: Custom status + // TODO: Lock icon + // TODO: DM indicator + // TODO: Presence indicator + // TODO: e2e shields + // TODO: Handle changes to room aesthetics (name, join rules, etc) + // TODO: scrollIntoView? + // TODO: hover, badge, etc + // TODO: isSelected for hover effects + // TODO: Context menu + // TODO: a11y + + public render(): React.ReactElement { + // TODO: Collapsed state + // TODO: Invites + // TODO: a11y proper + // TODO: Render more than bare minimum + + const classes = classNames({ + 'mx_RoomTile': true, + // 'mx_RoomTile_selected': this.state.selected, + // 'mx_RoomTile_unread': this.props.unread, + // 'mx_RoomTile_unreadNotify': notifBadges, + // 'mx_RoomTile_highlight': mentionBadges, + // 'mx_RoomTile_invited': isInvite, + // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, + 'mx_RoomTile_noBadges': true, // !badges + // 'mx_RoomTile_transparent': this.props.transparent, + // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, + }); + + const avatarClasses = classNames({ + 'mx_RoomTile_avatar': true, + }); + + // TODO: the original RoomTile uses state for the room name. Do we need to? + let name = this.props.room.name; + if (typeof name !== 'string') name = ''; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + + const nameClasses = classNames({ + 'mx_RoomTile_name': true, + 'mx_RoomTile_invite': false, + 'mx_RoomTile_badgeShown': false, + }); + + return ( + + + {({onFocus, isActive, ref}) => + +
    +
    + +
    +
    +
    +
    +
    + {name} +
    +
    +
    +
    + } +
    +
    + ); + } +} From df3d5c415917324a3784535c23fc695f5e3ff1ff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 14:25:30 -0600 Subject: [PATCH 167/241] Update i18n for room list --- src/i18n/strings/en_EN.json | 10 +++++----- src/settings/Settings.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fd474f378c..d6ff5f70eb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,7 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)", + "Use the improved room list component (refresh to apply changes, in development)": "Use the improved room list component (refresh to apply changes, in development)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", @@ -1117,7 +1117,7 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", - "TODO": "TODO", + "Create room": "Create room", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", @@ -1161,6 +1161,9 @@ "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", "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.", @@ -2052,9 +2055,6 @@ "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/settings/Settings.js b/src/settings/Settings.js index 554cf6b968..110fd4238b 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -133,7 +133,7 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list component (refresh to apply changes)"), + displayName: _td("Use the improved room list component (refresh to apply changes, in development)"), supportedLevels: LEVELS_FEATURE, default: false, }, From 9f0810240f6ae8d627708d8d51e4f058d0173f55 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:12:45 -0600 Subject: [PATCH 168/241] Clean up imports and other minor lints --- src/components/views/rooms/RoomList2.tsx | 2 +- src/stores/room-list/RoomListStore2.ts | 5 ++--- src/stores/room-list/RoomListStoreTempProxy.ts | 4 +--- src/stores/room-list/algorithms/tag_sorting/index.ts | 2 +- src/stores/room-list/membership.ts | 3 +-- src/stores/room-list/models.ts | 1 + 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 9b0d4579f0..402a7af014 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -27,7 +27,7 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; -import RoomSublist2 from "./RoomSublist2"; +import RoomSublist2 from "./RoomSublist2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 3a6d911dde..8bbcfc3c8d 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -21,11 +21,10 @@ import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; -import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; -import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { getListAlgorithmInstance } from "./algorithms/list_ordering"; interface IState { tagsEnabled?: boolean; diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 4edca2b9cd..0268cf0a46 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { TagID } from "./models"; -import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/models"; import { UPDATE_EVENT } from "../AsyncStore"; +import { ITagMap } from "./algorithms/models"; /** * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts index 07f8f484d8..155c0f0118 100644 --- a/src/stores/room-list/algorithms/tag_sorting/index.ts +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -19,7 +19,7 @@ import { SortAlgorithm } from "../models"; import { ManualAlgorithm } from "./ManualAlgorithm"; import { IAlgorithm } from "./IAlgorithm"; import { TagID } from "../../models"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { [SortAlgorithm.Recent]: new ChaoticAlgorithm(), diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts index 884e2a4a04..3cb4bf146c 100644 --- a/src/stores/room-list/membership.ts +++ b/src/stores/room-list/membership.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Room} from "matrix-js-sdk/src/models/room"; -import {Event} from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; /** * Approximation of a membership status for a given room. diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 5294680773..428378a7aa 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -23,6 +23,7 @@ export enum DefaultTagID { DM = "im.vector.fake.direct", ServerNotice = "m.server_notice", } + export const OrderedDefaultTagIDs = [ DefaultTagID.Invite, DefaultTagID.Favourite, From 715dd7e1b6126ec0f858d80af1873917d9ebe714 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:20:26 -0600 Subject: [PATCH 169/241] Prepare tooltip for collapsed support --- src/components/views/rooms/RoomTile2.tsx | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index cb9ce5cf1a..21ba32ae75 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -23,6 +23,7 @@ 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"; interface IProps { room: Room; @@ -34,6 +35,7 @@ interface IProps { } interface IState { + hover: boolean; } // TODO: Finish stub @@ -52,6 +54,22 @@ export default class RoomTile2 extends React.Component { // TODO: Context menu // TODO: a11y + constructor(props: IProps) { + super(props); + + this.state = { + hover: false, + }; + } + + private onTileMouseEnter = () => { + this.setState({hover: true}); + }; + + private onTileMouseLeave = () => { + this.setState({hover: false}); + }; + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites @@ -86,6 +104,13 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_badgeShown': false, }); + let tooltip = null; + if (false) { // isCollapsed + if (this.state.hover) { + tooltip = + } + } + return ( @@ -95,6 +120,8 @@ export default class RoomTile2 extends React.Component { tabIndex={isActive ? 0 : -1} inputRef={ref} className={classes} + onMouseEnter={this.onTileMouseEnter} + onMouseLeave={this.onTileMouseLeave} role="treeitem" >
    @@ -109,6 +136,7 @@ export default class RoomTile2 extends React.Component {
    + {tooltip} } From 6bdcbd0f3d2137caedc19f9659eecac0636632f9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:29:32 -0600 Subject: [PATCH 170/241] Support switching rooms --- src/components/views/rooms/RoomTile2.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 21ba32ae75..f32bd7924f 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -24,6 +24,8 @@ 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'; +import { Key } from "../../../Keyboard"; interface IProps { room: Room; @@ -31,7 +33,6 @@ interface IProps { // TODO: Allow faslifying counts (for invites and stuff) // TODO: Transparency? // TODO: Incoming call? - // TODO: onClick } interface IState { @@ -70,6 +71,16 @@ export default class RoomTile2 extends React.Component { this.setState({hover: false}); }; + private onTileClick = (ev: React.KeyboardEvent) => { + dis.dispatch({ + action: 'view_room', + // TODO: Support show_room_tile in new room list + show_room_tile: true, // make sure the room is visible in the list + room_id: this.props.room.roomId, + clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), + }); + }; + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites @@ -122,6 +133,7 @@ export default class RoomTile2 extends React.Component { className={classes} onMouseEnter={this.onTileMouseEnter} onMouseLeave={this.onTileMouseLeave} + onClick={this.onTileClick} role="treeitem" >
    From e8c33161ec3871805403ef0e508cd79321570a8d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 21:09:32 -0600 Subject: [PATCH 171/241] Initial work on badges This doesn't work for bold rooms --- src/components/views/rooms/RoomTile2.tsx | 68 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index f32bd7924f..c6c400ce52 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,6 +26,10 @@ import RoomAvatar from "../../views/avatars/RoomAvatar"; import Tooltip from "../../views/elements/Tooltip"; import dis from '../../../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"; interface IProps { room: Room; @@ -35,7 +39,14 @@ interface IProps { // TODO: Incoming call? } -interface IState { +interface IBadgeState { + showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room + numUnread: number; // used only if showBadge or showBadgeHighlight is true + showBadgeHighlight: boolean; // make the badge red + isInvite: boolean; // show a `!` instead of a number +} + +interface IState extends IBadgeState { hover: boolean; } @@ -60,6 +71,35 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, + + ...this.getBadgeState(), + }; + } + + public componentWillUnmount() { + + } + + private updateBadgeCount() { + this.setState({...this.getBadgeState()}); + } + + private getBadgeState(): IBadgeState { + // TODO: Make this code path faster + const highlightCount = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight'); + const numUnread = RoomNotifs.getUnreadNotificationCount(this.props.room); + const showBadge = Unread.doesRoomHaveUnreadMessages(this.props.room); + const myMembership = getEffectiveMembership(this.props.room.getMyMembership()); + const isInvite = myMembership === EffectiveMembership.Invite; + const notifState = RoomNotifs.getRoomNotifsState(this.props.room.roomId); + const shouldShowNotifBadge = RoomNotifs.shouldShowNotifBadge(notifState); + const shouldShowHighlightBadge = RoomNotifs.shouldShowMentionBadge(notifState); + + return { + showBadge: (showBadge && shouldShowNotifBadge) || isInvite, + numUnread, + showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite, + isInvite, }; } @@ -90,12 +130,12 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile': true, // 'mx_RoomTile_selected': this.state.selected, - // 'mx_RoomTile_unread': this.props.unread, - // 'mx_RoomTile_unreadNotify': notifBadges, - // 'mx_RoomTile_highlight': mentionBadges, - // 'mx_RoomTile_invited': isInvite, + 'mx_RoomTile_unread': this.state.numUnread > 0, + 'mx_RoomTile_unreadNotify': this.state.showBadge, + 'mx_RoomTile_highlight': this.state.showBadgeHighlight, + 'mx_RoomTile_invited': this.state.isInvite, // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile_noBadges': true, // !badges + 'mx_RoomTile_noBadges': !this.state.showBadge, // 'mx_RoomTile_transparent': this.props.transparent, // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); @@ -104,6 +144,17 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_avatar': true, }); + + let badge; + if (this.state.showBadge) { + const badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + }); + const formattedCount = this.state.isInvite ? `!` : FormattingUtils.formatCount(this.state.numUnread); + badge =
    {formattedCount}
    ; + } + // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; if (typeof name !== 'string') name = ''; @@ -111,8 +162,8 @@ export default class RoomTile2 extends React.Component { const nameClasses = classNames({ 'mx_RoomTile_name': true, - 'mx_RoomTile_invite': false, - 'mx_RoomTile_badgeShown': false, + 'mx_RoomTile_invite': this.state.isInvite, + 'mx_RoomTile_badgeShown': this.state.showBadge, }); let tooltip = null; @@ -147,6 +198,7 @@ export default class RoomTile2 extends React.Component { {name}
    + {badge}
    {tooltip} From c37352679d72aec70468d541b2f7cd9483406d30 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 09:34:31 -0600 Subject: [PATCH 172/241] Fix bold rooms not bolding --- src/components/views/rooms/RoomTile2.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c6c400ce52..c4025fdb53 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -42,6 +42,7 @@ interface IProps { interface IBadgeState { showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room numUnread: number; // used only if showBadge or showBadgeHighlight is true + hasUnread: number; // used to make the room bold showBadgeHighlight: boolean; // make the badge red isInvite: boolean; // show a `!` instead of a number } @@ -98,6 +99,7 @@ export default class RoomTile2 extends React.Component { return { showBadge: (showBadge && shouldShowNotifBadge) || isInvite, numUnread, + hasUnread: showBadge, showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite, isInvite, }; @@ -130,7 +132,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile': true, // 'mx_RoomTile_selected': this.state.selected, - 'mx_RoomTile_unread': this.state.numUnread > 0, + 'mx_RoomTile_unread': this.state.numUnread > 0 || this.state.hasUnread, 'mx_RoomTile_unreadNotify': this.state.showBadge, 'mx_RoomTile_highlight': this.state.showBadgeHighlight, 'mx_RoomTile_invited': this.state.isInvite, From f8cbadaba564cf29e06a81261705882f2783c30f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 12:53:00 -0600 Subject: [PATCH 173/241] Clean up comments in skeleton components --- src/components/views/rooms/RoomList2.tsx | 19 ++++++++++--------- src/components/views/rooms/RoomSublist2.tsx | 16 +++++++++++++--- src/components/views/rooms/RoomTile2.tsx | 20 ++++++++++++++------ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 402a7af014..12a0117505 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -29,6 +29,15 @@ import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; import RoomSublist2 from "./RoomSublist2"; +/******************************************************************* + * 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 { onKeyDown: (ev: React.KeyboardEvent) => void; onFocus: (ev: React.FocusEvent) => void; @@ -152,6 +161,7 @@ export default class RoomList2 extends React.Component { } private prepareLayouts() { + // TODO: Change layout engine for FTUE support this.unfilteredLayout = new Layout((tagId: string, height: number) => { const sublist = this.sublistRefs[tagId]; if (sublist) sublist.current.setHeight(height); @@ -177,15 +187,6 @@ export default class RoomList2 extends React.Component { }); } - private collectSublistRef(tagId: string, ref: React.RefObject) { - // TODO: Is this needed? - if (!ref) { - delete this.sublistRefs[tagId]; - } else { - this.sublistRefs[tagId] = ref; - } - } - private renderSublists(): React.ReactElement[] { const components: React.ReactElement[] = []; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 21e58abd12..4c3f65b323 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -29,6 +29,15 @@ import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButto import * as FormattingUtils from '../../../utils/FormattingUtils'; import RoomTile2 from "./RoomTile2"; +/******************************************************************* + * 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 { forRooms: boolean; rooms?: Room[]; @@ -50,12 +59,11 @@ interface IProps { interface IState { } -// TODO: Finish stub export default class RoomSublist2 extends React.Component { private headerButton = createRef(); public setHeight(size: number) { - // TODO: Do a thing + // TODO: Do a thing (maybe - height changes are different in FTUE) } private hasTiles(): boolean { @@ -107,8 +115,10 @@ export default class RoomSublist2 extends React.Component { return ( {({onFocus, isActive, ref}) => { + // TODO: Use onFocus const tabIndex = isActive ? 0 : -1; + // TODO: Collapsed state let badge; if (true) { // !isCollapsed const badgeClasses = classNames({ @@ -156,7 +166,7 @@ export default class RoomSublist2 extends React.Component { ); } - // TODO: a11y + // TODO: a11y (see old component) return (
    { private roomTile = createRef(); // TODO: Custom status // TODO: Lock icon - // TODO: DM indicator // TODO: Presence indicator // TODO: e2e shields // TODO: Handle changes to room aesthetics (name, join rules, etc) @@ -78,7 +85,7 @@ export default class RoomTile2 extends React.Component { } public componentWillUnmount() { - + // TODO: Listen for changes to the badge count and update as needed } private updateBadgeCount() { @@ -168,6 +175,7 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_badgeShown': this.state.showBadge, }); + // TODO: Support collapsed state properly let tooltip = null; if (false) { // isCollapsed if (this.state.hover) { From eb6796bd0eef258bbd278b247e3fd6d773efd6fe Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 May 2020 19:53:09 +0100 Subject: [PATCH 174/241] Migrate PasswordScorer to TypeScript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/utils/{PasswordScorer.js => PasswordScorer.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/utils/{PasswordScorer.js => PasswordScorer.ts} (98%) diff --git a/src/utils/PasswordScorer.js b/src/utils/PasswordScorer.ts similarity index 98% rename from src/utils/PasswordScorer.js rename to src/utils/PasswordScorer.ts index 9d89942bf5..d8f3b0fb96 100644 --- a/src/utils/PasswordScorer.js +++ b/src/utils/PasswordScorer.ts @@ -63,7 +63,7 @@ _td("Short keyboard patterns are easy to guess"); * @param {string} password Password to score * @returns {object} Score result with `score` and `feedback` properties */ -export function scorePassword(password) { +export function scorePassword(password: string) { if (password.length === 0) return null; const userInputs = ZXCVBN_USER_INPUTS.slice(); From aafbd7f2089446aa0c8a9ffc3e2e131ad7ef2e7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:01:51 -0600 Subject: [PATCH 175/241] Update misc documentation and spell indices correctly --- src/stores/RoomListStore.js | 2 +- src/stores/room-list/RoomListStore2.ts | 4 +- .../list_ordering/ImportanceAlgorithm.ts | 49 +++++++++---------- src/stores/room-list/models.ts | 2 +- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index ccccbcc313..d452d1589e 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -121,7 +121,7 @@ class RoomListStore extends Store { _checkDisabled() { this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); if (this.disabled) { - console.warn("DISABLING LEGACY ROOM LIST STORE"); + console.warn("👋 legacy room list store has been disabled"); } } diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 8bbcfc3c8d..c461aeab66 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -67,7 +67,7 @@ class _RoomListStore extends AsyncStore { private checkEnabled() { this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); if (this.enabled) { - console.log("ENABLING NEW ROOM LIST STORE"); + console.log("⚡ new room list store engaged"); } } @@ -225,7 +225,7 @@ class _RoomListStore extends AsyncStore { } if (this.state.tagsEnabled) { - // TODO: Find a more reliable way to get tags + // TODO: Find a more reliable way to get tags (this doesn't work) const roomTags = TagOrderStore.getOrderedTags() || []; console.log("rtags", roomTags); } diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 0ebdad1ed1..fd5d4c8163 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -79,7 +79,6 @@ const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idl */ export class ImportanceAlgorithm extends Algorithm { - // TODO: Update documentation // HOW THIS WORKS // -------------- // @@ -93,19 +92,17 @@ export class ImportanceAlgorithm extends Algorithm { // can be found from `this.indices[tag][category]` and the sticky room information // from `this.stickyRoom`. // - // Room categories are constantly re-evaluated and tracked in the `this.categorized` - // object. Note that this doesn't track rooms by category but instead by room ID. - // The theory is that by knowing the previous position, new desired position, and - // category indices we can avoid tracking multiple complicated maps in memory. - // // The room list store is always provided with the `this.cached` results, which are // updated as needed and not recalculated often. For example, when a room needs to // move within a tag, the array in `this.cached` will be spliced instead of iterated. + // The `indices` help track the positions of each category to make splicing easier. private indices: { // @ts-ignore - TS wants this to be a string but we know better than it [tag: TagID]: ICategoryIndex; } = {}; + + // TODO: Use this (see docs above) private stickyRoom: { roomId: string; tag: TagID; @@ -178,14 +175,14 @@ export class ImportanceAlgorithm extends Algorithm { } const newlyOrganized: Room[] = []; - const newIndicies: ICategoryIndex = {}; + const newIndices: ICategoryIndex = {}; for (const category of CATEGORY_ORDER) { - newIndicies[category] = newlyOrganized.length; + newIndices[category] = newlyOrganized.length; newlyOrganized.push(...categorized[category]); } - this.indices[tagId] = newIndicies; + this.indices[tagId] = newIndices; updatedTagMap[tagId] = newlyOrganized; } } @@ -205,7 +202,7 @@ export class ImportanceAlgorithm extends Algorithm { } const taggedRooms = this.cached[tag]; - const indicies = this.indices[tag]; + const indices = this.indices[tag]; let roomIdx = taggedRooms.indexOf(room); if (roomIdx === -1) { console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); @@ -217,16 +214,16 @@ export class ImportanceAlgorithm extends Algorithm { // Try to avoid doing array operations if we don't have to: only move rooms within // the categories if we're jumping categories - const oldCategory = this.getCategoryFromIndicies(roomIdx, indicies); + const oldCategory = this.getCategoryFromIndices(roomIdx, indices); if (oldCategory !== category) { - // Move the room and update the indicies - this.moveRoomIndexes(1, oldCategory, category, indicies); + // Move the room and update the indices + this.moveRoomIndexes(1, oldCategory, category, indices); taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) - taggedRooms.splice(indicies[category], 0, room); // splice in the new room (pre-adjusted) + taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted) // Note: if moveRoomIndexes() is called after the splice then the insert operation // will happen in the wrong place. Because we would have already adjusted the index // for the category, we don't need to determine how the room is moving in the list. - // If we instead tried to insert before updating the indicies, we'd have to determine + // If we instead tried to insert before updating the indices, we'd have to determine // whether the room was moving later (towards IDLE) or earlier (towards RED) from its // current position, as it'll affect the category's start index after we remove the // room from the array. @@ -240,8 +237,8 @@ export class ImportanceAlgorithm extends Algorithm { // array and slot the changed room in quickly. const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] ? Number.MAX_SAFE_INTEGER - : indicies[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; - const startIdx = indicies[category]; + : indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = indices[category]; const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine const unsortedSlice = taggedRooms.splice(startIdx, numSort); const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); @@ -253,12 +250,12 @@ export class ImportanceAlgorithm extends Algorithm { return changed; } - private getCategoryFromIndicies(index: number, indicies: ICategoryIndex): Category { + private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category { for (let i = 0; i < CATEGORY_ORDER.length; i++) { const category = CATEGORY_ORDER[i]; const isLast = i === (CATEGORY_ORDER.length - 1); - const startIdx = indicies[category]; - const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indicies[CATEGORY_ORDER[i + 1]]; + const startIdx = indices[category]; + const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indices[CATEGORY_ORDER[i + 1]]; if (index >= startIdx && index < endIdx) { return category; } @@ -268,21 +265,21 @@ export class ImportanceAlgorithm extends Algorithm { throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); } - private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indicies: ICategoryIndex) { + private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) { // We have to update the index of the category *after* the from/toCategory variables - // in order to update the indicies correctly. Because the room is moving from/to those + // in order to update the indices correctly. Because the room is moving from/to those // categories, the next category's index will change - not the category we're modifying. // We also need to update subsequent categories as they'll all shift by nRooms, so we // loop over the order to achieve that. for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { const nextCategory = CATEGORY_ORDER[i]; - indicies[nextCategory] -= nRooms; + indices[nextCategory] -= nRooms; } for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { const nextCategory = CATEGORY_ORDER[i]; - indicies[nextCategory] += nRooms; + indices[nextCategory] += nRooms; } // Do a quick check to see if we've completely broken the index @@ -290,9 +287,9 @@ export class ImportanceAlgorithm extends Algorithm { const lastCat = CATEGORY_ORDER[i - 1]; const thisCat = CATEGORY_ORDER[i]; - if (indicies[lastCat] > indicies[thisCat]) { + if (indices[lastCat] > indices[thisCat]) { // "should never happen" disclaimer goes here - console.warn(`!! Room list index corruption: ${lastCat} (i:${indicies[lastCat]}) is greater than ${thisCat} (i:${indicies[thisCat]}) - category indicies are likely desynced from reality`); + console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`); // TODO: Regenerate index when this happens } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 428378a7aa..a0c2621077 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -38,5 +38,5 @@ export type TagID = string | DefaultTagID; export enum RoomUpdateCause { Timeline = "TIMELINE", - RoomRead = "ROOM_READ", + RoomRead = "ROOM_READ", // TODO: Use this. } From 9fbd489b3b1fb83e7f3710ee22fd56a2c8e533e2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:03:43 -0600 Subject: [PATCH 176/241] Update i18n --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d6ff5f70eb..c4912eb4d0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,7 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list component (refresh to apply changes, in development)": "Use the improved room list component (refresh to apply changes, in development)", + "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 110fd4238b..cd9ec430bf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -133,7 +133,7 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list component (refresh to apply changes, in development)"), + displayName: _td("Use the improved room list (in development - refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, }, From cf3c4d9e5f53a387a60cad742b6304b7c39f5b14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 May 2020 20:19:15 +0100 Subject: [PATCH 177/241] Extract Password field from Registration into a reusable component Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- package.json | 1 + res/css/_components.scss | 1 + res/css/views/auth/_AuthBody.scss | 6 - .../_PassphraseField.scss} | 7 +- src/components/views/auth/PassphraseField.tsx | 121 ++++++++++++++++++ src/components/views/auth/RegistrationForm.js | 63 +-------- src/components/views/elements/Validation.tsx | 13 +- yarn.lock | 5 + 8 files changed, 148 insertions(+), 69 deletions(-) rename res/css/views/{elements/_ZxcvbnProgressBar.scss => auth/_PassphraseField.scss} (92%) create mode 100644 src/components/views/auth/PassphraseField.tsx diff --git a/package.json b/package.json index 92d228a812..797b57d306 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,7 @@ "@types/classnames": "^2.2.10", "@types/modernizr": "^3.5.3", "@types/react": "16.9", + "@types/zxcvbn": "^4.4.0", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "chokidar": "^3.3.1", diff --git a/res/css/_components.scss b/res/css/_components.scss index 671e156585..b871045fa1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -41,6 +41,7 @@ @import "./views/auth/_CountryDropdown.scss"; @import "./views/auth/_InteractiveAuthEntryComponents.scss"; @import "./views/auth/_LanguageSelector.scss"; +@import "./views/auth/_PassphraseField.scss"; @import "./views/auth/_ServerConfig.scss"; @import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index f4967ce202..120da4c4f1 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -146,9 +146,3 @@ limitations under the License. .mx_AuthBody_spinner { margin: 1em 0; } - -.mx_AuthBody_passwordScore { - height: 4px; - position: absolute; - top: -12px; -} diff --git a/res/css/views/elements/_ZxcvbnProgressBar.scss b/res/css/views/auth/_PassphraseField.scss similarity index 92% rename from res/css/views/elements/_ZxcvbnProgressBar.scss rename to res/css/views/auth/_PassphraseField.scss index f7786348db..d810198213 100644 --- a/res/css/views/elements/_ZxcvbnProgressBar.scss +++ b/res/css/views/auth/_PassphraseField.scss @@ -18,6 +18,8 @@ $PassphraseStrengthHigh: $accent-color; $PassphraseStrengthMedium: $username-variant5-color; $PassphraseStrengthLow: $notice-primary-color; +.mx_PassphraseField {} + @define-mixin ProgressBarColour $colour { color: $colour; &::-moz-progress-bar { @@ -28,10 +30,13 @@ $PassphraseStrengthLow: $notice-primary-color; } } -progress.mx_ZxcvbnProgressBar { +progress.mx_PassphraseField_progress { appearance: none; width: 100%; border: 0; + height: 4px; + position: absolute; + top: -12px; border-radius: 2px; &::-moz-progress-bar { diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx new file mode 100644 index 0000000000..425921cd7c --- /dev/null +++ b/src/components/views/auth/PassphraseField.tsx @@ -0,0 +1,121 @@ +/* +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, {PureComponent, RefCallback, RefObject} from "react"; +import classNames from "classnames"; +import zxcvbn from "zxcvbn"; + +import SdkConfig from "../../../SdkConfig"; +import withValidation, {IFieldState, IValidationResult} from "../elements/Validation"; +import {_t, _td} from "../../../languageHandler"; +import Field from "../elements/Field"; + +interface IProps { + id?: string; + className?: string; + minScore: 0 | 1 | 2 | 3 | 4; + value: string; + fieldRef: RefCallback | RefObject; + + label?: string; + labelEnterPassword?: string; + labelStrongPassword?: string; + labelAllowedButUnsafe?: string; + + onChange(ev: KeyboardEvent); + onValidate(result: IValidationResult); +} + +interface IState { + complexity: zxcvbn.ZXCVBNResult; +} + +class PassphraseField extends PureComponent { + static defaultProps = { + label: _td("Password"), + labelEnterPassword: _td("Enter password"), + labelStrongPassword: _td("Nice, strong password!"), + labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), + }; + + public readonly validate = withValidation({ + description: function() { + const complexity = this.state.complexity; + const score = complexity ? complexity.score : 0; + return ; + }, + rules: [ + { + key: "required", + test: ({ value, allowEmpty }) => allowEmpty || !!value, + invalid: () => _t(this.props.labelEnterPassword), + }, + { + key: "complexity", + test: async function({ value }) { + if (!value) { + return false; + } + const { scorePassword } = await import('../../../utils/PasswordScorer'); + const complexity = scorePassword(value); + this.setState({ complexity }); + const safe = complexity.score >= this.props.minScore; + const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; + return allowUnsafe || safe; + }, + valid: function() { + // Unsafe passwords that are valid are only possible through a + // configuration flag. We'll print some helper text to signal + // to the user that their password is allowed, but unsafe. + if (this.state.complexity.score >= this.props.minScore) { + return _t(this.props.labelStrongPassword); + } + return _t(this.props.labelAllowedButUnsafe); + }, + invalid: function() { + const complexity = this.state.complexity; + if (!complexity) { + return null; + } + const { feedback } = complexity; + return feedback.warning || feedback.suggestions[0] || _t("Keep going..."); + }, + }, + ], + }); + + onValidate = async (fieldState: IFieldState) => { + const result = await this.validate(fieldState); + this.props.onValidate(result); + return result; + }; + + render() { + return + } +} + +export default PassphraseField; diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 4e2860c0cf..7bbd15d8d3 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -29,7 +29,7 @@ import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import withValidation from '../elements/Validation'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; -import ZxcvbnProgressBar from "../elements/ZxcvbnProgressBar"; +import PassphraseField from "./PassphraseField"; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -264,60 +264,10 @@ export default createReactClass({ }); }, - async onPasswordValidate(fieldState) { - const result = await this.validatePasswordRules(fieldState); + onPasswordValidate(result) { this.markFieldValid(FIELD_PASSWORD, result.valid); - return result; }, - validatePasswordRules: withValidation({ - description: function() { - const complexity = this.state.passwordComplexity; - const score = complexity ? complexity.score : 0; - return ; - }, - rules: [ - { - key: "required", - test: ({ value, allowEmpty }) => allowEmpty || !!value, - invalid: () => _t("Enter password"), - }, - { - key: "complexity", - test: async function({ value }) { - if (!value) { - return false; - } - const { scorePassword } = await import('../../../utils/PasswordScorer'); - const complexity = scorePassword(value); - this.setState({ - passwordComplexity: complexity, - }); - const safe = complexity.score >= PASSWORD_MIN_SCORE; - const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; - return allowUnsafe || safe; - }, - valid: function() { - // Unsafe passwords that are valid are only possible through a - // configuration flag. We'll print some helper text to signal - // to the user that their password is allowed, but unsafe. - if (this.state.passwordComplexity.score >= PASSWORD_MIN_SCORE) { - return _t("Nice, strong password!"); - } - return _t("Password is allowed, but unsafe"); - }, - invalid: function() { - const complexity = this.state.passwordComplexity; - if (!complexity) { - return null; - } - const { feedback } = complexity; - return feedback.warning || feedback.suggestions[0] || _t("Keep going..."); - }, - }, - ], - }), - onPasswordConfirmChange(ev) { this.setState({ passwordConfirm: ev.target.value, @@ -479,13 +429,10 @@ export default createReactClass({ }, renderPassword() { - const Field = sdk.getComponent('elements.Field'); - return this[FIELD_PASSWORD] = field} - type="password" - autoComplete="new-password" - label={_t("Password")} + fieldRef={field => this[FIELD_PASSWORD] = field} + minScore={PASSWORD_MIN_SCORE} value={this.state.password} onChange={this.onPasswordChange} onValidate={this.onPasswordValidate} diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 09d6ec12f7..50544c9f51 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -19,7 +19,7 @@ limitations under the License. import React from "react"; import classNames from "classnames"; -type Data = Pick; +type Data = Pick; interface IRule { key: string; @@ -32,15 +32,20 @@ interface IRule { interface IArgs { rules: IRule[]; - description(): React.ReactChild; + description(this: T): React.ReactChild; } -interface IValidateArgs { +export interface IFieldState { value: string; focused: boolean; allowEmpty: boolean; } +export interface IValidationResult { + valid?: boolean; + feedback?: React.ReactChild; +} + /** * Creates a validation function from a set of rules describing what to validate. * Generic T is the "this" type passed to the rule methods @@ -62,7 +67,7 @@ interface IValidateArgs { * the overall validity and a feedback UI that can be rendered for more detail. */ export default function withValidation({ description, rules }: IArgs) { - return async function onValidate({ value, focused, allowEmpty = true }: IValidateArgs) { + return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise { if (!value && allowEmpty) { return { valid: null, diff --git a/yarn.lock b/yarn.lock index 520e976b17..b2c703c0d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1318,6 +1318,11 @@ dependencies: "@types/yargs-parser" "*" +"@types/zxcvbn@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@types/zxcvbn/-/zxcvbn-4.4.0.tgz#fbc1d941cc6d9d37d18405c513ba6b294f89b609" + integrity sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg== + "@typescript-eslint/experimental-utils@^2.5.0": version "2.27.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a" From 8e047c3731a436e9cc660ad0b1078f3b971d8121 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:26:17 -0600 Subject: [PATCH 178/241] Update README for room list store --- src/stores/room-list/README.md | 49 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index 0dd6c104d8..020108878b 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -7,20 +7,21 @@ It's so complicated it needs its own README. There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting -algorithm determines how individual tags (lists of rooms, sometimes called sublists) are ordered. +algorithm determines how rooms get ordered within tags affected by the list algorithm. -Behaviour of the room list takes the shape of default sorting on tags in most cases, though it can -override what is happening at the tag level depending on the algorithm used (as is the case with the -importance algorithm, described later). +Behaviour of the room list takes the shape of determining what features the room list supports, as well +as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which +is described later in this doc, is an example of an algorithm which makes heavy behavioural changes +to the room list. Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm the power to decide when and how to apply the tag sorting, if at all. ### Tag sorting algorithm: Alphabetical -When used, rooms in a given tag will be sorted alphabetically, where the alphabet is determined by a -simple string comparison operation (essentially giving the browser the problem of figuring out if A -comes before Z). +When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem +for the browser. All we do is a simple string comparison and expect the browser to return something +useful. ### Tag sorting algorithm: Manual @@ -30,7 +31,7 @@ of `order` cause rooms to appear closer to the top of the list. ### Tag sorting algorithm: Recent -Rooms are ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm in the room list system which determines whether an event type is capable of bubbling up in the room list. Normally events like room messages, stickers, and room security changes will be considered useful enough to cause a shift in time. @@ -49,7 +50,7 @@ its relative deterministic behaviour. ### List ordering algorithm: Importance On the other end of the spectrum, this is the most complicated algorithm which exists. There's major -behavioural changes and the tag sorting algorithm is selectively applied depending on circumstances. +behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances. Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags simply get the manual sorting algorithm applied to them with no further involvement from the importance @@ -58,34 +59,37 @@ relative (perceived) importance to the user: * **Red**: The room has unread mentions waiting for the user. * **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - messages which cause a push notification or badge count. Typically this is the default as rooms are + messages which cause a push notification or badge count. Typically, this is the default as rooms get set to 'All Messages'. * **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without a badge/notification count (or 'Mentions Only'/'Muted'). -* **Idle**: No relevant activity has occurred in the room since the user last read it. +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. -Conveniently, each tag is ordered by those categories as presented: red rooms appear above grey, grey -above idle, etc. +Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey +above bold, etc. Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm -is applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) -being sorted alphabetically amongst each other and the grey rooms sorted amongst each other, but +gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top. + + The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. The sticky room will remain in position on the room list regardless of other factors going on as typically clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms -above the selected room at all times where N is the number of rooms above the selected rooms when it was +above the selected room at all times, where N is the number of rooms above the selected rooms when it was selected. For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one -room above their selection at all times. If they receive another notification and the tag ordering is set -to Recent, they'll see the new notification go to the top position and the one that was previously there -fall behind the sticky room. +room above their selection at all times. If they receive another notification, and the tag ordering is +specified as Recent, they'll see the new notification go to the top position, and the one that was previously +there fall behind the sticky room. The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another -room, the previous sticky room is recalculated to determine which category it needs to be in as the user +room, the previous sticky room gets recalculated to determine which category it needs to be in as the user could have been scrolled up while new messages were received. Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what @@ -112,7 +116,10 @@ all kinds of filtering. The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: -tags are defined on rooms and are not defined as a collection of rooms (unlike how they are presented +tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific utilities are also included, though they are expected to move somewhere more general when needed. For example, the `membership` utilities could easily be moved elsewhere as needed. + +The various bits throughout the room list store should also have jsdoc of some kind to help describe +what they do and how they work. From 6cb1efc1a48b476891b488ace83108358883f0b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:45:17 -0600 Subject: [PATCH 179/241] Use the new TS dispatcher --- src/actions/RoomListActions.ts | 5 +++-- src/components/views/rooms/RoomList2.tsx | 4 ++-- src/components/views/rooms/RoomTile2.tsx | 2 +- src/dispatcher-types.ts | 28 ------------------------ src/stores/AsyncStore.ts | 2 +- src/stores/room-list/RoomListStore2.ts | 3 ++- 6 files changed, 9 insertions(+), 35 deletions(-) delete mode 100644 src/dispatcher-types.ts diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index eb9831ec47..e15e1b0c65 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -16,7 +16,7 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore, { TAG_DM } from '../stores/RoomListStore'; +import { TAG_DM } from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -24,6 +24,7 @@ import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; +import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; export default class RoomListActions { /** @@ -51,7 +52,7 @@ export default class RoomListActions { // Is the tag ordered manually? if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const newList = [...lists[newTag]]; newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 12a0117505..a5d0175f01 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -25,9 +25,9 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/Roo import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; -import { ActionPayload } from "../../../dispatcher-types"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; +import { ActionPayload } from "../../../dispatcher/payloads"; /******************************************************************* * CAUTION * diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8f1b0a3f7a..53e56d7a1e 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -24,7 +24,7 @@ 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'; +import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import * as RoomNotifs from '../../../RoomNotifs'; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; diff --git a/src/dispatcher-types.ts b/src/dispatcher-types.ts deleted file mode 100644 index 16fac0c849..0000000000 --- a/src/dispatcher-types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -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 flux from "flux"; -import dis from "./dispatcher"; - -// TODO: Merge this with the dispatcher and centralize types - -export interface ActionPayload { - [property: string]: any; // effectively "extends Object" - action: string; -} - -// For ease of reference in TypeScript classes -export const defaultDispatcher: flux.Dispatcher = dis; diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts index 5e19e17248..d79fd220b2 100644 --- a/src/stores/AsyncStore.ts +++ b/src/stores/AsyncStore.ts @@ -16,8 +16,8 @@ limitations under the License. import { EventEmitter } from 'events'; import AwaitLock from 'await-lock'; -import { ActionPayload } from "../dispatcher-types"; import { Dispatcher } from "flux"; +import { ActionPayload } from "../dispatcher/payloads"; /** * The event/channel to listen for in an AsyncStore. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index c461aeab66..4f38a25d95 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -16,7 +16,6 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; -import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; @@ -25,6 +24,8 @@ import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { getListAlgorithmInstance } from "./algorithms/list_ordering"; +import { ActionPayload } from "../../dispatcher/payloads"; +import defaultDispatcher from "../../dispatcher/dispatcher"; interface IState { tagsEnabled?: boolean; From 865495dd695293fa5c8cc9ee17bb24dfdc66d097 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 14 May 2020 20:33:50 +0100 Subject: [PATCH 180/241] replace zxcvbn field in CreateSecretStorageDialog with PassphraseField Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../_CreateSecretStorageDialog.scss | 11 -- .../CreateSecretStorageDialog.js | 106 +++++------------- src/components/views/auth/PassphraseField.tsx | 6 +- 3 files changed, 36 insertions(+), 87 deletions(-) diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss index a9ebd54b31..63e5a3de09 100644 --- a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss +++ b/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss @@ -68,17 +68,6 @@ limitations under the License. margin-top: 0px; } -.mx_CreateSecretStorageDialog_passPhraseHelp { - flex: 1; - height: 64px; - margin-left: 20px; - font-size: 80%; -} - -.mx_CreateSecretStorageDialog_passPhraseHelp progress { - width: 100%; -} - .mx_CreateSecretStorageDialog_passPhraseMatch { width: 200px; margin-left: 20px; diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 4c1faa3e6a..b77c71d08c 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -15,18 +15,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../../index'; import {MatrixClientPeg} from '../../../../MatrixClientPeg'; -import { scorePassword } from '../../../../utils/PasswordScorer'; import FileSaver from 'file-saver'; -import { _t } from '../../../../languageHandler'; +import {_t, _td} from '../../../../languageHandler'; import Modal from '../../../../Modal'; import { promptForBackupPassphrase } from '../../../../CrossSigningManager'; import {copyNode} from "../../../../utils/strings"; import {SSOAuthEntry} from "../../../../components/views/auth/InteractiveAuthEntryComponents"; -import ZxcvbnProgressBar from "../../../../components/views/elements/ZxcvbnProgressBar"; +import PassphraseField from "../../../../components/views/auth/PassphraseField"; const PHASE_LOADING = 0; const PHASE_LOADERROR = 1; @@ -40,7 +39,6 @@ const PHASE_DONE = 8; const PHASE_CONFIRM_SKIP = 9; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. -const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. /* * Walks the user through the process of creating a passphrase to guard Secure @@ -69,10 +67,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.state = { phase: PHASE_LOADING, passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', copied: false, downloaded: false, - zxcvbnResult: null, backupInfo: null, backupSigStatus: null, // does the server offer a UI auth flow with just m.login.password @@ -84,6 +82,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { useKeyBackup: true, }; + this._passphraseField = createRef(); + this._fetchBackupInfo(); if (this.state.accountPassword) { // If we have an account password in memory, let's simplify and @@ -365,22 +365,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _onPassPhraseNextClick = async (e) => { e.preventDefault(); + if (!this._passphraseField.current) return; // unmounting - // If we're waiting for the timeout before updating the result at this point, - // skip ahead and do it now, otherwise we'll deny the attempt to proceed - // even if the user entered a valid passphrase - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - this._setZxcvbnResultTimeout = null; - await new Promise((resolve) => { - this.setState({ - zxcvbnResult: scorePassword(this.state.passPhrase), - }, resolve); - }); - } - if (this._passPhraseIsValid()) { - this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); + await this._passphraseField.current.validate({ allowEmpty: false }); + if (!this._passphraseField.current.state.valid) { + this._passphraseField.current.focus(); + this._passphraseField.current.validate({ allowEmpty: false, focused: true }); + return; } + + this.setState({phase: PHASE_PASSPHRASE_CONFIRM}); }; _onPassPhraseConfirmNextClick = async (e) => { @@ -400,9 +394,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _onSetAgainClick = () => { this.setState({ passPhrase: '', + passPhraseValid: false, passPhraseConfirm: '', phase: PHASE_PASSPHRASE, - zxcvbnResult: null, }); } @@ -412,23 +406,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } + _onPassPhraseValidate = (result) => { + this.setState({ + passPhraseValid: result.valid, + }); + }; + _onPassPhraseChange = (e) => { this.setState({ passPhrase: e.target.value, }); - - if (this._setZxcvbnResultTimeout !== null) { - clearTimeout(this._setZxcvbnResultTimeout); - } - this._setZxcvbnResultTimeout = setTimeout(() => { - this._setZxcvbnResultTimeout = null; - this.setState({ - // precompute this and keep it in state: zxcvbn is fast but - // we use it in a couple of different places so no point recomputing - // it unnecessarily. - zxcvbnResult: scorePassword(this.state.passPhrase), - }); - }, PASSPHRASE_FEEDBACK_DELAY); } _onPassPhraseConfirmChange = (e) => { @@ -437,10 +424,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { }); } - _passPhraseIsValid() { - return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; - } - _onAccountPasswordChange = (e) => { this.setState({ accountPassword: e.target.value, @@ -503,37 +486,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Field = sdk.getComponent('views.elements.Field'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch'); - let strengthMeter; - let helpText; - if (this.state.zxcvbnResult) { - if (this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE) { - helpText = _t("Great! This recovery passphrase looks strong enough."); - } else { - // We take the warning from zxcvbn or failing that, the first - // suggestion. In practice The first is generally the most relevant - // and it's probably better to present the user with one thing to - // improve about their password than a whole collection - it can - // spit out a warning and multiple suggestions which starts getting - // very information-dense. - const suggestion = ( - this.state.zxcvbnResult.feedback.warning || - this.state.zxcvbnResult.feedback.suggestions[0] - ); - const suggestionBlock =
    {suggestion || _t("Keep going...")}
    ; - - helpText =
    - {suggestionBlock} -
    ; - } - strengthMeter =
    - -
    ; - } - return

    {_t( "Set a recovery passphrase to secure encrypted information and recover it if you log out. " + @@ -541,19 +496,20 @@ export default class CreateSecretStorageDialog extends React.PureComponent { )}

    - -
    - {strengthMeter} - {helpText} -
    .": "Mitte midagi ei kuvata? Kõik Matrix'i kliendid ei toeta veel interaktiivset verifitseerimist. .", + "Waiting for %(userId)s to confirm...": "Ootan kinnitust kasutajalt %(userId)s…", + "Skip": "Jäta vahele", + "Token incorrect": "Vigane tunnusluba" } From 7c5842c208fe08b9e48964214663a447216bc928 Mon Sep 17 00:00:00 2001 From: Xose M Date: Sun, 17 May 2020 09:37:45 +0000 Subject: [PATCH 202/241] Translated using Weblate (Galician) Currently translated at 40.0% (925 of 2312 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index 346dda09f2..3328292be0 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -698,9 +698,9 @@ "Failed to remove tag %(tagName)s from room": "Fallo ao eliminar a etiqueta %(tagName)s da sala", "Failed to add tag %(tagName)s to room": "Fallo ao engadir a etiqueta %(tagName)s a sala", "Key request sent.": "Petición de chave enviada.", - "Flair": "Aura", - "Showing flair for these communities:": "Mostrar a aura para estas comunidades:", - "Display your community flair in rooms configured to show it.": "Mostrar a aura da súa comunidade nas salas configuradas para que a mostren.", + "Flair": "Popularidade", + "Showing flair for these communities:": "Mostrar a popularidade destas comunidades:", + "Display your community flair in rooms configured to show it.": "Mostrar a popularidade da túa comunidade nas salas configuradas para que a mostren.", "Did you know: you can use communities to filter your Riot.im experience!": "Sabía que pode utilizar as comunidades para mellorar a súa experiencia con Riot.im!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Para establecer un filtro, arrastre un avatar da comunidade sobre o panel de filtros na parte esquerda da pantalla. Pode pulsar nun avatar no panel de filtrado en calquera momento para ver só salas e xente asociada a esa comunidade.", "Deops user with given id": "Degradar o usuario con esa ID", @@ -822,7 +822,7 @@ "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Os informes de depuración conteñen datos de utilización do aplicativo como o seu nome de usuario, os IDs ou alcumes de salas e grupos que vostede visitou e os nomes de usuarios doutras usuarias. Non conteñen mensaxes.", "Unhide Preview": "Desagochar a vista previa", "Unable to join network": "Non se puido conectar a rede", - "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Pode que os configurase nun cliente diferente de Riot. Non pode establecelos desde Riot pero aínda así aplicaranse", + "You might have configured them in a client other than Riot. You cannot tune them in Riot but they still apply": "Pode que os configurases nun cliente diferente de Riot. Non podes establecelos desde Riot pero aínda así aplicaranse", "Sorry, your browser is not able to run Riot.": "Desculpe, o seu navegador non pode executar Riot.", "Uploaded on %(date)s by %(user)s": "Subido a %(date)s por %(user)s", "Messages in group chats": "Mensaxes en grupos de chat", @@ -919,5 +919,23 @@ "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "A súa mensaxe non foi enviada porque este servidor acadou o Límite Mensual de Usuaria Activa. Por favor contacte coa administración do servizo para continuar utilizando o servizo.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "A súa mensaxe non foi enviada porque o servidor superou o límite de recursos. Por favor contacte coa administración do servizo para continuar utilizando o servizo.", "Legal": "Legal", - "Please contact your service administrator to continue using this service.": "Por favor contacte coa administración do servizo para continuar utilizando o servizo." + "Please contact your service administrator to continue using this service.": "Por favor contacte coa administración do servizo para continuar utilizando o servizo.", + "Use Single Sign On to continue": "Usar Single Sign On para continuar", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Confirma que queres engadir este email usando Single Sign On como proba de identidade.", + "Single Sign On": "Single Sign On", + "Confirm adding email": "Confirma novo email", + "Click the button below to confirm adding this email address.": "Preme no botón inferior para confirmar que queres engadir o email.", + "Confirm": "Confirmar", + "Add Email Address": "Engadir email", + "Confirm adding this phone number by using Single Sign On to prove your identity.": "Confirma que queres engadir este teléfono usando Single Sign On como proba de identidade.", + "Confirm adding phone number": "Confirma a adición do teléfono", + "Click the button below to confirm adding this phone number.": "Preme no botón inferior para confirmar que engades este número.", + "Add Phone Number": "Engadir novo Número", + "The version of Riot": "A versión de Riot", + "Whether or not you're logged in (we don't record your username)": "Se estás conectada ou non (non rexistramos o teu nome de usuaria)", + "Whether you're using Riot on a device where touch is the primary input mechanism": "Se estás conectada utilizando Riot nun dispositivo maiormente táctil", + "Whether you're using Riot as an installed Progressive Web App": "Se estás a usar Riot como unha Progressive Web App instalada", + "Your user agent": "User Agent do navegador", + "The information being sent to us to help make Riot better includes:": "Información que nos envías para mellorar Riot inclúe:", + "Please install Chrome, Firefox, or Safari for the best experience.": "Instala Chrome, Firefox, ou Safari para ter unha mellor experiencia." } From 5ea4cc1c5b209b5ea8b3401ea3cabf80a20da11a Mon Sep 17 00:00:00 2001 From: yuuki-san Date: Mon, 18 May 2020 08:31:13 +0000 Subject: [PATCH 203/241] Translated using Weblate (Slovak) Currently translated at 62.3% (1441 of 2312 strings) Translation: Riot Web/matrix-react-sdk Translate-URL: https://translate.riot.im/projects/riot-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index fbecaa4845..e85732ed82 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -628,7 +628,7 @@ "Export room keys": "Exportovať kľúče miestností", "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Tento proces vás prevedie exportom kľúčov určených na dešifrovanie správ, ktoré ste dostali v šifrovaných miestnostiach do lokálneho súboru. Tieto kľúče zo súboru môžete neskôr importovať do iného Matrix klienta, aby ste v ňom mohli dešifrovať vaše šifrované správy.", "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Tento súbor umožní komukoľvek, k to má ku nemu prístup dešifrovať všetky vami viditeľné šifrované správy, mali by ste teda byť opatrní a tento súbor si bezpečne uchovať. Aby bolo toto pre vás jednoduchšie, nižšie zadajte heslo, ktorým budú údaje v súbore zašifrované. Importovať údaje zo súboru bude možné len po zadaní tohoto istého hesla.", - "Enter passphrase": "Zadajte heslo", + "Enter passphrase": "Zadajte (dlhé) heslo", "Confirm passphrase": "Potvrďte heslo", "Export": "Exportovať", "Import room keys": "Importovať kľúče miestností", @@ -1519,5 +1519,17 @@ "Whether you're using Riot on a device where touch is the primary input mechanism": "Či používate Riot na zariadení, ktorého hlavným vstupným mechanizmom je dotyk (mobil, tablet,...)", "Whether you're using Riot as an installed Progressive Web App": "Či používate Riot ako nainštalovanú Progresívnu Webovú Aplikáciu", "Your user agent": "Identifikátor vášho prehliadača", - "The information being sent to us to help make Riot better includes:": "Informácie, ktoré nám posielate, aby sme zlepšili Riot, zahŕňajú:" + "The information being sent to us to help make Riot better includes:": "Informácie, ktoré nám posielate, aby sme zlepšili Riot, zahŕňajú:", + "There are unknown sessions in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "V miestnosti je neznáma relácia: pokiaľ budete pokračovať bez jej overenia, bude schopná odpočúvať váš hovor.", + "Review Sessions": "Overiť reláciu", + "If you cancel now, you won't complete verifying the other user.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie druhého používateľa.", + "If you cancel now, you won't complete verifying your other session.": "Pokiaľ teraz proces zrušíte, nedokončíte overenie vašej druhej relácie.", + "If you cancel now, you won't complete your operation.": "Pokiaľ teraz proces zrušíte, nedokončíte ho.", + "Cancel entering passphrase?": "Zrušiť zadávanie (dlhého) hesla.", + "Setting up keys": "Príprava kľúčov", + "Verify this session": "Overiť túto reláciu", + "Keep recovery passphrase in memory for this session": "Ponechať (dlhé) heslo pre obnovu zálohy v pamäti pre túto reláciu", + "Enter recovery passphrase": "Zadajte (dlhé) heslo pre obnovu zálohy", + "Unable to access secret storage. Please verify that you entered the correct recovery passphrase.": "Nemožno sa dostať do tajného úložiska. Prosím, overte, že ste zadali správne (dlhé) heslo pre obnovu zálohy.", + "Access your secure message history and your cross-signing identity for verifying other sessions by entering your recovery passphrase.": "Získajte prístup k vašej zabezpečenej histórií správ a vašemu krížom-podpísanej identite na potvrdenie iných relácií zadaním vášho (dlhého) hesla na obnovu zálohy." } From b6ac8cb28d92822ea1cf128fd46b687113e1a74d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@googlemail.com> Date: Wed, 13 May 2020 06:36:14 +0100 Subject: [PATCH 204/241] Revert "ImageView make clicking off it easier" (cherry picked from commit 52e3c97f8c9a2032fe92b2b4bb5fc68c0f6957b5) --- res/css/views/elements/_ImageView.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 983ef074f2..0a4ed2a194 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -37,7 +37,7 @@ limitations under the License. order: 2; /* min-width hack needed for FF */ min-width: 0px; - max-height: 90%; + height: 90%; flex: 15 15 0; display: flex; align-items: center; From a864643d98197ae6bc3bf34c8ee3da4c4a94a013 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 18 May 2020 10:37:02 +0100 Subject: [PATCH 205/241] Label the create room button something better than "Add room" Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 1 + src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 289a89a206..c46b946b5c 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -785,6 +785,7 @@ export default createReactClass({ label: _t('Rooms'), incomingCall: incomingCallIfTaggedAs('im.vector.fake.recent'), onAddRoom: () => {dis.dispatch({action: 'view_create_room'});}, + addRoomLabel: _t("Create room"), }, ]; const tagSubLists = Object.keys(this.state.lists) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f16a0d7755..c9ac768b3e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1113,6 +1113,7 @@ "Direct Messages": "Direct Messages", "Start chat": "Start chat", "Rooms": "Rooms", + "Create room": "Create room", "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", From 17f535e5f878337854b7599ca8da2b3e8528c81b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 18 May 2020 16:37:10 +0100 Subject: [PATCH 206/241] Fix review problems - removed superfluous position and classes - fixed compact view - fixed event list summary avatar and text overlap - fixed a problem where the mention list refuses to load. --- res/css/views/rooms/_EventTile.scss | 79 ------------------ res/css/views/rooms/_GroupLayout.scss | 82 ++++++++++++++++++- res/css/views/rooms/_IRCLayout.scss | 4 +- src/components/structures/MessagePanel.js | 2 +- .../views/elements/ErrorBoundary.js | 2 +- .../elements/IRCTimelineProfileResizer.tsx | 4 +- src/components/views/rooms/EventTile.js | 2 - 7 files changed, 88 insertions(+), 87 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index b9a41c4310..c278f813d0 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -37,7 +37,6 @@ limitations under the License. } .mx_EventTile_avatar { - position: absolute; top: 14px; left: 8px; cursor: pointer; @@ -576,84 +575,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { /* end of overrides */ -.mx_MatrixChat_useCompactLayout { - .mx_EventTile { - padding-top: 4px; - } - - .mx_EventTile.mx_EventTile_info { - // same as the padding for non-compact .mx_EventTile.mx_EventTile_info - padding-top: 0px; - font-size: $font-13px; - .mx_EventTile_line, .mx_EventTile_reply { - line-height: $font-20px; - } - .mx_EventTile_avatar { - top: 4px; - } - } - - .mx_EventTile .mx_SenderProfile { - font-size: $font-13px; - } - - .mx_EventTile.mx_EventTile_emote { - // add a bit more space for emotes so that avatars don't collide - padding-top: 8px; - .mx_EventTile_avatar { - top: 2px; - } - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 1px; - } - } - - .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { - padding-top: 0; - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - } - - .mx_EventTile_line, .mx_EventTile_reply { - padding-top: 0px; - padding-bottom: 0px; - } - - .mx_EventTile_avatar { - top: 2px; - } - - .mx_EventTile_e2eIcon { - top: 3px; - } - - .mx_EventTile_readAvatars { - top: 27px; - } - - .mx_EventTile_continuation .mx_EventTile_readAvatars, - .mx_EventTile_emote .mx_EventTile_readAvatars { - top: 5px; - } - - .mx_EventTile_info .mx_EventTile_readAvatars { - top: 4px; - } - - .mx_RoomView_MessageList h2 { - margin-top: 6px; - } - - .mx_EventTile_content .markdown-body { - p, ul, ol, dl, blockquote, pre, table { - margin-bottom: 4px; // 1/4 of the non-compact margin-bottom - } - } -} - .mx_EventTile_tileError { color: red; text-align: center; diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 6528d6c6cd..bfe463ed49 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -47,6 +47,86 @@ $left-gutter: 65px; } .mx_EventTile_info .mx_EventTile_line { - padding-left: 83px; + padding-left: calc($left-gutter + 18px); } } + +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile { + padding-top: 4px; + } + + .mx_EventTile.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + padding-top: 0px; + font-size: $font-13px; + .mx_EventTile_line, .mx_EventTile_reply { + line-height: $font-20px; + } + .mx_EventTile_avatar { + top: 4px; + } + } + + .mx_EventTile .mx_SenderProfile { + font-size: $font-13px; + } + + .mx_EventTile.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide + padding-top: 8px; + .mx_EventTile_avatar { + top: 2px; + } + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + .mx_EventTile.mx_EventTile_emote.mx_EventTile_continuation { + padding-top: 0; + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_e2eIcon { + top: 3px; + } + + .mx_EventTile_readAvatars { + top: 27px; + } + + .mx_EventTile_continuation .mx_EventTile_readAvatars, + .mx_EventTile_emote .mx_EventTile_readAvatars { + top: 5px; + } + + .mx_EventTile_info .mx_EventTile_readAvatars { + top: 4px; + } + + .mx_RoomView_MessageList h2 { + margin-top: 6px; + } + + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } +} \ No newline at end of file diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index f2a616f9c9..5f88473c5f 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -138,7 +138,7 @@ $irc-line-height: $font-18px; .mx_EventListSummary_avatars { padding: 0; - margin: 0; + margin: 0 9px 0 0; } } @@ -185,7 +185,7 @@ $irc-line-height: $font-18px; .mx_SenderProfile_hover:hover { overflow: visible; - width: auto; + width: max(auto, 100%); z-index: 10; } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 1c10efb346..f875467e4f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -822,7 +822,7 @@ export default class MessagePanel extends React.Component { let ircResizer = null; if (this.state.useIRCLayout) { - ircResizer = ; + ircResizer = ; } return ( diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.js index 1abd11f838..a043b350ab 100644 --- a/src/components/views/elements/ErrorBoundary.js +++ b/src/components/views/elements/ErrorBoundary.js @@ -73,7 +73,7 @@ export default class ErrorBoundary extends React.PureComponent { if (this.state.error) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const newIssueUrl = "https://github.com/vector-im/riot-web/issues/new"; - return
    + return

    {_t("Something went wrong!")}

    {_t( diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index 44ceeb9b7b..596d46bf36 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -77,7 +77,9 @@ export default class IRCTimelineProfileResizer extends React.Component Date: Mon, 18 May 2020 16:43:47 +0100 Subject: [PATCH 207/241] lint --- res/css/views/rooms/_GroupLayout.scss | 2 +- src/components/structures/MessagePanel.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index bfe463ed49..40440f7d49 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -129,4 +129,4 @@ $left-gutter: 65px; margin-bottom: 4px; // 1/4 of the non-compact margin-bottom } } -} \ No newline at end of file +} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index f875467e4f..b3f9b40ada 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -822,7 +822,11 @@ export default class MessagePanel extends React.Component { let ircResizer = null; if (this.state.useIRCLayout) { - ircResizer = ; + ircResizer = ; } return ( From 4deeef5fca9afcaf35d3c76f20ea84c8ac4808aa Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 18 May 2020 16:57:00 +0100 Subject: [PATCH 208/241] Remove ability to remove avatars --- src/components/structures/MessagePanel.js | 13 ------------- src/components/views/elements/ReplyThread.js | 7 ++----- src/components/views/rooms/EventTile.js | 6 +----- src/settings/Settings.js | 6 ------ 4 files changed, 3 insertions(+), 29 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index b3f9b40ada..404e950d7f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -174,11 +174,6 @@ export default class MessagePanel extends React.Component { SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); this._layoutWatcherRef = SettingsStore.watchSetting("feature_irc_ui", null, this.onLayoutChange); - this._displayAvatarsWatcherRef = SettingsStore.watchSetting( - "feature_no_timeline_avatars", - null, - this.onDisplayAvatarsChange, - ); } componentDidMount() { @@ -189,7 +184,6 @@ export default class MessagePanel extends React.Component { this._isMounted = false; SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._layoutWatcherRef); - SettingsStore.unwatchSetting(this._displayAvatarsWatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -214,12 +208,6 @@ export default class MessagePanel extends React.Component { }); } - onDisplayAvatarsChange = () => { - this.setState({ - displayAvatars: SettingsStore.getValue("feature_no_timeline_avatars"), - }); - } - /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { @@ -622,7 +610,6 @@ export default class MessagePanel extends React.Component { getRelationsForEvent={this.props.getRelationsForEvent} showReactions={this.props.showReactions} useIRCLayout={this.state.useIRCLayout} - displayAvatars={this.state.displayAvatars} /> , diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 6bfda5dd94..04e31d6490 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -39,8 +39,6 @@ export default class ReplyThread extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, // Specifies which layout to use. useIRCLayout: PropTypes.bool, - // Specifies whether to display avatars. - displayAvatars: PropTypes.bool, }; static contextType = MatrixClientContext; @@ -180,7 +178,7 @@ export default class ReplyThread extends React.Component { }; } - static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout, displayAvatars) { + static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, useIRCLayout) { if (!ReplyThread.getParentEventId(parentEv)) { return

    ; } @@ -190,7 +188,7 @@ export default class ReplyThread extends React.Component { ref={ref} permalinkCreator={permalinkCreator} useIRCLayout={useIRCLayout} - displayAvatars={displayAvatars} />; + />; } componentDidMount() { @@ -342,7 +340,6 @@ export default class ReplyThread extends React.Component { isRedacted={ev.isRedacted()} isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")} useIRCLayout={this.props.useIRCLayout} - displayAvatars={this.props.displayAvatars} /> ; }); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 4ff3a5ccfc..1cb632b2e8 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -209,9 +209,6 @@ export default createReactClass({ // whether to use the irc layout useIRCLayout: PropTypes.bool, - - // whether to display avatars - displayAvatars: PropTypes.bool, }, getDefaultProps: function() { @@ -714,7 +711,7 @@ export default createReactClass({ needsSenderProfile = true; } - if (this.props.mxEvent.sender && avatarSize && this.props.displayAvatars) { + if (this.props.mxEvent.sender && avatarSize) { avatar = (
    Date: Mon, 18 May 2020 17:06:54 +0100 Subject: [PATCH 209/241] Appease i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 37b9c1dfc8..12838968f7 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,7 +406,6 @@ "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Support adding custom themes": "Support adding custom themes", "Use IRC layout": "Use IRC layout", - "Display user avatars on messages": "Display user avatars on messages", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", From 7bb7f30b8f4ca9c8bd3d02e69ff2928317626fa9 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Mon, 18 May 2020 22:02:22 +0100 Subject: [PATCH 210/241] missed one --- src/components/structures/MessagePanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 404e950d7f..cac04d84f1 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -119,7 +119,6 @@ export default class MessagePanel extends React.Component { ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), useIRCLayout: SettingsStore.getValue("feature_irc_ui"), - displayAvatars: SettingsStore.getValue("feature_no_timeline_avatars"), }; // opaque readreceipt info for each userId; used by ReadReceiptMarker From 406839e89a43a559e6529c32998e40f8d5a77f21 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 May 2020 11:13:57 +0100 Subject: [PATCH 211/241] Upgrade matrix-js-sdk to 6.1.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8338a91bf7..99e0399d8e 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "6.1.0-rc.1", + "matrix-js-sdk": "6.1.0", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index d05c444470..0edea5433e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5720,10 +5720,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@6.1.0-rc.1: - version "6.1.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-6.1.0-rc.1.tgz#521d5683d10e49bb437b2457d1f0c6696fc7a4e3" - integrity sha512-q3XScyroUwY3qTBglRgE1lldfrLlSML5nU4gJzXrEwn2gGpR1P4IU8aT8EcumyOmeZhrrysS2UEE9fWgUmhkBw== +matrix-js-sdk@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-6.1.0.tgz#c28ad67c113c4aa9c8bce409c7ba550170bdc2ee" + integrity sha512-N+vCgxWORvhh7AGyWZlU5Z2brojbbnHnWlMkBF6JjWe6a+pfpjmRKp5/jeQpOz6yfe56sIQvU7ikBZl3JjlMiw== dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From bc8a53a7d797a513bf0fb2c000fd63cc7e90f4a8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 May 2020 11:21:59 +0100 Subject: [PATCH 212/241] Prepare changelog for v2.6.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c34bfba87b..7901062b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +Changes in [2.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0) (2020-05-19) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0-rc.1...v2.6.0) + + * Upgrade to JS SDK 6.1.0 + * Revert "ImageView make clicking off it easier" + [\#4602](https://github.com/matrix-org/matrix-react-sdk/pull/4602) + * Remove debugging that causes email addresses to load forever (to release) + [\#4598](https://github.com/matrix-org/matrix-react-sdk/pull/4598) + Changes in [2.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0-rc.1) (2020-05-14) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.5.0...v2.6.0-rc.1) From 26e6447be5331614668b0d0e3ae516a0988ef9bc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 May 2020 11:22:00 +0100 Subject: [PATCH 213/241] v2.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99e0399d8e..28f3d1633c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.6.0-rc.1", + "version": "2.6.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 833eccf5cd901d04ddfcc06a7f95e2e2ebe6f5d5 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Tue, 19 May 2020 11:25:07 +0100 Subject: [PATCH 214/241] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 3bdda8a583..7c008d5ccc 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.6", "lodash": "^4.17.14", - "matrix-js-sdk": "6.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "minimist": "^1.2.0", "pako": "^1.0.5", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index b7e5017fd7..93118dab22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1285,6 +1285,13 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/qrcode@^1.3.4": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.4.tgz#984d97bb72caa558d470158701081ccb712f616b" + integrity sha512-aILE5yvKaqQXlY0YPMEYwK/KwdD43fwQTyagj0ffBBTQj8h//085Zp8LUrOnZ9FT69x64f5UgDo0EueY4BPAdg== + dependencies: + "@types/node" "*" + "@types/react@*": version "16.9.35" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" @@ -1293,13 +1300,6 @@ "@types/prop-types" "*" csstype "^2.2.0" -"@types/qrcode@^1.3.4": - version "1.3.4" - resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.3.4.tgz#984d97bb72caa558d470158701081ccb712f616b" - integrity sha512-aILE5yvKaqQXlY0YPMEYwK/KwdD43fwQTyagj0ffBBTQj8h//085Zp8LUrOnZ9FT69x64f5UgDo0EueY4BPAdg== - dependencies: - "@types/node" "*" - "@types/react@16.9": version "16.9.32" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383" @@ -5753,10 +5753,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@6.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "6.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-6.1.0.tgz#c28ad67c113c4aa9c8bce409c7ba550170bdc2ee" - integrity sha512-N+vCgxWORvhh7AGyWZlU5Z2brojbbnHnWlMkBF6JjWe6a+pfpjmRKp5/jeQpOz6yfe56sIQvU7ikBZl3JjlMiw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3c6a0e1a08a3812ba988e60eb5a2a013bb27404" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0" From 4bf2e5fd8eaafe22533e5e3a02752a9d2251d129 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 May 2020 15:40:26 +0100 Subject: [PATCH 215/241] Remove SSSS key upgrade check from rageshake This code doesn't exist anymore as the SSSS symmetric upgrade stuff has been removed. Fixes https://github.com/vector-im/riot-web/issues/13715 --- src/rageshake/submit-rageshake.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index e5027e0d37..9f9d7898cb 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -133,7 +133,6 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_ready", String(await client.isCrossSigningReady())); - body.append("ssss_key_needs_upgrade", String(await client.secretStorageKeyNeedsUpgrade())); } } From 286828b3bb8a3556726c625be4b6034c1e83d87b Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 19 May 2020 16:15:13 +0100 Subject: [PATCH 216/241] Disable irc mode in notifiactions panel --- src/components/structures/MessagePanel.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index cac04d84f1..93e4668f66 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -110,15 +110,16 @@ export default class MessagePanel extends React.Component { showReactions: PropTypes.bool, }; - constructor() { - super(); + // Force props to be loaded for useIRCLayout + constructor(props) { + super(props); this.state = { // previous positions the read marker has been in, so we can // display 'ghost' read markers that are animating away ghostReadMarkers: [], showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")), }; // opaque readreceipt info for each userId; used by ReadReceiptMarker @@ -203,10 +204,15 @@ export default class MessagePanel extends React.Component { onLayoutChange = () => { this.setState({ - useIRCLayout: SettingsStore.getValue("feature_irc_ui"), + useIRCLayout: this.useIRCLayout(SettingsStore.getValue("feature_irc_ui")), }); } + useIRCLayout(ircLayoutSelected) { + // if room is null we are not in a normal room list + return ircLayoutSelected && this.props.room; + } + /* get the DOM node representing the given event */ getNodeForEventId(eventId) { if (!this.eventNodes) { From 86ad6de41ee455285695270242a5d0983b014899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 May 2020 11:01:56 +0200 Subject: [PATCH 217/241] EventIndex: Handle null tokens in the crawler loop as well. This is similar to 5eb510387c9aa08a061d5fd17bed3efc104b48eb. But now the checkpoint arrived during a crawl. --- src/indexing/EventIndex.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 02151f8474..16b0183da1 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -489,14 +489,20 @@ export default class EventIndex extends EventEmitter { return object; }); - // Create a new checkpoint so we can continue crawling the room for - // messages. - const newCheckpoint = { - roomId: checkpoint.roomId, - token: res.end, - fullCrawl: checkpoint.fullCrawl, - direction: checkpoint.direction, - }; + let newCheckpoint; + + // The token can be null for some reason. Don't create a checkpoint + // in that case since adding it to the db will fail. + if (res.end) { + // Create a new checkpoint so we can continue crawling the room + // for messages. + newCheckpoint = { + roomId: checkpoint.roomId, + token: res.end, + fullCrawl: checkpoint.fullCrawl, + direction: checkpoint.direction, + }; + } try { for (let i = 0; i < redactionEvents.length; i++) { @@ -506,6 +512,15 @@ export default class EventIndex extends EventEmitter { const eventsAlreadyAdded = await indexManager.addHistoricEvents( events, newCheckpoint, checkpoint); + + // We didn't get a valid new checkpoint from the server, nothing + // to do here anymore. + if (!newCheckpoint) { + console.log("EventIndex: The server didn't return a valid ", + "new checkpoint, not continuing the crawl.", checkpoint); + continue + } + // If all events were already indexed we assume that we catched // up with our index and don't need to crawl the room further. // Let us delete the checkpoint in that case, otherwise push From db72c645bae492a5a3daba70695ef137ef07d485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 20 May 2020 11:14:37 +0200 Subject: [PATCH 218/241] EventIndex: Add a missing semicolon. --- src/indexing/EventIndex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/indexing/EventIndex.js b/src/indexing/EventIndex.js index 16b0183da1..d372c38405 100644 --- a/src/indexing/EventIndex.js +++ b/src/indexing/EventIndex.js @@ -518,7 +518,7 @@ export default class EventIndex extends EventEmitter { if (!newCheckpoint) { console.log("EventIndex: The server didn't return a valid ", "new checkpoint, not continuing the crawl.", checkpoint); - continue + continue; } // If all events were already indexed we assume that we catched From 3e30df17fbc9a3a5efeb4c7bbf24ea62c828b235 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 13:07:33 +0100 Subject: [PATCH 219/241] Slider is more responsive --- res/css/views/elements/_Slider.scss | 5 ++++- src/components/views/elements/Slider.tsx | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_Slider.scss b/res/css/views/elements/_Slider.scss index 06c3c4c98b..58ba2813b4 100644 --- a/res/css/views/elements/_Slider.scss +++ b/res/css/views/elements/_Slider.scss @@ -38,7 +38,9 @@ limitations under the License. .mx_Slider_bar > hr { width: 100%; - border: 0.2em solid $slider-background-color; + height: 0.4em; + background-color: $slider-background-color; + border: 0; } .mx_Slider_selection { @@ -47,6 +49,7 @@ limitations under the License. width: calc(100% - 1em); // 2 * half the width of a dot height: 1em; position: absolute; + pointer-events: none; } .mx_Slider_selectionDot { diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index e181f0d9e3..f76a4684d3 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -93,7 +93,7 @@ export default class Slider extends React.Component { return
    -
    +
    {} : this.onClick.bind(this)}/> { selection }
    @@ -102,6 +102,15 @@ export default class Slider extends React.Component {
    ; } + + onClick(event: React.MouseEvent) { + const width = (event.target as HTMLElement).clientWidth; + // nativeEvent is safe to use because https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/offsetX + // is supported by all modern browsers + const relativeClick = (event.nativeEvent.offsetX / width); + const nearestValue = this.props.values[Math.round(relativeClick * (this.props.values.length - 1))]; + this.props.onSelectionChange(nearestValue); + } } interface IDotProps { From 55e72dd5bf826fd8fb9cfb3f7b769ad682247b67 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 13:45:54 +0100 Subject: [PATCH 220/241] Remove min and max font setting --- src/FontWatcher.js | 9 +++++---- .../settings/tabs/user/AppearanceUserSettingsTab.js | 6 +++--- src/settings/Settings.js | 10 ---------- 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/FontWatcher.js b/src/FontWatcher.js index 561edc4662..1128ac1bd5 100644 --- a/src/FontWatcher.js +++ b/src/FontWatcher.js @@ -18,6 +18,10 @@ import dis from './dispatcher'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { + + static minSize = 13; + static maxSize = 20; + constructor() { this._dispatcherRef = null; } @@ -38,10 +42,7 @@ export class FontWatcher { }; _setRootFontSize = (size) => { - const min = SettingsStore.getValue("fontSizeMin"); - const max = SettingsStore.getValue("fontSizeMax"); - - const fontSize = Math.max(Math.min(max, size), min); + const fontSize = Math.max(Math.min(this.maxSize, size), this.minSize); if (fontSize != size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 63857ed9c2..308b7098d1 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -25,7 +25,7 @@ import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; import dis from "../../../../../dispatcher"; -import _range from "lodash/range"; +import { FontWatcher } from "../../../../../FontWatcher"; export default class AppearanceUserSettingsTab extends React.Component { constructor() { @@ -109,8 +109,8 @@ export default class AppearanceUserSettingsTab extends React.Component { console.log({value}); const parsedSize = parseFloat(value); - const min = SettingsStore.getValue("fontSizeMin"); - const max = SettingsStore.getValue("fontSizeMax"); + const min = FontWatcher.minSize; + const max = FontWatcher.maxSize; if (isNaN(parsedSize)) { return {valid: false, feedback: _t("Size must be a number")}; diff --git a/src/settings/Settings.js b/src/settings/Settings.js index afe8d2cecc..5f093f6a70 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -171,16 +171,6 @@ export const SETTINGS = { default: 16, controller: new FontSizeController(), }, - "fontSizeMin": { - displayName: _td("Min font size"), - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 13, - }, - "fontSizeMax": { - displayName: _td("Max font size"), - supportedLevels: LEVELS_ACCOUNT_SETTINGS, - default: 20, - }, "useCustomFontSize": { displayName: _td("Custom font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 82b76192aee6fe64865c1bc4ff456d0c4614dad0 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 14:44:56 +0100 Subject: [PATCH 221/241] Fixes, lints and i18n --- src/FontWatcher.js | 4 ++-- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 2 +- src/i18n/strings/en_EN.json | 2 -- src/settings/controllers/FontSizeController.js | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/FontWatcher.js b/src/FontWatcher.js index 1128ac1bd5..b45d9065ce 100644 --- a/src/FontWatcher.js +++ b/src/FontWatcher.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import dis from './dispatcher'; +import dis from './dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { @@ -42,7 +42,7 @@ export class FontWatcher { }; _setRootFontSize = (size) => { - const fontSize = Math.max(Math.min(this.maxSize, size), this.minSize); + const fontSize = Math.max(Math.min(FontWatcher.maxSize, size), FontWatcher.minSize); if (fontSize != size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 308b7098d1..3d04e10df7 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -24,7 +24,7 @@ import {enumerateThemes, ThemeWatcher} from "../../../../../theme"; import Field from "../../../elements/Field"; import Slider from "../../../elements/Slider"; import AccessibleButton from "../../../elements/AccessibleButton"; -import dis from "../../../../../dispatcher"; +import dis from "../../../../../dispatcher/dispatcher"; import { FontWatcher } from "../../../../../FontWatcher"; export default class AppearanceUserSettingsTab extends React.Component { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d6670e85b3..b7417762f1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -412,8 +412,6 @@ "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", "Font size": "Font size", - "Min font size": "Min font size", - "Max font size": "Max font size", "Custom font size": "Custom font size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", diff --git a/src/settings/controllers/FontSizeController.js b/src/settings/controllers/FontSizeController.js index 8e855e31ec..3ef01ab99b 100644 --- a/src/settings/controllers/FontSizeController.js +++ b/src/settings/controllers/FontSizeController.js @@ -15,7 +15,7 @@ limitations under the License. */ import SettingController from "./SettingController"; -import dis from "../../dispatcher"; +import dis from "../../dispatcher/dispatcher"; export default class FontSizeController extends SettingController { constructor() { From 4e9a139e8b45a8597b3fa51c357fca5ea1691fa7 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 14:49:18 +0100 Subject: [PATCH 222/241] lint --- src/FontWatcher.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/FontWatcher.js b/src/FontWatcher.js index b45d9065ce..c547ef8ce0 100644 --- a/src/FontWatcher.js +++ b/src/FontWatcher.js @@ -18,7 +18,6 @@ import dis from './dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { - static minSize = 13; static maxSize = 20; From adec5a4f921bb9e24d7f0d52240bbcc84959b871 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 15:09:10 +0100 Subject: [PATCH 223/241] fix test --- test/components/views/messages/TextualBody-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index 59671327ce..4e93b3bb64 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -206,7 +206,7 @@ describe("", () => { 'Hey ' + '' + 'Member' + ''); }); From 50a44405f012f9f987709025d1ea4c0e1e275eac Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 20 May 2020 15:17:47 +0100 Subject: [PATCH 224/241] CONSTANT_CASING --- src/FontWatcher.js | 6 +++--- .../views/settings/tabs/user/AppearanceUserSettingsTab.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/FontWatcher.js b/src/FontWatcher.js index c547ef8ce0..006df202ad 100644 --- a/src/FontWatcher.js +++ b/src/FontWatcher.js @@ -18,8 +18,8 @@ import dis from './dispatcher/dispatcher'; import SettingsStore, {SettingLevel} from './settings/SettingsStore'; export class FontWatcher { - static minSize = 13; - static maxSize = 20; + static MIN_SIZE = 13; + static MAX_SIZE = 20; constructor() { this._dispatcherRef = null; @@ -41,7 +41,7 @@ export class FontWatcher { }; _setRootFontSize = (size) => { - const fontSize = Math.max(Math.min(FontWatcher.maxSize, size), FontWatcher.minSize); + const fontSize = Math.max(Math.min(FontWatcher.MAX_SIZE, size), FontWatcher.MIN_SIZE); if (fontSize != size) { SettingsStore.setValue("fontSize", null, SettingLevel.Device, fontSize); diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js index 3d04e10df7..5b49dd0abd 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.js @@ -109,8 +109,8 @@ export default class AppearanceUserSettingsTab extends React.Component { console.log({value}); const parsedSize = parseFloat(value); - const min = FontWatcher.minSize; - const max = FontWatcher.maxSize; + const min = FontWatcher.MIN_SIZE; + const max = FontWatcher.MAX_SIZE; if (isNaN(parsedSize)) { return {valid: false, feedback: _t("Size must be a number")}; From 5618c382bd4b974ee68e8010c71af1a4733c0600 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 20 May 2020 16:01:34 +0100 Subject: [PATCH 225/241] Update code style to mention switch statements --- code_style.md | 1 + 1 file changed, 1 insertion(+) diff --git a/code_style.md b/code_style.md index 3ad0d38873..01c1f37146 100644 --- a/code_style.md +++ b/code_style.md @@ -151,6 +151,7 @@ General Style Don't set things to undefined. Reserve that value to mean "not yet set to anything." Boolean objects are verboten. - Use JSDoc +- Use switch-case statements where there are 5 or more branches running against the same variable. ECMAScript ---------- From 5d1c01fd6fd006443bbf4d6f1e0e731a99ad042e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 May 2020 12:26:27 +0100 Subject: [PATCH 226/241] Fix key backup restore with SSSS The room / session ID params come after the backupInfo for restoring from SSSS so the options object was being passed into the wrong param. Roll on TypeScript. This meant restoring backups worked fine when the key was cached but failed when it wasn't. Regressed in https://github.com/matrix-org/matrix-react-sdk/pull/4507 --- .../views/dialogs/keybackup/RestoreKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 7e51e76f6c..a16202ed93 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { // `accessSecretStorage` may prompt for storage access as needed. const recoverInfo = await accessSecretStorage(async () => { return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( - this.state.backupInfo, + this.state.backupInfo, undefined, undefined, { progressCallback: this._progressCallback }, ); }); From b27f1fa6db112bb6da4bc3e562fb4782fb7a0f88 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 May 2020 18:06:36 +0100 Subject: [PATCH 227/241] Convert BasePlatform and BaseEventIndexManager to Typescript Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/{BasePlatform.js => BasePlatform.ts} | 55 ++++++++----------- ...dexManager.js => BaseEventIndexManager.ts} | 12 ++-- 2 files changed, 28 insertions(+), 39 deletions(-) rename src/{BasePlatform.js => BasePlatform.ts} (79%) rename src/indexing/{BaseEventIndexManager.js => BaseEventIndexManager.ts} (97%) diff --git a/src/BasePlatform.js b/src/BasePlatform.ts similarity index 79% rename from src/BasePlatform.js rename to src/BasePlatform.ts index e3cbc4dcf0..3f11f25d62 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.ts @@ -1,5 +1,3 @@ -// @flow - /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd @@ -19,9 +17,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClient} from "matrix-js-sdk"; +import {MatrixClient} from "matrix-js-sdk/src/client"; import dis from './dispatcher/dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; +import {ActionPayload} from "./dispatcher/payloads"; /** * Base class for classes that provide platform-specific functionality @@ -29,27 +28,25 @@ import BaseEventIndexManager from './indexing/BaseEventIndexManager'; * * Instances of this class are provided by the application. */ -export default class BasePlatform { - constructor() { - this.notificationCount = 0; - this.errorDidOccur = false; +export default abstract class BasePlatform { + protected notificationCount: number = 0; + protected errorDidOccur: boolean = false; - dis.register(this._onAction.bind(this)); + constructor() { + dis.register(this.onAction); } - _onAction(payload: Object) { + protected onAction = (payload: ActionPayload) => { switch (payload.action) { case 'on_client_not_viable': case 'on_logged_out': this.setNotificationCount(0); break; } - } + }; // Used primarily for Analytics - getHumanReadableName(): string { - return 'Base Platform'; - } + abstract getHumanReadableName(): string; setNotificationCount(count: number) { this.notificationCount = count; @@ -84,22 +81,16 @@ export default class BasePlatform { * that is 'granted' if the user allowed the request or * 'denied' otherwise. */ - requestNotificationPermission(): Promise { - } + abstract requestNotificationPermission(): Promise; - displayNotification(title: string, msg: string, avatarUrl: string, room: Object) { - } + abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object); - loudNotification(ev: Event, room: Object) { - } + abstract loudNotification(ev: Event, room: Object); /** - * Returns a promise that resolves to a string representing - * the current version of the application. + * Returns a promise that resolves to a string representing the current version of the application. */ - getAppVersion(): Promise { - throw new Error("getAppVersion not implemented!"); - } + abstract getAppVersion(): Promise; /* * If it's not expected that capturing the screen will work @@ -114,20 +105,18 @@ export default class BasePlatform { * Restarts the application, without neccessarily reloading * any application code */ - reload() { - throw new Error("reload not implemented!"); - } + abstract reload(); supportsAutoLaunch(): boolean { return false; } // XXX: Surely this should be a setting like any other? - async getAutoLaunchEnabled(): boolean { + async getAutoLaunchEnabled(): Promise { return false; } - async setAutoLaunchEnabled(enabled: boolean): void { + async setAutoLaunchEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -135,11 +124,11 @@ export default class BasePlatform { return false; } - async getAutoHideMenuBarEnabled(): boolean { + async getAutoHideMenuBarEnabled(): Promise { return false; } - async setAutoHideMenuBarEnabled(enabled: boolean): void { + async setAutoHideMenuBarEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } @@ -147,11 +136,11 @@ export default class BasePlatform { return false; } - async getMinimizeToTrayEnabled(): boolean { + async getMinimizeToTrayEnabled(): Promise { return false; } - async setMinimizeToTrayEnabled(enabled: boolean): void { + async setMinimizeToTrayEnabled(enabled: boolean): Promise { throw new Error("Unimplemented"); } diff --git a/src/indexing/BaseEventIndexManager.js b/src/indexing/BaseEventIndexManager.ts similarity index 97% rename from src/indexing/BaseEventIndexManager.js rename to src/indexing/BaseEventIndexManager.ts index f780c8e9ce..c40d1300ea 100644 --- a/src/indexing/BaseEventIndexManager.js +++ b/src/indexing/BaseEventIndexManager.ts @@ -1,5 +1,5 @@ /* -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. @@ -20,7 +20,7 @@ export interface MatrixEvent { content: {}; event_id: string; origin_server_ts: number; - unsigned: ?{}; + unsigned?: {}; room_id: string; } @@ -59,7 +59,7 @@ export interface SearchArgs { before_limit: number; after_limit: number; order_by_recency: boolean; - room_id: ?string; + room_id?: string; } export interface EventAndProfile { @@ -85,7 +85,7 @@ export interface IndexStats { * * Instances of this class are provided by the application. */ -export default class BaseEventIndexManager { +export default abstract class BaseEventIndexManager { /** * Does our EventIndexManager support event indexing. * @@ -119,7 +119,7 @@ export default class BaseEventIndexManager { * @return {Promise} A promise that will resolve when the was queued up for * addition. */ - async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> { + async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise { throw new Error("Unimplemented"); } @@ -188,7 +188,7 @@ export default class BaseEventIndexManager { events: [EventAndProfile], checkpoint: CrawlerCheckpoint | null, oldCheckpoint: CrawlerCheckpoint | null, - ): Promise { + ): Promise { throw new Error("Unimplemented"); } From 7b0e51a703ea8c4aa7420577dad0c1f9dbf11265 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 May 2020 18:11:21 +0100 Subject: [PATCH 228/241] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index 3f11f25d62..f7e32584c3 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -29,8 +29,8 @@ import {ActionPayload} from "./dispatcher/payloads"; * Instances of this class are provided by the application. */ export default abstract class BasePlatform { - protected notificationCount: number = 0; - protected errorDidOccur: boolean = false; + protected notificationCount = 0; + protected errorDidOccur = false; constructor() { dis.register(this.onAction); From 3ae38714c10de32f0064550e067401566bc3c728 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 21 May 2020 18:25:52 +0100 Subject: [PATCH 229/241] make ts happy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index f7e32584c3..d4a6c34daf 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -85,7 +85,8 @@ export default abstract class BasePlatform { abstract displayNotification(title: string, msg: string, avatarUrl: string, room: Object); - abstract loudNotification(ev: Event, room: Object); + loudNotification(ev: Event, room: Object) { + }; /** * Returns a promise that resolves to a string representing the current version of the application. From 559dd98d01c0192f9a3d5c7ee56d77b9324d0ac6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:53:16 -0600 Subject: [PATCH 230/241] Fix comment style to be less bothersome --- src/components/views/rooms/RoomList2.tsx | 3 +-- src/components/views/rooms/RoomSublist2.tsx | 3 +-- src/components/views/rooms/RoomTile2.tsx | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index a5d0175f01..d0c147c953 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -35,8 +35,7 @@ import { ActionPayload } from "../../../dispatcher/payloads"; * 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 { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 4c3f65b323..e2f489b959 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -35,8 +35,7 @@ import RoomTile2 from "./RoomTile2"; * 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 { forRooms: boolean; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 53e56d7a1e..42b65cba87 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -37,8 +37,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils"; * 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 { room: Room; From a11985f239831290c4da4507c46535ea74f06838 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:54:38 -0600 Subject: [PATCH 231/241] Which component? The room list! --- src/stores/room-list/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index 020108878b..82a6e841db 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -106,7 +106,7 @@ put the sticky room in a position where it's had to decrease N will not increase ## Responsibilities of the store -The store is responsible for the ordering, upkeep, and tracking of all rooms. The component simply gets +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets an object containing the tags it needs to worry about and the rooms within. The room list component will decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering. From e3c0b4711686231c4704882dfbfbe72000911c05 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:56:04 -0600 Subject: [PATCH 232/241] Hyphenize algorithm directories --- src/stores/room-list/RoomListStore2.ts | 4 ++-- .../algorithms/{list_ordering => list-ordering}/Algorithm.ts | 0 .../{list_ordering => list-ordering}/ImportanceAlgorithm.ts | 2 +- .../{list_ordering => list-ordering}/NaturalAlgorithm.ts | 2 +- .../algorithms/{list_ordering => list-ordering}/index.ts | 0 .../{tag_sorting => tag-sorting}/AlphabeticAlgorithm.ts | 0 .../algorithms/{tag_sorting => tag-sorting}/IAlgorithm.ts | 0 .../{tag_sorting => tag-sorting}/ManualAlgorithm.ts | 0 .../{tag_sorting => tag-sorting}/RecentAlgorithm.ts | 0 .../algorithms/{tag_sorting => tag-sorting}/index.ts | 0 10 files changed, 4 insertions(+), 4 deletions(-) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/Algorithm.ts (100%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/ImportanceAlgorithm.ts (99%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/NaturalAlgorithm.ts (97%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/index.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/AlphabeticAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/IAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/ManualAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/RecentAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/index.ts (100%) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 4f38a25d95..881b8fd3cf 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -18,12 +18,12 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; -import { Algorithm } from "./algorithms/list_ordering/Algorithm"; +import { Algorithm } from "./algorithms/list-ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { getListAlgorithmInstance } from "./algorithms/list_ordering"; +import { getListAlgorithmInstance } from "./algorithms/list-ordering"; import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/list_ordering/Algorithm.ts rename to src/stores/room-list/algorithms/list-ordering/Algorithm.ts diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts similarity index 99% rename from src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index fd5d4c8163..c72cdc2e1c 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -19,7 +19,7 @@ import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; -import { sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; import * as Unread from '../../../../Unread'; /** diff --git a/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts similarity index 97% rename from src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts rename to src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 1265564352..44a501e592 100644 --- a/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -16,7 +16,7 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { ITagMap } from "../models"; -import { sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; /** * Uses the natural tag sorting algorithm order to determine tag ordering. No diff --git a/src/stores/room-list/algorithms/list_ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts similarity index 100% rename from src/stores/room-list/algorithms/list_ordering/index.ts rename to src/stores/room-list/algorithms/list-ordering/index.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag-sorting/index.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/index.ts rename to src/stores/room-list/algorithms/tag-sorting/index.ts From a425c5440b16c5d4d002a4e47115d93c15dcdc68 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 16:34:23 -0600 Subject: [PATCH 233/241] Fix visibility of message timestamps Fixes https://github.com/vector-im/riot-web/issues/13736 This also fixes an unreported but complained about issue regarding the 'always show timestamps' option not working. Looks like this regressed in https://github.com/matrix-org/matrix-react-sdk/pull/4531 when things got shuffled around. --- res/css/views/rooms/_EventTile.scss | 9 ++++++++- res/css/views/rooms/_GroupLayout.scss | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 4297bf809c..20652c6037 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -98,6 +98,7 @@ limitations under the License. .mx_EventTile .mx_MessageTimestamp { display: block; + visibility: hidden; white-space: nowrap; left: 0px; text-align: center; @@ -158,10 +159,16 @@ limitations under the License. } // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) +// The first set is to handle the 'group layout' (default) and the second for the IRC layout .mx_EventTile_last > div > a > .mx_MessageTimestamp, .mx_EventTile:hover > div > a > .mx_MessageTimestamp, .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, -.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp { +.mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, + +.mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, +.mx_IRCLayout .mx_EventTile.focus-visible:focus-within > a > .mx_MessageTimestamp { visibility: visible; } diff --git a/res/css/views/rooms/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss index 40440f7d49..928ea75a79 100644 --- a/res/css/views/rooms/_GroupLayout.scss +++ b/res/css/views/rooms/_GroupLayout.scss @@ -34,7 +34,6 @@ $left-gutter: 65px; } .mx_MessageTimestamp { - visibility: hidden; position: absolute; width: 46px; /* 8 + 30 (avatar) + 8 */ } From ef4d0a805194e627606d3306f3b64fb22e1d87d8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 May 2020 10:33:19 +0100 Subject: [PATCH 234/241] Make linter happy --- res/css/views/rooms/_EventTile.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 20652c6037..40a80f17bb 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -164,7 +164,6 @@ limitations under the License. .mx_EventTile:hover > div > a > .mx_MessageTimestamp, .mx_EventTile.mx_EventTile_actionBarFocused > div > a > .mx_MessageTimestamp, .mx_EventTile.focus-visible:focus-within > div > a > .mx_MessageTimestamp, - .mx_IRCLayout .mx_EventTile_last > a > .mx_MessageTimestamp, .mx_IRCLayout .mx_EventTile:hover > a > .mx_MessageTimestamp, .mx_IRCLayout .mx_EventTile.mx_EventTile_actionBarFocused > a > .mx_MessageTimestamp, From 6bfa31cd47d626fe10c9148f16557c6a2a86bedd Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 May 2020 10:34:00 +0100 Subject: [PATCH 235/241] run i18n which is unhappy about 'create room' for some reason --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f79d93b98f..96ccf1589d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1127,7 +1127,6 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", - "Create room": "Create room", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", From badda5ae815d77165a90c6f5804192c8e1321a3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 May 2020 15:40:26 +0100 Subject: [PATCH 236/241] Remove SSSS key upgrade check from rageshake This code doesn't exist anymore as the SSSS symmetric upgrade stuff has been removed. Fixes https://github.com/vector-im/riot-web/issues/13715 --- src/rageshake/submit-rageshake.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index e5027e0d37..9f9d7898cb 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -133,7 +133,6 @@ export default async function sendBugReport(bugReportEndpoint: string, opts: IOp body.append("cross_signing_supported_by_hs", String(await client.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing"))); body.append("cross_signing_ready", String(await client.isCrossSigningReady())); - body.append("ssss_key_needs_upgrade", String(await client.secretStorageKeyNeedsUpgrade())); } } From 0201655538d44c05e0135e6db92caf060ee166e7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 May 2020 12:26:27 +0100 Subject: [PATCH 237/241] Fix key backup restore with SSSS The room / session ID params come after the backupInfo for restoring from SSSS so the options object was being passed into the wrong param. Roll on TypeScript. This meant restoring backups worked fine when the key was cached but failed when it wasn't. Regressed in https://github.com/matrix-org/matrix-react-sdk/pull/4507 --- .../views/dialogs/keybackup/RestoreKeyBackupDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 7e51e76f6c..a16202ed93 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -201,7 +201,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { // `accessSecretStorage` may prompt for storage access as needed. const recoverInfo = await accessSecretStorage(async () => { return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( - this.state.backupInfo, + this.state.backupInfo, undefined, undefined, { progressCallback: this._progressCallback }, ); }); From ac0d794877f4f2d2634087cfa57eecf1044ca83d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 22 May 2020 13:49:56 +0100 Subject: [PATCH 238/241] Fix: Tag_DM is not defined Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 1e5002419d..e4290f87d9 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -44,6 +44,7 @@ import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTem import {DefaultTagID} from "../../../stores/room-list/models"; import * as Unread from "../../../Unread"; import RoomViewStore from "../../../stores/RoomViewStore"; +import {TAG_DM} from "../../../stores/RoomListStore"; const HIDE_CONFERENCE_CHANS = true; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; From 762918d664e4b5ab6ac2e7e36eabb0b26b75e6a6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 May 2020 14:00:29 +0100 Subject: [PATCH 239/241] Prepare changelog for v2.6.1 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7901062b89..e5515f1015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +Changes in [2.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.1) (2020-05-22) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0...v2.6.1) + + * Fix key backup restore with SSSS + [\#4617](https://github.com/matrix-org/matrix-react-sdk/pull/4617) + * Remove SSSS key upgrade check from rageshake + [\#4616](https://github.com/matrix-org/matrix-react-sdk/pull/4616) + Changes in [2.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.6.0) (2020-05-19) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.6.0-rc.1...v2.6.0) From 5a4da13e863ed554323e00fc1783eaaa776c923e Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 May 2020 14:00:29 +0100 Subject: [PATCH 240/241] v2.6.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28f3d1633c..6ceef02f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.6.0", + "version": "2.6.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From af59d3a09423153056864ebd16eb10626a7d2ab7 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Fri, 22 May 2020 14:14:09 +0100 Subject: [PATCH 241/241] Reset matrix-js-sdk back to develop branch --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 6cdc771c5b..a97c093b87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5760,7 +5760,7 @@ mathml-tag-names@^2.0.1: "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "6.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e3c6a0e1a08a3812ba988e60eb5a2a013bb27404" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/a4a7097c103da42075f2c70e070fd01fa6fb0d48" dependencies: "@babel/runtime" "^7.8.3" another-json "^0.2.0"