Merge pull request #5013 from matrix-org/travis/room-list/rm-old

[BREAKING] Remove the old room list
This commit is contained in:
Travis Ralston 2020-07-20 06:46:05 -06:00 committed by GitHub
commit 7996013cd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 864 additions and 6844 deletions

View file

@ -226,7 +226,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
} }
#mx_theme_tertiaryAccentColor { #mx_theme_tertiaryAccentColor {
color: $roomsublist-label-bg-color; color: $tertiary-accent-color;
} }
/* Expected z-indexes for dialogs: /* Expected z-indexes for dialogs:

View file

@ -12,7 +12,6 @@
@import "./structures/_HeaderButtons.scss"; @import "./structures/_HeaderButtons.scss";
@import "./structures/_HomePage.scss"; @import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.scss"; @import "./structures/_LeftPanel.scss";
@import "./structures/_LeftPanel2.scss";
@import "./structures/_MainSplit.scss"; @import "./structures/_MainSplit.scss";
@import "./structures/_MatrixChat.scss"; @import "./structures/_MatrixChat.scss";
@import "./structures/_MyGroups.scss"; @import "./structures/_MyGroups.scss";
@ -21,14 +20,12 @@
@import "./structures/_RoomDirectory.scss"; @import "./structures/_RoomDirectory.scss";
@import "./structures/_RoomSearch.scss"; @import "./structures/_RoomSearch.scss";
@import "./structures/_RoomStatusBar.scss"; @import "./structures/_RoomStatusBar.scss";
@import "./structures/_RoomSubList.scss";
@import "./structures/_RoomView.scss"; @import "./structures/_RoomView.scss";
@import "./structures/_ScrollPanel.scss"; @import "./structures/_ScrollPanel.scss";
@import "./structures/_SearchBox.scss"; @import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss"; @import "./structures/_TabbedView.scss";
@import "./structures/_TagPanel.scss"; @import "./structures/_TagPanel.scss";
@import "./structures/_ToastContainer.scss"; @import "./structures/_ToastContainer.scss";
@import "./structures/_TopLeftMenuButton.scss";
@import "./structures/_UploadBar.scss"; @import "./structures/_UploadBar.scss";
@import "./structures/_UserMenu.scss"; @import "./structures/_UserMenu.scss";
@import "./structures/_ViewSource.scss"; @import "./structures/_ViewSource.scss";
@ -167,7 +164,6 @@
@import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_InviteOnlyIcon.scss";
@import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_JumpToBottomButton.scss";
@import "./views/rooms/_LinkPreviewWidget.scss"; @import "./views/rooms/_LinkPreviewWidget.scss";
@import "./views/rooms/_MemberInfo.scss"; @import "./views/rooms/_MemberInfo.scss";
@ -180,23 +176,18 @@
@import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_PresenceLabel.scss";
@import "./views/rooms/_ReplyPreview.scss"; @import "./views/rooms/_ReplyPreview.scss";
@import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss";
@import "./views/rooms/_RoomBreadcrumbs2.scss";
@import "./views/rooms/_RoomDropTarget.scss";
@import "./views/rooms/_RoomHeader.scss"; @import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss"; @import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomList2.scss";
@import "./views/rooms/_RoomPreviewBar.scss"; @import "./views/rooms/_RoomPreviewBar.scss";
@import "./views/rooms/_RoomRecoveryReminder.scss"; @import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist2.scss"; @import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomTile2.scss";
@import "./views/rooms/_RoomTileIcon.scss"; @import "./views/rooms/_RoomTileIcon.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss";
@import "./views/rooms/_SearchBar.scss"; @import "./views/rooms/_SearchBar.scss";
@import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_SendMessageComposer.scss";
@import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_Stickers.scss";
@import "./views/rooms/_TopUnreadMessagesBar.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss";
@import "./views/rooms/_UserOnlineDot.scss";
@import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/rooms/_WhoIsTypingTile.scss";
@import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_AvatarSetting.scss";
@import "./views/settings/_CrossSigningPanel.scss"; @import "./views/settings/_CrossSigningPanel.scss";
@ -227,6 +218,4 @@
@import "./views/verification/_VerificationShowSas.scss"; @import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_CallView2.scss";
@import "./views/voip/_IncomingCallbox.scss";
@import "./views/voip/_VideoView.scss"; @import "./views/voip/_VideoView.scss";

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,164 +14,182 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_LeftPanel_container { $tagPanelWidth: 56px; // only applies in this file, used for calculations
display: flex;
/* LeftPanel 260px */
min-width: 260px;
max-width: 50%;
flex: 0 0 auto;
}
.mx_LeftPanel_container.collapsed {
min-width: unset;
/* Collapsed LeftPanel 50px */
flex: 0 0 50px;
}
.mx_LeftPanel_container.collapsed.mx_LeftPanel_container_hasTagPanel {
/* TagPanel 70px + Collapsed LeftPanel 50px */
flex: 0 0 120px;
}
.mx_LeftPanel_tagPanelContainer {
flex: 0 0 70px;
height: 100%;
}
.mx_LeftPanel_hideButton {
position: absolute;
top: 10px;
right: 0px;
padding: 8px;
cursor: pointer;
}
.mx_LeftPanel { .mx_LeftPanel {
flex: 1; background-color: $roomlist-bg-color;
overflow-x: hidden; min-width: 260px;
max-width: 50%;
// Create a row-based flexbox for the TagPanel and the room list
display: flex;
.mx_LeftPanel_tagPanelContainer {
flex-grow: 0;
flex-shrink: 0;
flex-basis: $tagPanelWidth;
height: 100%;
// Create another flexbox so the TagPanel fills the container
display: flex;
// TagPanel handles its own CSS
}
&:not(.mx_LeftPanel_hasTagPanel) {
.mx_LeftPanel_roomListContainer {
width: 100%;
}
}
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel_roomListContainer {
width: calc(100% - $tagPanelWidth);
background-color: $roomlist-bg-color;
// Create another flexbox (this time a column) for the room list components
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 0;
}
.mx_LeftPanel .mx_AppTile_mini { .mx_LeftPanel_userHeader {
height: 132px; /* 12px top, 12px sides, 20px bottom (using 13px bottom to account
} * for internal whitespace in the breadcrumbs)
*/
padding: 12px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
.mx_LeftPanel .mx_RoomList_scrollbar { // Create another flexbox column for the rows to stack within
order: 1;
flex: 1 1 0;
overflow-y: auto;
z-index: 6;
}
.mx_LeftPanel .mx_BottomLeftMenu {
order: 3;
border-top: 1px solid $panel-divider-color;
margin-left: 16px; /* gutter */
margin-right: 16px; /* gutter */
flex: 0 0 60px;
z-index: 1;
}
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
flex: 0 0 160px;
margin-bottom: 9px;
}
.mx_LeftPanel .mx_BottomLeftMenu_options {
margin-top: 18px;
}
.mx_BottomLeftMenu_options object {
pointer-events: none;
}
.mx_BottomLeftMenu_options > div {
display: inline-block;
}
.mx_BottomLeftMenu_options .mx_RoleButton {
margin-left: 0px;
margin-right: 10px;
height: 30px;
}
.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings {
float: right;
}
.mx_BottomLeftMenu_options .mx_BottomLeftMenu_settings .mx_RoleButton {
margin-right: 0px;
}
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu_settings {
float: none;
}
.mx_MatrixChat_useCompactLayout {
.mx_LeftPanel .mx_BottomLeftMenu {
flex: 0 0 50px;
}
.mx_LeftPanel_container.collapsed .mx_BottomLeftMenu {
flex: 0 0 160px;
}
.mx_LeftPanel .mx_BottomLeftMenu_options {
margin-top: 12px;
}
}
.mx_LeftPanel_exploreAndFilterRow {
display: flex; display: flex;
flex-direction: column;
}
.mx_SearchBox { .mx_LeftPanel_breadcrumbsContainer {
flex: 1 1 0; overflow-y: hidden;
min-width: 0; overflow-x: scroll;
margin: 4px 9px 1px 9px; margin: 12px 12px 0 12px;
flex: 0 0 auto;
// Create yet another flexbox, this time within the row, to ensure items stay
// aligned correctly. This is also a row-based flexbox.
display: flex;
align-items: center;
&.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 5%);
}
&.mx_IndicatorScrollbar_rightOverflow {
mask-image: linear-gradient(90deg, black, black 95%, transparent);
}
&.mx_IndicatorScrollbar_rightOverflow.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 5%, black 95%, transparent);
} }
} }
.mx_LeftPanel_explore { .mx_LeftPanel_filterContainer {
flex: 0 0 50%; margin-left: 12px;
overflow: hidden; margin-right: 12px;
transition: flex-basis 0.2s;
box-sizing: border-box;
&.mx_LeftPanel_explore_hidden { flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create a flexbox to organize the inputs
display: flex;
align-items: center;
.mx_RoomSearch_expanded + .mx_LeftPanel_exploreButton {
// Cheaty way to return the occupied space to the filter input
flex-basis: 0; flex-basis: 0;
margin: 0;
width: 0;
// Don't forget to hide the masked ::before icon,
// using display:none or visibility:hidden would break accessibility
&::before {
content: none;
}
} }
.mx_AccessibleButton { .mx_LeftPanel_exploreButton {
font-size: $font-14px; width: 28px;
margin: 4px 0 1px 9px; height: 28px;
padding: 9px; border-radius: 20px;
padding-left: 42px; background-color: $roomlist-button-bg-color;
font-weight: 600;
color: $notice-secondary-color;
position: relative; position: relative;
border-radius: 4px; margin-left: 8px;
&:hover {
background-color: $primary-bg-color;
}
&::before { &::before {
cursor: pointer; content: '';
mask: url('$(res)/img/explore.svg'); position: absolute;
mask-repeat: no-repeat; top: 6px;
mask-position: center center; left: 6px;
content: "";
left: 14px;
top: 10px;
width: 16px; width: 16px;
height: 16px; height: 16px;
background-color: $notice-secondary-color; mask-image: url('$(res)/img/feather-customised/compass.svg');
position: absolute; mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
}
.mx_LeftPanel_roomListWrapper {
// Create a flexbox to ensure the containing items cause appropriate overflow.
display: flex;
flex-grow: 1;
overflow: hidden;
min-height: 0;
margin-top: 10px; // so we're not up against the search/filter
&.mx_LeftPanel_roomListWrapper_stickyBottom {
padding-bottom: 32px;
}
&.mx_LeftPanel_roomListWrapper_stickyTop {
padding-top: 32px;
}
}
.mx_LeftPanel_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
width: 100%;
max-width: 100%;
position: relative; // for sticky headers
// Create a flexbox to trick the layout engine
display: flex;
}
}
// These styles override the defaults for the minimized (66px) layout
&.mx_LeftPanel_minimized {
min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute.
&.mx_LeftPanel_hasTagPanel {
width: calc(68px + $tagPanelWidth) !important;
}
&:not(.mx_LeftPanel_hasTagPanel) {
width: 68px !important;
}
.mx_LeftPanel_roomListContainer {
width: 68px;
.mx_LeftPanel_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;
justify-content: center;
.mx_LeftPanel_exploreButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
}
} }
} }
} }

View file

@ -1,197 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
$tagPanelWidth: 56px; // only applies in this file, used for calculations
.mx_LeftPanel2 {
background-color: $roomlist2-bg-color;
min-width: 260px;
max-width: 50%;
// Create a row-based flexbox for the TagPanel and the room list
display: flex;
.mx_LeftPanel2_tagPanelContainer {
flex-grow: 0;
flex-shrink: 0;
flex-basis: $tagPanelWidth;
height: 100%;
// Create another flexbox so the TagPanel fills the container
display: flex;
// TagPanel handles its own CSS
}
&:not(.mx_LeftPanel2_hasTagPanel) {
.mx_LeftPanel2_roomListContainer {
width: 100%;
}
}
// Note: The 'room list' in this context is actually everything that isn't the tag
// panel, such as the menu options, breadcrumbs, filtering, etc
.mx_LeftPanel2_roomListContainer {
width: calc(100% - $tagPanelWidth);
background-color: $roomlist2-bg-color;
// Create another flexbox (this time a column) for the room list components
display: flex;
flex-direction: column;
.mx_LeftPanel2_userHeader {
/* 12px top, 12px sides, 20px bottom (using 13px bottom to account
* for internal whitespace in the breadcrumbs)
*/
padding: 12px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create another flexbox column for the rows to stack within
display: flex;
flex-direction: column;
}
.mx_LeftPanel2_breadcrumbsContainer {
overflow-y: hidden;
overflow-x: scroll;
margin: 12px 12px 0 12px;
flex: 0 0 auto;
// Create yet another flexbox, this time within the row, to ensure items stay
// aligned correctly. This is also a row-based flexbox.
display: flex;
align-items: center;
&.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 5%);
}
&.mx_IndicatorScrollbar_rightOverflow {
mask-image: linear-gradient(90deg, black, black 95%, transparent);
}
&.mx_IndicatorScrollbar_rightOverflow.mx_IndicatorScrollbar_leftOverflow {
mask-image: linear-gradient(90deg, transparent, black 5%, black 95%, transparent);
}
}
.mx_LeftPanel2_filterContainer {
margin-left: 12px;
margin-right: 12px;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
// Create a flexbox to organize the inputs
display: flex;
align-items: center;
.mx_RoomSearch_expanded + .mx_LeftPanel2_exploreButton {
// Cheaty way to return the occupied space to the filter input
flex-basis: 0;
margin: 0;
width: 0;
// Don't forget to hide the masked ::before icon,
// using display:none or visibility:hidden would break accessibility
&::before {
content: none;
}
}
.mx_LeftPanel2_exploreButton {
width: 28px;
height: 28px;
border-radius: 20px;
background-color: $roomlist2-button-bg-color;
position: relative;
margin-left: 8px;
&::before {
content: '';
position: absolute;
top: 6px;
left: 6px;
width: 16px;
height: 16px;
mask-image: url('$(res)/img/feather-customised/compass.svg');
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
}
.mx_LeftPanel2_roomListWrapper {
// Create a flexbox to ensure the containing items cause appropriate overflow.
display: flex;
flex-grow: 1;
overflow: hidden;
min-height: 0;
margin-top: 10px; // so we're not up against the search/filter
&.mx_LeftPanel2_roomListWrapper_stickyBottom {
padding-bottom: 32px;
}
&.mx_LeftPanel2_roomListWrapper_stickyTop {
padding-top: 32px;
}
}
.mx_LeftPanel2_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
width: 100%;
max-width: 100%;
position: relative; // for sticky headers
// Create a flexbox to trick the layout engine
display: flex;
}
}
// These styles override the defaults for the minimized (66px) layout
&.mx_LeftPanel2_minimized {
min-width: unset;
// We have to forcefully set the width to override the resizer's style attribute.
&.mx_LeftPanel2_hasTagPanel {
width: calc(68px + $tagPanelWidth) !important;
}
&:not(.mx_LeftPanel2_hasTagPanel) {
width: 68px !important;
}
.mx_LeftPanel2_roomListContainer {
width: 68px;
.mx_LeftPanel2_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;
justify-content: center;
.mx_LeftPanel2_exploreButton {
margin-left: 0;
margin-top: 8px;
background-color: transparent;
}
}
}
}
}

View file

@ -66,7 +66,7 @@ limitations under the License.
} }
/* not the left panel, and not the resize handle, so the roomview/groupview/... */ /* not the left panel, and not the resize handle, so the roomview/groupview/... */
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_LeftPanel2):not(.mx_ResizeHandle) { .mx_MatrixChat > :not(.mx_LeftPanel):not(.mx_ResizeHandle) {
background-color: $primary-bg-color; background-color: $primary-bg-color;
flex: 1 1 0; flex: 1 1 0;

View file

@ -18,7 +18,7 @@ limitations under the License.
.mx_RoomSearch { .mx_RoomSearch {
flex: 1; flex: 1;
border-radius: 20px; border-radius: 20px;
background-color: $roomlist2-button-bg-color; background-color: $roomlist-button-bg-color;
height: 28px; height: 28px;
padding: 2px; padding: 2px;

View file

@ -1,187 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
/* a word of explanation about the flex-shrink values employed here:
there are 3 priotized categories of screen real-estate grabbing,
each with a flex-shrink difference of 4 order of magnitude,
so they ideally wouldn't affect each other.
lowest category: .mx_RoomSubList
flex-shrink: 10000000
distribute size of items within the same category by their size
middle category: .mx_RoomSubList.resized-sized
flex-shrink: 1000
applied when using the resizer, will have a max-height set to it,
to limit the size
highest category: .mx_RoomSubList.resized-all
flex-shrink: 1
small flex-shrink value (1), is only added if you can drag the resizer so far
so in practice you can only assign this category if there is enough space.
*/
.mx_RoomSubList {
display: flex;
flex-direction: column;
}
.mx_RoomSubList_nonEmpty .mx_AutoHideScrollbar_offset {
padding-bottom: 4px;
}
.mx_RoomSubList_labelContainer {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 0 auto;
margin: 0 8px;
padding: 0 8px;
height: 36px;
}
.mx_RoomSubList_labelContainer.focus-visible:focus-within {
background-color: $roomtile-focused-bg-color;
}
.mx_RoomSubList_label {
flex: 1;
cursor: pointer;
display: flex;
align-items: center;
padding: 0 6px;
}
.mx_RoomSubList_label > span {
flex: 1 1 auto;
text-transform: uppercase;
color: $roomsublist-label-fg-color;
font-weight: 700;
font-size: $font-12px;
margin-left: 8px;
}
.mx_RoomSubList_badge > div {
flex: 0 0 auto;
border-radius: $font-16px;
font-weight: 600;
font-size: $font-12px;
padding: 0 5px;
color: $roomtile-badge-fg-color;
background-color: $roomtile-name-color;
cursor: pointer;
}
.mx_RoomSubList_addRoom, .mx_RoomSubList_badge {
margin-left: 7px;
}
.mx_RoomSubList_addRoom {
background-color: $roomheader-addroom-bg-color;
border-radius: 10px; // 16/2 + 2 padding
height: 16px;
flex: 0 0 16px;
position: relative;
&::before {
background-color: $roomheader-addroom-fg-color;
mask: url('$(res)/img/icons-room-add.svg');
mask-repeat: no-repeat;
mask-position: center;
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
.mx_RoomSubList_badgeHighlight > div {
color: $accent-fg-color;
background-color: $warning-color;
}
.mx_RoomSubList_chevron {
pointer-events: none;
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
mask-repeat: no-repeat;
transition: transform 0.2s ease-in;
width: 10px;
height: 6px;
margin-left: 2px;
background-color: $roomsublist-label-fg-color;
}
.mx_RoomSubList_chevronDown {
transform: rotateZ(0deg);
}
.mx_RoomSubList_chevronUp {
transform: rotateZ(180deg);
}
.mx_RoomSubList_chevronRight {
transform: rotateZ(-90deg);
}
.mx_RoomSubList_scroll {
/* let rooms list grab as much space as it needs (auto),
potentially overflowing and showing a scrollbar */
flex: 0 1 auto;
padding: 0 8px;
}
.collapsed {
.mx_RoomSubList_scroll {
padding: 0;
}
.mx_RoomSubList_labelContainer {
margin-right: 8px;
margin-left: 2px;
padding: 0;
}
.mx_RoomSubList_addRoom {
margin-left: 3px;
margin-right: 10px;
}
.mx_RoomSubList_label > span {
display: none;
}
}
// overflow indicators
.mx_RoomSubList:not(.resized-all) > .mx_RoomSubList_scroll {
&.mx_IndicatorScrollbar_topOverflow::before {
position: sticky;
content: "";
top: 0;
left: 0;
right: 0;
height: 8px;
z-index: 100;
display: block;
pointer-events: none;
transition: background-image 0.1s ease-in;
background: linear-gradient(to top, $panel-gradient);
}
&.mx_IndicatorScrollbar_topOverflow {
margin-top: -8px;
}
}

View file

@ -157,7 +157,7 @@ limitations under the License.
font-weight: 600; font-weight: 600;
font-size: $font-14px; font-size: $font-14px;
padding: 0 5px; padding: 0 5px;
background-color: $roomtile-name-color; background-color: $muted-fg-color;
} }
.mx_TagTile_badgeHighlight { .mx_TagTile_badgeHighlight {

View file

@ -1,49 +0,0 @@
/*
Copyright 2018 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.
*/
.mx_TopLeftMenuButton {
flex: 0 0 52px;
border-bottom: 1px solid $panel-divider-color;
color: $topleftmenu-color;
background-color: $primary-bg-color;
display: flex;
align-items: center;
min-width: 0;
padding: 0 4px;
overflow: hidden;
}
.mx_TopLeftMenuButton .mx_BaseAvatar {
margin: 0 7px;
}
.mx_TopLeftMenuButton_name {
margin: 0 7px;
font-size: $font-18px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-weight: 600;
}
.mx_TopLeftMenuButton_chevron {
margin: 0 7px;
mask: url('$(res)/img/feather-customised/dropdown-arrow.svg');
mask-repeat: no-repeat;
width: $font-22px;
height: 6px;
background-color: $roomsublist-label-fg-color;
}

View file

@ -24,7 +24,7 @@ limitations under the License.
right: 0; right: 0;
} }
.mx_NotificationBadge, .mx_RoomTile2_badgeContainer { .mx_NotificationBadge, .mx_RoomTile_badgeContainer {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;

View file

@ -1,58 +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.
*/
@define-mixin mx_InviteOnlyIcon {
width: 12px;
height: 12px;
position: relative;
display: block !important;
}
@define-mixin mx_InviteOnlyIcon_padlock {
background-color: $roomtile-name-color;
mask-image: url("$(res)/img/feather-customised/lock-solid.svg");
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.mx_InviteOnlyIcon_large {
@mixin mx_InviteOnlyIcon;
margin: 0 4px;
&::before {
@mixin mx_InviteOnlyIcon_padlock;
width: 12px;
height: 12px;
}
}
.mx_InviteOnlyIcon_small {
@mixin mx_InviteOnlyIcon;
left: -2px;
&::before {
@mixin mx_InviteOnlyIcon_padlock;
width: 10px;
height: 10px;
}
}

View file

@ -41,8 +41,8 @@ limitations under the License.
// with text-align in parent // with text-align in parent
display: inline-block; display: inline-block;
padding: 0 4px; padding: 0 4px;
color: $roomtile-badge-fg-color; color: $accent-fg-color;
background-color: $roomtile-name-color; background-color: $muted-fg-color;
} }
.mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge { .mx_JumpToBottomButton_highlight .mx_JumpToBottomButton_badge {
@ -56,7 +56,7 @@ limitations under the License.
border-radius: 19px; border-radius: 19px;
box-sizing: border-box; box-sizing: border-box;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
cursor: pointer; cursor: pointer;
} }
@ -70,5 +70,5 @@ limitations under the License.
mask: url('$(res)/img/icon-jump-to-bottom.svg'); mask: url('$(res)/img/icon-jump-to-bottom.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 9px 14px; mask-position: 9px 14px;
background: $roomtile-name-color; background: $muted-fg-color;
} }

View file

@ -27,7 +27,7 @@ limitations under the License.
// ^- The count is an element floating within that. // ^- The count is an element floating within that.
&.mx_NotificationBadge_visible { &.mx_NotificationBadge_visible {
background-color: $roomtile2-default-badge-bg-color; background-color: $roomtile-default-badge-bg-color;
// Create a flexbox to order the count a bit easier // Create a flexbox to order the count a bit easier
display: flex; display: flex;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,98 +15,42 @@ limitations under the License.
*/ */
.mx_RoomBreadcrumbs { .mx_RoomBreadcrumbs {
position: relative; width: 100%;
height: 42px;
padding: 8px; // Create a flexbox for the crumbs
padding-bottom: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: flex-start;
// repeating circles as empty placeholders
background:
radial-gradient(
circle at center,
$breadcrumb-placeholder-bg-color,
$breadcrumb-placeholder-bg-color 15px,
transparent 16px
);
background-size: 36px;
background-position: 6px -1px;
background-repeat: repeat-x;
// Autohide the scrollbar
overflow-x: hidden;
&:hover {
overflow-x: visible;
}
.mx_AutoHideScrollbar {
display: flex;
flex-direction: row;
height: 100%;
}
.mx_RoomBreadcrumbs_crumb { .mx_RoomBreadcrumbs_crumb {
margin-left: 4px; margin-right: 8px;
height: 32px;
display: inline-block;
transition: transform 0.3s, width 0.3s;
position: relative;
.mx_RoomTile_badge {
position: absolute;
top: -3px;
right: -4px;
}
.mx_RoomBreadcrumbs_dmIndicator {
position: absolute;
bottom: 0;
right: -4px;
}
}
.mx_RoomBreadcrumbs_animate {
margin-left: 0;
width: 32px; width: 32px;
transform: scale(1);
} }
.mx_RoomBreadcrumbs_preAnimate { // These classes come from the CSSTransition component. There's many more classes we
width: 0; // could care about, but this is all we worried about for now. The animation works by
transform: scale(0); // first triggering the enter state with the newest breadcrumb off screen (-40px) then
// sliding it into view.
&.mx_RoomBreadcrumbs-enter {
margin-left: -40px; // 32px for the avatar, 8px for the margin
}
&.mx_RoomBreadcrumbs-enter-active {
margin-left: 0;
// Timing function is as-requested by design.
// NOTE: The transition time MUST match the value passed to CSSTransition!
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
} }
.mx_RoomBreadcrumbs_left { .mx_RoomBreadcrumbs_placeholder {
opacity: 0.5; font-weight: 600;
font-size: $font-14px;
line-height: 32px; // specifically to match the height this is not scaled
height: 32px;
}
} }
// Note: we have to manually control the gradient and stuff, but the IndicatorScrollbar .mx_RoomBreadcrumbs_Tooltip {
// will deal with left/right positioning for us. Normally we'd use position:sticky on margin-left: -42px;
// a few key elements, however that doesn't work in horizontal scrolling scenarios. margin-top: -42px;
.mx_IndicatorScrollbar_leftOverflowIndicator,
.mx_IndicatorScrollbar_rightOverflowIndicator {
display: none;
}
.mx_IndicatorScrollbar_leftOverflowIndicator {
background: linear-gradient(to left, $panel-gradient);
}
.mx_IndicatorScrollbar_rightOverflowIndicator {
background: linear-gradient(to right, $panel-gradient);
}
&.mx_IndicatorScrollbar_leftOverflow .mx_IndicatorScrollbar_leftOverflowIndicator,
&.mx_IndicatorScrollbar_rightOverflow .mx_IndicatorScrollbar_rightOverflowIndicator {
position: absolute;
top: 0;
bottom: 0;
width: 15px;
display: block;
pointer-events: none;
z-index: 100;
}
} }

View file

@ -1,58 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
.mx_RoomBreadcrumbs2 {
width: 100%;
// Create a flexbox for the crumbs
display: flex;
flex-direction: row;
align-items: flex-start;
.mx_RoomBreadcrumbs2_crumb {
margin-right: 8px;
width: 32px;
}
// These classes come from the CSSTransition component. There's many more classes we
// could care about, but this is all we worried about for now. The animation works by
// first triggering the enter state with the newest breadcrumb off screen (-40px) then
// sliding it into view.
&.mx_RoomBreadcrumbs2-enter {
margin-left: -40px; // 32px for the avatar, 8px for the margin
}
&.mx_RoomBreadcrumbs2-enter-active {
margin-left: 0;
// Timing function is as-requested by design.
// NOTE: The transition time MUST match the value passed to CSSTransition!
transition: margin-left 640ms cubic-bezier(0.66, 0.02, 0.36, 1);
}
.mx_RoomBreadcrumbs2_placeholder {
font-weight: 600;
font-size: $font-14px;
line-height: 32px; // specifically to match the height this is not scaled
height: 32px;
}
}
.mx_RoomBreadcrumbs2_Tooltip {
margin-left: -42px;
margin-top: -42px;
}

View file

@ -1,55 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
.mx_RoomDropTarget_container {
background-color: $secondary-accent-color;
padding-left: 18px;
padding-right: 18px;
padding-top: 8px;
padding-bottom: 7px;
}
.collapsed .mx_RoomDropTarget_container {
padding-right: 10px;
padding-left: 10px;
}
.mx_RoomDropTarget {
font-size: $font-13px;
padding-top: 5px;
padding-bottom: 5px;
border: 1px dashed $accent-color;
color: $primary-fg-color;
background-color: $droptarget-bg-color;
border-radius: 4px;
}
.mx_RoomDropTarget_label {
position: relative;
margin-top: 3px;
line-height: $font-21px;
z-index: 1;
text-align: center;
}
.collapsed .mx_RoomDropTarget_avatar {
float: none;
}
.collapsed .mx_RoomDropTarget_label {
display: none;
}

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,56 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_RoomList.mx_RoomList2 {
overflow-y: auto;
}
.mx_RoomList { .mx_RoomList {
/* take up remaining space below TopLeftMenu */ width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
flex: 1;
min-height: 0;
overflow-y: hidden;
}
.mx_RoomList .mx_ResizeHandle { // Create a column-based flexbox for the sublists. That's pretty much all we have to
// needed so the z-index takes effect // worry about in this stylesheet.
position: relative; display: flex;
} flex-direction: column;
flex-wrap: nowrap; // let the column overflow
/* hide resize handles next to collapsed / empty sublists */
.mx_RoomList .mx_RoomSubList:not(.mx_RoomSubList_nonEmpty) + .mx_ResizeHandle {
display: none;
}
.mx_RoomList_expandButton {
margin-left: 8px;
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
}
.mx_RoomList_emptySubListTip_container {
padding-left: 18px;
padding-right: 18px;
padding-top: 8px;
padding-bottom: 7px;
}
.mx_RoomList_emptySubListTip {
font-size: $font-13px;
padding: 5px;
border: 1px dashed $accent-color;
color: $primary-fg-color;
background-color: $droptarget-bg-color;
border-radius: 4px;
line-height: $font-16px;
}
.mx_RoomList_emptySubListTip .mx_RoleButton {
vertical-align: -2px;
}
.mx_RoomList_headerButtons {
position: absolute;
right: 60px;
} }

View file

@ -1,27 +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.
*/
// TODO: Rename to mx_RoomList during replacement of old component
.mx_RoomList2 {
width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
// Create a column-based flexbox for the sublists. That's pretty much all we have to
// worry about in this stylesheet.
display: flex;
flex-direction: column;
flex-wrap: nowrap; // let the column overflow
}

View file

@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367 .mx_RoomSublist {
.mx_RoomSublist2 {
// The sublist is a column of rows, essentially // The sublist is a column of rows, essentially
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -27,7 +25,7 @@ limitations under the License.
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
.mx_RoomSublist2_headerContainer { .mx_RoomSublist_headerContainer {
// Create a flexbox to make alignment easy // Create a flexbox to make alignment easy
display: flex; display: flex;
align-items: center; align-items: center;
@ -43,13 +41,13 @@ limitations under the License.
// all works by ensuring the header text has a fixed height when sticky so the // all works by ensuring the header text has a fixed height when sticky so the
// fixed height of the container can maintain the scroll position. // fixed height of the container can maintain the scroll position.
// The combined height must be set in the LeftPanel2 component for sticky headers // The combined height must be set in the LeftPanel component for sticky headers
// to work correctly. // to work correctly.
padding-bottom: 8px; padding-bottom: 8px;
height: 24px; height: 24px;
color: $roomlist2-header-color; color: $roomlist-header-color;
.mx_RoomSublist2_stickable { .mx_RoomSublist_stickable {
flex: 1; flex: 1;
max-width: 100%; max-width: 100%;
@ -61,26 +59,26 @@ limitations under the License.
// to identify when a header is sticky. If we didn't have a consistent sticky class, // to identify when a header is sticky. If we didn't have a consistent sticky class,
// we'd have to do the "is sticky" checks again on click, as clicking the header // we'd have to do the "is sticky" checks again on click, as clicking the header
// when sticky scrolls instead of collapses the list. // when sticky scrolls instead of collapses the list.
&.mx_RoomSublist2_headerContainer_sticky { &.mx_RoomSublist_headerContainer_sticky {
position: fixed; position: fixed;
height: 32px; // to match the header container height: 32px; // to match the header container
// width set by JS // width set by JS
width: calc(100% - 22px); width: calc(100% - 22px);
} }
&.mx_RoomSublist2_headerContainer_stickyBottom { &.mx_RoomSublist_headerContainer_stickyBottom {
bottom: 0; bottom: 0;
} }
// We don't have a top style because the top is dependent on the room list header's // We don't have a top style because the top is dependent on the room list header's
// height, and is therefore calculated in JS. // height, and is therefore calculated in JS.
// The class, mx_RoomSublist2_headerContainer_stickyTop, is applied though. // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
} }
// Sticky Headers End // Sticky Headers End
// *************************** // ***************************
.mx_RoomSublist2_badgeContainer { .mx_RoomSublist_badgeContainer {
// Create another flexbox row because it's super easy to position the badge this way. // Create another flexbox row because it's super easy to position the badge this way.
display: flex; display: flex;
align-items: center; align-items: center;
@ -93,14 +91,14 @@ limitations under the License.
} }
} }
&:not(.mx_RoomSublist2_headerContainer_withAux) { &:not(.mx_RoomSublist_headerContainer_withAux) {
.mx_NotificationBadge { .mx_NotificationBadge {
margin-right: 4px; // just to push it over a bit, aligning it with the other elements margin-right: 4px; // just to push it over a bit, aligning it with the other elements
} }
} }
.mx_RoomSublist2_auxButton, .mx_RoomSublist_auxButton,
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
margin-left: 8px; // should be the same as the notification badge margin-left: 8px; // should be the same as the notification badge
position: relative; position: relative;
width: 24px; width: 24px;
@ -122,21 +120,21 @@ limitations under the License.
} }
// Hide the menu button by default // Hide the menu button by default
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: hidden; visibility: hidden;
width: 0; width: 0;
margin: 0; margin: 0;
} }
.mx_RoomSublist2_auxButton::before { .mx_RoomSublist_auxButton::before {
mask-image: url('$(res)/img/feather-customised/plus.svg'); mask-image: url('$(res)/img/feather-customised/plus.svg');
} }
.mx_RoomSublist2_menuButton::before { .mx_RoomSublist_menuButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg'); mask-image: url('$(res)/img/element-icons/context-menu.svg');
} }
.mx_RoomSublist2_headerText { .mx_RoomSublist_headerText {
flex: 1; flex: 1;
max-width: calc(100% - 16px); // 16px is the badge width max-width: calc(100% - 16px); // 16px is the badge width
line-height: $font-16px; line-height: $font-16px;
@ -148,7 +146,7 @@ limitations under the License.
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
.mx_RoomSublist2_collapseBtn { .mx_RoomSublist_collapseBtn {
display: inline-block; display: inline-block;
position: relative; position: relative;
width: 12px; width: 12px;
@ -169,7 +167,7 @@ limitations under the License.
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
} }
&.mx_RoomSublist2_collapseBtn_collapsed::before { &.mx_RoomSublist_collapseBtn_collapsed::before {
mask-image: url('$(res)/img/feather-customised/chevron-right.svg'); mask-image: url('$(res)/img/feather-customised/chevron-right.svg');
} }
} }
@ -181,12 +179,12 @@ limitations under the License.
// when scrolled to the top above the first sublist (whose header can only // when scrolled to the top above the first sublist (whose header can only
// ever stick to top), so we force height to 0 for only that first header. // ever stick to top), so we force height to 0 for only that first header.
// See also https://github.com/vector-im/riot-web/issues/14429. // See also https://github.com/vector-im/riot-web/issues/14429.
&:first-child .mx_RoomSublist2_headerContainer { &:first-child .mx_RoomSublist_headerContainer {
height: 0; height: 0;
padding-bottom: 4px; padding-bottom: 4px;
} }
.mx_RoomSublist2_resizeBox { .mx_RoomSublist_resizeBox {
position: relative; position: relative;
// Create another flexbox column for the tiles // Create another flexbox column for the tiles
@ -194,7 +192,7 @@ limitations under the License.
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.mx_RoomSublist2_tiles { .mx_RoomSublist_tiles {
flex: 1 0 0; flex: 1 0 0;
overflow: hidden; overflow: hidden;
// need this to be flex otherwise the overflow hidden from above // need this to be flex otherwise the overflow hidden from above
@ -206,18 +204,18 @@ limitations under the License.
mask-image: linear-gradient(0deg, transparent, black 4px); mask-image: linear-gradient(0deg, transparent, black 4px);
} }
.mx_RoomSublist2_resizerHandles_showNButton { .mx_RoomSublist_resizerHandles_showNButton {
flex: 0 0 32px; flex: 0 0 32px;
} }
.mx_RoomSublist2_resizerHandles { .mx_RoomSublist_resizerHandles {
flex: 0 0 4px; flex: 0 0 4px;
} }
// Class name comes from the ResizableBox component // Class name comes from the ResizableBox component
// The hover state needs to use the whole sublist, not just the resizable box, // The hover state needs to use the whole sublist, not just the resizable box,
// so that selector is below and one level higher. // so that selector is below and one level higher.
.mx_RoomSublist2_resizerHandle { .mx_RoomSublist_resizerHandle {
cursor: ns-resize; cursor: ns-resize;
border-radius: 3px; border-radius: 3px;
@ -235,21 +233,21 @@ limitations under the License.
right: calc(50% - 32px) !important; right: calc(50% - 32px) !important;
} }
&:hover, &.mx_RoomSublist2_hasMenuOpen { &:hover, &.mx_RoomSublist_hasMenuOpen {
.mx_RoomSublist2_resizerHandle { .mx_RoomSublist_resizerHandle {
opacity: 0.8; opacity: 0.8;
background-color: $primary-fg-color; background-color: $primary-fg-color;
} }
} }
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
cursor: pointer; cursor: pointer;
font-size: $font-13px; font-size: $font-13px;
line-height: $font-18px; line-height: $font-18px;
color: $roomtile2-preview-color; color: $roomtile-preview-color;
// Update the render() function for RoomSublist2 if these change // Update the render() function for RoomSublist if these change
// Update the ListLayout class for minVisibleTiles if these change. // Update the ListLayout class for minVisibleTiles if these change.
height: 24px; height: 24px;
padding-bottom: 4px; padding-bottom: 4px;
@ -258,7 +256,7 @@ limitations under the License.
display: flex; display: flex;
align-items: center; align-items: center;
.mx_RoomSublist2_showNButtonChevron { .mx_RoomSublist_showNButtonChevron {
position: relative; position: relative;
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -267,52 +265,52 @@ limitations under the License.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background: $roomtile2-preview-color; background: $roomtile-preview-color;
} }
.mx_RoomSublist2_showMoreButtonChevron { .mx_RoomSublist_showMoreButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
} }
.mx_RoomSublist2_showLessButtonChevron { .mx_RoomSublist_showLessButtonChevron {
mask-image: url('$(res)/img/feather-customised/chevron-up.svg'); mask-image: url('$(res)/img/feather-customised/chevron-up.svg');
} }
} }
&.mx_RoomSublist2_hasMenuOpen, &.mx_RoomSublist_hasMenuOpen,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:focus-within, &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:focus-within,
&:not(.mx_RoomSublist2_minimized) > .mx_RoomSublist2_headerContainer:hover { &:not(.mx_RoomSublist_minimized) > .mx_RoomSublist_headerContainer:hover {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: visible; visibility: visible;
width: 24px; width: 24px;
margin-left: 8px; margin-left: 8px;
} }
} }
&.mx_RoomSublist2_minimized { &.mx_RoomSublist_minimized {
.mx_RoomSublist2_headerContainer { .mx_RoomSublist_headerContainer {
height: auto; height: auto;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
.mx_RoomSublist2_badgeContainer { .mx_RoomSublist_badgeContainer {
order: 0; order: 0;
align-self: flex-end; align-self: flex-end;
margin-right: 0; margin-right: 0;
} }
.mx_RoomSublist2_stickable { .mx_RoomSublist_stickable {
order: 1; order: 1;
max-width: 100%; max-width: 100%;
} }
.mx_RoomSublist2_auxButton { .mx_RoomSublist_auxButton {
order: 2; order: 2;
visibility: visible; visibility: visible;
width: 32px !important; // !important to override hover styles width: 32px !important; // !important to override hover styles
height: 32px !important; // !important to override hover styles height: 32px !important; // !important to override hover styles
margin-left: 0 !important; // !important to override hover styles margin-left: 0 !important; // !important to override hover styles
background-color: $roomlist2-button-bg-color; background-color: $roomlist-button-bg-color;
margin-top: 8px; margin-top: 8px;
&::before { &::before {
@ -322,25 +320,25 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_resizeBox { .mx_RoomSublist_resizeBox {
align-items: center; align-items: center;
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
flex-direction: column; flex-direction: column;
.mx_RoomSublist2_showNButtonChevron { .mx_RoomSublist_showNButtonChevron {
margin-right: 12px; // to center margin-right: 12px; // to center
} }
} }
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
height: 16px; height: 16px;
} }
&.mx_RoomSublist2_hasMenuOpen, &.mx_RoomSublist_hasMenuOpen,
& > .mx_RoomSublist2_headerContainer:hover { & > .mx_RoomSublist_headerContainer:hover {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
visibility: visible; visibility: visible;
position: absolute; position: absolute;
bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment bottom: 48px; // align to middle of name, 40px for aux button (with padding) and 8px for alignment
@ -352,7 +350,7 @@ limitations under the License.
// This is the same color as the left panel background because it needs // This is the same color as the left panel background because it needs
// to occlude the sublist title // to occlude the sublist title
background-color: $roomlist2-bg-color; background-color: $roomlist-bg-color;
&::before { &::before {
top: 0; top: 0;
@ -360,8 +358,8 @@ limitations under the License.
} }
} }
&.mx_RoomSublist2_headerContainer:not(.mx_RoomSublist2_headerContainer_withAux) { &.mx_RoomSublist_headerContainer:not(.mx_RoomSublist_headerContainer_withAux) {
.mx_RoomSublist2_menuButton { .mx_RoomSublist_menuButton {
bottom: 8px; // align to the middle of name, 40px less than the `bottom` above. bottom: 8px; // align to the middle of name, 40px less than the `bottom` above.
} }
} }
@ -369,7 +367,7 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_contextMenu { .mx_RoomSublist_contextMenu {
padding: 20px 16px; padding: 20px 16px;
width: 250px; width: 250px;
@ -377,11 +375,11 @@ limitations under the License.
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
margin-right: 16px; // additional 16px margin-right: 16px; // additional 16px
border: 1px solid $roomsublist2-divider-color; border: 1px solid $roomsublist-divider-color;
opacity: 0.1; opacity: 0.1;
} }
.mx_RoomSublist2_contextMenu_title { .mx_RoomSublist_contextMenu_title {
font-size: $font-15px; font-size: $font-15px;
line-height: $font-20px; line-height: $font-20px;
font-weight: 600; font-weight: 600;
@ -393,6 +391,6 @@ limitations under the License.
} }
} }
.mx_RoomSublist2_addRoomTooltip { .mx_RoomSublist_addRoomTooltip {
margin-top: -3px; margin-top: -3px;
} }

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,214 +14,226 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile { .mx_RoomTile {
margin-bottom: 4px;
padding: 4px;
// The tile is also a flexbox row itself
display: flex; display: flex;
flex-direction: row;
align-items: center;
cursor: pointer;
height: 34px;
margin: 0;
padding: 0 8px 0 10px;
position: relative;
.mx_RoomTile_menuButton { &.mx_RoomTile_selected,
display: none; &:hover,
flex: 0 0 16px; &:focus-within,
height: 16px; &.mx_RoomTile_hasMenuOpen {
background-image: url('$(res)/img/icon_context.svg'); background-color: $roomtile-selected-bg-color;
background-repeat: no-repeat; border-radius: 8px;
background-position: center;
} }
.mx_UserOnlineDot { .mx_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer {
display: block; margin-right: 8px;
margin-right: 5px;
}
}
.mx_RoomTile:focus {
filter: none !important;
background-color: $roomtile-focused-bg-color;
}
.mx_RoomTile_tooltip {
display: inline-block;
position: relative;
top: -54px;
left: -12px;
} }
.mx_RoomTile_nameContainer { .mx_RoomTile_nameContainer {
display: flex; flex-grow: 1;
align-items: center; min-width: 0; // allow flex to shrink it
flex: 1; margin-right: 8px; // spacing to buttons/badges
vertical-align: middle;
min-width: 0;
}
.mx_RoomTile_labelContainer { // Create a new column layout flexbox for the name parts
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; justify-content: center;
min-width: 0;
}
.mx_RoomTile_subtext { .mx_RoomTile_name,
display: inline-block; .mx_RoomTile_messagePreview {
font-size: $font-11px; margin: 0 2px;
padding: 0 0 0 7px; width: 100%;
margin: 0;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: clip;
position: relative;
bottom: 4px;
}
.mx_RoomTile_avatar_container {
position: relative;
display: flex;
}
.mx_RoomTile_avatar {
flex: 0;
padding: 4px;
width: 24px;
vertical-align: middle;
}
.mx_RoomTile_hasSubtext .mx_RoomTile_avatar {
padding-top: 0;
vertical-align: super;
}
.mx_RoomTile_dm {
display: block;
position: absolute;
bottom: 0;
right: -5px;
z-index: 2;
}
// Note we match .mx_E2EIcon to make sure this matches more tightly than just
// .mx_E2EIcon on its own
.mx_RoomTile_e2eIcon.mx_E2EIcon {
height: 14px;
width: 14px;
display: block;
position: absolute;
bottom: -2px;
right: -5px;
z-index: 1;
margin: 0;
} }
.mx_RoomTile_name { .mx_RoomTile_name {
font-size: $font-14px; font-size: $font-14px;
padding: 0 4px; line-height: $font-18px;
color: $roomtile-name-color;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
} }
.mx_RoomTile_badge { .mx_RoomTile_name.mx_RoomTile_nameHasUnreadEvents {
flex: 0 1 content;
border-radius: 0.8em;
padding: 0 0.4em;
color: $roomtile-badge-fg-color;
font-weight: 600; font-weight: 600;
font-size: $font-12px;
} }
.collapsed { .mx_RoomTile_messagePreview {
.mx_RoomTile { font-size: $font-13px;
margin: 0 6px; line-height: $font-18px;
padding: 0 2px; color: $roomtile-preview-color;
}
.mx_RoomTile_nameWithPreview {
margin-top: -4px; // shift the name up a bit more
}
}
.mx_RoomTile_notificationsButton {
margin-left: 4px; // spacing between buttons
}
.mx_RoomTile_badgeContainer {
height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
// Create a flexbox to make aligning dot badges easier
display: flex;
align-items: center;
.mx_NotificationBadge {
margin-right: 2px; // centering
}
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin-left: 5px;
margin-right: 7px;
}
}
// The context menu buttons are hidden by default
.mx_RoomTile_menuButton,
.mx_RoomTile_notificationsButton {
width: 20px;
min-width: 20px; // yay flex
height: 20px;
margin-top: auto;
margin-bottom: auto;
position: relative; position: relative;
justify-content: center; display: none;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
} }
.mx_RoomTile_name { // If the room has an overriden notification setting then we always show the notifications menu button
.mx_RoomTile_notificationsButton.mx_RoomTile_notificationsButton_show {
display: block;
}
.mx_RoomTile_menuButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg');
}
&:not(.mx_RoomTile_minimized) {
&:hover,
&:focus-within,
&.mx_RoomTile_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_RoomTile_badgeContainer {
width: 0;
height: 0;
display: none; display: none;
} }
.mx_RoomTile_badge { .mx_RoomTile_notificationsButton,
position: absolute;
right: 6px;
top: 0px;
border-radius: 16px;
z-index: 3;
border: 0.18em solid $secondary-accent-color;
}
.mx_RoomTile_menuButton {
display: none; // no design for this for now
}
.mx_UserOnlineDot {
display: none; // no design for this for now
}
}
// toggle menuButton and badge on menu displayed
.mx_RoomTile_menuDisplayed,
// or on keyboard focus of room tile
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:focus-within,
// or on pointer hover
.mx_LeftPanel_container:not(.collapsed) .mx_RoomTile:hover {
.mx_RoomTile_menuButton { .mx_RoomTile_menuButton {
display: block; display: block;
} }
.mx_UserOnlineDot {
display: none;
} }
} }
.mx_RoomTile_unreadNotify .mx_RoomTile_badge, &.mx_RoomTile_minimized {
.mx_RoomTile_badge.mx_RoomTile_badgeUnread { flex-direction: column;
background-color: $roomtile-name-color; align-items: center;
position: relative;
.mx_DecoratedRoomAvatar, .mx_RoomTile_avatarContainer {
margin-right: 0;
}
}
} }
.mx_RoomTile_highlight .mx_RoomTile_badge, // We use these both in context menus and the room tiles
.mx_RoomTile_badge.mx_RoomTile_badgeRed { .mx_RoomTile_iconBell::before {
color: $accent-fg-color; mask-image: url('$(res)/img/element-icons/notifications.svg');
}
.mx_RoomTile_iconBellDot::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
}
.mx_RoomTile_iconBellCrossed::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
.mx_RoomTile_iconBellMentions::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
}
.mx_RoomTile_iconCheck::before {
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
}
.mx_RoomTile_contextMenu {
.mx_RoomTile_contextMenu_redRow {
.mx_AccessibleButton {
color: $warning-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $warning-color; background-color: $warning-color;
} }
}
.mx_RoomTile_unread, .mx_RoomTile_highlight { .mx_RoomTile_contextMenu_activeRow {
.mx_RoomTile_name { &.mx_AccessibleButton, .mx_AccessibleButton {
font-weight: 600; color: $accent-color !important; // !important to override styles from context menu
color: $roomtile-selected-color; }
.mx_IconizedContextMenu_icon::before {
background-color: $accent-color;
} }
} }
.mx_RoomTile_selected { .mx_IconizedContextMenu_icon {
border-radius: 4px; position: relative;
background-color: $roomtile-selected-bg-color; width: 16px;
} height: 16px;
.mx_DNDRoomTile { &::before {
transform: none; content: '';
transition: transform 0.2s; width: 16px;
} height: 16px;
.mx_DNDRoomTile_dragging {
transform: scale(1.05, 1.05);
}
.mx_RoomTile_arrow {
position: absolute; position: absolute;
right: 0px; mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
} }
.mx_RoomTile.mx_RoomTile_transparent { .mx_RoomTile_iconStar::before {
background-color: transparent; mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
} }
.mx_RoomTile.mx_RoomTile_transparent:focus { .mx_RoomTile_iconFavorite::before {
background-color: $roomtile-transparent-focused-color; mask-image: url('$(res)/img/feather-customised/favourites.svg');
} }
.mx_GroupInviteTile .mx_RoomTile_name { .mx_RoomTile_iconArrowDown::before {
flex: 1; mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
}
.mx_RoomTile_iconSettings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_RoomTile_iconSignOut::before {
mask-image: url('$(res)/img/element-icons/leave.svg');
}
} }

View file

@ -1,241 +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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
// Note: the room tile expects to be in a flexbox column container
.mx_RoomTile2 {
margin-bottom: 4px;
padding: 4px;
// The tile is also a flexbox row itself
display: flex;
&.mx_RoomTile2_selected,
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
background-color: $roomtile2-selected-bg-color;
border-radius: 8px;
}
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 8px;
}
.mx_RoomTile2_nameContainer {
flex-grow: 1;
min-width: 0; // allow flex to shrink it
margin-right: 8px; // spacing to buttons/badges
// Create a new column layout flexbox for the name parts
display: flex;
flex-direction: column;
justify-content: center;
.mx_RoomTile2_name,
.mx_RoomTile2_messagePreview {
margin: 0 2px;
width: 100%;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.mx_RoomTile2_name {
font-size: $font-14px;
line-height: $font-18px;
}
.mx_RoomTile2_name.mx_RoomTile2_nameHasUnreadEvents {
font-weight: 600;
}
.mx_RoomTile2_messagePreview {
font-size: $font-13px;
line-height: $font-18px;
color: $roomtile2-preview-color;
}
.mx_RoomTile2_nameWithPreview {
margin-top: -4px; // shift the name up a bit more
}
}
.mx_RoomTile2_notificationsButton {
margin-left: 4px; // spacing between buttons
}
.mx_RoomTile2_badgeContainer {
height: 16px;
// don't set width so that it takes no space when there is no badge to show
margin: auto 0; // vertically align
// Create a flexbox to make aligning dot badges easier
display: flex;
align-items: center;
.mx_NotificationBadge {
margin-right: 2px; // centering
}
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
margin-left: 5px;
margin-right: 7px;
}
}
// The context menu buttons are hidden by default
.mx_RoomTile2_menuButton,
.mx_RoomTile2_notificationsButton {
width: 20px;
min-width: 20px; // yay flex
height: 20px;
margin-top: auto;
margin-bottom: auto;
position: relative;
display: none;
&::before {
top: 2px;
left: 2px;
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
// If the room has an overriden notification setting then we always show the notifications menu button
.mx_RoomTile2_notificationsButton.mx_RoomTile2_notificationsButton_show {
display: block;
}
.mx_RoomTile2_menuButton::before {
mask-image: url('$(res)/img/element-icons/context-menu.svg');
}
&:not(.mx_RoomTile2_minimized) {
&:hover,
&:focus-within,
&.mx_RoomTile2_hasMenuOpen {
// Hide the badge container on hover because it'll be a menu button
.mx_RoomTile2_badgeContainer {
width: 0;
height: 0;
display: none;
}
.mx_RoomTile2_notificationsButton,
.mx_RoomTile2_menuButton {
display: block;
}
}
}
&.mx_RoomTile2_minimized {
flex-direction: column;
align-items: center;
position: relative;
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
margin-right: 0;
}
}
}
// We use these both in context menus and the room tiles
.mx_RoomTile2_iconBell::before {
mask-image: url('$(res)/img/element-icons/notifications.svg');
}
.mx_RoomTile2_iconBellDot::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-default.svg');
}
.mx_RoomTile2_iconBellCrossed::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-off.svg');
}
.mx_RoomTile2_iconBellMentions::before {
mask-image: url('$(res)/img/element-icons/roomlist/notifications-dm.svg');
}
.mx_RoomTile2_iconCheck::before {
mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
}
.mx_RoomTile2_contextMenu {
.mx_RoomTile2_contextMenu_redRow {
.mx_AccessibleButton {
color: $warning-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $warning-color;
}
}
.mx_RoomTile2_contextMenu_activeRow {
&.mx_AccessibleButton, .mx_AccessibleButton {
color: $accent-color !important; // !important to override styles from context menu
}
.mx_IconizedContextMenu_icon::before {
background-color: $accent-color;
}
}
.mx_IconizedContextMenu_icon {
position: relative;
width: 16px;
height: 16px;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background: $primary-fg-color;
}
}
.mx_RoomTile2_iconStar::before {
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
}
.mx_RoomTile2_iconFavorite::before {
mask-image: url('$(res)/img/feather-customised/favourites.svg');
}
.mx_RoomTile2_iconArrowDown::before {
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
}
.mx_RoomTile2_iconSettings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_RoomTile2_iconSignOut::before {
mask-image: url('$(res)/img/element-icons/leave.svg');
}
}

View file

@ -18,7 +18,7 @@ limitations under the License.
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 12px; border-radius: 12px;
background-color: $roomlist2-bg-color; // to match the room list itself background-color: $roomlist-bg-color; // to match the room list itself
} }
.mx_RoomTileIcon_globe::before { .mx_RoomTileIcon_globe::before {

View file

@ -42,7 +42,7 @@ limitations under the License.
border-radius: 19px; border-radius: 19px;
box-sizing: border-box; box-sizing: border-box;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
cursor: pointer; cursor: pointer;
} }
@ -54,7 +54,7 @@ limitations under the License.
mask-image: url('$(res)/img/icon-jump-to-first-unread.svg'); mask-image: url('$(res)/img/icon-jump-to-first-unread.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: 9px 13px; mask-position: 9px 13px;
background: $roomtile-name-color; background: $muted-fg-color;
} }
.mx_TopUnreadMessagesBar_markAsRead { .mx_TopUnreadMessagesBar_markAsRead {
@ -62,7 +62,7 @@ limitations under the License.
width: 18px; width: 18px;
height: 18px; height: 18px;
background: $primary-bg-color; background: $primary-bg-color;
border: 1.3px solid $roomtile-name-color; border: 1.3px solid $muted-fg-color;
border-radius: 10px; border-radius: 10px;
margin: 5px auto; margin: 5px auto;
} }
@ -76,5 +76,5 @@ limitations under the License.
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: 10px; mask-size: 10px;
mask-position: 4px 4px; mask-position: 4px 4px;
background: $roomtile-name-color; background: $muted-fg-color;
} }

View file

@ -1,23 +0,0 @@
/*
Copyright 2019 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_UserOnlineDot {
border-radius: 50%;
background-color: $accent-color;
height: 6px;
width: 6px;
display: inline-block;
}

View file

@ -36,12 +36,12 @@ limitations under the License.
} }
} }
.mx_IncomingCallBox2 { .mx_IncomingCallBox {
min-width: 250px; min-width: 250px;
background-color: $primary-bg-color; background-color: $primary-bg-color;
padding: 8px; padding: 8px;
.mx_IncomingCallBox2_CallerInfo { .mx_IncomingCallBox_CallerInfo {
display: flex; display: flex;
direction: row; direction: row;
@ -68,12 +68,12 @@ limitations under the License.
} }
} }
.mx_IncomingCallBox2_buttons { .mx_IncomingCallBox_buttons {
padding: 8px; padding: 8px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
> .mx_IncomingCallBox2_spacer { > .mx_IncomingCallBox_spacer {
width: 8px; width: 8px;
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,8 +19,76 @@ limitations under the License.
background-color: $accent-color; background-color: $accent-color;
color: $accent-fg-color; color: $accent-fg-color;
cursor: pointer; cursor: pointer;
text-align: center;
padding: 6px; padding: 6px;
font-weight: bold; font-weight: bold;
font-size: $font-13px;
border-radius: 8px;
min-width: 200px;
display: flex;
align-items: center;
img {
margin: 4px;
margin-right: 10px;
}
> div {
display: flex;
flex-direction: column;
// Hacky vertical align
padding-top: 3px;
}
> div > p,
> div > h1 {
padding: 0;
margin: 0;
font-size: $font-13px;
line-height: $font-15px;
}
> div > p {
font-weight: bold;
}
> * {
flex-grow: 0;
flex-shrink: 0;
}
}
.mx_CallView_hangup {
position: absolute;
right: 8px;
bottom: 10px;
height: 35px;
width: 35px;
border-radius: 35px;
background-color: $notice-primary-color;
z-index: 101;
cursor: pointer;
&::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
top: 6.5px;
left: 7.5px;
mask: url('$(res)/img/hangup.svg');
mask-size: contain;
background-size: contain;
background-color: $primary-fg-color;
}
} }

View file

@ -1,96 +0,0 @@
/*
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.
*/
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
.mx_CallView2_voice {
background-color: $accent-color;
color: $accent-fg-color;
cursor: pointer;
padding: 6px;
font-weight: bold;
border-radius: 8px;
min-width: 200px;
display: flex;
align-items: center;
img {
margin: 4px;
margin-right: 10px;
}
> div {
display: flex;
flex-direction: column;
// Hacky vertical align
padding-top: 3px;
}
> div > p,
> div > h1 {
padding: 0;
margin: 0;
font-size: $font-13px;
line-height: $font-15px;
}
> div > p {
font-weight: bold;
}
> * {
flex-grow: 0;
flex-shrink: 0;
}
}
.mx_CallView2_hangup {
position: absolute;
right: 8px;
bottom: 10px;
height: 35px;
width: 35px;
border-radius: 35px;
background-color: $notice-primary-color;
z-index: 101;
cursor: pointer;
&::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
top: 6.5px;
left: 7.5px;
mask: url('$(res)/img/hangup.svg');
mask-size: contain;
background-size: contain;
background-color: $primary-fg-color;
}
}

View file

@ -1,69 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket 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.
*/
.mx_IncomingCallBox {
text-align: center;
border: 1px solid #a4a4a4;
border-radius: 8px;
background-color: $primary-bg-color;
position: fixed;
z-index: 1000;
padding: 6px;
margin-top: -3px;
margin-left: -20px;
width: 200px;
}
.mx_IncomingCallBox_chevron {
padding: 12px;
position: absolute;
left: -21px;
top: 0px;
}
.mx_IncomingCallBox_title {
padding: 6px;
font-weight: bold;
}
.mx_IncomingCallBox_buttons {
display: flex;
}
.mx_IncomingCallBox_buttons_cell {
vertical-align: middle;
padding: 6px;
flex: 1;
}
.mx_IncomingCallBox_buttons_decline,
.mx_IncomingCallBox_buttons_accept {
vertical-align: middle;
width: 80px;
height: 36px;
line-height: $font-36px;
border-radius: 36px;
color: $accent-fg-color;
margin: auto;
}
.mx_IncomingCallBox_buttons_decline {
background-color: $voip-decline-color;
}
.mx_IncomingCallBox_buttons_accept {
background-color: $voip-accept-color;
}

View file

@ -108,32 +108,20 @@ $composer-e2e-icon-color: $header-panel-text-primary-color;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-bg-color: rgba(33, 38, 44, 0.90);
$roomlist-header-color: #8E99A4;
$roomsublist-divider-color: $primary-fg-color;
$roomlist2-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomtile-preview-color: #A9B2BC;
$roomlist2-bg-color: rgba(33, 38, 44, 0.90); $roomtile-default-badge-bg-color: #61708b;
$roomlist2-header-color: #8E99A4; $roomtile-selected-bg-color: rgba(141, 151, 165, 0.2);
$roomsublist2-divider-color: $primary-fg-color;
$roomtile2-preview-color: #A9B2BC;
$roomtile2-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: rgba(141, 151, 165, 0.2);
// ******************** // ********************
$notice-secondary-color: $roomlist2-header-color; $notice-secondary-color: $roomlist-header-color;
$roomtile-name-color: $header-panel-text-primary-color;
$roomtile-selected-color: $text-primary-color;
$roomtile-notified-color: $text-primary-color;
$roomtile-selected-bg-color: $room-highlight-color;
$roomtile-focused-bg-color: $room-highlight-color;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$panel-divider-color: transparent; $panel-divider-color: transparent;

View file

@ -107,30 +107,19 @@ $composer-e2e-icon-color: $header-panel-text-primary-color;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #1A1D23; $roomtile-selected-bg-color: #1A1D23;
// ******************** // ********************
$roomtile-name-color: $header-panel-text-primary-color;
$roomtile-selected-color: $text-primary-color;
$roomtile-notified-color: $text-primary-color;
$roomtile-selected-bg-color: $room-highlight-color;
$roomtile-focused-bg-color: $room-highlight-color;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$panel-divider-color: $header-panel-border-color; $panel-divider-color: $header-panel-border-color;
$widget-menu-bar-bg-color: $header-panel-bg-color; $widget-menu-bar-bg-color: $header-panel-bg-color;

View file

@ -174,19 +174,16 @@ $header-divider-color: #91a1c0;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomlist2-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #fff; $roomtile-selected-bg-color: #fff;
$presence-online: $accent-color; $presence-online: $accent-color;
$presence-away: #d9b072; $presence-away: #d9b072;
@ -194,13 +191,6 @@ $presence-offline: #e3e8f0;
// ******************** // ********************
$roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color;
$roomtile-selected-color: #212121;
$roomtile-notified-color: #212121;
$roomtile-selected-bg-color: #fff;
$roomtile-focused-bg-color: #fff;
$username-variant1-color: #368bd6; $username-variant1-color: #368bd6;
$username-variant2-color: #ac3ba8; $username-variant2-color: #ac3ba8;
$username-variant3-color: #03b381; $username-variant3-color: #03b381;
@ -210,13 +200,6 @@ $username-variant6-color: #2dc2c5;
$username-variant7-color: #5c56f5; $username-variant7-color: #5c56f5;
$username-variant8-color: #74d12c; $username-variant8-color: #74d12c;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$roomsublist-background: $secondary-accent-color;
$roomsublist-label-fg-color: $roomtile-name-color;
$roomsublist-label-bg-color: $tertiary-accent-color;
$roomsublist-chevron-color: $accent-color;
$panel-divider-color: #dee1f3; $panel-divider-color: #dee1f3;
// ******************** // ********************
@ -292,7 +275,7 @@ $progressbar-color: #000;
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
$memberstatus-placeholder-color: $roomtile-name-color; $memberstatus-placeholder-color: $muted-fg-color;
$authpage-bg-color: #2e3649; $authpage-bg-color: #2e3649;
$authpage-modal-bg-color: rgba(255, 255, 255, 0.59); $authpage-modal-bg-color: rgba(255, 255, 255, 0.59);

View file

@ -25,7 +25,6 @@ $button-link-fg-color: var(--accent-color);
$button-primary-bg-color: var(--accent-color); $button-primary-bg-color: var(--accent-color);
$input-valid-border-color: var(--accent-color); $input-valid-border-color: var(--accent-color);
$reaction-row-button-selected-border-color: var(--accent-color); $reaction-row-button-selected-border-color: var(--accent-color);
$roomsublist-chevron-color: var(--accent-color);
$tab-label-active-bg-color: var(--accent-color); $tab-label-active-bg-color: var(--accent-color);
$togglesw-on-color: var(--accent-color); $togglesw-on-color: var(--accent-color);
$username-variant3-color: var(--accent-color); $username-variant3-color: var(--accent-color);
@ -40,7 +39,6 @@ $menu-bg-color: var(--timeline-background-color);
$avatar-bg-color: var(--timeline-background-color); $avatar-bg-color: var(--timeline-background-color);
$message-action-bar-bg-color: var(--timeline-background-color); $message-action-bar-bg-color: var(--timeline-background-color);
$primary-bg-color: var(--timeline-background-color); $primary-bg-color: var(--timeline-background-color);
$roomtile-focused-bg-color: var(--timeline-background-color);
$togglesw-ball-color: var(--timeline-background-color); $togglesw-ball-color: var(--timeline-background-color);
$droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5 $droptarget-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .5
$authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59 $authpage-modal-bg-color: var(--timeline-background-color-50pct); //still needs alpha at .59
@ -48,14 +46,13 @@ $roomheader-bg-color: var(--timeline-background-color);
// //
// --roomlist-highlights-color // --roomlist-highlights-color
$roomtile-selected-bg-color: var(--roomlist-highlights-color); $roomtile-selected-bg-color: var(--roomlist-highlights-color);
$roomtile2-selected-bg-color: var(--roomlist-highlights-color);
// //
// --sidebar-color // --sidebar-color
$interactive-tooltip-bg-color: var(--sidebar-color); $interactive-tooltip-bg-color: var(--sidebar-color);
$tagpanel-bg-color: var(--sidebar-color); $tagpanel-bg-color: var(--sidebar-color);
$tooltip-timeline-bg-color: var(--sidebar-color); $tooltip-timeline-bg-color: var(--sidebar-color);
$dialog-backdrop-color: var(--sidebar-color-50pct); $dialog-backdrop-color: var(--sidebar-color-50pct);
$roomlist2-button-bg-color: var(--sidebar-color-15pct); $roomlist-button-bg-color: var(--sidebar-color-15pct);
// //
// --roomlist-background-color // --roomlist-background-color
$header-panel-bg-color: var(--roomlist-background-color); $header-panel-bg-color: var(--roomlist-background-color);
@ -65,12 +62,10 @@ $panel-gradient: var(--roomlist-background-color-0pct), var(--roomlist-backgroun
$dark-panel-bg-color: var(--roomlist-background-color); $dark-panel-bg-color: var(--roomlist-background-color);
$input-lighter-bg-color: var(--roomlist-background-color); $input-lighter-bg-color: var(--roomlist-background-color);
$plinth-bg-color: var(--roomlist-background-color); $plinth-bg-color: var(--roomlist-background-color);
$roomsublist-background: var(--roomlist-background-color);
$secondary-accent-color: var(--roomlist-background-color); $secondary-accent-color: var(--roomlist-background-color);
$selected-color: var(--roomlist-background-color); $selected-color: var(--roomlist-background-color);
$widget-menu-bar-bg-color: var(--roomlist-background-color); $widget-menu-bar-bg-color: var(--roomlist-background-color);
$roomtile-badge-fg-color: var(--roomlist-background-color); $roomlist-bg-color: var(--roomlist-background-color);
$roomlist2-bg-color: var(--roomlist-background-color);
// //
// --timeline-text-color // --timeline-text-color
$message-action-bar-fg-color: var(--timeline-text-color); $message-action-bar-fg-color: var(--timeline-text-color);
@ -87,23 +82,17 @@ $tab-label-fg-color: var(--timeline-text-color);
// was #4e5054 // was #4e5054
$authpage-lang-color: var(--timeline-text-color); $authpage-lang-color: var(--timeline-text-color);
$roomheader-color: var(--timeline-text-color); $roomheader-color: var(--timeline-text-color);
//
// --roomlist-text-color
$roomtile-notified-color: var(--roomlist-text-color);
$roomtile-selected-color: var(--roomlist-text-color);
// --roomlist-text-secondary-color // --roomlist-text-secondary-color
$roomsublist-label-fg-color: var(--roomlist-text-secondary-color); $roomtile-preview-color: var(--roomlist-text-secondary-color);
$roomtile-name-color: var(--roomlist-text-secondary-color); $roomlist-header-color: var(--roomlist-text-secondary-color);
$roomtile2-preview-color: var(--roomlist-text-secondary-color); $roomtile-default-badge-bg-color: var(--roomlist-text-secondary-color);
$roomlist2-header-color: var(--roomlist-text-secondary-color);
$roomtile2-default-badge-bg-color: var(--roomlist-text-secondary-color);
// //
// --roomlist-separator-color // --roomlist-separator-color
$input-darker-bg-color: var(--roomlist-separator-color); $input-darker-bg-color: var(--roomlist-separator-color);
$panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough $panel-divider-color: var(--roomlist-separator-color);// originally #dee1f3, but close enough
$primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough $primary-hairline-color: var(--roomlist-separator-color);// originally #e5e5e5, but close enough
$roomsublist2-divider-color: var(--roomlist-separator-color); $roomsublist-divider-color: var(--roomlist-separator-color);
// //
// --timeline-text-secondary-color // --timeline-text-secondary-color
$authpage-secondary-color: var(--timeline-text-secondary-color); $authpage-secondary-color: var(--timeline-text-secondary-color);

View file

@ -19,8 +19,8 @@ $accent-bg-color: rgba(3, 179, 129, 0.16);
$notice-primary-color: #ff4b55; $notice-primary-color: #ff4b55;
$notice-primary-bg-color: rgba(255, 75, 85, 0.16); $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32; $primary-fg-color: #2e2f32;
$roomlist2-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$notice-secondary-color: $roomlist2-header-color; $notice-secondary-color: $roomlist-header-color;
$header-panel-bg-color: #f3f8fd; $header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin) // typical text (dark-on-white in light skin)
@ -174,32 +174,22 @@ $header-divider-color: #91A1C0;
// ******************** // ********************
// V2 Room List
// TODO: Remove the 2 from all of these when the new list takes over
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$roomlist2-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist2-bg-color: rgba(245, 245, 245, 0.90); $roomlist-bg-color: rgba(245, 245, 245, 0.90);
$roomsublist2-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomtile2-preview-color: #737D8C; $roomtile-preview-color: #737D8C;
$roomtile2-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;
$roomtile2-selected-bg-color: #FFF; $roomtile-selected-bg-color: #FFF;
$presence-online: $accent-color; $presence-online: $accent-color;
$presence-away: orange; // TODO: Get color $presence-away: #d9b072;
$presence-offline: #E3E8F0; $presence-offline: #E3E8F0;
// ******************** // ********************
$roomtile-name-color: #61708b;
$roomtile-badge-fg-color: $accent-fg-color;
$roomtile-selected-color: #212121;
$roomtile-notified-color: #212121;
$roomtile-selected-bg-color: #fff;
$roomtile-focused-bg-color: #fff;
$username-variant1-color: #368bd6; $username-variant1-color: #368bd6;
$username-variant2-color: #ac3ba8; $username-variant2-color: #ac3ba8;
$username-variant3-color: #0DBD8B; $username-variant3-color: #0DBD8B;
@ -209,13 +199,6 @@ $username-variant6-color: #2dc2c5;
$username-variant7-color: #5c56f5; $username-variant7-color: #5c56f5;
$username-variant8-color: #74d12c; $username-variant8-color: #74d12c;
$roomtile-transparent-focused-color: rgba(0, 0, 0, 0.1);
$roomsublist-background: $secondary-accent-color;
$roomsublist-label-fg-color: $roomtile-name-color;
$roomsublist-label-bg-color: $tertiary-accent-color;
$roomsublist-chevron-color: $accent-color;
$panel-divider-color: transparent; $panel-divider-color: transparent;
// ******************** // ********************
@ -291,7 +274,7 @@ $progressbar-color: #000;
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
$memberstatus-placeholder-color: $roomtile-name-color; $memberstatus-placeholder-color: $muted-fg-color;
$authpage-bg-color: #2e3649; $authpage-bg-color: #2e3649;
$authpage-modal-bg-color: rgba(245, 245, 245, 0.90); $authpage-modal-bg-color: rgba(245, 245, 245, 0.90);

View file

@ -5,7 +5,7 @@
// it can be blurred by the tag panel and room list // it can be blurred by the tag panel and room list
@supports (backdrop-filter: none) { @supports (backdrop-filter: none) {
.mx_LeftPanel2 { .mx_LeftPanel {
background-image: var(--avatar-url); background-image: var(--avatar-url);
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
@ -16,12 +16,12 @@
backdrop-filter: blur($tagpanel-background-blur-amount); backdrop-filter: blur($tagpanel-background-blur-amount);
} }
.mx_LeftPanel2 .mx_LeftPanel2_roomListContainer { .mx_LeftPanel .mx_LeftPanel_roomListContainer {
backdrop-filter: blur($roomlist-background-blur-amount); backdrop-filter: blur($roomlist-background-blur-amount);
} }
} }
.mx_RoomSublist2_showNButton { .mx_RoomSublist_showNButton {
background-color: transparent !important; background-color: transparent !important;
} }

View file

@ -20,7 +20,7 @@ import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore"; import ToastStore from "../stores/ToastStore";
import DeviceListener from "../DeviceListener"; import DeviceListener from "../DeviceListener";
import RebrandListener from "../RebrandListener"; import RebrandListener from "../RebrandListener";
import { RoomListStore2 } from "../stores/room-list/RoomListStore2"; import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg"; import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {IntegrationManagers} from "../integrations/IntegrationManagers";
@ -37,7 +37,7 @@ declare global {
mx_ToastStore: ToastStore; mx_ToastStore: ToastStore;
mx_DeviceListener: DeviceListener; mx_DeviceListener: DeviceListener;
mx_RebrandListener: RebrandListener; mx_RebrandListener: RebrandListener;
mx_RoomListStore2: RoomListStore2; mx_RoomListStore: RoomListStoreClass;
mx_RoomListLayoutStore: RoomListLayoutStore; mx_RoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg; mxPlatformPeg: PlatformPeg;
mxIntegrationManagers: typeof IntegrationManagers; mxIntegrationManagers: typeof IntegrationManagers;

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import { asyncAction } from './actionCreators'; import { asyncAction } from './actionCreators';
import { TAG_DM } from '../stores/RoomListStore';
import Modal from '../Modal'; import Modal from '../Modal';
import * as Rooms from '../Rooms'; import * as Rooms from '../Rooms';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
@ -24,7 +23,9 @@ import * as sdk from '../index';
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { AsyncActionPayload } from "../dispatcher/payloads"; import { AsyncActionPayload } from "../dispatcher/payloads";
import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; import RoomListStore from "../stores/room-list/RoomListStore";
import { SortAlgorithm } from "../stores/room-list/algorithms/models";
import { DefaultTagID } from "../stores/room-list/models";
export default class RoomListActions { export default class RoomListActions {
/** /**
@ -51,9 +52,9 @@ export default class RoomListActions {
let metaData = null; let metaData = null;
// Is the tag ordered manually? // Is the tag ordered manually?
if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { const store = RoomListStore.instance;
const lists = RoomListStoreTempProxy.getRoomLists(); if (newTag && store.getTagSorting(newTag) === SortAlgorithm.Manual) {
const newList = [...lists[newTag]]; const newList = [...store.orderedLists[newTag]];
newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order);
@ -81,11 +82,11 @@ export default class RoomListActions {
const roomId = room.roomId; const roomId = room.roomId;
// Evil hack to get DMs behaving // Evil hack to get DMs behaving
if ((oldTag === undefined && newTag === TAG_DM) || if ((oldTag === undefined && newTag === DefaultTagID.DM) ||
(oldTag === TAG_DM && newTag === undefined) (oldTag === DefaultTagID.DM && newTag === undefined)
) { ) {
return Rooms.guessAndSetDMRoom( return Rooms.guessAndSetDMRoom(
room, newTag === TAG_DM, room, newTag === DefaultTagID.DM,
).catch((err) => { ).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to set direct chat tag " + err); console.error("Failed to set direct chat tag " + err);
@ -102,7 +103,7 @@ export default class RoomListActions {
// but we avoid ever doing a request with TAG_DM. // but we avoid ever doing a request with TAG_DM.
// //
// if we moved lists, remove the old tag // if we moved lists, remove the old tag
if (oldTag && oldTag !== TAG_DM && if (oldTag && oldTag !== DefaultTagID.DM &&
hasChangedSubLists hasChangedSubLists
) { ) {
const promiseToDelete = matrixClient.deleteRoomTag( const promiseToDelete = matrixClient.deleteRoomTag(
@ -120,7 +121,7 @@ export default class RoomListActions {
} }
// if we moved lists or the ordering changed, add the new tag // if we moved lists or the ordering changed, add the new tag
if (newTag && newTag !== TAG_DM && if (newTag && newTag !== DefaultTagID.DM &&
(hasChangedSubLists || metaData) (hasChangedSubLists || metaData)
) { ) {
// metaData is the body of the PUT to set the tag, so it must // metaData is the body of the PUT to set the tag, so it must

View file

@ -1,305 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Key } from '../../Keyboard';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as VectorConferenceHandler from '../../VectorConferenceHandler';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
import {Action} from "../../dispatcher/actions";
const LeftPanel = createReactClass({
displayName: 'LeftPanel',
// NB. If you add props, don't forget to update
// shouldComponentUpdate!
propTypes: {
collapsed: PropTypes.bool.isRequired,
},
getInitialState: function() {
return {
searchFilter: '',
breadcrumbs: false,
};
},
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount: function() {
this.focusedElement = null;
this._breadcrumbsWatcherRef = SettingsStore.watchSetting(
"breadcrumbs", null, this._onBreadcrumbsChanged);
this._tagPanelWatcherRef = SettingsStore.watchSetting(
"TagPanel.enableTagPanel", null, () => this.forceUpdate());
const useBreadcrumbs = !!SettingsStore.getValue("breadcrumbs");
Analytics.setBreadcrumbs(useBreadcrumbs);
this.setState({breadcrumbs: useBreadcrumbs});
},
componentWillUnmount: function() {
SettingsStore.unwatchSetting(this._breadcrumbsWatcherRef);
SettingsStore.unwatchSetting(this._tagPanelWatcherRef);
},
shouldComponentUpdate: function(nextProps, nextState) {
// MatrixChat will update whenever the user switches
// rooms, but propagating this change all the way down
// the react tree is quite slow, so we cut this off
// here. The RoomTiles listen for the room change
// events themselves to know when to update.
// We just need to update if any of these things change.
if (
this.props.collapsed !== nextProps.collapsed ||
this.props.disabled !== nextProps.disabled
) {
return true;
}
if (this.state.searchFilter !== nextState.searchFilter) {
return true;
}
if (this.state.searchExpanded !== nextState.searchExpanded) {
return true;
}
return false;
},
componentDidUpdate(prevProps, prevState) {
if (prevState.breadcrumbs !== this.state.breadcrumbs) {
Analytics.setBreadcrumbs(this.state.breadcrumbs);
}
},
_onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) {
// Features are only possible at a single level, so we can get away with using valueAtLevel.
// The SettingsStore runs on the same tick as the update, so `value` will be wrong.
this.setState({breadcrumbs: valueAtLevel});
// For some reason the setState doesn't trigger a render of the component, so force one.
// Probably has to do with the change happening outside of a change detector cycle.
this.forceUpdate();
},
_onFocus: function(ev) {
this.focusedElement = ev.target;
},
_onBlur: function(ev) {
this.focusedElement = null;
},
_onFilterKeyDown: function(ev) {
if (!this.focusedElement) return;
switch (ev.key) {
// On enter of rooms filter select and activate first room if such one exists
case Key.ENTER: {
const firstRoom = ev.target.closest(".mx_LeftPanel").querySelector(".mx_RoomTile");
if (firstRoom) {
firstRoom.click();
}
break;
}
}
},
_onKeyDown: function(ev) {
if (!this.focusedElement) return;
switch (ev.key) {
case Key.ARROW_UP:
this._onMoveFocus(ev, true, true);
break;
case Key.ARROW_DOWN:
this._onMoveFocus(ev, false, true);
break;
}
},
_onMoveFocus: function(ev, up, trap) {
let element = this.focusedElement;
// unclear why this isn't needed
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
// this.focusDirection = up;
let descending = false; // are we currently descending or ascending through the DOM tree?
let classes;
do {
const child = up ? element.lastElementChild : element.firstElementChild;
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
if (descending) {
if (child) {
element = child;
} else if (sibling) {
element = sibling;
} else {
descending = false;
element = element.parentElement;
}
} else {
if (sibling) {
element = sibling;
descending = true;
} else {
element = element.parentElement;
}
}
if (element) {
classes = element.classList;
}
} while (element && !(
classes.contains("mx_RoomTile") ||
classes.contains("mx_RoomSubList_label") ||
classes.contains("mx_LeftPanel_filterRooms")));
if (element) {
ev.stopPropagation();
ev.preventDefault();
element.focus();
this.focusedElement = element;
} else if (trap) {
// if navigation is via up/down arrow-keys, trap in the widget so it doesn't send to composer
ev.stopPropagation();
ev.preventDefault();
}
},
onSearch: function(term) {
this.setState({ searchFilter: term });
},
onSearchCleared: function(source) {
if (source === "keyboard") {
dis.fire(Action.FocusComposer);
}
this.setState({searchExpanded: false});
},
collectRoomList: function(ref) {
this._roomList = ref;
},
_onSearchFocus: function() {
this.setState({searchExpanded: true});
},
_onSearchBlur: function(event) {
if (event.target.value.length === 0) {
this.setState({searchExpanded: false});
}
},
render: function() {
const RoomList = sdk.getComponent('rooms.RoomList');
const RoomBreadcrumbs = sdk.getComponent('rooms.RoomBreadcrumbs');
const TagPanel = sdk.getComponent('structures.TagPanel');
const CustomRoomTagPanel = sdk.getComponent('structures.CustomRoomTagPanel');
const TopLeftMenuButton = sdk.getComponent('structures.TopLeftMenuButton');
const SearchBox = sdk.getComponent('structures.SearchBox');
const CallPreview = sdk.getComponent('voip.CallPreview');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const tagPanelEnabled = SettingsStore.getValue("TagPanel.enableTagPanel");
let tagPanelContainer;
const isCustomTagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags");
if (tagPanelEnabled) {
tagPanelContainer = (<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel />
{ isCustomTagsEnabled ? <CustomRoomTagPanel /> : undefined }
</div>);
}
const containerClasses = classNames(
"mx_LeftPanel_container", "mx_fadable",
{
"collapsed": this.props.collapsed,
"mx_LeftPanel_container_hasTagPanel": tagPanelEnabled,
"mx_fadable_faded": this.props.disabled,
},
);
let exploreButton;
if (!this.props.collapsed) {
exploreButton = (
<div className={classNames("mx_LeftPanel_explore", {"mx_LeftPanel_explore_hidden": this.state.searchExpanded})}>
<AccessibleButton onClick={() => dis.fire(Action.ViewRoomDirectory)}>{_t("Explore")}</AccessibleButton>
</div>
);
}
const searchBox = (<SearchBox
className="mx_LeftPanel_filterRooms"
enableRoomSearchFocus={true}
blurredPlaceholder={ _t('Filter') }
placeholder={ _t('Filter rooms…') }
onKeyDown={this._onFilterKeyDown}
onSearch={ this.onSearch }
onCleared={ this.onSearchCleared }
onFocus={this._onSearchFocus}
onBlur={this._onSearchBlur}
collapsed={this.props.collapsed} />);
let breadcrumbs;
if (this.state.breadcrumbs) {
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}
const roomList = <RoomList
onKeyDown={this._onKeyDown}
onFocus={this._onFocus}
onBlur={this._onBlur}
ref={this.collectRoomList}
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapsed}
searchFilter={this.state.searchFilter}
ConferenceHandler={VectorConferenceHandler} />;
return (
<div className={containerClasses}>
{ tagPanelContainer }
<aside className="mx_LeftPanel dark-panel">
<TopLeftMenuButton collapsed={this.props.collapsed} />
{ breadcrumbs }
<CallPreview ConferenceHandler={VectorConferenceHandler} />
<div className="mx_LeftPanel_exploreAndFilterRow" onKeyDown={this._onKeyDown} onFocus={this._onFocus} onBlur={this._onBlur}>
{ exploreButton }
{ searchBox }
</div>
{roomList}
</aside>
</div>
);
},
});
export default LeftPanel;

View file

@ -20,24 +20,21 @@ import TagPanel from "./TagPanel";
import classNames from "classnames"; import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList2 from "../views/rooms/RoomList2"; import RoomList from "../views/rooms/RoomList";
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist2"; import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import UserMenu from "./UserMenu"; import UserMenu from "./UserMenu";
import RoomSearch from "./RoomSearch"; import RoomSearch from "./RoomSearch";
import AccessibleButton from "../views/elements/AccessibleButton"; import RoomBreadcrumbs from "../views/rooms/RoomBreadcrumbs";
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore";
import {Key} from "../../Keyboard"; import {Key} from "../../Keyboard";
import IndicatorScrollbar from "../structures/IndicatorScrollbar"; import IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
@ -53,12 +50,12 @@ interface IState {
const cssClasses = [ const cssClasses = [
"mx_RoomSearch_input", "mx_RoomSearch_input",
"mx_RoomSearch_icon", // minimized <RoomSearch /> "mx_RoomSearch_icon", // minimized <RoomSearch />
"mx_RoomSublist2_headerText", "mx_RoomSublist_headerText",
"mx_RoomTile2", "mx_RoomTile",
"mx_RoomSublist2_showNButton", "mx_RoomSublist_showNButton",
]; ];
export default class LeftPanel2 extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef(); private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string; private tagPanelWatcherRef: string;
private focusedElement = null; private focusedElement = null;
@ -122,7 +119,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private doStickyHeaders(list: HTMLDivElement) { private doStickyHeaders(list: HTMLDivElement) {
const topEdge = list.scrollTop; const topEdge = list.scrollTop;
const bottomEdge = list.offsetHeight + list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2"); const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin; const headerStickyWidth = list.clientWidth - headerRightMargin;
@ -138,7 +135,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
let lastTopHeader; let lastTopHeader;
let firstBottomHeader; let firstBottomHeader;
for (const sublist of sublists) { for (const sublist of sublists) {
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable"); const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist_stickable");
header.style.removeProperty("display"); // always clear display:none first header.style.removeProperty("display"); // always clear display:none first
// When an element is <=40% off screen, make it take over // When an element is <=40% off screen, make it take over
@ -174,8 +171,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
if (style.stickyTop) { if (style.stickyTop) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.add("mx_RoomSublist_headerContainer_stickyTop");
} }
const newTop = `${list.parentElement.offsetTop}px`; const newTop = `${list.parentElement.offsetTop}px`;
@ -183,8 +180,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.top = newTop; header.style.top = newTop;
} }
} else { } else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyTop")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop"); header.classList.remove("mx_RoomSublist_headerContainer_stickyTop");
} }
if (header.style.top) { if (header.style.top) {
header.style.removeProperty('top'); header.style.removeProperty('top');
@ -192,18 +189,18 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
} }
if (style.stickyBottom) { if (style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
} }
} else { } else {
if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom"); header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
} }
} }
if (style.stickyTop || style.stickyBottom) { if (style.stickyTop || style.stickyBottom) {
if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.add("mx_RoomSublist2_headerContainer_sticky"); header.classList.add("mx_RoomSublist_headerContainer_sticky");
} }
const newWidth = `${headerStickyWidth}px`; const newWidth = `${headerStickyWidth}px`;
@ -211,8 +208,8 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
header.style.width = newWidth; header.style.width = newWidth;
} }
} else if (!style.stickyTop && !style.stickyBottom) { } else if (!style.stickyTop && !style.stickyBottom) {
if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) { if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
header.classList.remove("mx_RoomSublist2_headerContainer_sticky"); header.classList.remove("mx_RoomSublist_headerContainer_sticky");
} }
if (header.style.width) { if (header.style.width) {
header.style.removeProperty('width'); header.style.removeProperty('width');
@ -222,16 +219,16 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
// add appropriate sticky classes to wrapper so it has // add appropriate sticky classes to wrapper so it has
// the necessary top/bottom padding to put the sticky header in // the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement; // .mx_LeftPanel2_roomListWrapper const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper
if (lastTopHeader) { if (lastTopHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyTop"); listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop");
} else { } else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyTop"); listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyTop");
} }
if (firstBottomHeader) { if (firstBottomHeader) {
listWrapper.classList.add("mx_LeftPanel2_roomListWrapper_stickyBottom"); listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyBottom");
} else { } else {
listWrapper.classList.remove("mx_LeftPanel2_roomListWrapper_stickyBottom"); listWrapper.classList.remove("mx_LeftPanel_roomListWrapper_stickyBottom");
} }
} }
@ -267,7 +264,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
}; };
private onEnter = () => { private onEnter = () => {
const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile2"); const firstRoom = this.listContainerRef.current.querySelector<HTMLDivElement>(".mx_RoomTile");
if (firstRoom) { if (firstRoom) {
firstRoom.click(); firstRoom.click();
return true; // to get the field to clear return true; // to get the field to clear
@ -315,7 +312,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderHeader(): React.ReactNode { private renderHeader(): React.ReactNode {
return ( return (
<div className="mx_LeftPanel2_userHeader"> <div className="mx_LeftPanel_userHeader">
<UserMenu isMinimized={this.props.isMinimized} /> <UserMenu isMinimized={this.props.isMinimized} />
</div> </div>
); );
@ -325,10 +322,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
if (this.state.showBreadcrumbs && !this.props.isMinimized) { if (this.state.showBreadcrumbs && !this.props.isMinimized) {
return ( return (
<IndicatorScrollbar <IndicatorScrollbar
className="mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar" className="mx_LeftPanel_breadcrumbsContainer mx_AutoHideScrollbar"
verticalScrollsHorizontally={true} verticalScrollsHorizontally={true}
> >
<RoomBreadcrumbs2 /> <RoomBreadcrumbs />
</IndicatorScrollbar> </IndicatorScrollbar>
); );
} }
@ -337,7 +334,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
private renderSearchExplore(): React.ReactNode { private renderSearchExplore(): React.ReactNode {
return ( return (
<div <div
className="mx_LeftPanel2_filterContainer" className="mx_LeftPanel_filterContainer"
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
@ -349,7 +346,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
onEnter={this.onEnter} onEnter={this.onEnter}
/> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_LeftPanel2_exploreButton" className="mx_LeftPanel_exploreButton"
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore rooms")} title={_t("Explore rooms")}
/> />
@ -359,12 +356,12 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const tagPanel = !this.state.showTagPanel ? null : ( const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel2_tagPanelContainer"> <div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/> <TagPanel/>
</div> </div>
); );
const roomList = <RoomList2 const roomList = <RoomList
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
resizeNotifier={null} resizeNotifier={null}
collapsed={false} collapsed={false}
@ -376,24 +373,24 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
/>; />;
const containerClasses = classNames({ const containerClasses = classNames({
"mx_LeftPanel2": true, "mx_LeftPanel": true,
"mx_LeftPanel2_hasTagPanel": !!tagPanel, "mx_LeftPanel_hasTagPanel": !!tagPanel,
"mx_LeftPanel2_minimized": this.props.isMinimized, "mx_LeftPanel_minimized": this.props.isMinimized,
}); });
const roomListClasses = classNames( const roomListClasses = classNames(
"mx_LeftPanel2_actualRoomListContainer", "mx_LeftPanel_actualRoomListContainer",
"mx_AutoHideScrollbar", "mx_AutoHideScrollbar",
); );
return ( return (
<div className={containerClasses}> <div className={containerClasses}>
{tagPanel} {tagPanel}
<aside className="mx_LeftPanel2_roomListContainer"> <aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()} {this.renderHeader()}
{this.renderSearchExplore()} {this.renderSearchExplore()}
{this.renderBreadcrumbs()} {this.renderBreadcrumbs()}
<div className="mx_LeftPanel2_roomListWrapper"> <div className="mx_LeftPanel_roomListWrapper">
<div <div
className={roomListClasses} className={roomListClasses}
onScroll={this.onScroll} onScroll={this.onScroll}

View file

@ -40,7 +40,6 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy";
import { DefaultTagID } from "../../stores/room-list/models"; import { DefaultTagID } from "../../stores/room-list/models";
import { import {
showToast as showSetPasswordToast, showToast as showSetPasswordToast,
@ -51,9 +50,10 @@ import {
hideToast as hideServerLimitToast hideToast as hideServerLimitToast
} from "../../toasts/ServerLimitToast"; } from "../../toasts/ServerLimitToast";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import LeftPanel2 from "./LeftPanel2"; import LeftPanel from "./LeftPanel";
import CallContainer from '../views/voip/CallContainer'; import CallContainer from '../views/voip/CallContainer';
import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import { ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import RoomListStore from "../../stores/room-list/RoomListStore";
// We need to fetch each pinned message individually (if we don't already have it) // 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. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -308,8 +308,8 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
onRoomStateEvents = (ev, state) => { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}; };
@ -328,11 +328,11 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
_updateServerNoticeEvents = async () => { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (!roomLists[DefaultTagID.ServerNotice]) return []; if (!serverNoticeList) return [];
const events = []; const events = [];
for (const room of roomLists[DefaultTagID.ServerNotice]) { for (const room of serverNoticeList) {
const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue;
@ -607,7 +607,6 @@ class LoggedInView extends React.Component<IProps, IState> {
}; };
render() { render() {
const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
const GroupView = sdk.getComponent('structures.GroupView'); const GroupView = sdk.getComponent('structures.GroupView');
@ -661,21 +660,12 @@ class LoggedInView extends React.Component<IProps, IState> {
bodyClasses += ' mx_MatrixChat_useCompactLayout'; bodyClasses += ' mx_MatrixChat_useCompactLayout';
} }
let leftPanel = ( const leftPanel = (
<LeftPanel <LeftPanel
resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false}
disabled={this.props.leftDisabled}
/>
);
if (SettingsStore.getValue("feature_new_room_list")) {
leftPanel = (
<LeftPanel2
isMinimized={this.props.collapseLhs || false} isMinimized={this.props.collapseLhs || false}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/> />
); );
}
return ( return (
<MatrixClientContext.Provider value={this._matrixClient}> <MatrixClientContext.Provider value={this._matrixClient}>

View file

@ -1,496 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 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 classNames from 'classnames';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import * as Unread from '../../Unread';
import * as RoomNotifs from '../../RoomNotifs';
import * as FormattingUtils from '../../utils/FormattingUtils';
import IndicatorScrollbar from './IndicatorScrollbar';
import {Key} from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler";
import {RovingTabIndexWrapper} from "../../accessibility/RovingTabIndex";
import {toPx} from "../../utils/units";
// turn this on for drop & drag console debugging galore
const debug = false;
class RoomTileErrorBoundary extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
error: null,
};
}
static getDerivedStateFromError(error) {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
console.error(
"The above error occured while React was rendering the following components:",
componentStack,
);
}
render() {
if (this.state.error) {
return (<div className="mx_RoomTile mx_RoomTileError">
{this.props.roomId}
</div>);
} else {
return this.props.children;
}
}
}
export default class RoomSubList extends React.PureComponent {
static displayName = 'RoomSubList';
static debug = debug;
static propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string.isRequired,
tagName: PropTypes.string,
addRoomLabel: PropTypes.string,
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
isInvite: PropTypes.bool,
startAsHidden: PropTypes.bool,
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
onHeaderClick: PropTypes.func,
incomingCall: PropTypes.object,
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
forceExpand: PropTypes.bool,
};
static defaultProps = {
onHeaderClick: function() {
}, // NOP
extraTiles: [],
isInvite: false,
};
static getDerivedStateFromProps(props, state) {
return {
listLength: props.list.length,
scrollTop: props.list.length === state.listLength ? state.scrollTop : 0,
};
}
constructor(props) {
super(props);
this.state = {
hidden: this.props.startAsHidden || false,
// some values to get LazyRenderList starting
scrollerHeight: 800,
scrollTop: 0,
// React 16's getDerivedStateFromProps(props, state) doesn't give the previous props so
// we have to store the length of the list here so we can see if it's changed or not...
listLength: null,
};
this._header = createRef();
this._subList = createRef();
this._scroller = createRef();
this._headerButton = createRef();
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
}
// The header is collapsible if it is hidden or not stuck
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
isCollapsibleOnClick() {
const stuck = this._header.current.dataset.stuck;
if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) {
return true;
} else {
return false;
}
}
onAction = (payload) => {
switch (payload.action) {
case 'on_room_read':
// XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched,
// but this is no longer true, so we must do it here (and can apply the small
// optimisation of checking that we care about the room being read).
//
// Ultimately we need to transition to a state pushing flow where something
// explicitly notifies the components concerned that the notif count for a room
// has change (e.g. a Flux store).
if (this.props.list.some((r) => r.roomId === payload.roomId)) {
this.forceUpdate();
}
break;
case 'view_room':
if (this.state.hidden && !this.props.forceExpand && payload.show_room_tile &&
this.props.list.some((r) => r.roomId === payload.room_id)
) {
this.toggle();
}
}
};
toggle = () => {
if (this.isCollapsibleOnClick()) {
// The header isCollapsible, so the click is to be interpreted as collapse and truncation logic
const isHidden = !this.state.hidden;
this.setState({hidden: isHidden}, () => {
this.props.onHeaderClick(isHidden);
});
} else {
// The header is stuck, so the click is to be interpreted as a scroll to the header
this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition);
}
};
onClick = (ev) => {
this.toggle();
};
onHeaderKeyDown = (ev) => {
switch (ev.key) {
case Key.ARROW_LEFT:
// On ARROW_LEFT collapse the room sublist
if (!this.state.hidden && !this.props.forceExpand) {
this.onClick();
}
ev.stopPropagation();
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
if (this.state.hidden && !this.props.forceExpand) {
// sublist is collapsed, expand it
this.onClick();
} else if (!this.props.forceExpand) {
// sublist is expanded, go to first room
const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile");
if (element) {
element.focus();
}
}
break;
}
}
};
onKeyDown = (ev) => {
switch (ev.key) {
// On ARROW_LEFT go to the sublist header
case Key.ARROW_LEFT:
ev.stopPropagation();
this._headerButton.current.focus();
break;
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
case Key.ARROW_RIGHT:
ev.stopPropagation();
}
};
onRoomTileClick = (roomId, ev) => {
dis.dispatch({
action: 'view_room',
show_room_tile: true, // to make sure the room gets scrolled into view
room_id: roomId,
clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)),
});
};
_updateSubListCount = () => {
// Force an update by setting the state to the current state
// Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate()
// method is honoured
this.setState(this.state);
};
makeRoomTile = (room) => {
return <RoomTileErrorBoundary roomId={room.roomId}><RoomTile
room={room}
roomSubList={this}
tagName={this.props.tagName}
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}
onClick={this.onRoomTileClick}
/></RoomTileErrorBoundary>;
};
_onNotifBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
const room = this.props.list.find(room => RoomNotifs.getRoomHasBadge(room));
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
}
};
_onInviteBadgeClick = (e) => {
// prevent the roomsublist collapsing
e.preventDefault();
e.stopPropagation();
// switch to first room in sortedList as that'll be the top of the list for the user
if (this.props.list && this.props.list.length > 0) {
dis.dispatch({
action: 'view_room',
room_id: this.props.list[0].roomId,
});
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
// Group Invites are different in that they are all extra tiles and not rooms
// XXX: this is a horrible special case because Group Invite sublist is a hack
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
dis.dispatch({
action: 'view_group',
group_id: this.props.extraTiles[0].props.group.groupId,
});
}
}
};
onAddRoom = (e) => {
e.stopPropagation();
if (this.props.onAddRoom) this.props.onAddRoom();
};
_getHeaderJsx(isCollapsed) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton');
const subListNotifications = !this.props.isInvite ?
RoomNotifs.aggregateNotificationCount(this.props.list) :
{count: 0, highlight: true};
const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight;
// When collapsed, allow a long hover on the header to show user
// the full tag name and room count
let title;
if (this.props.collapsed) {
title = this.props.label;
}
let incomingCall;
if (this.props.incomingCall) {
// We can assume that if we have an incoming call then it is for this list
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
incomingCall =
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
}
const len = this.props.list.length + this.props.extraTiles.length;
let chevron;
if (len) {
const chevronClasses = classNames({
'mx_RoomSubList_chevron': true,
'mx_RoomSubList_chevronRight': isCollapsed,
'mx_RoomSubList_chevronDown': !isCollapsed,
});
chevron = (<div className={chevronClasses} />);
}
return <RovingTabIndexWrapper inputRef={this._headerButton}>
{({onFocus, isActive, ref}) => {
const tabIndex = isActive ? 0 : -1;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
}
checkOverflow = () => {
if (this._scroller.current) {
this._scroller.current.checkOverflow();
}
};
setHeight = (height) => {
if (this._subList.current) {
this._subList.current.style.height = toPx(height);
}
this._updateLazyRenderHeight(height);
};
_updateLazyRenderHeight(height) {
this.setState({scrollerHeight: height});
}
_onScroll = () => {
this.setState({scrollTop: this._scroller.current.getScrollTop()});
};
_canUseLazyListRendering() {
// for now disable lazy rendering as they are already rendered tiles
// not rooms like props.list we pass to LazyRenderList
return !this.props.extraTiles || !this.props.extraTiles.length;
}
render() {
const len = this.props.list.length + this.props.extraTiles.length;
const isCollapsed = this.state.hidden && !this.props.forceExpand;
const subListClasses = classNames({
"mx_RoomSubList": true,
"mx_RoomSubList_hidden": len && isCollapsed,
"mx_RoomSubList_nonEmpty": len && !isCollapsed,
});
let content;
if (len) {
if (isCollapsed) {
// no body
} else if (this._canUseLazyListRendering()) {
content = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
<LazyRenderList
scrollTop={this.state.scrollTop }
height={ this.state.scrollerHeight }
renderItem={ this.makeRoomTile }
itemHeight={34}
items={ this.props.list } />
</IndicatorScrollbar>
);
} else {
const roomTiles = this.props.list.map(r => this.makeRoomTile(r));
const tiles = roomTiles.concat(this.props.extraTiles);
content = (
<IndicatorScrollbar ref={this._scroller} className="mx_RoomSubList_scroll" onScroll={this._onScroll}>
{ tiles }
</IndicatorScrollbar>
);
}
} else {
if (this.props.showSpinner && !isCollapsed) {
const Loader = sdk.getComponent("elements.Spinner");
content = <Loader />;
}
}
return (
<div
ref={this._subList}
className={subListClasses}
role="group"
aria-label={this.props.label}
onKeyDown={this.onKeyDown}
>
{ this._getHeaderJsx(isCollapsed) }
{ content }
</div>
);
}
}

View file

@ -1,158 +0,0 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import TopLeftMenu from '../views/context_menus/TopLeftMenu';
import BaseAvatar from '../views/avatars/BaseAvatar';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import * as Avatar from '../../Avatar';
import { _t } from '../../languageHandler';
import dis from "../../dispatcher/dispatcher";
import {ContextMenu, ContextMenuButton} from "./ContextMenu";
import {Action} from "../../dispatcher/actions";
const AVATAR_SIZE = 28;
export default class TopLeftMenuButton extends React.Component {
static propTypes = {
collapsed: PropTypes.bool.isRequired,
};
static displayName = 'TopLeftMenuButton';
constructor() {
super();
this.state = {
menuDisplayed: false,
profileInfo: null,
};
}
async _getProfileInfo() {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const profileInfo = await cli.getProfileInfo(userId);
const avatarUrl = Avatar.avatarUrlForUser(
{avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop");
return {
userId,
name: profileInfo.displayname,
avatarUrl,
};
}
async componentDidMount() {
this._dispatcherRef = dis.register(this.onAction);
try {
const profileInfo = await this._getProfileInfo();
this.setState({profileInfo});
} catch (ex) {
console.log("could not fetch profile");
console.error(ex);
}
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
}
onAction = (payload) => {
// For accessibility
if (payload.action === Action.ToggleUserMenu) {
if (this._buttonRef) this._buttonRef.click();
}
};
_getDisplayName() {
if (MatrixClientPeg.get().isGuest()) {
return _t("Guest");
} else if (this.state.profileInfo) {
return this.state.profileInfo.name;
} else {
return MatrixClientPeg.get().getUserId();
}
}
openMenu = (e) => {
e.preventDefault();
e.stopPropagation();
this.setState({ menuDisplayed: true });
};
closeMenu = () => {
this.setState({
menuDisplayed: false,
});
};
render() {
const cli = MatrixClientPeg.get().getUserId();
const name = this._getDisplayName();
let nameElement;
let chevronElement;
if (!this.props.collapsed) {
nameElement = <div className="mx_TopLeftMenuButton_name">
{ name }
</div>;
chevronElement = <span className="mx_TopLeftMenuButton_chevron" />;
}
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._buttonRef.getBoundingClientRect();
contextMenu = (
<ContextMenu
chevronFace="none"
left={elementRect.left}
top={elementRect.top + elementRect.height}
onFinished={this.closeMenu}
>
<TopLeftMenu displayName={name} userId={cli} onFinished={this.closeMenu} />
</ContextMenu>
);
}
return <React.Fragment>
<ContextMenuButton
className="mx_TopLeftMenuButton"
onClick={this.openMenu}
inputRef={(r) => this._buttonRef = r}
label={_t("Your profile")}
isExpanded={this.state.menuDisplayed}
>
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={name}
url={this.state.profileInfo && this.state.profileInfo.avatarUrl}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
resizeMethod="crop"
/>
{ nameElement }
{ chevronElement }
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
}
}

View file

@ -1,44 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'CreateRoomButton',
propTypes: {
onCreateRoom: PropTypes.func,
},
getDefaultProps: function() {
return {
onCreateRoom: function() {},
};
},
onClick: function() {
this.props.onCreateRoom();
},
render: function() {
return (
<button className="mx_CreateRoomButton" onClick={this.onClick}>{ _t("Create Room") }</button>
);
},
});

View file

@ -35,8 +35,8 @@ import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../
import {inviteMultipleToRoom} from "../../../RoomInvite"; import {inviteMultipleToRoom} from "../../../RoomInvite";
import {Key} from "../../../Keyboard"; import {Key} from "../../../Keyboard";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy";
import {DefaultTagID} from "../../../stores/room-list/models"; import {DefaultTagID} from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
export const KIND_DM = "dm"; export const KIND_DM = "dm";
export const KIND_INVITE = "invite"; export const KIND_INVITE = "invite";
@ -346,8 +346,7 @@ export default class InviteDialog extends React.PureComponent {
// Also pull in all the rooms tagged as DefaultTagID.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. // room list doesn't tag the room for the DMRoomMap, but does for the room list.
const taggedRooms = RoomListStoreTempProxy.getRoomLists(); const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM];
const dmTaggedRooms = taggedRooms[DefaultTagID.DM];
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
for (const dmRoom of dmTaggedRooms) { for (const dmRoom of dmTaggedRooms) {
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);

View file

@ -1,40 +0,0 @@
/*
Copyright 2017 Vector Creations 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 * as sdk from '../../../index';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const CreateRoomButton = function(props) {
const ActionButton = sdk.getComponent('elements.ActionButton');
return (
<ActionButton action="view_create_room"
mouseOverAction={props.callout ? "callout_create_room" : null}
label={_t("Create new room")}
iconPath={require("../../../../res/img/icons-create-room.svg")}
size={props.size}
tooltip={props.tooltip}
/>
);
};
CreateRoomButton.propTypes = {
size: PropTypes.string,
tooltip: PropTypes.bool,
};
export default CreateRoomButton;

View file

@ -28,6 +28,7 @@ import classNames from 'classnames';
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import CallView from "../voip/CallView";
export default createReactClass({ export default createReactClass({
@ -142,7 +143,6 @@ export default createReactClass({
}, },
render: function() { render: function() {
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
let fileDropTarget = null; let fileDropTarget = null;

View file

@ -1,53 +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 React from 'react';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
export default class InviteOnlyIcon extends React.Component {
constructor() {
super();
this.state = {
hover: false,
};
}
onHoverStart = () => {
this.setState({hover: true});
};
onHoverEnd = () => {
this.setState({hover: false});
};
render() {
const classes = this.props.collapsedPanel ? "mx_InviteOnlyIcon_small": "mx_InviteOnlyIcon_large";
const Tooltip = sdk.getComponent("elements.Tooltip");
let tooltip;
if (this.state.hover) {
tooltip = <Tooltip className="mx_InviteOnlyIcon_tooltip" label={_t("Invite only")} dir="auto" />;
}
return (<div className={classes}
onMouseEnter={this.onHoverStart}
onMouseLeave={this.onHoverEnd}
>
{ tooltip }
</div>);
}
}

View file

@ -1,394 +0,0 @@
/*
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, {createRef} from "react";
import dis from "../../../dispatcher/dispatcher";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import RoomAvatar from '../avatars/RoomAvatar';
import classNames from 'classnames';
import * as sdk from "../../../index";
import Analytics from "../../../Analytics";
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from "../../../utils/FormattingUtils";
import DMRoomMap from "../../../utils/DMRoomMap";
import {_t} from "../../../languageHandler";
const MAX_ROOMS = 20;
const MIN_ROOMS_BEFORE_ENABLED = 10;
// The threshold time in milliseconds to wait for an autojoined room to show up.
const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90 seconds
export default class RoomBreadcrumbs extends React.Component {
constructor(props) {
super(props);
this.state = {rooms: [], enabled: false};
this.onAction = this.onAction.bind(this);
this._dispatcherRef = null;
// The room IDs we're waiting to come down the Room handler and when we
// started waiting for them. Used to track a room over an upgrade/autojoin.
this._waitingRoomQueue = [/* { roomId, addedTs } */];
this._scroller = createRef();
}
// TODO: [REACT-WARNING] Move this to constructor
UNSAFE_componentWillMount() { // eslint-disable-line camelcase
this._dispatcherRef = dis.register(this.onAction);
const storedRooms = SettingsStore.getValue("breadcrumb_rooms");
this._loadRoomIds(storedRooms || []);
this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged);
this.setState({enabled: this._shouldEnable()});
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().on("Room", this.onRoom);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
SettingsStore.unwatchSetting(this._settingWatchRef);
const client = MatrixClientPeg.get();
if (client) {
client.removeListener("Room.myMembership", this.onMyMembership);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Event.decrypted", this.onEventDecrypted);
client.removeListener("Room", this.onRoom);
}
}
componentDidUpdate() {
const rooms = this.state.rooms.slice();
if (rooms.length) {
const roomModel = rooms[0];
if (!roomModel.animated) {
roomModel.animated = true;
setTimeout(() => this.setState({rooms}), 0);
}
}
}
onAction(payload) {
switch (payload.action) {
case 'view_room':
if (payload.auto_join && !MatrixClientPeg.get().getRoom(payload.room_id)) {
// Queue the room instead of pushing it immediately - we're probably just waiting
// for a join to complete (ie: joining the upgraded room).
this._waitingRoomQueue.push({roomId: payload.room_id, addedTs: (new Date).getTime()});
break;
}
this._appendRoomId(payload.room_id);
break;
// XXX: slight hack in order to zero the notification count when a room
// is read. Copied from RoomTile
case 'on_room_read': {
const room = MatrixClientPeg.get().getRoom(payload.roomId);
this._calculateRoomBadges(room, /*zero=*/true);
break;
}
}
}
onMyMembership = (room, membership) => {
if (membership === "leave" || membership === "ban") {
const rooms = this.state.rooms.slice();
const roomState = rooms.find((r) => r.room.roomId === room.roomId);
if (roomState) {
roomState.left = true;
this.setState({rooms});
}
}
this.onRoomMembershipChanged();
};
onRoomReceipt = (event, room) => {
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onRoomTimeline = (event, room) => {
if (!room) return; // Can be null for the notification timeline, etc.
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onEventDecrypted = (event) => {
if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) {
this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId()));
}
};
onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => {
if (!value) return;
const currentState = this.state.rooms.map((r) => r.room.roomId);
if (currentState.length === value.length) {
let changed = false;
for (let i = 0; i < currentState.length; i++) {
if (currentState[i] !== value[i]) {
changed = true;
break;
}
}
if (!changed) return;
}
this._loadRoomIds(value);
};
onRoomMembershipChanged = () => {
if (!this.state.enabled && this._shouldEnable()) {
this.setState({enabled: true});
}
};
onRoom = (room) => {
// Always check for membership changes when we see new rooms
this.onRoomMembershipChanged();
const waitingRoom = this._waitingRoomQueue.find(r => r.roomId === room.roomId);
if (!waitingRoom) return;
this._waitingRoomQueue.splice(this._waitingRoomQueue.indexOf(waitingRoom), 1);
const now = (new Date()).getTime();
if ((now - waitingRoom.addedTs) > AUTOJOIN_WAIT_THRESHOLD_MS) return; // Too long ago.
this._appendRoomId(room.roomId); // add the room we've been waiting for
};
_shouldEnable() {
const client = MatrixClientPeg.get();
const joinedRoomCount = client.getRooms().reduce((count, r) => {
return count + (r.getMyMembership() === "join" ? 1 : 0);
}, 0);
return joinedRoomCount >= MIN_ROOMS_BEFORE_ENABLED;
}
_loadRoomIds(roomIds) {
if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms
// If we're here, the list changed.
const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => {
const badges = this._calculateBadgesForRoom(r) || {};
return {
room: r,
animated: false,
...badges,
};
});
this.setState({
rooms: rooms,
});
}
_calculateBadgesForRoom(room, zero=false) {
if (!room) return null;
// Reset the notification variables for simplicity
const roomModel = {
redBadge: false,
formattedCount: "0",
showCount: false,
};
if (zero) return roomModel;
const notifState = RoomNotifs.getRoomNotifsState(room.roomId);
if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) {
const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight');
const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room);
const redBadge = highlightNotifs > 0;
const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState));
if (redBadge || greyBadge) {
const notifCount = redBadge ? highlightNotifs : unreadNotifs;
const limitedCount = FormattingUtils.formatCount(notifCount);
roomModel.redBadge = redBadge;
roomModel.formattedCount = limitedCount;
roomModel.showCount = true;
}
}
return roomModel;
}
_calculateRoomBadges(room, zero=false) {
if (!room) return;
const rooms = this.state.rooms.slice();
const roomModel = rooms.find((r) => r.room.roomId === room.roomId);
if (!roomModel) return; // No applicable room, so don't do math on it
const badges = this._calculateBadgesForRoom(room, zero);
if (!badges) return; // No badges for some reason
Object.assign(roomModel, badges);
this.setState({rooms});
}
_appendRoomId(roomId) {
let room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return;
const rooms = this.state.rooms.slice();
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
if (history.length > 1) {
room = history[history.length - 1]; // Last room is most recent
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1);
}
}
const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
rooms.splice(0, 0, {room, animated: false});
if (rooms.length > MAX_ROOMS) {
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
this.setState({rooms});
if (this._scroller.current) {
this._scroller.current.moveToOrigin();
}
// We don't track room aesthetics (badges, membership, etc) over the wire so we
// don't need to do this elsewhere in the file. Just where we alter the room IDs
// and their order.
const roomIds = rooms.map((r) => r.room.roomId);
if (roomIds.length > 0) {
SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
_viewRoom(room, index) {
Analytics.trackEvent("Breadcrumbs", "click_node", index);
dis.dispatch({action: "view_room", room_id: room.roomId});
}
_onMouseEnter(room) {
this._onHover(room);
}
_onMouseLeave(room) {
this._onHover(null); // clear hover states
}
_onHover(room) {
const rooms = this.state.rooms.slice();
for (const r of rooms) {
r.hover = room && r.room.roomId === room.roomId;
}
this.setState({rooms});
}
_isDmRoom(room) {
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
return Boolean(dmRooms);
}
render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
// check for collapsed here and not at parent so we keep rooms in our state
// when collapsing and expanding
if (this.props.collapsed || !this.state.enabled) {
return null;
}
const rooms = this.state.rooms;
const avatars = rooms.map((r, i) => {
const isFirst = i === 0;
const classes = classNames({
"mx_RoomBreadcrumbs_crumb": true,
"mx_RoomBreadcrumbs_preAnimate": isFirst && !r.animated,
"mx_RoomBreadcrumbs_animate": isFirst,
"mx_RoomBreadcrumbs_left": r.left,
});
let tooltip = null;
if (r.hover) {
tooltip = <Tooltip label={r.room.name} />;
}
let badge;
if (r.showCount) {
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': true,
'mx_RoomTile_badgeRed': r.redBadge,
'mx_RoomTile_badgeUnread': !r.redBadge,
});
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
}
return (
<AccessibleButton
className={classes}
key={r.room.roomId}
onClick={() => this._viewRoom(r.room, i)}
onMouseEnter={() => this._onMouseEnter(r.room)}
onMouseLeave={() => this._onMouseLeave(r.room)}
aria-label={_t("Room %(name)s", {name: r.room.name})}
>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
{tooltip}
</AccessibleButton>
);
});
return (
<div role="toolbar" aria-label={_t("Recent rooms")}>
<IndicatorScrollbar
ref={this._scroller}
className="mx_RoomBreadcrumbs"
trackHorizontalOverflow={true}
verticalScrollsHorizontally={true}
>
{ avatars }
</IndicatorScrollbar>
</div>
);
}
}

View file

@ -23,13 +23,11 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import Analytics from "../../../Analytics"; import Analytics from "../../../Analytics";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { CSSTransition } from "react-transition-group"; import { CSSTransition } from "react-transition-group";
import RoomListStore from "../../../stores/room-list/RoomListStore2"; import RoomListStore from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models"; import { DefaultTagID } from "../../../stores/room-list/models";
import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex";
import Toolbar from "../../../accessibility/Toolbar"; import Toolbar from "../../../accessibility/Toolbar";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
interface IProps { interface IProps {
} }
@ -44,7 +42,7 @@ interface IState {
skipFirst: boolean; skipFirst: boolean;
} }
export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState> { export default class RoomBreadcrumbs extends React.PureComponent<IProps, IState> {
private isMounted = true; private isMounted = true;
constructor(props: IProps) { constructor(props: IProps) {
@ -88,12 +86,12 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
return ( return (
<RovingAccessibleTooltipButton <RovingAccessibleTooltipButton
className="mx_RoomBreadcrumbs2_crumb" className="mx_RoomBreadcrumbs_crumb"
key={r.roomId} key={r.roomId}
onClick={() => this.viewRoom(r, i)} onClick={() => this.viewRoom(r, i)}
aria-label={_t("Room %(name)s", {name: r.name})} aria-label={_t("Room %(name)s", {name: r.name})}
title={r.name} title={r.name}
tooltipClassName="mx_RoomBreadcrumbs2_Tooltip" tooltipClassName="mx_RoomBreadcrumbs_Tooltip"
> >
<DecoratedRoomAvatar <DecoratedRoomAvatar
room={r} room={r}
@ -111,17 +109,17 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
return ( return (
<CSSTransition <CSSTransition
appear={true} in={this.state.doAnimation} timeout={640} appear={true} in={this.state.doAnimation} timeout={640}
classNames='mx_RoomBreadcrumbs2' classNames='mx_RoomBreadcrumbs'
> >
<Toolbar className='mx_RoomBreadcrumbs2'> <Toolbar className='mx_RoomBreadcrumbs'>
{tiles.slice(this.state.skipFirst ? 1 : 0)} {tiles.slice(this.state.skipFirst ? 1 : 0)}
</Toolbar> </Toolbar>
</CSSTransition> </CSSTransition>
); );
} else { } else {
return ( return (
<div className='mx_RoomBreadcrumbs2'> <div className='mx_RoomBreadcrumbs'>
<div className="mx_RoomBreadcrumbs2_placeholder"> <div className="mx_RoomBreadcrumbs_placeholder">
{_t("No recently visited rooms")} {_t("No recently visited rooms")}
</div> </div>
</div> </div>

View file

@ -1,35 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 createReactClass from 'create-react-class';
export default createReactClass({
displayName: 'RoomDropTarget',
render: function() {
return (
<div className="mx_RoomDropTarget_container">
<div className="mx_RoomDropTarget">
<div className="mx_RoomDropTarget_label">
{ this.props.label }
</div>
</div>
</div>
);
},
});

View file

@ -1,838 +0,0 @@
/*
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 SettingsStore from "../../../settings/SettingsStore";
import Timer from "../../../utils/Timer";
import React from "react";
import ReactDOM from "react-dom";
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import * as utils from "matrix-js-sdk/src/utils";
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import rate_limited_func from "../../../ratelimitedfunc";
import * as Rooms from '../../../Rooms';
import DMRoomMap from '../../../utils/DMRoomMap';
import TagOrderStore from '../../../stores/TagOrderStore';
import CustomRoomTagStore from '../../../stores/CustomRoomTagStore';
import GroupStore from '../../../stores/GroupStore';
import RoomSubList from '../../structures/RoomSubList';
import ResizeHandle from '../elements/ResizeHandle';
import CallHandler from "../../../CallHandler";
import dis from "../../../dispatcher/dispatcher";
import * as sdk from "../../../index";
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";
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))$/;
const HOVER_MOVE_TIMEOUT = 1000;
function labelForTagName(tagName) {
if (tagName.startsWith('u.')) return tagName.slice(2);
return tagName;
}
export default createReactClass({
displayName: 'RoomList',
propTypes: {
ConferenceHandler: PropTypes.any,
collapsed: PropTypes.bool.isRequired,
searchFilter: PropTypes.string,
},
getInitialState: function() {
this._hoverClearTimer = null;
this._subListRefs = {
// key => RoomSubList ref
};
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
this.subListSizes = sizesJson ? JSON.parse(sizesJson) : {};
this.collapsedState = collapsedJson ? JSON.parse(collapsedJson) : {};
this._layoutSections = [];
const unfilteredOptions = {
allowWhitespace: false,
handleHeight: 1,
};
this._unfilteredlayout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
// update overflow indicators
this._checkSubListsOverflow();
// don't store height for collapsed sublists
if (!this.collapsedState[key]) {
this.subListSizes[key] = size;
window.localStorage.setItem("mx_roomlist_sizes",
JSON.stringify(this.subListSizes));
}
}, this.subListSizes, this.collapsedState, unfilteredOptions);
this._filteredLayout = new Layout((key, size) => {
const subList = this._subListRefs[key];
if (subList) {
subList.setHeight(size);
}
}, null, null, {
allowWhitespace: false,
handleHeight: 0,
});
this._layout = this._unfilteredlayout;
return {
isLoadingLeftRooms: false,
totalRoomCount: null,
lists: {},
incomingCallTag: null,
incomingCall: null,
selectedTags: [],
hover: false,
customTags: CustomRoomTagStore.getTags(),
};
},
// TODO: [REACT-WARNING] Replace component with real class, put this in the constructor.
UNSAFE_componentWillMount: function() {
this.mounted = false;
const cli = MatrixClientPeg.get();
cli.on("Room", this.onRoom);
cli.on("deleteRoom", this.onDeleteRoom);
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("Event.decrypted", this.onEventDecrypted);
cli.on("accountData", this.onAccountData);
cli.on("Group.myMembership", this._onGroupMyMembership);
cli.on("RoomState.events", this.onRoomStateEvents);
const dmRoomMap = DMRoomMap.shared();
// A map between tags which are group IDs and the room IDs of rooms that should be kept
// in the room list when filtering by that tag.
this._visibleRoomsForGroup = {
// $groupId: [$roomId1, $roomId2, ...],
};
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
// Listen to updates to group data. RoomList cares about members and rooms in order
// to filter the room list when group tags are selected.
this._groupStoreToken = GroupStore.registerListener(null, () => {
(TagOrderStore.getOrderedTags() || []).forEach((tag) => {
if (tag[0] !== '+') {
return;
}
// This group's rooms or members may have updated, update rooms for its tag
this.updateVisibleRoomsForTag(dmRoomMap, tag);
this.updateVisibleRooms();
});
});
this._tagStoreToken = TagOrderStore.addListener(() => {
// Filters themselves have changed
this.updateVisibleRooms();
});
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => {
this._delayedRefreshRoomList();
});
if (SettingsStore.isFeatureEnabled("feature_custom_tags")) {
this._customTagStoreToken = CustomRoomTagStore.addListener(() => {
this.setState({
customTags: CustomRoomTagStore.getTags(),
});
});
}
this.refreshRoomList();
// order of the sublists
//this.listOrder = [];
// loop count to stop a stack overflow if the user keeps waggling the
// mouse for >30s in a row, or if running under mocha
this._delayedRefreshRoomListLoopCount = 0;
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
const cfg = {
getLayout: () => this._layout,
};
this.resizer = new Resizer(this.resizeContainer, Distributor, cfg);
this.resizer.setClassNames({
handle: "mx_ResizeHandle",
vertical: "mx_ResizeHandle_vertical",
reverse: "mx_ResizeHandle_reverse",
});
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.offsetHeight,
);
this._checkSubListsOverflow();
this.resizer.attach();
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("leftPanelResized", this.onResize);
}
this.mounted = true;
},
componentDidUpdate: function(prevProps) {
let forceLayoutUpdate = false;
this._repositionIncomingCallBox(undefined, false);
if (!this.props.searchFilter && prevProps.searchFilter) {
this._layout = this._unfilteredlayout;
forceLayoutUpdate = true;
} else if (this.props.searchFilter && !prevProps.searchFilter) {
this._layout = this._filteredLayout;
forceLayoutUpdate = true;
}
this._layout.update(
this._layoutSections,
this.resizeContainer && this.resizeContainer.clientHeight,
forceLayoutUpdate,
);
this._checkSubListsOverflow();
},
onAction: function(payload) {
switch (payload.action) {
case 'call_state':
var call = CallHandler.getCall(payload.room_id);
if (call && call.call_state === 'ringing') {
this.setState({
incomingCall: call,
incomingCallTag: this.getTagNameForRoomId(payload.room_id),
});
this._repositionIncomingCallBox(undefined, true);
} else {
this.setState({
incomingCall: null,
incomingCallTag: null,
});
}
break;
case 'view_room_delta': {
const currentRoomId = RoomViewStore.getRoomId();
const {
"im.vector.fake.invite": inviteRooms,
"m.favourite": favouriteRooms,
[TAG_DM]: dmRooms,
"im.vector.fake.recent": recentRooms,
"m.lowpriority": lowPriorityRooms,
"im.vector.fake.archived": historicalRooms,
"m.server_notice": serverNoticeRooms,
...tags
} = this.state.lists;
const shownCustomTagRooms = Object.keys(tags).filter(tagName => {
return (!this.state.customTags || this.state.customTags[tagName]) &&
!tagName.match(STANDARD_TAGS_REGEX);
}).map(tagName => tags[tagName]);
// this order matches the one when generating the room sublists below.
let rooms = this._applySearchFilter([
...inviteRooms,
...favouriteRooms,
...dmRooms,
...recentRooms,
...[].concat.apply([], shownCustomTagRooms), // eslint-disable-line prefer-spread
...lowPriorityRooms,
...historicalRooms,
...serverNoticeRooms,
], this.props.searchFilter);
if (payload.unread) {
// filter to only notification rooms (and our current active room so we can index properly)
rooms = rooms.filter(room => {
return room.roomId === currentRoomId || Unread.doesRoomHaveUnreadMessages(room);
});
}
const currentIndex = rooms.findIndex(room => room.roomId === currentRoomId);
// use slice to account for looping around the start
const [room] = rooms.slice((currentIndex + payload.delta) % rooms.length);
if (room) {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
show_room_tile: true, // to make sure the room gets scrolled into view
});
}
break;
}
}
},
componentWillUnmount: function() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("deleteRoom", this.onDeleteRoom);
MatrixClientPeg.get().removeListener("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().removeListener("RoomMember.name", this.onRoomMemberName);
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
if (this.props.resizeNotifier) {
this.props.resizeNotifier.removeListener("leftPanelResized", this.onResize);
}
if (this._tagStoreToken) {
this._tagStoreToken.remove();
}
if (this._roomListStoreToken) {
this._roomListStoreToken.remove();
}
if (this._customTagStoreToken) {
this._customTagStoreToken.remove();
}
// NB: GroupStore is not a Flux.Store
if (this._groupStoreToken) {
this._groupStoreToken.unregister();
}
// cancel any pending calls to the rate_limited_funcs
this._delayedRefreshRoomList.cancelPendingCall();
},
onResize: function() {
if (this.mounted && this._layout && this.resizeContainer &&
Array.isArray(this._layoutSections)
) {
this._layout.update(
this._layoutSections,
this.resizeContainer.offsetHeight,
);
}
},
onRoom: function(room) {
this.updateVisibleRooms();
},
onRoomStateEvents: function(ev, state) {
if (ev.getType() === "m.room.create" || ev.getType() === "m.room.tombstone") {
this.updateVisibleRooms();
}
},
onDeleteRoom: function(roomId) {
this.updateVisibleRooms();
},
onArchivedHeaderClick: function(isHidden, scrollToPosition) {
if (!isHidden) {
const self = this;
this.setState({ isLoadingLeftRooms: true });
// we don't care about the response since it comes down via "Room"
// events.
MatrixClientPeg.get().syncLeftRooms().catch(function(err) {
console.error("Failed to sync left rooms: %s", err);
console.error(err);
}).finally(function() {
self.setState({ isLoadingLeftRooms: false });
});
}
},
onRoomReceipt: function(receiptEvent, room) {
// because if we read a notification, it will affect notification count
// only bother updating if there's a receipt from us
if (Receipt.findReadReceiptFromUserId(receiptEvent, MatrixClientPeg.get().credentials.userId)) {
this._delayedRefreshRoomList();
}
},
onRoomMemberName: function(ev, member) {
this._delayedRefreshRoomList();
},
onEventDecrypted: function(ev) {
// An event being decrypted may mean we need to re-order the room list
this._delayedRefreshRoomList();
},
onAccountData: function(ev) {
if (ev.getType() == 'm.direct') {
this._delayedRefreshRoomList();
}
},
_onGroupMyMembership: function(group) {
this.forceUpdate();
},
onMouseMove: async function(ev) {
if (!this._hoverClearTimer) {
this.setState({hover: true});
this._hoverClearTimer = new Timer(HOVER_MOVE_TIMEOUT);
this._hoverClearTimer.start();
let finished = true;
try {
await this._hoverClearTimer.finished();
} catch (err) {
finished = false;
}
this._hoverClearTimer = null;
if (finished) {
this.setState({hover: false});
this._delayedRefreshRoomList();
}
} else {
this._hoverClearTimer.restart();
}
},
onMouseLeave: function(ev) {
if (this._hoverClearTimer) {
this._hoverClearTimer.abort();
this._hoverClearTimer = null;
}
this.setState({hover: false});
// Refresh the room list just in case the user missed something.
this._delayedRefreshRoomList();
},
_delayedRefreshRoomList: rate_limited_func(function() {
this.refreshRoomList();
}, 500),
// Update which rooms and users should appear in RoomList for a given group tag
updateVisibleRoomsForTag: function(dmRoomMap, tag) {
if (!this.mounted) return;
// For now, only handle group tags
if (tag[0] !== '+') return;
this._visibleRoomsForGroup[tag] = [];
GroupStore.getGroupRooms(tag).forEach((room) => this._visibleRoomsForGroup[tag].push(room.roomId));
GroupStore.getGroupMembers(tag).forEach((member) => {
if (member.userId === MatrixClientPeg.get().credentials.userId) return;
dmRoomMap.getDMRoomsForUserId(member.userId).forEach(
(roomId) => this._visibleRoomsForGroup[tag].push(roomId),
);
});
// TODO: Check if room has been tagged to the group by the user
},
// Update which rooms and users should appear according to which tags are selected
updateVisibleRooms: function() {
const selectedTags = TagOrderStore.getSelectedTags();
const visibleGroupRooms = [];
selectedTags.forEach((tag) => {
(this._visibleRoomsForGroup[tag] || []).forEach(
(roomId) => visibleGroupRooms.push(roomId),
);
});
// If there are any tags selected, constrain the rooms listed to the
// visible rooms as determined by visibleGroupRooms. Here, we
// de-duplicate and filter out rooms that the client doesn't know
// about (hence the Set and the null-guard on `room`).
if (selectedTags.length > 0) {
const roomSet = new Set();
visibleGroupRooms.forEach((roomId) => {
const room = MatrixClientPeg.get().getRoom(roomId);
if (room) {
roomSet.add(room);
}
});
this._visibleRooms = Array.from(roomSet);
} else {
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
}
this._delayedRefreshRoomList();
},
refreshRoomList: function() {
if (this.state.hover) {
// Don't re-sort the list if we're hovering over the list
return;
}
// TODO: ideally we'd calculate this once at start, and then maintain
// any changes to it incrementally, updating the appropriate sublists
// as needed.
// Alternatively we'd do something magical with Immutable.js or similar.
const lists = this.getRoomLists();
let totalRooms = 0;
for (const l of Object.values(lists)) {
totalRooms += l.length;
}
this.setState({
lists,
totalRoomCount: totalRooms,
// Do this here so as to not render every time the selected tags
// themselves change.
selectedTags: TagOrderStore.getSelectedTags(),
}, () => {
// we don't need to restore any size here, do we?
// i guess we could have triggered a new group to appear
// that already an explicit size the last time it appeared ...
this._checkSubListsOverflow();
});
// this._lastRefreshRoomListTs = Date.now();
},
getTagNameForRoomId: function(roomId) {
const lists = RoomListStoreTempProxy.getRoomLists();
for (const tagName of Object.keys(lists)) {
for (const room of lists[tagName]) {
// Should be impossible, but guard anyways.
if (!room) {
continue;
}
const myUserId = MatrixClientPeg.get().getUserId();
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(room, myUserId, this.props.ConferenceHandler)) {
continue;
}
if (room.roomId === roomId) return tagName;
}
}
return null;
},
getRoomLists: function() {
const lists = RoomListStoreTempProxy.getRoomLists();
const filteredLists = {};
const isRoomVisible = {
// $roomId: true,
};
this._visibleRooms.forEach((r) => {
isRoomVisible[r.roomId] = true;
});
Object.keys(lists).forEach((tagName) => {
const filteredRooms = lists[tagName].filter((taggedRoom) => {
// Somewhat impossible, but guard against it anyway
if (!taggedRoom) {
return;
}
const myUserId = MatrixClientPeg.get().getUserId();
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) {
return;
}
return Boolean(isRoomVisible[taggedRoom.roomId]);
});
if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) {
filteredLists[tagName] = filteredRooms;
}
});
return filteredLists;
},
_getScrollNode: function() {
if (!this.mounted) return null;
const panel = ReactDOM.findDOMNode(this);
if (!panel) return null;
if (panel.classList.contains('gm-prevented')) {
return panel;
} else {
return panel.children[2]; // XXX: Fragile!
}
},
_repositionIncomingCallBox: function(e, firstTime) {
const incomingCallBox = document.getElementById("incomingCallBox");
if (incomingCallBox && incomingCallBox.parentElement) {
const scrollArea = this._getScrollNode();
if (!scrollArea) return;
// Use the offset of the top of the scroll area from the window
// as this is used to calculate the CSS fixed top position for the stickies
const scrollAreaOffset = scrollArea.getBoundingClientRect().top + window.pageYOffset;
// Use the offset of the top of the component from the window
// as this is used to calculate the CSS fixed top position for the stickies
const scrollAreaHeight = ReactDOM.findDOMNode(this).getBoundingClientRect().height;
let top = (incomingCallBox.parentElement.getBoundingClientRect().top + window.pageYOffset);
// Make sure we don't go too far up, if the headers aren't sticky
top = (top < scrollAreaOffset) ? scrollAreaOffset : top;
// make sure we don't go too far down, if the headers aren't sticky
const bottomMargin = scrollAreaOffset + (scrollAreaHeight - 45);
top = (top > bottomMargin) ? bottomMargin : top;
incomingCallBox.style.top = top + "px";
incomingCallBox.style.left = scrollArea.offsetLeft + scrollArea.offsetWidth + 12 + "px";
}
},
_makeGroupInviteTiles(filter) {
const ret = [];
const lcFilter = filter && filter.toLowerCase();
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
for (const group of MatrixClientPeg.get().getGroups()) {
const {groupId, name, myMembership} = group;
// filter to only groups in invite state and group_id starts with filter or group name includes it
if (myMembership !== 'invite') continue;
if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) &&
!(name && name.toLowerCase().includes(lcFilter))) continue;
ret.push(<GroupInviteTile key={groupId} group={group} collapsed={this.props.collapsed} />);
}
return ret;
},
_applySearchFilter: function(list, filter) {
if (filter === "") return list;
const lcFilter = filter.toLowerCase();
// apply toLowerCase before and after removeHiddenChars because different rules get applied
// e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H
const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase();
// case insensitive if room name includes filter,
// or if starts with `#` and one of room's aliases starts with filter
return list.filter((room) => {
if (filter[0] === "#") {
if (room.getCanonicalAlias() && room.getCanonicalAlias().toLowerCase().startsWith(lcFilter)) {
return true;
}
if (room.getAltAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) {
return true;
}
}
return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter);
});
},
_handleCollapsedState: function(key, collapsed) {
// persist collapsed state
this.collapsedState[key] = collapsed;
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.collapsedState));
// load the persisted size configuration of the expanded sub list
if (collapsed) {
this._layout.collapseSection(key);
} else {
this._layout.expandSection(key, this.subListSizes[key]);
}
// check overflow, as sub lists sizes have changed
// important this happens after calling resize above
this._checkSubListsOverflow();
},
// check overflow for scroll indicator gradient
_checkSubListsOverflow() {
Object.values(this._subListRefs).forEach(l => l.checkOverflow());
},
_subListRef: function(key, ref) {
if (!ref) {
delete this._subListRefs[key];
} else {
this._subListRefs[key] = ref;
}
},
_mapSubListProps: function(subListsProps) {
this._layoutSections = [];
const defaultProps = {
collapsed: this.props.collapsed,
isFiltered: !!this.props.searchFilter,
};
subListsProps.forEach((p) => {
p.list = this._applySearchFilter(p.list, this.props.searchFilter);
});
subListsProps = subListsProps.filter((props => {
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
return len !== 0 || props.onAddRoom;
}));
return subListsProps.reduce((components, props, i) => {
props = {...defaultProps, ...props};
const isLast = i === subListsProps.length - 1;
const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0);
const {key, label, onHeaderClick, ...otherProps} = props;
const chosenKey = key || label;
const onSubListHeaderClick = (collapsed) => {
this._handleCollapsedState(chosenKey, collapsed);
if (onHeaderClick) {
onHeaderClick(collapsed);
}
};
const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey];
this._layoutSections.push({
id: chosenKey,
count: len,
});
const subList = (<RoomSubList
ref={this._subListRef.bind(this, chosenKey)}
startAsHidden={startAsHidden}
forceExpand={!!this.props.searchFilter}
onHeaderClick={onSubListHeaderClick}
key={chosenKey}
label={label}
{...otherProps} />);
if (!isLast) {
return components.concat(
subList,
<ResizeHandle key={chosenKey+"-resizer"} vertical={true} id={chosenKey} />
);
} else {
return components.concat(subList);
}
}, []);
},
_collectResizeContainer: function(el) {
this.resizeContainer = el;
},
render: function() {
const incomingCallIfTaggedAs = (tagName) => {
if (!this.state.incomingCall) return null;
if (this.state.incomingCallTag !== tagName) return null;
return this.state.incomingCall;
};
let subLists = [
{
list: [],
extraTiles: this._makeGroupInviteTiles(this.props.searchFilter),
label: _t('Community Invites'),
isInvite: true,
},
{
list: this.state.lists['im.vector.fake.invite'],
label: _t('Invites'),
incomingCall: incomingCallIfTaggedAs('im.vector.fake.invite'),
isInvite: true,
},
{
list: this.state.lists['m.favourite'],
label: _t('Favourites'),
tagName: "m.favourite",
incomingCall: incomingCallIfTaggedAs('m.favourite'),
},
{
list: this.state.lists[DefaultTagID.DM],
label: _t('Direct Messages'),
tagName: DefaultTagID.DM,
incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM),
onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});},
addRoomLabel: _t("Start chat"),
},
{
list: this.state.lists['im.vector.fake.recent'],
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)
.filter((tagName) => {
return (!this.state.customTags || this.state.customTags[tagName]) &&
!tagName.match(STANDARD_TAGS_REGEX);
}).map((tagName) => {
return {
list: this.state.lists[tagName],
key: tagName,
label: labelForTagName(tagName),
tagName: tagName,
incomingCall: incomingCallIfTaggedAs(tagName),
};
});
subLists = subLists.concat(tagSubLists);
subLists = subLists.concat([
{
list: this.state.lists['m.lowpriority'],
label: _t('Low priority'),
tagName: "m.lowpriority",
incomingCall: incomingCallIfTaggedAs('m.lowpriority'),
},
{
list: this.state.lists['im.vector.fake.archived'],
label: _t('Historical'),
incomingCall: incomingCallIfTaggedAs('im.vector.fake.archived'),
startAsHidden: true,
showSpinner: this.state.isLoadingLeftRooms,
onHeaderClick: this.onArchivedHeaderClick,
},
{
list: this.state.lists['m.server_notice'],
label: _t('System Alerts'),
tagName: "m.lowpriority",
incomingCall: incomingCallIfTaggedAs('m.server_notice'),
},
]);
const subListComponents = this._mapSubListProps(subLists);
const {resizeNotifier, collapsed, searchFilter, ConferenceHandler, onKeyDown, ...props} = this.props; // eslint-disable-line
return (
<RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({onKeyDownHandler}) => <div
{...props}
onKeyDown={onKeyDownHandler}
ref={this._collectResizeContainer}
className="mx_RoomList"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to
// overflow:scroll;, so force it out of tab order.
tabIndex="-1"
onMouseMove={this.onMouseMove}
onMouseLeave={this.onMouseLeave}
>
{ subListComponents }
</div> }
</RovingTabIndexProvider>
);
},
});

View file

@ -23,13 +23,13 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2"; import RoomSublist from "./RoomSublist";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -42,8 +42,6 @@ import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDelta
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
onFocus: (ev: React.FocusEvent) => void; onFocus: (ev: React.FocusEvent) => void;
@ -132,7 +130,7 @@ const TAG_AESTHETICS: {
}, },
}; };
export default class RoomList2 extends React.Component<IProps, IState> { export default class RoomList extends React.Component<IProps, IState> {
private searchFilter: NameFilterCondition = new NameFilterCondition(); private searchFilter: NameFilterCondition = new NameFilterCondition();
private dispatcherRef; private dispatcherRef;
@ -212,7 +210,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
private updateLists = () => { private updateLists = () => {
const newLists = RoomListStore.instance.orderedLists; const newLists = RoomListStore.instance.orderedLists;
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("new lists", newLists); console.log("new lists", newLists);
} }
@ -277,7 +275,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
components.push( components.push(
<RoomSublist2 <RoomSublist
key={`sublist-${orderedTagId}`} key={`sublist-${orderedTagId}`}
tagId={orderedTagId} tagId={orderedTagId}
forRooms={true} forRooms={true}
@ -306,7 +304,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
onFocus={this.props.onFocus} onFocus={this.props.onFocus}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
onKeyDown={onKeyDownHandler} onKeyDown={onKeyDownHandler}
className="mx_RoomList2" className="mx_RoomList"
role="tree" role="tree"
aria-label={_t("Rooms")} aria-label={_t("Rooms")}
>{sublists}</div> >{sublists}</div>

View file

@ -23,7 +23,7 @@ import classNames from 'classnames';
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../../views/elements/AccessibleButton"; import AccessibleButton from "../../views/elements/AccessibleButton";
import RoomTile2 from "./RoomTile2"; import RoomTile from "./RoomTile";
import { ListLayout } from "../../../stores/room-list/ListLayout"; import { ListLayout } from "../../../stores/room-list/ListLayout";
import { import {
ChevronFace, ChevronFace,
@ -32,7 +32,7 @@ import {
StyledMenuItemCheckbox, StyledMenuItemCheckbox,
StyledMenuItemRadio, StyledMenuItemRadio,
} from "../../structures/ContextMenu"; } from "../../structures/ContextMenu";
import RoomListStore from "../../../stores/room-list/RoomListStore2"; import RoomListStore from "../../../stores/room-list/RoomListStore";
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
@ -48,8 +48,6 @@ import { polyfillTouchEvent } from "../../../@types/polyfill";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
export const HEADER_HEIGHT = 32; // As defined by CSS export const HEADER_HEIGHT = 32; // As defined by CSS
@ -94,7 +92,7 @@ interface IState {
height: number; height: number;
} }
export default class RoomSublist2 extends React.Component<IProps, IState> { export default class RoomSublist extends React.Component<IProps, IState> {
private headerButton = createRef<HTMLDivElement>(); private headerButton = createRef<HTMLDivElement>();
private sublistRef = createRef<HTMLDivElement>(); private sublistRef = createRef<HTMLDivElement>();
private dispatcherRef: string; private dispatcherRef: string;
@ -144,7 +142,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
private get numTiles(): number { private get numTiles(): number {
return RoomSublist2.calcNumTiles(this.props); return RoomSublist.calcNumTiles(this.props);
} }
private static calcNumTiles(props) { private static calcNumTiles(props) {
@ -167,7 +165,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} }
// as the rooms can come in one by one we need to reevaluate // as the rooms can come in one by one we need to reevaluate
// the amount of available rooms to cap the amount of requested visible rooms by the layout // the amount of available rooms to cap the amount of requested visible rooms by the layout
if (RoomSublist2.calcNumTiles(prevProps) !== this.numTiles) { if (RoomSublist.calcNumTiles(prevProps) !== this.numTiles) {
this.setState({height: this.calculateInitialHeight()}); this.setState({height: this.calculateInitialHeight()});
} }
} }
@ -252,7 +250,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
private focusRoomTile = (index: number) => { private focusRoomTile = (index: number) => {
if (!this.sublistRef.current) return; if (!this.sublistRef.current) return;
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile2"); const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile");
const element = elements && elements[index]; const element = elements && elements[index];
if (element) { if (element) {
element.focus(); element.focus();
@ -326,11 +324,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const possibleSticky = this.headerButton.current.parentElement; const possibleSticky = this.headerButton.current.parentElement;
const sublist = possibleSticky.parentElement.parentElement; const sublist = possibleSticky.parentElement.parentElement;
const list = sublist.parentElement.parentElement; const list = sublist.parentElement.parentElement;
// the scrollTop is capped at the height of the header in LeftPanel2, the top header is always sticky // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
const isAtTop = list.scrollTop <= HEADER_HEIGHT; const isAtTop = list.scrollTop <= HEADER_HEIGHT;
const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight; const isAtBottom = list.scrollTop >= list.scrollHeight - list.offsetHeight;
const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyTop'); const isStickyTop = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyTop');
const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_stickyBottom'); const isStickyBottom = possibleSticky.classList.contains('mx_RoomSublist_headerContainer_stickyBottom');
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) { if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
// is sticky - jump to list // is sticky - jump to list
@ -370,7 +368,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
this.toggleCollapsed(); this.toggleCollapsed();
} else if (this.sublistRef.current) { } else if (this.sublistRef.current) {
// otherwise focus the first room // otherwise focus the first room
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement; const element = this.sublistRef.current.querySelector(".mx_RoomTile") as HTMLDivElement;
if (element) { if (element) {
element.focus(); element.focus();
} }
@ -405,7 +403,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
for (const room of visibleRooms) { for (const room of visibleRooms) {
tiles.push( tiles.push(
<RoomTile2 <RoomTile
room={room} room={room}
key={`room-${room.roomId}`} key={`room-${room.roomId}`}
showMessagePreview={this.layout.showPreviews} showMessagePreview={this.layout.showPreviews}
@ -444,7 +442,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<React.Fragment> <React.Fragment>
<hr /> <hr />
<div> <div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Appearance")}</div> <div className='mx_RoomSublist_contextMenu_title'>{_t("Appearance")}</div>
<StyledMenuItemCheckbox <StyledMenuItemCheckbox
onClose={this.onCloseMenu} onClose={this.onCloseMenu}
onChange={this.onUnreadFirstChanged} onChange={this.onUnreadFirstChanged}
@ -471,9 +469,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height} top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
onFinished={this.onCloseMenu} onFinished={this.onCloseMenu}
> >
<div className="mx_RoomSublist2_contextMenu"> <div className="mx_RoomSublist_contextMenu">
<div> <div>
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div> <div className='mx_RoomSublist_contextMenu_title'>{_t("Sort by")}</div>
<StyledMenuItemRadio <StyledMenuItemRadio
onClose={this.onCloseMenu} onClose={this.onCloseMenu}
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)} onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
@ -500,7 +498,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
return ( return (
<React.Fragment> <React.Fragment>
<ContextMenuTooltipButton <ContextMenuTooltipButton
className="mx_RoomSublist2_menuButton" className="mx_RoomSublist_menuButton"
onClick={this.onOpenMenuClick} onClick={this.onOpenMenuClick}
title={_t("List options")} title={_t("List options")}
isExpanded={!!this.state.contextMenuPosition} isExpanded={!!this.state.contextMenuPosition}
@ -537,26 +535,26 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
<AccessibleTooltipButton <AccessibleTooltipButton
tabIndex={tabIndex} tabIndex={tabIndex}
onClick={this.onAddRoom} onClick={this.onAddRoom}
className="mx_RoomSublist2_auxButton" className="mx_RoomSublist_auxButton"
aria-label={this.props.addRoomLabel || _t("Add room")} aria-label={this.props.addRoomLabel || _t("Add room")}
title={this.props.addRoomLabel} title={this.props.addRoomLabel}
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"} tooltipClassName={"mx_RoomSublist_addRoomTooltip"}
/> />
); );
} }
const collapseClasses = classNames({ const collapseClasses = classNames({
'mx_RoomSublist2_collapseBtn': true, 'mx_RoomSublist_collapseBtn': true,
'mx_RoomSublist2_collapseBtn_collapsed': !this.state.isExpanded, 'mx_RoomSublist_collapseBtn_collapsed': !this.state.isExpanded,
}); });
const classes = classNames({ const classes = classNames({
'mx_RoomSublist2_headerContainer': true, 'mx_RoomSublist_headerContainer': true,
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton, 'mx_RoomSublist_headerContainer_withAux': !!addRoomButton,
}); });
const badgeContainer = ( const badgeContainer = (
<div className="mx_RoomSublist2_badgeContainer"> <div className="mx_RoomSublist_badgeContainer">
{badge} {badge}
</div> </div>
); );
@ -573,12 +571,12 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// The same applies to the notification badge. // The same applies to the notification badge.
return ( return (
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}> <div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
<div className="mx_RoomSublist2_stickable"> <div className="mx_RoomSublist_stickable">
<Button <Button
onFocus={onFocus} onFocus={onFocus}
inputRef={ref} inputRef={ref}
tabIndex={tabIndex} tabIndex={tabIndex}
className="mx_RoomSublist2_headerText" className="mx_RoomSublist_headerText"
role="treeitem" role="treeitem"
aria-expanded={this.state.isExpanded} aria-expanded={this.state.isExpanded}
aria-level={1} aria-level={1}
@ -611,9 +609,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
public render(): React.ReactElement { public render(): React.ReactElement {
const visibleTiles = this.renderVisibleTiles(); const visibleTiles = this.renderVisibleTiles();
const classes = classNames({ const classes = classNames({
'mx_RoomSublist2': true, 'mx_RoomSublist': true,
'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition, 'mx_RoomSublist_hasMenuOpen': !!this.state.contextMenuPosition,
'mx_RoomSublist2_minimized': this.props.isMinimized, 'mx_RoomSublist_minimized': this.props.isMinimized,
}); });
let content = null; let content = null;
@ -626,7 +624,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding); const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding); let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
const showMoreBtnClasses = classNames({ const showMoreBtnClasses = classNames({
'mx_RoomSublist2_showNButton': true, 'mx_RoomSublist_showNButton': true,
}); });
// If we're hiding rooms, show a 'show more' button to the user. This button // If we're hiding rooms, show a 'show more' button to the user. This button
@ -640,14 +638,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight); const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
const numMissing = this.numTiles - amountFullyShown; const numMissing = this.numTiles - amountFullyShown;
let showMoreText = ( let showMoreText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist_showNButtonText'>
{_t("Show %(count)s more", {count: numMissing})} {_t("Show %(count)s more", {count: numMissing})}
</span> </span>
); );
if (this.props.isMinimized) showMoreText = null; if (this.props.isMinimized) showMoreText = null;
showNButton = ( showNButton = (
<RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}> <RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}>
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'> <span className='mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */} {/* set by CSS masking */}
</span> </span>
{showMoreText} {showMoreText}
@ -656,14 +654,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
} else if (this.numTiles > this.layout.defaultVisibleTiles) { } else if (this.numTiles > this.layout.defaultVisibleTiles) {
// we have all tiles visible - add a button to show less // we have all tiles visible - add a button to show less
let showLessText = ( let showLessText = (
<span className='mx_RoomSublist2_showNButtonText'> <span className='mx_RoomSublist_showNButtonText'>
{_t("Show less")} {_t("Show less")}
</span> </span>
); );
if (this.props.isMinimized) showLessText = null; if (this.props.isMinimized) showLessText = null;
showNButton = ( showNButton = (
<RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}> <RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}>
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'> <span className='mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron'>
{/* set by CSS masking */} {/* set by CSS masking */}
</span> </span>
{showLessText} {showLessText}
@ -698,8 +696,8 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
// only mathematically 7 possible). // only mathematically 7 possible).
const handleWrapperClasses = classNames({ const handleWrapperClasses = classNames({
'mx_RoomSublist2_resizerHandles': true, 'mx_RoomSublist_resizerHandles': true,
'mx_RoomSublist2_resizerHandles_showNButton': !!showNButton, 'mx_RoomSublist_resizerHandles_showNButton': !!showNButton,
}); });
content = ( content = (
@ -712,11 +710,11 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
onResizeStop={this.onResizeStop} onResizeStop={this.onResizeStop}
onResize={this.onResize} onResize={this.onResize}
handleWrapperClass={handleWrapperClasses} handleWrapperClass={handleWrapperClasses}
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}} handleClasses={{bottom: "mx_RoomSublist_resizerHandle"}}
className="mx_RoomSublist2_resizeBox" className="mx_RoomSublist_resizeBox"
enable={handles} enable={handles}
> >
<div className="mx_RoomSublist2_tiles" onScroll={this.onScrollPrevent}> <div className="mx_RoomSublist_tiles" onScroll={this.onScrollPrevent}>
{visibleTiles} {visibleTiles}
</div> </div>
{showNButton} {showNButton}

View file

@ -1,565 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import classNames from 'classnames';
import dis from '../../../dispatcher/dispatcher';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as sdk from '../../../index';
import {ContextMenu, ContextMenuButton, toRightOf} from '../../structures/ContextMenu';
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler";
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
import E2EIcon from './E2EIcon';
import InviteOnlyIcon from './InviteOnlyIcon';
// eslint-disable-next-line camelcase
import rate_limited_func from '../../../ratelimitedfunc';
import { shieldStatusForRoom } from '../../../utils/ShieldUtils';
export default createReactClass({
displayName: 'RoomTile',
propTypes: {
onClick: PropTypes.func,
room: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired,
unread: PropTypes.bool.isRequired,
highlight: PropTypes.bool.isRequired,
// If true, apply mx_RoomTile_transparent class
transparent: PropTypes.bool,
isInvite: PropTypes.bool.isRequired,
incomingCall: PropTypes.object,
},
getDefaultProps: function() {
return {
isDragging: false,
};
},
getInitialState: function() {
const joinRules = this.props.room.currentState.getStateEvents("m.room.join_rules", "");
const joinRule = joinRules && joinRules.getContent().join_rule;
return ({
joinRule,
hover: false,
badgeHover: false,
contextMenuPosition: null, // DOM bounding box, null if non-shown
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
notificationCount: this.props.room.getUnreadNotificationCount(),
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
statusMessage: this._getStatusMessage(),
e2eStatus: null,
});
},
_shouldShowStatusMessage() {
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
return false;
}
const isInvite = this.props.room.getMyMembership() === "invite";
const isJoined = this.props.room.getMyMembership() === "join";
const looksLikeDm = this.props.room.getInvitedAndJoinedMemberCount() === 2;
return !isInvite && isJoined && looksLikeDm;
},
_getStatusMessageUser() {
if (!MatrixClientPeg.get()) return null; // We've probably been logged out
const selfId = MatrixClientPeg.get().getUserId();
const otherMember = this.props.room.currentState.getMembersExcept([selfId])[0];
if (!otherMember) {
return null;
}
return otherMember.user;
},
_getStatusMessage() {
const statusUser = this._getStatusMessageUser();
if (!statusUser) {
return "";
}
return statusUser._unstable_statusMessage;
},
onRoomStateMember: function(ev, state, member) {
// we only care about leaving users
// because trust state will change if someone joins a megolm session anyway
if (member.membership !== "leave") {
return;
}
// ignore members in other rooms
if (member.roomId !== this.props.room.roomId) {
return;
}
this._updateE2eStatus();
},
onUserVerificationChanged: function(userId, _trustStatus) {
if (!this.props.room.getMember(userId)) {
// Not in this room
return;
}
this._updateE2eStatus();
},
onCrossSigningKeysChanged: function() {
this._updateE2eStatus();
},
onRoomTimeline: function(ev, room) {
if (!room) return;
if (room.roomId != this.props.room.roomId) return;
if (ev.getType() !== "m.room.encryption") return;
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
this.onFindingRoomToBeEncrypted();
},
onFindingRoomToBeEncrypted: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("userTrustStatusChanged", this.onUserVerificationChanged);
cli.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
this._updateE2eStatus();
},
_updateE2eStatus: async function() {
const cli = MatrixClientPeg.get();
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
e2eStatus: await shieldStatusForRoom(cli, this.props.room),
});
},
onRoomName: function(room) {
if (room !== this.props.room) return;
this.setState({
roomName: this.props.room.name,
});
},
onJoinRule: function(ev) {
if (ev.getType() !== "m.room.join_rules") return;
if (ev.getRoomId() !== this.props.room.roomId) return;
this.setState({ joinRule: ev.getContent().join_rule });
},
onAccountData: function(accountDataEvent) {
if (accountDataEvent.getType() === 'm.push_rules') {
this.setState({
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
});
}
},
onAction: function(payload) {
switch (payload.action) {
// XXX: slight hack in order to zero the notification count when a room
// is read. Ideally this state would be given to this via props (as we
// do with `unread`). This is still better than forceUpdating the entire
// RoomList when a room is read.
case 'on_room_read':
if (payload.roomId !== this.props.room.roomId) break;
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
break;
// RoomTiles are one of the few components that may show custom status and
// also remain on screen while in Settings toggling the feature. This ensures
// you can clearly see the status hide and show when toggling the feature.
case 'feature_custom_status_changed':
this.forceUpdate();
break;
case 'view_room':
// when the room is selected make sure its tile is visible, for breadcrumbs/keyboard shortcut access
if (payload.room_id === this.props.room.roomId && payload.show_room_tile) {
this._scrollIntoView();
}
break;
}
},
_scrollIntoView: function() {
if (!this._roomTile.current) return;
this._roomTile.current.scrollIntoView({
block: "nearest",
behavior: "auto",
});
},
_onActiveRoomChange: function() {
this.setState({
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
});
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._roomTile = createRef();
},
componentDidMount: function() {
/* We bind here rather than in the definition because otherwise we wind up with the
method only being callable once every 500ms across all instances, which would be wrong */
this._updateE2eStatus = rate_limited_func(this._updateE2eStatus, 500);
const cli = MatrixClientPeg.get();
cli.on("accountData", this.onAccountData);
cli.on("Room.name", this.onRoomName);
cli.on("RoomState.events", this.onJoinRule);
if (cli.isRoomEncrypted(this.props.room.roomId)) {
this.onFindingRoomToBeEncrypted();
} else {
cli.on("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.addListener(this.props.room.roomId, this._onActiveRoomChange);
this.dispatcherRef = dis.register(this.onAction);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
}
}
// when we're first rendered (or our sublist is expanded) make sure we are visible if we're active
if (this.state.selected) {
this._scrollIntoView();
}
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
MatrixClientPeg.get().removeListener("Room.name", this.onRoomName);
cli.removeListener("RoomState.events", this.onJoinRule);
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
cli.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged);
cli.removeListener("Room.timeline", this.onRoomTimeline);
}
ActiveRoomObserver.removeListener(this.props.room.roomId, this._onActiveRoomChange);
dis.unregister(this.dispatcherRef);
if (this._shouldShowStatusMessage()) {
const statusUser = this._getStatusMessageUser();
if (statusUser) {
statusUser.removeListener(
"User._unstable_statusMessage",
this._onStatusMessageCommitted,
);
}
}
},
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps: function(props) {
// XXX: This could be a lot better - this makes the assumption that
// the notification count may have changed when the properties of
// the room tile change.
this.setState({
notificationCount: this.props.room.getUnreadNotificationCount(),
});
},
// Do a simple shallow comparison of props and state to avoid unnecessary
// renders. The assumption made here is that only state and props are used
// in rendering this component and children.
//
// RoomList is frequently made to forceUpdate, so this decreases number of
// RoomTile renderings.
shouldComponentUpdate: function(newProps, newState) {
if (Object.keys(newProps).some((k) => newProps[k] !== this.props[k])) {
return true;
}
if (Object.keys(newState).some((k) => newState[k] !== this.state[k])) {
return true;
}
return false;
},
_onStatusMessageCommitted() {
// The status message `User` object has observed a message change.
this.setState({
statusMessage: this._getStatusMessage(),
});
},
onClick: function(ev) {
if (this.props.onClick) {
this.props.onClick(this.props.room.roomId, ev);
}
},
onMouseEnter: function() {
this.setState( { hover: true });
this.badgeOnMouseEnter();
},
onMouseLeave: function() {
this.setState( { hover: false });
this.badgeOnMouseLeave();
},
badgeOnMouseEnter: function() {
// Only allow non-guests to access the context menu
// and only change it if it needs to change
if (!MatrixClientPeg.get().isGuest() && !this.state.badgeHover) {
this.setState( { badgeHover: true } );
}
},
badgeOnMouseLeave: function() {
this.setState( { badgeHover: false } );
},
_showContextMenu: function(boundingClientRect) {
// Only allow non-guests to access the context menu
if (MatrixClientPeg.get().isGuest()) return;
const state = {
contextMenuPosition: boundingClientRect,
};
// If the badge is clicked, then no longer show tooltip
if (this.props.collapsed) {
state.hover = false;
}
this.setState(state);
},
onContextMenuButtonClick: function(e) {
// Prevent the RoomTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
this._showContextMenu(e.target.getBoundingClientRect());
},
onContextMenu: function(e) {
// Prevent the native context menu
e.preventDefault();
this._showContextMenu({
right: e.clientX,
top: e.clientY,
height: 0,
});
},
closeMenu: function() {
this.setState({
contextMenuPosition: null,
});
this.props.refreshSubList();
},
render: function() {
const isInvite = this.props.room.getMyMembership() === "invite";
const notificationCount = this.props.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
const notifBadges = notificationCount > 0 && RoomNotifs.shouldShowNotifBadge(this.state.notifState);
const mentionBadges = this.props.highlight && RoomNotifs.shouldShowMentionBadge(this.state.notifState);
const badges = notifBadges || mentionBadges;
let subtext = null;
if (this._shouldShowStatusMessage()) {
subtext = this.state.statusMessage;
}
const isMenuDisplayed = Boolean(this.state.contextMenuPosition);
const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId);
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': !badges,
'mx_RoomTile_transparent': this.props.transparent,
'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed,
});
const avatarClasses = classNames({
'mx_RoomTile_avatar': true,
});
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': this.state.badgeHover || isMenuDisplayed,
});
let name = this.state.roomName;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badge;
if (badges) {
const limitedCount = FormattingUtils.formatCount(notificationCount);
const badgeContent = notificationCount ? limitedCount : '!';
badge = <div className={badgeClasses}>{ badgeContent }</div>;
}
let label;
let subtextLabel;
let tooltip;
if (!this.props.collapsed) {
const nameClasses = classNames({
'mx_RoomTile_name': true,
'mx_RoomTile_invite': this.props.isInvite,
'mx_RoomTile_badgeShown': badges || this.state.badgeHover || isMenuDisplayed,
});
subtextLabel = subtext ? <span className="mx_RoomTile_subtext">{ subtext }</span> : null;
// XXX: this is a workaround for Firefox giving this div a tabstop :( [tabIndex]
label = <div title={name} className={nameClasses} tabIndex={-1} dir="auto">{ name }</div>;
} else if (this.state.hover) {
const Tooltip = sdk.getComponent("elements.Tooltip");
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
}
//var incomingCallBox;
//if (this.props.incomingCall) {
// var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
// incomingCallBox = <IncomingCallBox incomingCall={ this.props.incomingCall }/>;
//}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let contextMenuButton;
if (!MatrixClientPeg.get().isGuest()) {
contextMenuButton = (
<ContextMenuButton
className="mx_RoomTile_menuButton"
label={_t("Options")}
isExpanded={isMenuDisplayed}
onClick={this.onContextMenuButtonClick} />
);
}
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
let ariaLabel = name;
let dmOnline;
const { room } = this.props;
const member = room.getMember(dmUserId);
if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) {
const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot');
dmOnline = <UserOnlineDot userId={dmUserId} />;
}
// The following labels are written in such a fashion to increase screen reader efficiency (speed).
if (notifBadges && mentionBadges && !isInvite) {
ariaLabel += " " + _t("%(count)s unread messages including mentions.", {
count: notificationCount,
});
} else if (notifBadges) {
ariaLabel += " " + _t("%(count)s unread messages.", { count: notificationCount });
} else if (mentionBadges && !isInvite) {
ariaLabel += " " + _t("Unread mentions.");
} else if (this.props.unread) {
ariaLabel += " " + _t("Unread messages.");
}
let contextMenu;
if (isMenuDisplayed) {
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
contextMenu = (
<ContextMenu {...toRightOf(this.state.contextMenuPosition)} onFinished={this.closeMenu}>
<RoomTileContextMenu room={this.props.room} onFinished={this.closeMenu} />
</ContextMenu>
);
}
let privateIcon = null;
if (this.state.joinRule === "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}
let e2eIcon = null;
if (this.state.e2eStatus) {
e2eIcon = <E2EIcon status={this.state.e2eStatus} className="mx_RoomTile_e2eIcon" />;
}
return <React.Fragment>
<RovingTabIndexWrapper inputRef={this._roomTile}>
{({onFocus, isActive, ref}) =>
<AccessibleButton
onFocus={onFocus}
tabIndex={isActive ? 0 : -1}
inputRef={ref}
className={classes}
onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onContextMenu={this.onContextMenu}
aria-label={ariaLabel}
aria-selected={this.state.selected}
role="treeitem"
>
<div className={avatarClasses}>
<div className="mx_RoomTile_avatar_container">
<RoomAvatar room={this.props.room} width={24} height={24} />
{ e2eIcon }
</div>
</div>
{ privateIcon }
<div className="mx_RoomTile_nameContainer">
<div className="mx_RoomTile_labelContainer">
{ label }
{ subtextLabel }
</div>
{ dmOnline }
{ contextMenuButton }
{ badge }
</div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu }
</React.Fragment>;
},
});

View file

@ -48,7 +48,7 @@ import {
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import NotificationBadge from "./NotificationBadge"; import NotificationBadge from "./NotificationBadge";
import { Volume } from "../../../RoomNotifsTypes"; import { Volume } from "../../../RoomNotifsTypes";
import RoomListStore from "../../../stores/room-list/RoomListStore2"; import RoomListStore from "../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../actions/RoomListActions"; import RoomListActions from "../../../actions/RoomListActions";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ActionPayload} from "../../../dispatcher/payloads"; import {ActionPayload} from "../../../dispatcher/payloads";
@ -56,8 +56,6 @@ import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNo
import { NotificationState } from "../../../stores/notifications/NotificationState"; import { NotificationState } from "../../../stores/notifications/NotificationState";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
interface IProps { interface IProps {
room: Room; room: Room;
showMessagePreview: boolean; showMessagePreview: boolean;
@ -77,7 +75,7 @@ interface IState {
generalMenuPosition: PartialDOMRect; generalMenuPosition: PartialDOMRect;
} }
const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`; const messagePreviewId = (roomId: string) => `mx_RoomTile_messagePreview_${roomId}`;
const contextMenuBelow = (elementRect: PartialDOMRect) => { const contextMenuBelow = (elementRect: PartialDOMRect) => {
// align the context menu's icons with the icon which opened the context menu // align the context menu's icons with the icon which opened the context menu
@ -96,12 +94,12 @@ interface INotifOptionProps {
const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassName, label}) => { const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassName, label}) => {
const classes = classNames({ const classes = classNames({
mx_RoomTile2_contextMenu_activeRow: active, mx_RoomTile_contextMenu_activeRow: active,
}); });
let activeIcon; let activeIcon;
if (active) { if (active) {
activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconCheck" />; activeIcon = <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconCheck" />;
} }
return ( return (
@ -113,7 +111,7 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
); );
}; };
export default class RoomTile2 extends React.Component<IProps, IState> { export default class RoomTile extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private roomTileRef = createRef<HTMLDivElement>(); private roomTileRef = createRef<HTMLDivElement>();
@ -328,30 +326,30 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
if (this.state.notificationsMenuPosition) { if (this.state.notificationsMenuPosition) {
contextMenu = ( contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}> <ContextMenu {...contextMenuBelow(this.state.notificationsMenuPosition)} onFinished={this.onCloseNotificationsMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList"> <div className="mx_IconizedContextMenu_optionList">
<NotifOption <NotifOption
label={_t("Use default")} label={_t("Use default")}
active={state === ALL_MESSAGES} active={state === ALL_MESSAGES}
iconClassName="mx_RoomTile2_iconBell" iconClassName="mx_RoomTile_iconBell"
onClick={this.onClickAllNotifs} onClick={this.onClickAllNotifs}
/> />
<NotifOption <NotifOption
label={_t("All messages")} label={_t("All messages")}
active={state === ALL_MESSAGES_LOUD} active={state === ALL_MESSAGES_LOUD}
iconClassName="mx_RoomTile2_iconBellDot" iconClassName="mx_RoomTile_iconBellDot"
onClick={this.onClickAlertMe} onClick={this.onClickAlertMe}
/> />
<NotifOption <NotifOption
label={_t("Mentions & Keywords")} label={_t("Mentions & Keywords")}
active={state === MENTIONS_ONLY} active={state === MENTIONS_ONLY}
iconClassName="mx_RoomTile2_iconBellMentions" iconClassName="mx_RoomTile_iconBellMentions"
onClick={this.onClickMentions} onClick={this.onClickMentions}
/> />
<NotifOption <NotifOption
label={_t("None")} label={_t("None")}
active={state === MUTE} active={state === MUTE}
iconClassName="mx_RoomTile2_iconBellCrossed" iconClassName="mx_RoomTile_iconBellCrossed"
onClick={this.onClickMute} onClick={this.onClickMute}
/> />
</div> </div>
@ -360,16 +358,16 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
); );
} }
const classes = classNames("mx_RoomTile2_notificationsButton", { const classes = classNames("mx_RoomTile_notificationsButton", {
// Show bell icon for the default case too. // Show bell icon for the default case too.
mx_RoomTile2_iconBell: state === ALL_MESSAGES, mx_RoomTile_iconBell: state === ALL_MESSAGES,
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD, mx_RoomTile_iconBellDot: state === ALL_MESSAGES_LOUD,
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY, mx_RoomTile_iconBellMentions: state === MENTIONS_ONLY,
mx_RoomTile2_iconBellCrossed: state === MUTE, mx_RoomTile_iconBellCrossed: state === MUTE,
// Only show the icon by default if the room is overridden to muted. // Only show the icon by default if the room is overridden to muted.
// TODO: [FTUE Notifications] Probably need to detect global mute state // TODO: [FTUE Notifications] Probably need to detect global mute state
mx_RoomTile2_notificationsButton_show: state === MUTE, mx_RoomTile_notificationsButton_show: state === MUTE,
}); });
return ( return (
@ -392,18 +390,18 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room); const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
const isFavorite = roomTags.includes(DefaultTagID.Favourite); const isFavorite = roomTags.includes(DefaultTagID.Favourite);
const favouriteIconClassName = isFavorite ? "mx_RoomTile2_iconFavorite" : "mx_RoomTile2_iconStar"; const favouriteIconClassName = isFavorite ? "mx_RoomTile_iconFavorite" : "mx_RoomTile_iconStar";
const favouriteLabelClassName = isFavorite ? "mx_RoomTile2_contextMenu_activeRow" : ""; const favouriteLabelClassName = isFavorite ? "mx_RoomTile_contextMenu_activeRow" : "";
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite"); const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
let contextMenu = null; let contextMenu = null;
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) { if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
contextMenu = ( contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}> <ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow"> <div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onForgetRoomClick} label={_t("Leave Room")}> <MenuItem onClick={this.onForgetRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" /> <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Forget Room")}</span> <span className="mx_IconizedContextMenu_label">{_t("Forget Room")}</span>
</MenuItem> </MenuItem>
</div> </div>
@ -413,7 +411,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
} else if (this.state.generalMenuPosition) { } else if (this.state.generalMenuPosition) {
contextMenu = ( contextMenu = (
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}> <ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile2_contextMenu"> <div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList"> <div className="mx_IconizedContextMenu_optionList">
<MenuItemCheckbox <MenuItemCheckbox
className={favouriteLabelClassName} className={favouriteLabelClassName}
@ -425,13 +423,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span> <span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
</MenuItemCheckbox> </MenuItemCheckbox>
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}> <MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSettings" /> <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span> <span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
</MenuItem> </MenuItem>
</div> </div>
<div className="mx_IconizedContextMenu_optionList mx_RoomTile2_contextMenu_redRow"> <div className="mx_IconizedContextMenu_optionList mx_RoomTile_contextMenu_redRow">
<MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}> <MenuItem onClick={this.onLeaveRoomClick} label={_t("Leave Room")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile2_iconSignOut" /> <span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSignOut" />
<span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span> <span className="mx_IconizedContextMenu_label">{_t("Leave Room")}</span>
</MenuItem> </MenuItem>
</div> </div>
@ -443,7 +441,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return ( return (
<React.Fragment> <React.Fragment>
<ContextMenuTooltipButton <ContextMenuTooltipButton
className="mx_RoomTile2_menuButton" className="mx_RoomTile_menuButton"
onClick={this.onGeneralMenuOpenClick} onClick={this.onGeneralMenuOpenClick}
title={_t("Room options")} title={_t("Room options")}
isExpanded={!!this.state.generalMenuPosition} isExpanded={!!this.state.generalMenuPosition}
@ -455,10 +453,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
public render(): React.ReactElement { public render(): React.ReactElement {
const classes = classNames({ const classes = classNames({
'mx_RoomTile2': true, 'mx_RoomTile': true,
'mx_RoomTile2_selected': this.state.selected, 'mx_RoomTile_selected': this.state.selected,
'mx_RoomTile2_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition), 'mx_RoomTile_hasMenuOpen': !!(this.state.generalMenuPosition || this.state.notificationsMenuPosition),
'mx_RoomTile2_minimized': this.props.isMinimized, 'mx_RoomTile_minimized': this.props.isMinimized,
}); });
const roomAvatar = <DecoratedRoomAvatar const roomAvatar = <DecoratedRoomAvatar
@ -472,7 +470,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
if (!this.props.isMinimized) { if (!this.props.isMinimized) {
// aria-hidden because we summarise the unread count/highlight status in a manual aria-label below // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
badge = ( badge = (
<div className="mx_RoomTile2_badgeContainer" aria-hidden="true"> <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
<NotificationBadge <NotificationBadge
notification={this.state.notificationState} notification={this.state.notificationState}
forceCount={false} forceCount={false}
@ -494,7 +492,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
// Only show the preview if there is one to show. // Only show the preview if there is one to show.
if (text) { if (text) {
messagePreview = ( messagePreview = (
<div className="mx_RoomTile2_messagePreview" id={messagePreviewId(this.props.room.roomId)}> <div className="mx_RoomTile_messagePreview" id={messagePreviewId(this.props.room.roomId)}>
{text} {text}
</div> </div>
); );
@ -502,13 +500,13 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
} }
const nameClasses = classNames({ const nameClasses = classNames({
"mx_RoomTile2_name": true, "mx_RoomTile_name": true,
"mx_RoomTile2_nameWithPreview": !!messagePreview, "mx_RoomTile_nameWithPreview": !!messagePreview,
"mx_RoomTile2_nameHasUnreadEvents": this.state.notificationState.isUnread, "mx_RoomTile_nameHasUnreadEvents": this.state.notificationState.isUnread,
}); });
let nameContainer = ( let nameContainer = (
<div className="mx_RoomTile2_nameContainer"> <div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto"> <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name} {name}
</div> </div>

View file

@ -59,10 +59,10 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
public render(): React.ReactElement { public render(): React.ReactElement {
// XXX: We copy classes because it's easier // XXX: We copy classes because it's easier
const classes = classNames({ const classes = classNames({
'mx_RoomTile2': true, 'mx_RoomTile': true,
'mx_TemporaryTile': true, 'mx_TemporaryTile': true,
'mx_RoomTile2_selected': this.props.isSelected, 'mx_RoomTile_selected': this.props.isSelected,
'mx_RoomTile2_minimized': this.props.isMinimized, 'mx_RoomTile_minimized': this.props.isMinimized,
}); });
const badge = ( const badge = (
@ -77,12 +77,12 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const nameClasses = classNames({ const nameClasses = classNames({
"mx_RoomTile2_name": true, "mx_RoomTile_name": true,
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.isUnread, "mx_RoomTile_nameHasUnreadEvents": this.props.notificationState.isUnread,
}); });
let nameContainer = ( let nameContainer = (
<div className="mx_RoomTile2_nameContainer"> <div className="mx_RoomTile_nameContainer">
<div title={name} className={nameClasses} tabIndex={-1} dir="auto"> <div title={name} className={nameClasses} tabIndex={-1} dir="auto">
{name} {name}
</div> </div>
@ -105,11 +105,11 @@ export default class TemporaryTile extends React.Component<IProps, IState> {
role="treeitem" role="treeitem"
title={this.props.isMinimized ? name : undefined} title={this.props.isMinimized ? name : undefined}
> >
<div className="mx_RoomTile2_avatarContainer"> <div className="mx_RoomTile_avatarContainer">
{this.props.avatar} {this.props.avatar}
</div> </div>
{nameContainer} {nameContainer}
<div className="mx_RoomTile2_badgeContainer"> <div className="mx_RoomTile_badgeContainer">
{badge} {badge}
</div> </div>
</Button> </Button>

View file

@ -1,48 +0,0 @@
/*
Copyright 2019 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, {useContext, useEffect, useMemo, useState, useCallback} from "react";
import PropTypes from "prop-types";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
const UserOnlineDot = ({userId}) => {
const cli = useContext(MatrixClientContext);
const user = useMemo(() => cli.getUser(userId), [cli, userId]);
const [isOnline, setIsOnline] = useState(false);
// Recheck if the user or client changes
useEffect(() => {
setIsOnline(user && (user.currentlyActive || user.presence === "online"));
}, [cli, user]);
// Recheck also if we receive a User.currentlyActive event
const currentlyActiveHandler = useCallback((ev) => {
const content = ev.getContent();
setIsOnline(content.currently_active || content.presence === "online");
}, []);
useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler);
useEventEmitter(user, "User.presence", currentlyActiveHandler);
return isOnline ? <span className="mx_UserOnlineDot" /> : null;
};
UserOnlineDot.propTypes = {
userId: PropTypes.string.isRequired,
};
export default UserOnlineDot;

View file

@ -22,7 +22,7 @@ import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal"; import Modal from "../../../../../Modal";
import dis from "../../../../../dispatcher/dispatcher"; import dis from "../../../../../dispatcher/dispatcher";
import RoomListStore from "../../../../../stores/room-list/RoomListStore2"; import RoomListStore from "../../../../../stores/room-list/RoomListStore";
import RoomListActions from "../../../../../actions/RoomListActions"; import RoomListActions from "../../../../../actions/RoomListActions";
import { DefaultTagID } from '../../../../../stores/room-list/models'; import { DefaultTagID } from '../../../../../stores/room-list/models';
import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch'; import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch';

View file

@ -23,28 +23,12 @@ import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field"; import Field from "../../../elements/Field";
import * as sdk from "../../../../.."; import * as sdk from "../../../../..";
import PlatformPeg from "../../../../../PlatformPeg"; import PlatformPeg from "../../../../../PlatformPeg";
import {RoomListStoreTempProxy} from "../../../../../stores/room-list/RoomListStoreTempProxy";
export default class PreferencesUserSettingsTab extends React.Component { export default class PreferencesUserSettingsTab extends React.Component {
static ROOM_LIST_SETTINGS = [ static ROOM_LIST_SETTINGS = [
'RoomList.orderAlphabetically',
'RoomList.orderByImportance',
'breadcrumbs', 'breadcrumbs',
]; ];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static ROOM_LIST_2_SETTINGS = [
'breadcrumbs',
];
// TODO: Remove temp structures: https://github.com/vector-im/riot-web/issues/14367
static eligibleRoomListSettings = () => {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return PreferencesUserSettingsTab.ROOM_LIST_2_SETTINGS;
}
return PreferencesUserSettingsTab.ROOM_LIST_SETTINGS;
};
static COMPOSER_SETTINGS = [ static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji', 'MessageComposerInput.suggestEmoji',
@ -189,7 +173,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span> <span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.eligibleRoomListSettings())} {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
</div> </div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">

View file

@ -15,8 +15,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import IncomingCallBox2 from './IncomingCallBox2'; import IncomingCallBox from './IncomingCallBox';
import CallPreview from './CallPreview2'; import CallPreview from './CallPreview';
import * as VectorConferenceHandler from '../../../VectorConferenceHandler'; import * as VectorConferenceHandler from '../../../VectorConferenceHandler';
interface IProps { interface IProps {
@ -30,7 +30,7 @@ interface IState {
export default class CallContainer extends React.PureComponent<IProps, IState> { export default class CallContainer extends React.PureComponent<IProps, IState> {
public render() { public render() {
return <div className="mx_CallContainer"> return <div className="mx_CallContainer">
<IncomingCallBox2 /> <IncomingCallBox />
<CallPreview ConferenceHandler={VectorConferenceHandler} /> <CallPreview ConferenceHandler={VectorConferenceHandler} />
</div>; </div>;
} }

View file

@ -1,101 +0,0 @@
/*
Copyright 2017, 2018 New Vector Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import RoomViewStore from '../../../stores/RoomViewStore';
import CallHandler from '../../../CallHandler';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'CallPreview',
propTypes: {
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler: PropTypes.object,
},
getInitialState: function() {
return {
roomId: RoomViewStore.getRoomId(),
activeCall: CallHandler.getAnyActiveCall(),
};
},
componentDidMount: function() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this.dispatcherRef = dis.register(this._onAction);
},
componentWillUnmount: function() {
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
dis.unregister(this.dispatcherRef);
},
_onRoomViewStoreUpdate: function(payload) {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({
roomId: RoomViewStore.getRoomId(),
});
},
_onAction: function(payload) {
switch (payload.action) {
// listen for call state changes to prod the render method, which
// may hide the global CallView if the call it is tracking is dead
case 'call_state':
this.setState({
activeCall: CallHandler.getAnyActiveCall(),
});
break;
}
},
_onCallViewClick: function() {
const call = CallHandler.getAnyActiveCall();
if (call) {
dis.dispatch({
action: 'view_room',
room_id: call.groupRoomId || call.roomId,
});
}
},
render: function() {
const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const showCall = (this.state.activeCall && this.state.activeCall.call_state === 'connected' && !callForRoom);
if (showCall) {
const CallView = sdk.getComponent('voip.CallView');
return (
<CallView
className="mx_LeftPanel_callView" showVoice={true} onClick={this._onCallViewClick}
ConferenceHandler={this.props.ConferenceHandler}
/>
);
}
const PersistentApp = sdk.getComponent('elements.PersistentApp');
return <PersistentApp />;
},
});

View file

@ -15,11 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React from 'react'; import React from 'react';
import CallView from "./CallView2"; import CallView from "./CallView";
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
@ -37,7 +35,6 @@ interface IProps {
interface IState { interface IState {
roomId: string; roomId: string;
activeCall: any; activeCall: any;
newRoomListActive: boolean;
} }
export default class CallPreview extends React.Component<IProps, IState> { export default class CallPreview extends React.Component<IProps, IState> {
@ -51,12 +48,7 @@ export default class CallPreview extends React.Component<IProps, IState> {
this.state = { this.state = {
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
activeCall: CallHandler.getAnyActiveCall(), activeCall: CallHandler.getAnyActiveCall(),
newRoomListActive: SettingsStore.getValue("feature_new_room_list"),
}; };
this.settingsWatcherRef = SettingsStore.watchSetting("feature_new_room_list", null, (name, roomId, level, valAtLevel, newVal) => this.setState({
newRoomListActive: newVal,
}));
} }
public componentDidMount() { public componentDidMount() {
@ -102,7 +94,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
}; };
public render() { public render() {
if (this.state.newRoomListActive) {
const callForRoom = CallHandler.getCallForRoom(this.state.roomId); const callForRoom = CallHandler.getCallForRoom(this.state.roomId);
const showCall = ( const showCall = (
this.state.activeCall && this.state.activeCall &&
@ -122,8 +113,5 @@ export default class CallPreview extends React.Component<IProps, IState> {
return <PersistentApp />; return <PersistentApp />;
} }
return null;
}
} }

View file

@ -1,167 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
export default createReactClass({
displayName: 'CallView',
propTypes: {
// js-sdk room object. If set, we will only show calls for the given
// room; if not, we will show any active call.
room: PropTypes.object,
// A Conference Handler implementation
// Must have a function signature:
// getConferenceCallForRoom(roomId: string): MatrixCall
ConferenceHandler: PropTypes.object,
// maxHeight style attribute for the video panel
maxVideoHeight: PropTypes.number,
// a callback which is called when the user clicks on the video div
onClick: PropTypes.func,
// a callback which is called when the content in the callview changes
// in a way that is likely to cause a resize.
onResize: PropTypes.func,
// render ongoing audio call details - useful when in LeftPanel
showVoice: PropTypes.bool,
},
getInitialState: function() {
return {
// the call this view is displaying (if any)
call: null,
};
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
this._video = createRef();
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.showCall();
},
componentWillUnmount: function() {
dis.unregister(this.dispatcherRef);
},
onAction: function(payload) {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (payload.action !== 'call_state') {
return;
}
this.showCall();
},
showCall: function() {
let call;
if (this.props.room) {
const roomId = this.props.room.roomId;
call = CallHandler.getCallForRoom(roomId) ||
(this.props.ConferenceHandler ?
this.props.ConferenceHandler.getConferenceCallForRoom(roomId) :
null
);
if (this.call) {
this.setState({ call: call });
}
} else {
call = CallHandler.getAnyActiveCall();
// Ignore calls if we can't get the room associated with them.
// I think the underlying problem is that the js-sdk sends events
// for calls before it has made the rooms available in the store,
// although this isn't confirmed.
if (MatrixClientPeg.get().getRoom(call.roomId) === null) {
call = null;
}
this.setState({ call: call });
}
if (call) {
call.setLocalVideoElement(this.getVideoView().getLocalVideoElement());
call.setRemoteVideoElement(this.getVideoView().getRemoteVideoElement());
// always use a separate element for audio stream playback.
// this is to let us move CallView around the DOM without interrupting remote audio
// during playback, by having the audio rendered by a top-level <audio/> element.
// rather than being rendered by the main remoteVideo <video/> element.
call.setRemoteAudioElement(this.getVideoView().getRemoteAudioElement());
}
if (call && call.type === "video" && call.call_state !== "ended" && call.call_state !== "ringing") {
// if this call is a conf call, don't display local video as the
// conference will have us in it
this.getVideoView().getLocalVideoElement().style.display = (
call.confUserId ? "none" : "block"
);
this.getVideoView().getRemoteVideoElement().style.display = "block";
} else {
this.getVideoView().getLocalVideoElement().style.display = "none";
this.getVideoView().getRemoteVideoElement().style.display = "none";
dis.dispatch({action: 'video_fullscreen', fullscreen: false});
}
if (this.props.onResize) {
this.props.onResize();
}
},
getVideoView: function() {
return this._video.current;
},
render: function() {
const VideoView = sdk.getComponent('voip.VideoView');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let voice;
if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) {
const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId);
voice = (
<AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
{ _t("Active call (%(roomName)s)", {roomName: callRoom.name}) }
</AccessibleButton>
);
}
return (
<div>
<VideoView
ref={this._video}
onClick={this.props.onClick}
onResize={this.props.onResize}
maxHeight={this.props.maxVideoHeight}
/>
{ voice }
</div>
);
},
});

View file

@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import Room from 'matrix-js-sdk/src/models/room'; import Room from 'matrix-js-sdk/src/models/room';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
@ -156,7 +154,7 @@ export default class CallView extends React.Component<IProps, IState> {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const callRoom = client.getRoom(this.state.call.roomId); const callRoom = client.getRoom(this.state.call.roomId);
view = <AccessibleButton className="mx_CallView2_voice" onClick={this.props.onClick}> view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}>
<PulsedAvatar> <PulsedAvatar>
<RoomAvatar <RoomAvatar
room={callRoom} room={callRoom}
@ -181,7 +179,7 @@ export default class CallView extends React.Component<IProps, IState> {
let hangup: React.ReactNode; let hangup: React.ReactNode;
if (this.props.showHangup) { if (this.props.showHangup) {
hangup = <div hangup = <div
className="mx_CallView2_hangup" className="mx_CallView_hangup"
onClick={() => { onClick={() => {
dis.dispatch({ dis.dispatch({
action: 'hangup', action: 'hangup',

View file

@ -1,91 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 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 PropTypes from 'prop-types';
import createReactClass from 'create-react-class';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
export default createReactClass({
displayName: 'IncomingCallBox',
propTypes: {
incomingCall: PropTypes.object,
},
onAnswerClick: function(e) {
e.stopPropagation();
dis.dispatch({
action: 'answer',
room_id: this.props.incomingCall.roomId,
});
},
onRejectClick: function(e) {
e.stopPropagation();
dis.dispatch({
action: 'hangup',
room_id: this.props.incomingCall.roomId,
});
},
render: function() {
let room = null;
if (this.props.incomingCall) {
room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId);
}
const caller = room ? room.name : _t("unknown caller");
let incomingCallText = null;
if (this.props.incomingCall) {
if (this.props.incomingCall.type === "voice") {
incomingCallText = _t("Incoming voice call from %(name)s", {name: caller});
} else if (this.props.incomingCall.type === "video") {
incomingCallText = _t("Incoming video call from %(name)s", {name: caller});
} else {
incomingCallText = _t("Incoming call from %(name)s", {name: caller});
}
}
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_IncomingCallBox" id="incomingCallBox">
<img className="mx_IncomingCallBox_chevron" src={require("../../../../res/img/chevron-left.png")} width="9" height="16" />
<div className="mx_IncomingCallBox_title">
{ incomingCallText }
</div>
<div className="mx_IncomingCallBox_buttons">
<div className="mx_IncomingCallBox_buttons_cell">
<AccessibleButton className="mx_IncomingCallBox_buttons_decline" onClick={this.onRejectClick}>
{ _t("Decline") }
</AccessibleButton>
</div>
<div className="mx_IncomingCallBox_buttons_cell">
<AccessibleButton className="mx_IncomingCallBox_buttons_accept" onClick={this.onAnswerClick}>
{ _t("Accept") }
</AccessibleButton>
</div>
</div>
</div>
);
},
});

View file

@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
import React from 'react'; import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
@ -35,7 +33,7 @@ interface IState {
incomingCall: any; incomingCall: any;
} }
export default class IncomingCallBox2 extends React.Component<IProps, IState> { export default class IncomingCallBox extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
constructor(props: IProps) { constructor(props: IProps) {
@ -106,8 +104,8 @@ export default class IncomingCallBox2 extends React.Component<IProps, IState> {
} }
} }
return <div className="mx_IncomingCallBox2"> return <div className="mx_IncomingCallBox">
<div className="mx_IncomingCallBox2_CallerInfo"> <div className="mx_IncomingCallBox_CallerInfo">
<PulsedAvatar> <PulsedAvatar>
<RoomAvatar <RoomAvatar
room={room} room={room}
@ -120,16 +118,16 @@ export default class IncomingCallBox2 extends React.Component<IProps, IState> {
<p>{incomingCallText}</p> <p>{incomingCallText}</p>
</div> </div>
</div> </div>
<div className="mx_IncomingCallBox2_buttons"> <div className="mx_IncomingCallBox_buttons">
<FormButton <FormButton
className={"mx_IncomingCallBox2_decline"} className={"mx_IncomingCallBox_decline"}
onClick={this.onRejectClick} onClick={this.onRejectClick}
kind="danger" kind="danger"
label={_t("Decline")} label={_t("Decline")}
/> />
<div className="mx_IncomingCallBox2_spacer" /> <div className="mx_IncomingCallBox_spacer" />
<FormButton <FormButton
className={"mx_IncomingCallBox2_accept"} className={"mx_IncomingCallBox_accept"}
onClick={this.onAnswerClick} onClick={this.onAnswerClick}
kind="primary" kind="primary"
label={_t("Accept")} label={_t("Accept")}

View file

@ -488,7 +488,6 @@
"Render simple counters in room header": "Render simple counters in room header", "Render simple counters in room header": "Render simple counters in room header",
"Multiple integration managers": "Multiple integration managers", "Multiple integration managers": "Multiple integration managers",
"Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)",
"Use the improved room list (will refresh to apply changes)": "Use the improved room list (will refresh to apply changes)",
"Support adding custom themes": "Support adding custom themes", "Support adding custom themes": "Support adding custom themes",
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
"Show info about bridges in room settings": "Show info about bridges in room settings", "Show info about bridges in room settings": "Show info about bridges in room settings",
@ -558,18 +557,13 @@
"When rooms are upgraded": "When rooms are upgraded", "When rooms are upgraded": "When rooms are upgraded",
"My Ban List": "My Ban List", "My Ban List": "My Ban List",
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
"Active call (%(roomName)s)": "Active call (%(roomName)s)",
"Active call": "Active call", "Active call": "Active call",
"unknown caller": "unknown caller",
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
"Incoming video call from %(name)s": "Incoming video call from %(name)s",
"Incoming call from %(name)s": "Incoming call from %(name)s",
"Decline": "Decline",
"Accept": "Accept",
"Unknown caller": "Unknown caller", "Unknown caller": "Unknown caller",
"Incoming voice call": "Incoming voice call", "Incoming voice call": "Incoming voice call",
"Incoming video call": "Incoming video call", "Incoming video call": "Incoming video call",
"Incoming call": "Incoming call", "Incoming call": "Incoming call",
"Decline": "Decline",
"Accept": "Accept",
"The other party cancelled the verification.": "The other party cancelled the verification.", "The other party cancelled the verification.": "The other party cancelled the verification.",
"Verified!": "Verified!", "Verified!": "Verified!",
"You've successfully verified this user.": "You've successfully verified this user.", "You've successfully verified this user.": "You've successfully verified this user.",
@ -1095,7 +1089,6 @@
"Encrypted by a deleted session": "Encrypted by a deleted session", "Encrypted by a deleted session": "Encrypted by a deleted session",
"The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.", "The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
"Please select the destination room for this message": "Please select the destination room for this message", "Please select the destination room for this message": "Please select the destination room for this message",
"Invite only": "Invite only",
"Scroll to most recent messages": "Scroll to most recent messages", "Scroll to most recent messages": "Scroll to most recent messages",
"Close preview": "Close preview", "Close preview": "Close preview",
"and %(count)s others...|other": "and %(count)s others...", "and %(count)s others...|other": "and %(count)s others...",
@ -1142,7 +1135,6 @@
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
"Replying": "Replying", "Replying": "Replying",
"Room %(name)s": "Room %(name)s", "Room %(name)s": "Room %(name)s",
"Recent rooms": "Recent rooms",
"No recently visited rooms": "No recently visited rooms", "No recently visited rooms": "No recently visited rooms",
"No rooms to show": "No rooms to show", "No rooms to show": "No rooms to show",
"Unnamed room": "Unnamed room", "Unnamed room": "Unnamed room",
@ -1155,17 +1147,15 @@
"Forget room": "Forget room", "Forget room": "Forget room",
"Search": "Search", "Search": "Search",
"Share room": "Share room", "Share room": "Share room",
"Community Invites": "Community Invites",
"Invites": "Invites", "Invites": "Invites",
"Favourites": "Favourites", "Favourites": "Favourites",
"Direct Messages": "Direct Messages", "People": "People",
"Start chat": "Start chat", "Start chat": "Start chat",
"Rooms": "Rooms", "Rooms": "Rooms",
"Create room": "Create room", "Create room": "Create room",
"Low priority": "Low priority", "Low priority": "Low priority",
"Historical": "Historical",
"System Alerts": "System Alerts", "System Alerts": "System Alerts",
"People": "People", "Historical": "Historical",
"This room": "This room", "This room": "This room",
"Joining room …": "Joining room …", "Joining room …": "Joining room …",
"Loading …": "Loading …", "Loading …": "Loading …",
@ -1221,13 +1211,6 @@
"Add room": "Add room", "Add room": "Add room",
"Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|other": "Show %(count)s more",
"Show %(count)s more|one": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more",
"Options": "Options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
"%(count)s unread messages.|other": "%(count)s unread messages.",
"%(count)s unread messages.|one": "1 unread message.",
"Unread mentions.": "Unread mentions.",
"Unread messages.": "Unread messages.",
"Use default": "Use default", "Use default": "Use default",
"All messages": "All messages", "All messages": "All messages",
"Mentions & Keywords": "Mentions & Keywords", "Mentions & Keywords": "Mentions & Keywords",
@ -1237,6 +1220,11 @@
"Leave Room": "Leave Room", "Leave Room": "Leave Room",
"Forget Room": "Forget Room", "Forget Room": "Forget Room",
"Room options": "Room options", "Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
"%(count)s unread messages.|other": "%(count)s unread messages.",
"%(count)s unread messages.|one": "1 unread message.",
"Unread messages.": "Unread messages.",
"This room is public": "This room is public", "This room is public": "This room is public",
"Away": "Away", "Away": "Away",
"Add a topic": "Add a topic", "Add a topic": "Add a topic",
@ -1334,6 +1322,7 @@
"Invite": "Invite", "Invite": "Invite",
"Share Link to User": "Share Link to User", "Share Link to User": "Share Link to User",
"Direct message": "Direct message", "Direct message": "Direct message",
"Options": "Options",
"Demote yourself?": "Demote yourself?", "Demote yourself?": "Demote yourself?",
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.",
"Demote": "Demote", "Demote": "Demote",
@ -1512,7 +1501,6 @@
"Maximize apps": "Maximize apps", "Maximize apps": "Maximize apps",
"Popout widget": "Popout widget", "Popout widget": "Popout widget",
"More options": "More options", "More options": "More options",
"Create new room": "Create new room",
"Join": "Join", "Join": "Join",
"No results": "No results", "No results": "No results",
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
@ -1725,6 +1713,7 @@
"Recent Conversations": "Recent Conversations", "Recent Conversations": "Recent Conversations",
"Suggestions": "Suggestions", "Suggestions": "Suggestions",
"Recently Direct Messaged": "Recently Direct Messaged", "Recently Direct Messaged": "Recently Direct Messaged",
"Direct Messages": "Direct Messages",
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.", "Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.",
"Go": "Go", "Go": "Go",
"Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.", "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>), email address or <a>share this room</a>.",
@ -2058,9 +2047,6 @@
"Send a Direct Message": "Send a Direct Message", "Send a Direct Message": "Send a Direct Message",
"Explore Public Rooms": "Explore Public Rooms", "Explore Public Rooms": "Explore Public Rooms",
"Create a Group Chat": "Create a Group Chat", "Create a Group Chat": "Create a Group Chat",
"Explore": "Explore",
"Filter": "Filter",
"Filter rooms…": "Filter rooms…",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
"Failed to reject invitation": "Failed to reject invitation", "Failed to reject invitation": "Failed to reject invitation",
"This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.",
@ -2136,7 +2122,6 @@
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position", "Failed to load timeline position": "Failed to load timeline position",
"Your profile": "Your profile",
"Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others",
"Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s",
"Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other",

View file

@ -1,132 +0,0 @@
/*
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 Sizer from "../sizer";
import ResizeItem from "../item";
class RoomSizer extends Sizer {
setItemSize(item, size) {
item.style.maxHeight = `${Math.round(size)}px`;
item.classList.add("resized-sized");
}
clearItemSize(item) {
item.style.maxHeight = null;
item.classList.remove("resized-sized");
}
}
class RoomSubListItem extends ResizeItem {
isCollapsed() {
return this.domNode.classList.contains("mx_RoomSubList_hidden");
}
maxSize() {
const header = this.domNode.querySelector(".mx_RoomSubList_labelContainer");
const scrollItem = this.domNode.querySelector(".mx_RoomSubList_scroll");
const headerHeight = this.sizer.getItemSize(header);
return headerHeight + (scrollItem ? scrollItem.scrollHeight : 0);
}
minSize() {
const isNotEmpty = this.domNode.classList.contains("mx_RoomSubList_nonEmpty");
return isNotEmpty ? 74 : 31; //size of header + 1? room tile (see room sub list css)
}
isSized() {
return this.domNode.classList.contains("resized-sized");
}
}
export default class RoomSubListDistributor {
static createItem(resizeHandle, resizer, sizer) {
return new RoomSubListItem(resizeHandle, resizer, sizer);
}
static createSizer(containerElement, vertical, reverse) {
return new RoomSizer(containerElement, vertical, reverse);
}
constructor(item) {
this.item = item;
}
_handleSize() {
return 1;
}
resize(size) {
//console.log("*** starting resize session with size", size);
let item = this.item;
while (item) {
const minSize = item.minSize();
if (item.isCollapsed()) {
item = item.previous();
} else if (size <= minSize) {
//console.log(" - resizing", item.id, "to min size", minSize);
item.setSize(minSize);
const remainder = minSize - size;
item = item.previous();
if (item) {
size = item.size() - remainder - this._handleSize();
}
} else {
const maxSize = item.maxSize();
if (size > maxSize) {
// console.log(" - resizing", item.id, "to maxSize", maxSize);
item.setSize(maxSize);
const remainder = size - maxSize;
item = item.previous();
if (item) {
size = item.size() + remainder; // todo: handle size here?
}
} else {
//console.log(" - resizing", item.id, "to size", size);
item.setSize(size);
item = null;
size = 0;
}
}
}
//console.log("*** ending resize session");
}
resizeFromContainerOffset(containerOffset) {
this.resize(containerOffset - this.item.offset());
}
start() {
// set all max-height props to the actual height.
let item = this.item.first();
const sizes = [];
while (item) {
if (!item.isCollapsed()) {
sizes.push(item.size());
} else {
sizes.push(100);
}
item = item.next();
}
item = this.item.first();
sizes.forEach((size) => {
item.setSize(size);
item = item.next();
});
}
finish() {
}
}

View file

@ -1,332 +0,0 @@
/*
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 FixedDistributor from "./fixed";
function clamp(height, min, max) {
if (height > max) return max;
if (height < min) return min;
return height;
}
export class Layout {
constructor(applyHeight, initialSizes, collapsedState, options) {
// callback to set height of section
this._applyHeight = applyHeight;
// list of {id, count} objects,
// determines sections and order of them
this._sections = [];
// stores collapsed by id
this._collapsedState = Object.assign({}, collapsedState);
// total available height to the layout
// (including resize handles, ...)
this._availableHeight = 0;
// heights stored by section section id
this._sectionHeights = Object.assign({}, initialSizes);
// in-progress heights, while dragging. Committed on mouse-up.
this._heights = [];
// use while manually resizing to cancel
// the resize for a given mouse position
// when the previous resize made the layout
// constrained
this._clampedOffset = 0;
// used while manually resizing, to clear
// _clampedOffset when the direction of resizing changes
this._lastOffset = 0;
this._allowWhitespace = options && options.allowWhitespace;
this._handleHeight = (options && options.handleHeight) || 0;
}
setAvailableHeight(newSize) {
this._availableHeight = newSize;
// needs more work
this._applyNewSize();
}
expandSection(id, height) {
this._collapsedState[id] = false;
this._applyNewSize();
this.openHandle(id).setHeight(height).finish();
}
collapseSection(id) {
this._collapsedState[id] = true;
this._applyNewSize();
}
update(sections, availableHeight, force = false) {
let heightChanged = false;
if (Number.isFinite(availableHeight) && availableHeight !== this._availableHeight) {
heightChanged = true;
this._availableHeight = availableHeight;
}
const sectionsChanged =
sections.length !== this._sections.length ||
sections.some((a, i) => {
const b = this._sections[i];
return a.id !== b.id || a.count !== b.count;
});
if (!heightChanged && !sectionsChanged && !force) {
return;
}
this._sections = sections;
const totalHeight = this._getAvailableHeight();
const defaultHeight = Math.floor(totalHeight / this._sections.length);
this._sections.forEach((section, i) => {
if (!this._sectionHeights[section.id]) {
this._sectionHeights[section.id] = clamp(
defaultHeight,
this._getMinHeight(i),
this._getMaxHeight(i),
);
}
});
this._applyNewSize();
}
openHandle(id) {
const index = this._getSectionIndex(id);
return new Handle(this, index, this._sectionHeights[id]);
}
_getAvailableHeight() {
const nonCollapsedSectionCount = this._sections.reduce((count, section) => {
const collapsed = this._collapsedState[section.id];
return count + (collapsed ? 0 : 1);
}, 0);
return this._availableHeight - ((nonCollapsedSectionCount - 1) * this._handleHeight);
}
_applyNewSize() {
const newHeight = this._getAvailableHeight();
const currHeight = this._sections.reduce((sum, section) => {
return sum + this._sectionHeights[section.id];
}, 0);
const offset = newHeight - currHeight;
this._heights = this._sections.map((section) => this._sectionHeights[section.id]);
const sections = this._sections.map((_, i) => i);
this._applyOverflow(-offset, sections, true);
this._applyHeights();
this._commitHeights();
}
_getSectionIndex(id) {
return this._sections.findIndex((s) => s.id === id);
}
_getMaxHeight(i) {
const section = this._sections[i];
const collapsed = this._collapsedState[section.id];
if (collapsed) {
return this._sectionHeight(0);
} else if (!this._allowWhitespace) {
return this._sectionHeight(section.count);
} else {
return 100000;
}
}
_sectionHeight(count) {
return 36 + (count === 0 ? 0 : 4 + (count * 34));
}
_getMinHeight(i) {
const section = this._sections[i];
const collapsed = this._collapsedState[section.id];
const maxItems = collapsed ? 0 : 1;
return this._sectionHeight(Math.min(section.count, maxItems));
}
_applyOverflow(overflow, sections, blend) {
// take the given overflow amount, and applies it to the given sections.
// calls itself recursively until it has distributed all the overflow
// or run out of unclamped sections.
const unclampedSections = [];
let overflowPerSection = blend ? (overflow / sections.length) : overflow;
for (const i of sections) {
const newHeight = clamp(
this._heights[i] - overflowPerSection,
this._getMinHeight(i),
this._getMaxHeight(i),
);
if (newHeight == this._heights[i] - overflowPerSection) {
unclampedSections.push(i);
}
// when section is growing, overflow increases?
// 100 -= 200 - 300
// 100 -= -100
// 200
overflow -= this._heights[i] - newHeight;
this._heights[i] = newHeight;
if (!blend) {
overflowPerSection = overflow;
if (Math.abs(overflow) < 1.0) break;
}
}
if (Math.abs(overflow) > 1.0 && unclampedSections.length > 0) {
// we weren't able to distribute all the overflow so recurse and try again
overflow = this._applyOverflow(overflow, unclampedSections, blend);
}
return overflow;
}
_rebalanceAbove(sectionIndex, overflowAbove) {
if (Math.abs(overflowAbove) > 1.0) {
const sections = [];
for (let i = sectionIndex - 1; i >= 0; i--) {
sections.push(i);
}
overflowAbove = this._applyOverflow(overflowAbove, sections);
}
return overflowAbove;
}
_rebalanceBelow(sectionIndex, overflowBelow) {
if (Math.abs(overflowBelow) > 1.0) {
const sections = [];
for (let i = sectionIndex + 1; i < this._sections.length; i++) {
sections.push(i);
}
overflowBelow = this._applyOverflow(overflowBelow, sections);
}
return overflowBelow;
}
// @param offset the amount the sectionIndex is moved from what is stored in _sectionHeights, positive if downwards
// if we're constrained, return the offset we should be constrained at.
_relayout(sectionIndex = 0, offset = 0, constrained = false) {
this._heights = this._sections.map((section) => this._sectionHeights[section.id]);
// are these the amounts the items above/below shrank/grew and need to be relayouted?
let overflowAbove;
let overflowBelow;
const maxHeight = this._getMaxHeight(sectionIndex);
const minHeight = this._getMinHeight(sectionIndex);
// new height > max ?
if (this._heights[sectionIndex] + offset > maxHeight) {
// we're pulling downwards and constrained
// overflowAbove = minus how much are we above max height
overflowAbove = (maxHeight - this._heights[sectionIndex]) - offset;
overflowBelow = offset;
} else if (this._heights[sectionIndex] + offset < minHeight) { // new height < min?
// we're pulling upwards and constrained
overflowAbove = (minHeight - this._heights[sectionIndex]) - offset;
overflowBelow = offset;
} else {
overflowAbove = 0;
overflowBelow = offset;
}
this._heights[sectionIndex] = clamp(this._heights[sectionIndex] + offset, minHeight, maxHeight);
// these are reassigned the amount of overflow that could not be rebalanced
// meaning we dragged the handle too far and it can't follow the cursor anymore
overflowAbove = this._rebalanceAbove(sectionIndex, overflowAbove);
overflowBelow = this._rebalanceBelow(sectionIndex, overflowBelow);
if (!constrained) { // to avoid risk of infinite recursion
// clamp to avoid overflowing or underflowing the page
if (Math.abs(overflowAbove) > 1.0) {
// here we do the layout again with offset - the amount of space we took too much
this._relayout(sectionIndex, offset + overflowAbove, true);
return offset + overflowAbove;
}
if (Math.abs(overflowBelow) > 1.0) {
// here we do the layout again with offset - the amount of space we took too much
this._relayout(sectionIndex, offset - overflowBelow, true);
return offset - overflowBelow;
}
}
this._applyHeights();
return undefined;
}
_applyHeights() {
// apply the heights
for (let i = 0; i < this._sections.length; i++) {
const section = this._sections[i];
this._applyHeight(section.id, this._heights[i]);
}
}
_commitHeights() {
this._sections.forEach((section, i) => {
this._sectionHeights[section.id] = this._heights[i];
});
}
_setUncommittedSectionHeight(sectionIndex, offset) {
if (Math.sign(offset) != Math.sign(this._lastOffset)) {
this._clampedOffset = undefined;
}
if (this._clampedOffset !== undefined) {
if (offset < 0 && offset < this._clampedOffset) {
return;
}
if (offset > 0 && offset > this._clampedOffset) {
return;
}
}
this._clampedOffset = this._relayout(sectionIndex, offset);
this._lastOffset = offset;
}
}
class Handle {
constructor(layout, sectionIndex, height) {
this._layout = layout;
this._sectionIndex = sectionIndex;
this._initialHeight = height;
}
setHeight(height) {
this._layout._setUncommittedSectionHeight(
this._sectionIndex,
height - this._initialHeight,
);
return this;
}
finish() {
this._layout._commitHeights();
return this;
}
}
export class Distributor extends FixedDistributor {
constructor(item, cfg) {
super(item);
this._handle = cfg.getLayout().openHandle(item.id);
}
finish() {
this._handle.finish();
}
resize(height) {
this._handle.setHeight(height);
}
}

View file

@ -17,5 +17,4 @@ limitations under the License.
export FixedDistributor from "./distributors/fixed"; export FixedDistributor from "./distributors/fixed";
export CollapseDistributor from "./distributors/collapse"; export CollapseDistributor from "./distributors/collapse";
export RoomSubListDistributor from "./distributors/roomsublist";
export Resizer from "./resizer"; export Resizer from "./resizer";

View file

@ -140,14 +140,6 @@ export const SETTINGS = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_new_room_list": {
// TODO: Remove setting: https://github.com/vector-im/riot-web/issues/14367
// XXX: We shouldn't have non-features appear like features.
displayName: _td("Use the improved room list (will refresh to apply changes)"),
supportedLevels: LEVELS_FEATURE,
default: true,
controller: new ReloadOnChangeController(),
},
"feature_custom_themes": { "feature_custom_themes": {
isFeature: true, isFeature: true,
displayName: _td("Support adding custom themes"), displayName: _td("Support adding custom themes"),

View file

@ -18,7 +18,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
import { AsyncStore } from "./AsyncStore"; import { AsyncStore } from "./AsyncStore";
import { ActionPayload } from "../dispatcher/payloads"; import { ActionPayload } from "../dispatcher/payloads";
export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> { export abstract class AsyncStoreWithClient<T extends Object> extends AsyncStore<T> {
protected matrixClient: MatrixClient; protected matrixClient: MatrixClient;

View file

@ -20,7 +20,6 @@ import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher"; import defaultDispatcher from "../dispatcher/dispatcher";
import { arrayHasDiff } from "../utils/arrays"; import { arrayHasDiff } from "../utils/arrays";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
const MAX_ROOMS = 20; // arbitrary const MAX_ROOMS = 20; // arbitrary
@ -62,9 +61,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return; if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'setting_updated') { if (payload.action === 'setting_updated') {
if (payload.settingName === 'breadcrumb_rooms') { if (payload.settingName === 'breadcrumb_rooms') {
await this.updateRooms(); await this.updateRooms();
@ -85,9 +81,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
protected async onReady() { protected async onReady() {
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
await this.updateRooms(); await this.updateRooms();
await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)}); await this.updateState({enabled: SettingsStore.getValue("breadcrumbs", null)});
@ -96,9 +89,6 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
protected async onNotReady() { protected async onNotReady() {
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
this.matrixClient.removeListener("Room.myMembership", this.onMyMembership); this.matrixClient.removeListener("Room.myMembership", this.onMyMembership);
this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room", this.onRoom);
} }

View file

@ -18,8 +18,9 @@ import * as RoomNotifs from '../RoomNotifs';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { throttle } from "lodash"; import { throttle } from "lodash";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
// TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
function commonPrefix(a, b) { function commonPrefix(a, b) {
@ -60,9 +61,7 @@ class CustomRoomTagStore extends EventEmitter {
trailing: true, trailing: true,
}, },
); );
this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { RoomListStore.instance.on(LISTS_UPDATE_EVENT, this._onListsUpdated);
this._setState({tags: this._getUpdatedTags()});
});
dis.register(payload => this._onDispatch(payload)); dis.register(payload => this._onDispatch(payload));
} }
@ -85,7 +84,7 @@ class CustomRoomTagStore extends EventEmitter {
} }
getSortedTags() { getSortedTags() {
const roomLists = RoomListStoreTempProxy.getRoomLists(); const roomLists = RoomListStore.instance.orderedLists;
const tagNames = Object.keys(this._state.tags).sort(); const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => { const prefixes = tagNames.map((name, i) => {
@ -109,6 +108,9 @@ class CustomRoomTagStore extends EventEmitter {
}); });
} }
_onListsUpdated = () => {
this._setState({tags: this._getUpdatedTags()});
};
_onDispatch(payload) { _onDispatch(payload) {
switch (payload.action) { switch (payload.action) {
@ -126,10 +128,7 @@ class CustomRoomTagStore extends EventEmitter {
case 'on_logged_out': { case 'on_logged_out': {
// we assume to always have a tags object in the state // we assume to always have a tags object in the state
this._state = {tags: {}}; this._state = {tags: {}};
if (this._roomListStoreToken) { RoomListStore.instance.off(LISTS_UPDATE_EVENT, this._onListsUpdated);
this._roomListStoreToken.remove();
this._roomListStoreToken = null;
}
} }
break; break;
} }
@ -140,7 +139,7 @@ class CustomRoomTagStore extends EventEmitter {
return; return;
} }
const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists()) const newTagNames = Object.keys(RoomListStore.instance.orderedLists)
.filter((tagName) => { .filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX); return !tagName.match(STANDARD_TAGS_REGEX);
}).sort(); }).sort();

View file

@ -1,813 +0,0 @@
/*
Copyright 2018, 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 {Store} from 'flux/utils';
import dis from '../dispatcher/dispatcher';
import DMRoomMap from '../utils/DMRoomMap';
import * as Unread from '../Unread';
import SettingsStore from "../settings/SettingsStore";
/*
Room sorting algorithm:
* Always prefer to have red > grey > bold > idle
* The room being viewed should be sticky (not jump down to the idle list)
* When switching to a new room, sort the last sticky room to the top of the idle list.
The approach taken by the store is to generate an initial representation of all the
tagged lists (accepting that it'll take a little bit longer to calculate) and make
small changes to that over time. This results in quick changes to the room list while
also having update operations feel more like popping/pushing to a stack.
*/
const CATEGORY_RED = "red"; // Mentions in the room
const CATEGORY_GREY = "grey"; // Unread notified messages (not mentions)
const CATEGORY_BOLD = "bold"; // Unread messages (not notified, 'Mentions Only' rooms)
const CATEGORY_IDLE = "idle"; // Nothing of interest
export const TAG_DM = "im.vector.fake.direct";
/**
* Identifier for manual sorting behaviour: sort by the user defined order.
* @type {string}
*/
export const ALGO_MANUAL = "manual";
/**
* Identifier for alphabetic sorting behaviour: sort by the room name alphabetically first.
* @type {string}
*/
export const ALGO_ALPHABETIC = "alphabetic";
/**
* Identifier for classic sorting behaviour: sort by the most recent message first.
* @type {string}
*/
export const ALGO_RECENT = "recent";
const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE];
const 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) {
case "im.vector.fake.invite":
case "im.vector.fake.recent":
case "im.vector.fake.archived":
case "m.lowpriority":
case TAG_DM:
return settingAlgorithm;
case "m.favourite":
default: // custom-tags
return ALGO_MANUAL;
}
};
const knownLists = new Set([
"m.favourite",
"im.vector.fake.invite",
"im.vector.fake.recent",
"im.vector.fake.archived",
"m.lowpriority",
TAG_DM,
]);
/**
* A class for storing application state for categorising rooms in
* the RoomList.
*/
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.getValue("feature_new_room_list");
if (this.disabled) {
console.warn("👋 legacy room list store has been disabled");
}
}
/**
* Changes the sorting algorithm used by the RoomListStore.
* @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants.
* @param {boolean} orderImportantFirst Whether to sort by categories of importance
*/
updateSortingAlgorithm(algorithm, orderImportantFirst) {
// Dev note: We only have two algorithms at the moment, but it isn't impossible that we want
// multiple in the future. Also constants make things slightly clearer.
console.log("Updating room sorting algorithm: ", {algorithm, orderImportantFirst});
this._setState({algorithm, orderImportantFirst});
// Trigger a resort of the entire list to reflect the change in algorithm
this._generateInitialRoomLists();
}
_init() {
if (this.disabled) return;
// Initialise state
const defaultLists = {
"m.server_notice": [/* { room: js-sdk room, category: string } */],
"im.vector.fake.invite": [],
"m.favourite": [],
"im.vector.fake.recent": [],
[TAG_DM]: [],
"m.lowpriority": [],
"im.vector.fake.archived": [],
};
this._state = {
// The rooms in these arrays are ordered according to either the
// 'recents' behaviour or 'manual' behaviour.
lists: defaultLists,
presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead
ready: false,
stickyRoomId: null,
algorithm: ALGO_RECENT,
orderImportantFirst: false,
};
SettingsStore.monitorSetting('RoomList.orderAlphabetically', null);
SettingsStore.monitorSetting('RoomList.orderByImportance', null);
SettingsStore.monitorSetting('feature_custom_tags', null);
}
_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
// categories.
if (newState['lists']) {
const presentationLists = {};
for (const key of Object.keys(newState['lists'])) {
presentationLists[key] = newState['lists'][key].map((e) => e.room);
}
newState['presentationLists'] = presentationLists;
}
this._state = Object.assign(this._state, newState);
this.__emitChange();
}
__onDispatch(payload) {
if (this.disabled) return;
const logicallyReady = this._matrixClient && this._state.ready;
switch (payload.action) {
case 'setting_updated': {
if (!logicallyReady) break;
switch (payload.settingName) {
case "RoomList.orderAlphabetically":
this.updateSortingAlgorithm(payload.newValue ? ALGO_ALPHABETIC : ALGO_RECENT,
this._state.orderImportantFirst);
break;
case "RoomList.orderByImportance":
this.updateSortingAlgorithm(this._state.algorithm, payload.newValue);
break;
case "feature_custom_tags":
this._setState({tagsEnabled: payload.newValue});
this._generateInitialRoomLists(); // Tags means we have to start from scratch
break;
}
}
break;
// Initialise state after initial sync
case 'MatrixActions.sync': {
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
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.
this._setState({tagsEnabled: SettingsStore.isFeatureEnabled("feature_custom_tags")});
this._matrixClient = payload.matrixClient;
const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance");
const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically");
this.updateSortingAlgorithm(orderAlphabetically ? ALGO_ALPHABETIC : ALGO_RECENT, orderByImportance);
}
break;
case 'MatrixActions.Room.receipt': {
if (!logicallyReady) break;
// 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)) {
this._roomUpdateTriggered(payload.room.roomId);
return;
}
}
}
break;
case 'MatrixActions.Room.tags': {
if (!logicallyReady) break;
// TODO: Figure out which rooms changed in the tag and only change those.
// This is very blunt and wipes out the sticky room stuff
this._generateInitialRoomLists();
}
break;
case 'MatrixActions.Room.timeline': {
if (!logicallyReady ||
!payload.isLiveEvent ||
!payload.isLiveUnfilteredRoomTimelineEvent ||
!this._eventTriggersRecentReorder(payload.event) ||
this._state.algorithm !== ALGO_RECENT
) {
break;
}
this._roomUpdateTriggered(payload.event.getRoomId());
}
break;
// When an event is decrypted, it could mean we need to reorder the room
// list because we now know the type of the event.
case 'MatrixActions.Event.decrypted': {
if (!logicallyReady) break;
const roomId = payload.event.getRoomId();
// We may have decrypted an event without a roomId (e.g to_device)
if (!roomId) break;
const room = this._matrixClient.getRoom(roomId);
// We somehow decrypted an event for a room our client is unaware of
if (!room) break;
const liveTimeline = room.getLiveTimeline();
const eventTimeline = room.getTimelineForEvent(payload.event.getId());
// Either this event was not added to the live timeline (e.g. pagination)
// or it doesn't affect the ordering of the room list.
if (liveTimeline !== eventTimeline || !this._eventTriggersRecentReorder(payload.event)) {
break;
}
this._roomUpdateTriggered(roomId);
}
break;
case 'MatrixActions.accountData': {
if (!logicallyReady) break;
if (payload.event_type !== 'm.direct') break;
// TODO: Figure out which rooms changed in the direct chat and only change those.
// This is very blunt and wipes out the sticky room stuff
this._generateInitialRoomLists();
}
break;
case 'MatrixActions.Room.myMembership': {
if (!logicallyReady) break;
this._roomUpdateTriggered(payload.room.roomId, true);
}
break;
// This could be a new room that we've been invited to, joined or created
// we won't get a RoomMember.membership for these cases if we're not already
// a member.
case 'MatrixActions.Room': {
if (!logicallyReady) break;
this._roomUpdateTriggered(payload.room.roomId, true);
}
break;
// TODO: Re-enable optimistic updates when we support dragging again
// case 'RoomListActions.tagRoom.pending': {
// if (!logicallyReady) break;
// // XXX: we only show one optimistic update at any one time.
// // Ideally we should be making a list of in-flight requests
// // that are backed by transaction IDs. Until the js-sdk
// // supports this, we're stuck with only being able to use
// // the most recent optimistic update.
// console.log("!! Optimistic tag: ", payload);
// }
// break;
// case 'RoomListActions.tagRoom.failure': {
// if (!logicallyReady) break;
// // Reset state according to js-sdk
// console.log("!! Optimistic tag failure: ", payload);
// }
// break;
case 'on_client_not_viable':
case 'on_logged_out': {
// Reset state without pushing an update to the view, which generally assumes that
// the matrix client isn't `null` and so causing a re-render will cause NPEs.
this._init();
this._matrixClient = null;
}
break;
case 'view_room': {
if (!logicallyReady) break;
// Note: it is important that we set a new stickyRoomId before setting the old room
// to IDLE. If we don't, the wrong room gets counted as sticky.
const currentStickyId = this._state.stickyRoomId;
this._setState({stickyRoomId: payload.room_id});
if (currentStickyId) {
this._setRoomCategory(this._matrixClient.getRoom(currentStickyId), CATEGORY_IDLE);
}
}
break;
}
}
_roomUpdateTriggered(roomId, ignoreSticky) {
// We don't calculate categories for sticky rooms because we have a moderate
// interest in trying to maintain the category that they were last in before
// being artificially flagged as IDLE. Also, this reduces the amount of time
// we spend in _setRoomCategory ever so slightly.
if (this._state.stickyRoomId !== roomId || ignoreSticky) {
// Micro optimization: Only look up the room if we're confident we'll need it.
const room = this._matrixClient.getRoom(roomId);
if (!room) return;
const category = this._calculateCategory(room);
this._setRoomCategory(room, category);
}
}
_filterTags(tags) {
tags = tags ? Object.keys(tags) : [];
if (this._state.tagsEnabled) return tags;
return tags.filter((t) => knownLists.has(t));
}
_getRecommendedTagsForRoom(room) {
const tags = [];
const myMembership = room.getMyMembership();
if (myMembership === 'join' || myMembership === 'invite') {
// Stack the user's tags on top
tags.push(...this._filterTags(room.tags));
// Order matters here: The DMRoomMap updates before invites
// are accepted, so we check to see if the room is an invite
// first, then if it is a direct chat, and finally default
// to the "recents" list.
const dmRoomMap = DMRoomMap.shared();
if (myMembership === 'invite') {
tags.push("im.vector.fake.invite");
} else if (dmRoomMap.getUserIdForRoomId(room.roomId) && tags.length === 0) {
// We intentionally don't duplicate rooms in other tags into the people list
// as a feature.
tags.push(TAG_DM);
} else if (tags.length === 0) {
tags.push("im.vector.fake.recent");
}
} else if (myMembership) { // null-guard as null means it was peeked
tags.push("im.vector.fake.archived");
}
return tags;
}
_slotRoomIntoList(room, category, tag, existingEntries, newList, lastTimestampFn) {
const targetCategoryIndex = CATEGORY_ORDER.indexOf(category);
let categoryComparator = (a, b) => lastTimestampFn(a.room) >= lastTimestampFn(b.room);
const sortAlgorithm = getListAlgorithm(tag, this._state.algorithm);
if (sortAlgorithm === ALGO_RECENT) {
categoryComparator = (a, b) => this._recentsComparator(a, b, lastTimestampFn);
} else if (sortAlgorithm === ALGO_ALPHABETIC) {
categoryComparator = (a, b) => this._lexicographicalComparator(a, b);
}
// The slotting algorithm works by trying to position the room in the most relevant
// category of the list (red > grey > etc). To accomplish this, we need to consider
// a couple cases: the category existing in the list but having other rooms in it and
// the case of the category simply not existing and needing to be started. In order to
// do this efficiently, we only want to iterate over the list once and solve our sorting
// problem as we go.
//
// Firstly, we'll remove any existing entry that references the room we're trying to
// insert. We don't really want to consider the old entry and want to recreate it. We
// also exclude the sticky (currently active) room from the categorization logic and
// let it pass through wherever it resides in the list: it shouldn't be moving around
// the list too much, so we want to keep it where it is.
//
// The case of the category we want existing is easy to handle: once we hit the category,
// find the room that has a most recent event later than our own and insert just before
// that (making us the more recent room). If we end up hitting the next category before
// we can slot the room in, insert the room at the top of the category as a fallback. We
// do this to ensure that the room doesn't go too far down the list given it was previously
// considered important (in the case of going down in category) or is now more important
// (suddenly becoming red, for instance). The boundary tracking is how we end up achieving
// this, as described in the next paragraphs.
//
// The other case of the category not already existing is a bit more complicated. We track
// the boundaries of each category relative to the list we're currently building so that
// when we miss the category we can insert the room at the right spot. Most importantly, we
// can't assume that the end of the list being built is the right spot because of the last
// paragraph's requirement: the room should be put to the top of a category if the category
// runs out of places to put it.
//
// All told, our tracking looks something like this:
//
// ------ A <- Category boundary (start of red)
// RED
// RED
// RED
// ------ B <- In this example, we have a grey room we want to insert.
// BOLD
// BOLD
// ------ C
// IDLE
// IDLE
// ------ D <- End of list
//
// Given that example, and our desire to insert a GREY room into the list, this iterates
// over the room list until it realizes that BOLD comes after GREY and we're no longer
// in the RED section. Because there's no rooms there, we simply insert there which is
// also a "category boundary". If we change the example to wanting to insert a BOLD room
// which can't be ordered by timestamp with the existing couple rooms, we would still make
// use of the boundary flag to insert at B before changing the boundary indicator to C.
let desiredCategoryBoundaryIndex = 0;
let foundBoundary = false;
let pushedEntry = false;
for (const entry of existingEntries) {
// We insert our own record as needed, so don't let the old one through.
if (entry.room.roomId === room.roomId) {
continue;
}
// if the list is a recent list, and the room appears in this list, and we're
// not looking at a sticky room (sticky rooms have unreliable categories), try
// to slot the new room in
if (entry.room.roomId !== this._state.stickyRoomId && !pushedEntry) {
const entryCategoryIndex = CATEGORY_ORDER.indexOf(entry.category);
// As per above, check if we're meeting that boundary we wanted to locate.
if (entryCategoryIndex >= targetCategoryIndex && !foundBoundary) {
desiredCategoryBoundaryIndex = newList.length - 1;
foundBoundary = true;
}
// If we've hit the top of a boundary beyond our target category, insert at the top of
// the grouping to ensure the room isn't slotted incorrectly. Otherwise, try to insert
// based on most recent timestamp.
const changedBoundary = entryCategoryIndex > targetCategoryIndex;
const currentCategory = entryCategoryIndex === targetCategoryIndex;
if (changedBoundary || (currentCategory && categoryComparator({room}, entry) <= 0)) {
if (changedBoundary) {
// If we changed a boundary, then we've gone too far - go to the top of the last
// section instead.
newList.splice(desiredCategoryBoundaryIndex, 0, {room, category});
} else {
// If we're ordering by timestamp, just insert normally
newList.push({room, category});
}
pushedEntry = true;
}
}
// Fall through and clone the list.
newList.push(entry);
}
if (!pushedEntry && desiredCategoryBoundaryIndex >= 0) {
console.warn(`!! Room ${room.roomId} nearly lost: Ran off the end of ${tag}`);
console.warn(`!! Inserting at position ${desiredCategoryBoundaryIndex} with category ${category}`);
newList.splice(desiredCategoryBoundaryIndex, 0, {room, category});
pushedEntry = true;
}
return pushedEntry;
}
_setRoomCategory(room, category) {
if (!room) return; // This should only happen in tests
const listsClone = {};
// Micro optimization: Support lazily loading the last timestamp in a room
const timestampCache = {}; // {roomId => ts}
const lastTimestamp = (room) => {
if (!timestampCache[room.roomId]) {
timestampCache[room.roomId] = this._tsOfNewestEvent(room);
}
return timestampCache[room.roomId];
};
const targetTags = this._getRecommendedTagsForRoom(room);
const insertedIntoTags = [];
// We need to make sure all the tags (lists) are updated with the room's new position. We
// generally only get called here when there's a new room to insert or a room has potentially
// changed positions within the list.
//
// We do all our checks by iterating over the rooms in the existing lists, trying to insert
// our room where we can. As a guiding principle, we should be removing the room from all
// tags, and insert the room into targetTags. We should perform the deletion before the addition
// where possible to keep a consistent state. By the end of this, targetTags should be the
// same as insertedIntoTags.
for (const key of Object.keys(this._state.lists)) {
const shouldHaveRoom = targetTags.includes(key);
// Speed optimization: Don't do complicated math if we don't have to.
if (!shouldHaveRoom) {
listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId);
} else if (getListAlgorithm(key, this._state.algorithm) === ALGO_MANUAL) {
// Manually ordered tags are sorted later, so for now we'll just clone the tag
// and add our room if needed
listsClone[key] = this._state.lists[key].filter((e) => e.room.roomId !== room.roomId);
listsClone[key].push({room, category});
insertedIntoTags.push(key);
} else {
listsClone[key] = [];
const pushedEntry = this._slotRoomIntoList(
room, category, key, this._state.lists[key], listsClone[key], lastTimestamp);
if (!pushedEntry) {
// This should rarely happen: _slotRoomIntoList has several checks which attempt
// to make sure that a room is not lost in the list. If we do lose the room though,
// we shouldn't throw it on the floor and forget about it. Instead, we should insert
// it somewhere. We'll insert it at the top for a couple reasons: 1) it is probably
// an important room for the user and 2) if this does happen, we'd want a bug report.
console.warn(`!! Room ${room.roomId} nearly lost: Failed to find a position`);
console.warn(`!! Inserting at position 0 in the list and flagging as inserted`);
console.warn("!! Additional info: ", {
category,
key,
upToIndex: listsClone[key].length,
expectedCount: this._state.lists[key].length,
});
listsClone[key].splice(0, 0, {room, category});
}
insertedIntoTags.push(key);
}
}
// Double check that we inserted the room in the right places.
// There should never be a discrepancy.
for (const targetTag of targetTags) {
let count = 0;
for (const insertedTag of insertedIntoTags) {
if (insertedTag === targetTag) count++;
}
if (count !== 1) {
console.warn(`!! Room ${room.roomId} inserted ${count} times to ${targetTag}`);
}
// This is a workaround for https://github.com/vector-im/riot-web/issues/11303
// The logging is to try and identify what happened exactly.
if (count === 0) {
// Something went very badly wrong - try to recover the room.
// We don't bother checking how the target list is ordered - we're expecting
// to just insert it.
console.warn(`!! Recovering ${room.roomId} for tag ${targetTag} at position 0`);
if (!listsClone[targetTag]) {
console.warn(`!! List for tag ${targetTag} does not exist - creating`);
listsClone[targetTag] = [];
}
listsClone[targetTag].splice(0, 0, {room, category});
}
}
// Sort the favourites before we set the clone
for (const tag of Object.keys(listsClone)) {
if (getListAlgorithm(tag, this._state.algorithm) !== ALGO_MANUAL) continue; // skip recents (pre-sorted)
listsClone[tag].sort(this._getManualComparator(tag));
}
this._setState({lists: listsClone});
}
_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.
console.log("Generating initial room lists");
const lists = {
"m.server_notice": [],
"im.vector.fake.invite": [],
"m.favourite": [],
"im.vector.fake.recent": [],
[TAG_DM]: [],
"m.lowpriority": [],
"im.vector.fake.archived": [],
};
const dmRoomMap = DMRoomMap.shared();
this._matrixClient.getRooms().forEach((room) => {
const myUserId = this._matrixClient.getUserId();
const membership = room.getMyMembership();
const me = room.getMember(myUserId);
if (membership === "invite") {
lists["im.vector.fake.invite"].push({room, category: CATEGORY_RED});
} else if (membership === "join" || membership === "ban" || (me && me.isKicked())) {
// Used to split rooms via tags
let tagNames = Object.keys(room.tags);
// ignore any m. tag names we don't know about
tagNames = tagNames.filter((t) => {
// Speed optimization: Avoid hitting the SettingsStore at all costs by making it the
// last condition possible.
return lists[t] !== undefined || (!t.startsWith('m.') && this._state.tagsEnabled);
});
if (tagNames.length) {
for (let i = 0; i < tagNames.length; i++) {
const tagName = tagNames[i];
lists[tagName] = lists[tagName] || [];
// Default to an arbitrary category for tags which aren't ordered by recents
let category = CATEGORY_IDLE;
if (getListAlgorithm(tagName, this._state.algorithm) !== ALGO_MANUAL) {
category = this._calculateCategory(room);
}
lists[tagName].push({room, category});
}
} else if (dmRoomMap.getUserIdForRoomId(room.roomId)) {
// "Direct Message" rooms (that we're still in and that aren't otherwise tagged)
lists[TAG_DM].push({room, category: this._calculateCategory(room)});
} else {
lists["im.vector.fake.recent"].push({room, category: this._calculateCategory(room)});
}
} else if (membership === "leave") {
// The category of these rooms is not super important, so deprioritize it to the lowest
// possible value.
lists["im.vector.fake.archived"].push({room, category: CATEGORY_IDLE});
}
});
// We use this cache in the recents comparator because _tsOfNewestEvent can take a while. This
// cache only needs to survive the sort operation below and should not be implemented outside
// of this function, otherwise the room lists will almost certainly be out of date and wrong.
const latestEventTsCache = {}; // roomId => timestamp
const tsOfNewestEventFn = (room) => {
if (!room) return Number.MAX_SAFE_INTEGER; // Should only happen in tests
if (latestEventTsCache[room.roomId]) {
return latestEventTsCache[room.roomId];
}
const ts = this._tsOfNewestEvent(room);
latestEventTsCache[room.roomId] = ts;
return ts;
};
Object.keys(lists).forEach((listKey) => {
let comparator;
switch (getListAlgorithm(listKey, this._state.algorithm)) {
case ALGO_RECENT:
comparator = (entryA, entryB) => this._recentsComparator(entryA, entryB, tsOfNewestEventFn);
break;
case ALGO_ALPHABETIC:
comparator = this._lexicographicalComparator;
break;
case ALGO_MANUAL:
default:
comparator = this._getManualComparator(listKey);
break;
}
if (this._state.orderImportantFirst) {
lists[listKey].sort((entryA, entryB) => {
if (entryA.category !== entryB.category) {
const idxA = CATEGORY_ORDER.indexOf(entryA.category);
const idxB = CATEGORY_ORDER.indexOf(entryB.category);
if (idxA > idxB) return 1;
if (idxA < idxB) return -1;
return 0; // Technically not possible
}
return comparator(entryA, entryB);
});
} else {
// skip the category comparison even though it should no-op when orderImportantFirst disabled
lists[listKey].sort(comparator);
}
});
this._setState({
lists,
ready: true, // Ready to receive updates to ordering
});
}
_eventTriggersRecentReorder(ev) {
return ev.getTs() && (
Unread.eventTriggersUnreadCount(ev) ||
ev.getSender() === this._matrixClient.credentials.userId
);
}
_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;
for (let i = room.timeline.length - 1; i >= 0; --i) {
const ev = room.timeline[i];
if (this._eventTriggersRecentReorder(ev)) {
return ev.getTs();
}
}
// we might only have events that don't trigger the unread indicator,
// in which case use the oldest event even if normally it wouldn't count.
// This is better than just assuming the last event was forever ago.
if (room.timeline.length && room.timeline[0].getTs()) {
return room.timeline[0].getTs();
} else {
return Number.MAX_SAFE_INTEGER;
}
}
_calculateCategory(room) {
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.
return CATEGORY_IDLE;
}
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;
}
_recentsComparator(entryA, entryB, tsOfNewestEventFn) {
const timestampA = tsOfNewestEventFn(entryA.room);
const timestampB = tsOfNewestEventFn(entryB.room);
return timestampB - timestampA;
}
_lexicographicalComparator(entryA, entryB) {
return entryA.room.name.localeCompare(entryB.room.name);
}
_getManualComparator(tagName, optimisticRequest) {
return (entryA, entryB) => {
const roomA = entryA.room;
const roomB = entryB.room;
let metaA = roomA.tags[tagName];
let metaB = roomB.tags[tagName];
if (optimisticRequest && roomA === optimisticRequest.room) metaA = optimisticRequest.metaData;
if (optimisticRequest && roomB === optimisticRequest.room) metaB = optimisticRequest.metaData;
// Make sure the room tag has an order element, if not set it to be the bottom
const a = metaA ? Number(metaA.order) : undefined;
const b = metaB ? Number(metaB.order) : undefined;
// Order undefined room tag orders to the bottom
if (a === undefined && b !== undefined) {
return 1;
} else if (a !== undefined && b === undefined) {
return -1;
}
return a === b ? this._lexicographicalComparator(entryA, entryB) : (a > b ? 1 : -1);
};
}
getRoomLists() {
return this._state.presentationLists;
}
}
if (global.singletonRoomListStore === undefined) {
global.singletonRoomListStore = new RoomListStore();
}
export default global.singletonRoomListStore;

View file

@ -21,7 +21,6 @@ import { DefaultTagID, TagID } from "../room-list/models";
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState"; import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { RoomNotificationState } from "./RoomNotificationState"; import { RoomNotificationState } from "./RoomNotificationState";
import { TagSpecificNotificationState } from "./TagSpecificNotificationState";
const INSPECIFIC_TAG = "INSPECIFIC_TAG"; const INSPECIFIC_TAG = "INSPECIFIC_TAG";
type INSPECIFIC_TAG = "INSPECIFIC_TAG"; type INSPECIFIC_TAG = "INSPECIFIC_TAG";
@ -72,11 +71,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
const forRoomMap = this.roomMap.get(room); const forRoomMap = this.roomMap.get(room);
if (!forRoomMap.has(targetTag)) { if (!forRoomMap.has(targetTag)) {
if (inTagId) { forRoomMap.set(inTagId ? inTagId : INSPECIFIC_TAG, new RoomNotificationState(room));
forRoomMap.set(inTagId, new TagSpecificNotificationState(room, inTagId));
} else {
forRoomMap.set(INSPECIFIC_TAG, new RoomNotificationState(room));
}
} }
return forRoomMap.get(targetTag); return forRoomMap.get(targetTag);

View file

@ -1,46 +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 { NotificationColor } from "./NotificationColor";
import { Room } from "matrix-js-sdk/src/models/room";
import { TagID } from "../room-list/models";
import { RoomNotificationState } from "./RoomNotificationState";
export class TagSpecificNotificationState extends RoomNotificationState {
private static TAG_TO_COLOR: {
// @ts-ignore - TS wants this to be a string key, but we know better
[tagId: TagID]: NotificationColor,
} = {
// TODO: Update for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
//[DefaultTagID.DM]: NotificationColor.Red,
};
private readonly colorWhenNotIdle?: NotificationColor;
constructor(room: Room, tagId: TagID) {
super(room);
const specificColor = TagSpecificNotificationState.TAG_TO_COLOR[tagId];
if (specificColor) this.colorWhenNotIdle = specificColor;
}
public get color(): NotificationColor {
if (!this.colorWhenNotIdle) return super.color;
if (super.color !== NotificationColor.None) return this.colorWhenNotIdle;
return super.color;
}
}

View file

@ -18,7 +18,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { RoomListStoreTempProxy } from "./RoomListStoreTempProxy";
import { MessageEventPreview } from "./previews/MessageEventPreview"; import { MessageEventPreview } from "./previews/MessageEventPreview";
import { NameEventPreview } from "./previews/NameEventPreview"; import { NameEventPreview } from "./previews/NameEventPreview";
import { TagID } from "./models"; import { TagID } from "./models";
@ -192,9 +191,6 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return; if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default: https://github.com/vector-im/riot-web/issues/14367
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') { if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher const event = payload.event; // TODO: Type out the dispatcher
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important

View file

@ -44,7 +44,7 @@ interface IState {
*/ */
export const LISTS_UPDATE_EVENT = "lists_update"; export const LISTS_UPDATE_EVENT = "lists_update";
export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> { export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
/** /**
* Set to true if you're running tests on the store. Should not be touched in * Set to true if you're running tests on the store. Should not be touched in
* any other environment. * any other environment.
@ -52,7 +52,6 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
public static TEST_MODE = false; public static TEST_MODE = false;
private initialListsGenerated = false; private initialListsGenerated = false;
private enabled = false;
private algorithm = new Algorithm(); private algorithm = new Algorithm();
private filterConditions: IFilterCondition[] = []; private filterConditions: IFilterCondition[] = [];
private tagWatcher = new TagWatcher(this); private tagWatcher = new TagWatcher(this);
@ -60,13 +59,13 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
private readonly watchedSettings = [ private readonly watchedSettings = [
'feature_custom_tags', 'feature_custom_tags',
'advancedRoomListLogging', // TODO: Remove watch: https://github.com/vector-im/riot-web/issues/14367 'advancedRoomListLogging', // TODO: Remove watch: https://github.com/vector-im/riot-web/issues/14602
]; ];
constructor() { constructor() {
super(defaultDispatcher); super(defaultDispatcher);
this.checkEnabled(); this.checkLoggingEnabled();
for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null);
RoomViewStore.addListener(() => this.handleRVSUpdate({})); RoomViewStore.addListener(() => this.handleRVSUpdate({}));
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated); this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
@ -106,9 +105,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
super.matrixClient = forcedClient; super.matrixClient = forcedClient;
} }
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14367 this.checkLoggingEnabled();
this.checkEnabled();
if (!this.enabled) return;
// Update any settings here, as some may have happened before we were logically ready. // Update any settings here, as some may have happened before we were logically ready.
// Update any settings here, as some may have happened before we were logically ready. // Update any settings here, as some may have happened before we were logically ready.
@ -121,12 +118,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
this.updateFn.trigger(); this.updateFn.trigger();
} }
// TODO: Remove enabled flag with the old RoomListStore: https://github.com/vector-im/riot-web/issues/14367 private checkLoggingEnabled() {
private checkEnabled() {
this.enabled = SettingsStore.getValue("feature_new_room_list");
if (this.enabled) {
console.log("⚡ new room list store engaged");
}
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
console.warn("Advanced room list logging is enabled"); console.warn("Advanced room list logging is enabled");
} }
@ -146,7 +138,6 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
* be used if the calling code will manually trigger the update. * be used if the calling code will manually trigger the update.
*/ */
private async handleRVSUpdate({trigger = true}) { private async handleRVSUpdate({trigger = true}) {
if (!this.enabled) return; // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
if (!this.matrixClient) return; // We assume there won't be RVS updates without a client if (!this.matrixClient) return; // We assume there won't be RVS updates without a client
const activeRoomId = RoomViewStore.getRoomId(); const activeRoomId = RoomViewStore.getRoomId();
@ -159,7 +150,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
await this.algorithm.setStickyRoom(null); await this.algorithm.setStickyRoom(null);
} else if (activeRoom !== this.algorithm.stickyRoom) { } else if (activeRoom !== this.algorithm.stickyRoom) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Changing sticky room to ${activeRoomId}`); console.log(`Changing sticky room to ${activeRoomId}`);
} }
await this.algorithm.setStickyRoom(activeRoom); await this.algorithm.setStickyRoom(activeRoom);
@ -180,7 +171,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
protected async onAction(payload: ActionPayload) { protected async onAction(payload: ActionPayload) {
// When we're running tests we can't reliably use setImmediate out of timing concerns. // When we're running tests we can't reliably use setImmediate out of timing concerns.
// As such, we use a more synchronous model. // As such, we use a more synchronous model.
if (RoomListStore2.TEST_MODE) { if (RoomListStoreClass.TEST_MODE) {
await this.onDispatchAsync(payload); await this.onDispatchAsync(payload);
return; return;
} }
@ -191,16 +182,13 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} }
protected async onDispatchAsync(payload: ActionPayload) { protected async onDispatchAsync(payload: ActionPayload) {
// TODO: Remove this once the RoomListStore becomes default
if (!this.enabled) return;
// Everything here requires a MatrixClient or some sort of logical readiness. // Everything here requires a MatrixClient or some sort of logical readiness.
const logicallyReady = this.matrixClient && this.initialListsGenerated; const logicallyReady = this.matrixClient && this.initialListsGenerated;
if (!logicallyReady) return; if (!logicallyReady) return;
if (payload.action === 'setting_updated') { if (payload.action === 'setting_updated') {
if (this.watchedSettings.includes(payload.settingName)) { if (this.watchedSettings.includes(payload.settingName)) {
// TODO: Remove with https://github.com/vector-im/riot-web/issues/14367 // TODO: Remove with https://github.com/vector-im/riot-web/issues/14602
if (payload.settingName === "advancedRoomListLogging") { if (payload.settingName === "advancedRoomListLogging") {
// Log when the setting changes so we know when it was turned on in the rageshake // Log when the setting changes so we know when it was turned on in the rageshake
const enabled = SettingsStore.getValue("advancedRoomListLogging"); const enabled = SettingsStore.getValue("advancedRoomListLogging");
@ -231,7 +219,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
return; return;
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`); console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
} }
await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt); await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
@ -241,7 +229,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} else if (payload.action === 'MatrixActions.Room.tags') { } else if (payload.action === 'MatrixActions.Room.tags') {
const roomPayload = (<any>payload); // TODO: Type out the dispatcher types const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`); console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
} }
await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange); await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
@ -256,13 +244,13 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
const room = this.matrixClient.getRoom(roomId); const room = this.matrixClient.getRoom(roomId);
const tryUpdate = async (updatedRoom: Room) => { const tryUpdate = async (updatedRoom: Room) => {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` + console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
` in ${updatedRoom.roomId}`); ` in ${updatedRoom.roomId}`);
} }
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') { if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
} }
const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']); const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']);
@ -295,7 +283,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
return; return;
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`); console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
} }
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
@ -303,7 +291,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Received updated DM map`); console.log(`[RoomListDebug] Received updated DM map`);
} }
const dmMap = eventPayload.event.getContent(); const dmMap = eventPayload.event.getContent();
@ -330,7 +318,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
const newMembership = getEffectiveMembership(membershipPayload.membership); const newMembership = getEffectiveMembership(membershipPayload.membership);
if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) { if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`); console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
} }
@ -339,7 +327,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", ""); const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", "");
if (createEvent && createEvent.getContent()['predecessor']) { if (createEvent && createEvent.getContent()['predecessor']) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Room has a predecessor`); console.log(`[RoomListDebug] Room has a predecessor`);
} }
const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']); const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']);
@ -347,7 +335,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
const isSticky = this.algorithm.stickyRoom === prevRoom; const isSticky = this.algorithm.stickyRoom === prevRoom;
if (isSticky) { if (isSticky) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`); console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
} }
await this.algorithm.setStickyRoom(null); await this.algorithm.setStickyRoom(null);
@ -356,7 +344,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
// Note: we hit the algorithm instead of our handleRoomUpdate() function to // Note: we hit the algorithm instead of our handleRoomUpdate() function to
// avoid redundant updates. // avoid redundant updates.
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Removing previous room from room list`); console.log(`[RoomListDebug] Removing previous room from room list`);
} }
await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved); await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
@ -364,7 +352,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Adding new room to room list`); console.log(`[RoomListDebug] Adding new room to room list`);
} }
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom); await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
@ -374,7 +362,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) { if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`); console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
} }
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom); await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
@ -385,7 +373,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
// If it's not a join, it's transitioning into a different list (possibly historical) // If it's not a join, it's transitioning into a different list (possibly historical)
if (oldMembership !== newMembership) { if (oldMembership !== newMembership) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`); console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
} }
await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange); await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
@ -399,7 +387,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
if (shouldUpdate) { if (shouldUpdate) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`); console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
} }
this.updateFn.mark(); this.updateFn.mark();
@ -514,15 +502,9 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} }
} }
protected async updateState(newState: IState) {
if (!this.enabled) return;
await super.updateState(newState);
}
private onAlgorithmListUpdated = () => { private onAlgorithmListUpdated = () => {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("Underlying algorithm has triggered a list update - marking"); console.log("Underlying algorithm has triggered a list update - marking");
} }
this.updateFn.mark(); this.updateFn.mark();
@ -558,7 +540,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
// TODO: Fix custom tags: https://github.com/vector-im/riot-web/issues/14091 // TODO: Fix custom tags: https://github.com/vector-im/riot-web/issues/14091
const roomTags = TagOrderStore.getOrderedTags() || []; const roomTags = TagOrderStore.getOrderedTags() || [];
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("rtags", roomTags); console.log("rtags", roomTags);
} }
@ -572,7 +554,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
public addFilter(filter: IFilterCondition): void { public addFilter(filter: IFilterCondition): void {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("Adding filter condition:", filter); console.log("Adding filter condition:", filter);
} }
this.filterConditions.push(filter); this.filterConditions.push(filter);
@ -584,7 +566,7 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
public removeFilter(filter: IFilterCondition): void { public removeFilter(filter: IFilterCondition): void {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("Removing filter condition:", filter); console.log("Removing filter condition:", filter);
} }
const idx = this.filterConditions.indexOf(filter); const idx = this.filterConditions.indexOf(filter);
@ -613,15 +595,15 @@ export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
} }
export default class RoomListStore { export default class RoomListStore {
private static internalInstance: RoomListStore2; private static internalInstance: RoomListStoreClass;
public static get instance(): RoomListStore2 { public static get instance(): RoomListStoreClass {
if (!RoomListStore.internalInstance) { if (!RoomListStore.internalInstance) {
RoomListStore.internalInstance = new RoomListStore2(); RoomListStore.internalInstance = new RoomListStoreClass();
} }
return RoomListStore.internalInstance; return RoomListStore.internalInstance;
} }
} }
window.mx_RoomListStore2 = RoomListStore.instance; window.mx_RoomListStore = RoomListStore.instance;

View file

@ -1,61 +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 SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "./RoomListStore2";
import OldRoomListStore from "../RoomListStore";
import { UPDATE_EVENT } from "../AsyncStore";
import { ITagMap } from "./algorithms/models";
/**
* Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when
* it is available to everyone.
*
* TODO: Delete this: https://github.com/vector-im/riot-web/issues/14367
*/
export class RoomListStoreTempProxy {
public static isUsingNewStore(): boolean {
return SettingsStore.getValue("feature_new_room_list");
}
public static addListener(handler: () => void): RoomListStoreTempToken {
if (RoomListStoreTempProxy.isUsingNewStore()) {
const offFn = () => RoomListStore.instance.off(UPDATE_EVENT, handler);
RoomListStore.instance.on(UPDATE_EVENT, handler);
return new RoomListStoreTempToken(offFn);
} else {
const token = OldRoomListStore.addListener(handler);
return new RoomListStoreTempToken(() => token.remove());
}
}
public static getRoomLists(): ITagMap {
if (RoomListStoreTempProxy.isUsingNewStore()) {
return RoomListStore.instance.orderedLists;
} else {
return OldRoomListStore.getRoomLists();
}
}
}
export class RoomListStoreTempToken {
constructor(private offFn: () => void) {
}
public remove(): void {
this.offFn();
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { RoomListStore2 } from "./RoomListStore2"; import { RoomListStoreClass } from "./RoomListStore";
import TagOrderStore from "../TagOrderStore"; import TagOrderStore from "../TagOrderStore";
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition"; import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
@ -26,7 +26,7 @@ export class TagWatcher {
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091 // TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
private filters = new Map<string, CommunityFilterCondition>(); private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStore2) { constructor(private store: RoomListStoreClass) {
TagOrderStore.addListener(this.onTagsUpdated); TagOrderStore.addListener(this.onTagsUpdated);
} }

View file

@ -323,7 +323,7 @@ export class Algorithm extends EventEmitter {
newMap[tagId] = allowedRoomsInThisTag; newMap[tagId] = allowedRoomsInThisTag;
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`); console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
} }
} }
@ -338,7 +338,7 @@ export class Algorithm extends EventEmitter {
if (!this.hasFilters) return; // don't bother doing work if there's nothing to do if (!this.hasFilters) return; // don't bother doing work if there's nothing to do
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Recalculating filtered rooms for ${tagId}`); console.log(`Recalculating filtered rooms for ${tagId}`);
} }
delete this.filteredRooms[tagId]; delete this.filteredRooms[tagId];
@ -350,7 +350,7 @@ export class Algorithm extends EventEmitter {
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`); console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
} }
} }
@ -392,7 +392,7 @@ export class Algorithm extends EventEmitter {
if (!this._cachedStickyRooms || !updatedTag) { if (!this._cachedStickyRooms || !updatedTag) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Generating clone of cached rooms for sticky room handling`); console.log(`Generating clone of cached rooms for sticky room handling`);
} }
const stickiedTagMap: ITagMap = {}; const stickiedTagMap: ITagMap = {};
@ -406,7 +406,7 @@ export class Algorithm extends EventEmitter {
// Update the tag indicated by the caller, if possible. This is mostly to ensure // Update the tag indicated by the caller, if possible. This is mostly to ensure
// our cache is up to date. // our cache is up to date.
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Replacing cached sticky rooms for ${updatedTag}`); console.log(`Replacing cached sticky rooms for ${updatedTag}`);
} }
this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
@ -418,7 +418,7 @@ export class Algorithm extends EventEmitter {
const sticky = this._stickyRoom; const sticky = this._stickyRoom;
if (!updatedTag || updatedTag === sticky.tag) { if (!updatedTag || updatedTag === sticky.tag) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`); console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
} }
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room); this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
@ -646,7 +646,7 @@ export class Algorithm extends EventEmitter {
*/ */
public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> { public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
} }
if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
@ -706,7 +706,7 @@ export class Algorithm extends EventEmitter {
if (diff.removed.length > 0 || diff.added.length > 0) { if (diff.removed.length > 0 || diff.added.length > 0) {
for (const rmTag of diff.removed) { for (const rmTag of diff.removed) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Removing ${room.roomId} from ${rmTag}`); console.log(`Removing ${room.roomId} from ${rmTag}`);
} }
const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
@ -716,7 +716,7 @@ export class Algorithm extends EventEmitter {
} }
for (const addTag of diff.added) { for (const addTag of diff.added) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Adding ${room.roomId} to ${addTag}`); console.log(`Adding ${room.roomId} to ${addTag}`);
} }
const algorithm: OrderingAlgorithm = this.algorithms[addTag]; const algorithm: OrderingAlgorithm = this.algorithms[addTag];
@ -729,14 +729,14 @@ export class Algorithm extends EventEmitter {
this.roomIdsToTags[room.roomId] = newTags; this.roomIdsToTags[room.roomId] = newTags;
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`); console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
} }
cause = RoomUpdateCause.Timeline; cause = RoomUpdateCause.Timeline;
didTagChange = true; didTagChange = true;
} else { } else {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`); console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
} }
cause = RoomUpdateCause.Timeline; cause = RoomUpdateCause.Timeline;
@ -765,7 +765,7 @@ export class Algorithm extends EventEmitter {
if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) { if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
if (this.stickyRoom === room) { if (this.stickyRoom === room) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`); console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
} }
return false; return false;
@ -775,14 +775,14 @@ export class Algorithm extends EventEmitter {
if (!this.roomIdsToTags[room.roomId]) { if (!this.roomIdsToTags[room.roomId]) {
if (CAUSES_REQUIRING_ROOM.includes(cause)) { if (CAUSES_REQUIRING_ROOM.includes(cause)) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`); console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
} }
return false; return false;
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`); console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
} }
@ -796,13 +796,13 @@ export class Algorithm extends EventEmitter {
this.roomIdsToTags[room.roomId] = roomTags; this.roomIdsToTags[room.roomId] = roomTags;
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags); console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
} }
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`); console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
} }
@ -827,7 +827,7 @@ export class Algorithm extends EventEmitter {
} }
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`); console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
} }
return changed; return changed;

View file

@ -14,7 +14,7 @@ import GroupStore from '../../../../src/stores/GroupStore.js';
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
import {DefaultTagID} from "../../../../src/stores/room-list/models"; import {DefaultTagID} from "../../../../src/stores/room-list/models";
import RoomListStore, {LISTS_UPDATE_EVENT, RoomListStore2} from "../../../../src/stores/room-list/RoomListStore2"; import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
function generateRoomId() { function generateRoomId() {
@ -49,7 +49,7 @@ describe('RoomList', () => {
let myOtherMember; let myOtherMember;
beforeEach(async function(done) { beforeEach(async function(done) {
RoomListStore2.TEST_MODE = true; RoomListStoreClass.TEST_MODE = true;
TestUtils.stubClient(); TestUtils.stubClient();
client = MatrixClientPeg.get(); client = MatrixClientPeg.get();
@ -62,7 +62,7 @@ describe('RoomList', () => {
parentDiv = document.createElement('div'); parentDiv = document.createElement('div');
document.body.appendChild(parentDiv); document.body.appendChild(parentDiv);
const RoomList = sdk.getComponent('views.rooms.RoomList2'); const RoomList = sdk.getComponent('views.rooms.RoomList');
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList); const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render( root = ReactDOM.render(
<DragDropContext> <DragDropContext>
@ -128,8 +128,8 @@ describe('RoomList', () => {
}); });
function expectRoomInSubList(room, subListTest) { function expectRoomInSubList(room, subListTest) {
const RoomSubList = sdk.getComponent('views.rooms.RoomSublist2'); const RoomSubList = sdk.getComponent('views.rooms.RoomSublist');
const RoomTile = sdk.getComponent('views.rooms.RoomTile2'); const RoomTile = sdk.getComponent('views.rooms.RoomTile');
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList); const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSubList);
const containingSubList = subLists.find(subListTest); const containingSubList = subLists.find(subListTest);

View file

@ -20,7 +20,7 @@ const {findSublist} = require("./create-room");
module.exports = async function acceptInvite(session, name) { module.exports = async function acceptInvite(session, name) {
session.log.step(`accepts "${name}" invite`); session.log.step(`accepts "${name}" invite`);
const inviteSublist = await findSublist(session, "invites"); const inviteSublist = await findSublist(session, "invites");
const invitesHandles = await inviteSublist.$$(".mx_RoomTile2_name"); const invitesHandles = await inviteSublist.$$(".mx_RoomTile_name");
const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => { const invitesWithText = await Promise.all(invitesHandles.map(async (inviteHandle) => {
const text = await session.innerText(inviteHandle); const text = await session.innerText(inviteHandle);
return {inviteHandle, text}; return {inviteHandle, text};

View file

@ -16,14 +16,14 @@ limitations under the License.
*/ */
async function openRoomDirectory(session) { async function openRoomDirectory(session) {
const roomDirectoryButton = await session.query('.mx_LeftPanel2_exploreButton'); const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
await roomDirectoryButton.click(); await roomDirectoryButton.click();
} }
async function findSublist(session, name) { async function findSublist(session, name) {
const sublists = await session.queryAll('.mx_RoomSublist2'); const sublists = await session.queryAll('.mx_RoomSublist');
for (const sublist of sublists) { for (const sublist of sublists) {
const header = await sublist.$('.mx_RoomSublist2_headerText'); const header = await sublist.$('.mx_RoomSublist_headerText');
const headerText = await session.innerText(header); const headerText = await session.innerText(header);
if (headerText.toLowerCase().includes(name.toLowerCase())) { if (headerText.toLowerCase().includes(name.toLowerCase())) {
return sublist; return sublist;
@ -36,7 +36,7 @@ async function createRoom(session, roomName, encrypted=false) {
session.log.step(`creates room "${roomName}"`); session.log.step(`creates room "${roomName}"`);
const roomsSublist = await findSublist(session, "rooms"); const roomsSublist = await findSublist(session, "rooms");
const addRoomButton = await roomsSublist.$(".mx_RoomSublist2_auxButton"); const addRoomButton = await roomsSublist.$(".mx_RoomSublist_auxButton");
await addRoomButton.click(); await addRoomButton.click();
const roomNameInput = await session.query('.mx_CreateRoomDialog_name input'); const roomNameInput = await session.query('.mx_CreateRoomDialog_name input');
@ -58,7 +58,7 @@ async function createDm(session, invitees) {
session.log.step(`creates DM with ${JSON.stringify(invitees)}`); session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
const dmsSublist = await findSublist(session, "people"); const dmsSublist = await findSublist(session, "people");
const startChatButton = await dmsSublist.$(".mx_RoomSublist2_auxButton"); const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton");
await startChatButton.click(); await startChatButton.click();
const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea'); const inviteesEditor = await session.query('.mx_InviteDialog_editor textarea');