mirror of https://github.com/vector-im/riot-web
Add Space Panel with Room List filtering
parent
7030c636f0
commit
f21aedc6cf
|
@ -27,6 +27,7 @@
|
||||||
@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/_SpacePanel.scss";
|
||||||
@import "./structures/_TabbedView.scss";
|
@import "./structures/_TabbedView.scss";
|
||||||
@import "./structures/_ToastContainer.scss";
|
@import "./structures/_ToastContainer.scss";
|
||||||
@import "./structures/_UploadBar.scss";
|
@import "./structures/_UploadBar.scss";
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||||
|
$roomListCollapsedWidth: 68px;
|
||||||
|
|
||||||
.mx_LeftPanel {
|
.mx_LeftPanel {
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
|
@ -37,18 +38,12 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
||||||
// GroupFilterPanel handles its own CSS
|
// GroupFilterPanel handles its own CSS
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
|
||||||
.mx_LeftPanel_roomListContainer {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||||
.mx_LeftPanel_roomListContainer {
|
.mx_LeftPanel_roomListContainer {
|
||||||
width: calc(100% - $groupFilterPanelWidth);
|
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
|
flex: 1 0 0;
|
||||||
|
min-width: 0;
|
||||||
// Create another flexbox (this time a column) for the room list components
|
// Create another flexbox (this time a column) for the room list components
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -168,17 +163,10 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
||||||
// These styles override the defaults for the minimized (66px) layout
|
// These styles override the defaults for the minimized (66px) layout
|
||||||
&.mx_LeftPanel_minimized {
|
&.mx_LeftPanel_minimized {
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
|
width: unset !important;
|
||||||
// We have to forcefully set the width to override the resizer's style attribute.
|
|
||||||
&.mx_LeftPanel_hasGroupFilterPanel {
|
|
||||||
width: calc(68px + $groupFilterPanelWidth) !important;
|
|
||||||
}
|
|
||||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
|
||||||
width: 68px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_LeftPanel_roomListContainer {
|
.mx_LeftPanel_roomListContainer {
|
||||||
width: 68px;
|
width: $roomListCollapsedWidth;
|
||||||
|
|
||||||
.mx_LeftPanel_userHeader {
|
.mx_LeftPanel_userHeader {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
$topLevelHeight: 32px;
|
||||||
|
$nestedHeight: 24px;
|
||||||
|
$gutterSize: 21px;
|
||||||
|
$activeStripeSize: 4px;
|
||||||
|
|
||||||
|
.mx_SpacePanel {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background-color: $groupFilterPanel-bg-color;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// Create another flexbox so the Panel fills the container
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.mx_SpacePanel_spaceTreeWrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpacePanel_toggleCollapse {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 32px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
margin-left: $gutterSize;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background-color: $roomlist-header-color;
|
||||||
|
mask-image: url('$(res)/img/element-icons/expand-space-panel.svg');
|
||||||
|
|
||||||
|
&.expanded {
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AutoHideScrollbar {
|
||||||
|
padding: 16px 12px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem.collapsed {
|
||||||
|
& > .mx_SpaceButton > .mx_SpaceButton_toggleCollapse {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .mx_SpaceTreeLevel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem:not(.hasSubSpaces) > .mx_SpaceButton {
|
||||||
|
margin-left: $gutterSize;
|
||||||
|
|
||||||
|
&.mx_SpaceButton_active {
|
||||||
|
&::before {
|
||||||
|
left: -$gutterSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceButton {
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mx_SpaceButton_name {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: block;
|
||||||
|
max-width: 150px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
width: calc($gutterSize - $activeStripeSize);
|
||||||
|
margin-left: 1px;
|
||||||
|
height: 20px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background-color: $roomlist-header-color;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_SpaceButton_active {
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
width: $activeStripeSize;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: $accent-color;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceButton_avatarPlaceholder {
|
||||||
|
width: $topLevelHeight;
|
||||||
|
min-width: $topLevelHeight;
|
||||||
|
height: $topLevelHeight;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
width: $topLevelHeight;
|
||||||
|
height: $topLevelHeight;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_SpaceButton_home .mx_SpaceButton_avatarPlaceholder {
|
||||||
|
background-color: #ffffff;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: #3f3d3d;
|
||||||
|
mask-image: url('$(res)/img/element-icons/home.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_SpaceButton_newCancel .mx_SpaceButton_avatarPlaceholder {
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BaseAvatar_image {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpacePanel_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: 0 2px; // centering
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge_dot {
|
||||||
|
// make the smaller dot occupy the same width for centering
|
||||||
|
margin-left: 7px;
|
||||||
|
margin-right: 7px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
.mx_SpaceButton {
|
||||||
|
.mx_SpacePanel_badgeContainer {
|
||||||
|
position: absolute;
|
||||||
|
right: -8px;
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.collapsed) {
|
||||||
|
.mx_SpaceButton:hover,
|
||||||
|
.mx_SpaceButton:focus-within,
|
||||||
|
.mx_SpaceButton_hasMenuOpen {
|
||||||
|
// Hide the badge container on hover because it'll be a menu button
|
||||||
|
.mx_SpacePanel_badgeContainer {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* root space buttons are bigger and not indented */
|
||||||
|
& > .mx_AutoHideScrollbar {
|
||||||
|
& > .mx_SpaceButton {
|
||||||
|
height: $topLevelHeight;
|
||||||
|
|
||||||
|
&.mx_SpaceButton_active::before {
|
||||||
|
height: $topLevelHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > ul {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7701 16.617H22.3721L18.614 20.3751C18.3137 20.6754 18.3137 21.1683 18.614 21.4686C18.9143 21.769 19.3995 21.769 19.6998 21.4686L24.7747 16.3937C25.0751 16.0934 25.0751 15.6082 24.7747 15.3079L19.7075 10.2253C19.4072 9.92492 18.922 9.92492 18.6217 10.2253C18.3214 10.5256 18.3214 11.0107 18.6217 11.3111L22.3721 15.0768H13.7701C13.3465 15.0768 13 15.4234 13 15.8469C13 16.2705 13.3465 16.617 13.7701 16.617Z" fill="#86888A"/>
|
||||||
|
<rect x="7" y="10" width="1.5" height="12" rx="0.75" fill="#86888A"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 651 B |
|
@ -16,6 +16,10 @@
|
||||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpacePanel {
|
||||||
|
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||||
backdrop-filter: blur($roomlist-background-blur-amount);
|
backdrop-filter: blur($roomlist-background-blur-amount);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||||
import LeftPanelWidget from "./LeftPanelWidget";
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -388,12 +389,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const groupFilterPanel = !this.state.showGroupFilterPanel ? null : (
|
let leftLeftPanel;
|
||||||
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
// Currently TagPanel.enableTagPanel is disabled when Legacy Communities are disabled so for now
|
||||||
<GroupFilterPanel />
|
// ignore it and force the rendering of SpacePanel if that Labs flag is enabled.
|
||||||
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
</div>
|
leftLeftPanel = <SpacePanel />;
|
||||||
);
|
} else if (this.state.showGroupFilterPanel) {
|
||||||
|
leftLeftPanel = (
|
||||||
|
<div className="mx_LeftPanel_GroupFilterPanelContainer">
|
||||||
|
<GroupFilterPanel />
|
||||||
|
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const roomList = <RoomList
|
const roomList = <RoomList
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
|
@ -406,7 +414,6 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
"mx_LeftPanel": true,
|
"mx_LeftPanel": true,
|
||||||
"mx_LeftPanel_hasGroupFilterPanel": !!groupFilterPanel,
|
|
||||||
"mx_LeftPanel_minimized": this.props.isMinimized,
|
"mx_LeftPanel_minimized": this.props.isMinimized,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -417,7 +424,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{groupFilterPanel}
|
{leftLeftPanel}
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
|
|
|
@ -24,7 +24,7 @@ import Modal from '../../../Modal';
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
import {ResizeMethod} from "../../../Avatar";
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick">{
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
// oobData.avatarUrl should be set (else there
|
// oobData.avatarUrl should be set (else there
|
||||||
// would be nowhere to get the avatar from)
|
// would be nowhere to get the avatar from)
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 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, {useState} from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import {_t} from "../../../languageHandler";
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import {SpaceItem} from "./SpaceTreeLevel";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
|
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
||||||
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
|
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||||
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
import {
|
||||||
|
RovingAccessibleButton,
|
||||||
|
RovingAccessibleTooltipButton,
|
||||||
|
RovingTabIndexProvider,
|
||||||
|
} from "../../../accessibility/RovingTabIndex";
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
|
interface IButtonProps {
|
||||||
|
space?: Room;
|
||||||
|
className?: string;
|
||||||
|
selected?: boolean;
|
||||||
|
tooltip?: string;
|
||||||
|
notificationState?: SpaceNotificationState;
|
||||||
|
isNarrow?: boolean;
|
||||||
|
onClick(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpaceButton: React.FC<IButtonProps> = ({
|
||||||
|
space,
|
||||||
|
className,
|
||||||
|
selected,
|
||||||
|
onClick,
|
||||||
|
tooltip,
|
||||||
|
notificationState,
|
||||||
|
isNarrow,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const classes = classNames("mx_SpaceButton", className, {
|
||||||
|
mx_SpaceButton_active: selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
let avatar = <div className="mx_SpaceButton_avatarPlaceholder" />;
|
||||||
|
if (space) {
|
||||||
|
avatar = <RoomAvatar width={32} height={32} room={space} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notifBadge;
|
||||||
|
if (notificationState) {
|
||||||
|
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||||
|
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let button;
|
||||||
|
if (isNarrow) {
|
||||||
|
button = (
|
||||||
|
<RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
|
||||||
|
{ avatar }
|
||||||
|
{ notifBadge }
|
||||||
|
{ children }
|
||||||
|
</RovingAccessibleTooltipButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
button = (
|
||||||
|
<RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
|
||||||
|
{ avatar }
|
||||||
|
<span className="mx_SpaceButton_name">{ tooltip }</span>
|
||||||
|
{ notifBadge }
|
||||||
|
{ children }
|
||||||
|
</RovingAccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <li className={classNames({
|
||||||
|
"mx_SpaceItem": true,
|
||||||
|
"collapsed": isNarrow,
|
||||||
|
})}>
|
||||||
|
{ button }
|
||||||
|
</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useSpaces = (): [Room[], Room | null] => {
|
||||||
|
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||||
|
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||||
|
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||||
|
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||||
|
return [spaces, activeSpace];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SpacePanel = () => {
|
||||||
|
const [spaces, activeSpace] = useSpaces();
|
||||||
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
|
||||||
|
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
let handled = true;
|
||||||
|
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.ARROW_UP:
|
||||||
|
onMoveFocus(ev.target as Element, true);
|
||||||
|
break;
|
||||||
|
case Key.ARROW_DOWN:
|
||||||
|
onMoveFocus(ev.target as Element, false);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handled) {
|
||||||
|
// consume all other keys in context menu
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMoveFocus = (element: Element, up: boolean) => {
|
||||||
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
|
let classes: DOMTokenList;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||||
|
element = up ? element.lastElementChild : element.firstElementChild;
|
||||||
|
descending = true;
|
||||||
|
}
|
||||||
|
classes = element.classList;
|
||||||
|
}
|
||||||
|
} while (element && !classes.contains("mx_SpaceButton"));
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
(element as HTMLElement).focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||||
|
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
||||||
|
// TODO drag and drop for re-arranging order
|
||||||
|
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||||
|
{({onKeyDownHandler}) => (
|
||||||
|
<ul
|
||||||
|
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||||
|
onKeyDown={onKeyDownHandler}
|
||||||
|
>
|
||||||
|
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
|
||||||
|
<div className="mx_SpaceTreeLevel">
|
||||||
|
<SpaceButton
|
||||||
|
className="mx_SpaceButton_home"
|
||||||
|
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||||
|
selected={!activeSpace}
|
||||||
|
tooltip={_t("Home")}
|
||||||
|
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||||
|
isNarrow={isPanelCollapsed}
|
||||||
|
/>
|
||||||
|
{ spaces.map(s => <SpaceItem
|
||||||
|
key={s.roomId}
|
||||||
|
space={s}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isPanelCollapsed={isPanelCollapsed}
|
||||||
|
onExpand={() => setPanelCollapsed(false)}
|
||||||
|
/>) }
|
||||||
|
</div>
|
||||||
|
</AutoHideScrollbar>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||||
|
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
|
||||||
|
title={expandCollapseButtonTitle}
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</RovingTabIndexProvider>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpacePanel;
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
|
import SpaceStore from "../../../stores/SpaceStore";
|
||||||
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
|
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
||||||
|
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||||
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
|
||||||
|
interface IItemProps {
|
||||||
|
space?: Room;
|
||||||
|
activeSpaces: Room[];
|
||||||
|
isNested?: boolean;
|
||||||
|
isPanelCollapsed?: boolean;
|
||||||
|
onExpand?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IItemState {
|
||||||
|
collapsed: boolean;
|
||||||
|
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||||
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
collapsed: !props.isNested, // default to collapsed for root items
|
||||||
|
contextMenuPosition: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleCollapse(evt) {
|
||||||
|
if (this.props.onExpand && this.state.collapsed) {
|
||||||
|
this.props.onExpand();
|
||||||
|
}
|
||||||
|
this.setState({collapsed: !this.state.collapsed});
|
||||||
|
// don't bubble up so encapsulating button for space
|
||||||
|
// doesn't get triggered
|
||||||
|
evt.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onContextMenu = (ev: React.MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.setState({
|
||||||
|
contextMenuPosition: {
|
||||||
|
right: ev.clientX,
|
||||||
|
top: ev.clientY,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClick = (ev: React.MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
SpaceStore.instance.setActiveSpace(this.props.space);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {space, activeSpaces, isNested} = this.props;
|
||||||
|
|
||||||
|
const forceCollapsed = this.props.isPanelCollapsed;
|
||||||
|
const isNarrow = this.props.isPanelCollapsed;
|
||||||
|
const collapsed = this.state.collapsed || forceCollapsed;
|
||||||
|
|
||||||
|
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
|
const isActive = activeSpaces.includes(space);
|
||||||
|
const itemClasses = classNames({
|
||||||
|
"mx_SpaceItem": true,
|
||||||
|
"collapsed": collapsed,
|
||||||
|
"hasSubSpaces": childSpaces && childSpaces.length,
|
||||||
|
});
|
||||||
|
const classes = classNames("mx_SpaceButton", {
|
||||||
|
mx_SpaceButton_active: isActive,
|
||||||
|
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||||
|
});
|
||||||
|
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
|
||||||
|
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
|
||||||
|
spaces={childSpaces}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
isNested={true}
|
||||||
|
/> : null;
|
||||||
|
let notifBadge;
|
||||||
|
if (notificationState) {
|
||||||
|
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||||
|
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarSize = isNested ? 24 : 32;
|
||||||
|
|
||||||
|
const toggleCollapseButton = childSpaces && childSpaces.length ?
|
||||||
|
<button
|
||||||
|
className="mx_SpaceButton_toggleCollapse"
|
||||||
|
onClick={evt => this.toggleCollapse(evt)}
|
||||||
|
/> : null;
|
||||||
|
|
||||||
|
let button;
|
||||||
|
if (isNarrow) {
|
||||||
|
button = (
|
||||||
|
<RovingAccessibleTooltipButton
|
||||||
|
className={classes}
|
||||||
|
title={space.name}
|
||||||
|
onClick={this.onClick}
|
||||||
|
onContextMenu={this.onContextMenu}
|
||||||
|
forceHide={!!this.state.contextMenuPosition}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
|
{ toggleCollapseButton }
|
||||||
|
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||||
|
{ notifBadge }
|
||||||
|
</RovingAccessibleTooltipButton>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
button = (
|
||||||
|
<RovingAccessibleButton
|
||||||
|
className={classes}
|
||||||
|
onClick={this.onClick}
|
||||||
|
onContextMenu={this.onContextMenu}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
|
{ toggleCollapseButton }
|
||||||
|
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||||
|
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||||
|
{ notifBadge }
|
||||||
|
</RovingAccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={itemClasses}>
|
||||||
|
{ button }
|
||||||
|
{ childItems }
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITreeLevelProps {
|
||||||
|
spaces: Room[];
|
||||||
|
activeSpaces: Room[];
|
||||||
|
isNested?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
||||||
|
spaces,
|
||||||
|
activeSpaces,
|
||||||
|
isNested,
|
||||||
|
}) => {
|
||||||
|
return <ul className="mx_SpaceTreeLevel">
|
||||||
|
{spaces.map(s => {
|
||||||
|
return (<SpaceItem
|
||||||
|
key={s.roomId}
|
||||||
|
activeSpaces={activeSpaces}
|
||||||
|
space={s}
|
||||||
|
isNested={isNested}
|
||||||
|
/>);
|
||||||
|
})}
|
||||||
|
</ul>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SpaceTreeLevel;
|
|
@ -978,6 +978,9 @@
|
||||||
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
"From %(deviceName)s (%(deviceId)s)": "From %(deviceName)s (%(deviceId)s)",
|
||||||
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
"Decline (%(counter)s)": "Decline (%(counter)s)",
|
||||||
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
|
||||||
|
"Expand space panel": "Expand space panel",
|
||||||
|
"Collapse space panel": "Collapse space panel",
|
||||||
|
"Home": "Home",
|
||||||
"Remove": "Remove",
|
"Remove": "Remove",
|
||||||
"Upload": "Upload",
|
"Upload": "Upload",
|
||||||
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
"This bridge was provisioned by <user />.": "This bridge was provisioned by <user />.",
|
||||||
|
@ -1941,7 +1944,6 @@
|
||||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||||
"And %(count)s more...|other": "And %(count)s more...",
|
"And %(count)s more...|other": "And %(count)s more...",
|
||||||
"Home": "Home",
|
|
||||||
"Enter a server name": "Enter a server name",
|
"Enter a server name": "Enter a server name",
|
||||||
"Looks good": "Looks good",
|
"Looks good": "Looks good",
|
||||||
"Can't find this server or its room list": "Can't find this server or its room list",
|
"Can't find this server or its room list": "Can't find this server or its room list",
|
||||||
|
|
|
@ -35,6 +35,7 @@ import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||||
|
import { SpaceWatcher } from "./SpaceWatcher";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -56,7 +57,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private initialListsGenerated = false;
|
private initialListsGenerated = false;
|
||||||
private algorithm = new Algorithm();
|
private algorithm = new Algorithm();
|
||||||
private filterConditions: IFilterCondition[] = [];
|
private filterConditions: IFilterCondition[] = [];
|
||||||
private tagWatcher = new TagWatcher(this);
|
private tagWatcher: TagWatcher;
|
||||||
|
private spaceWatcher: SpaceWatcher;
|
||||||
private updateFn = new MarkedExecution(() => {
|
private updateFn = new MarkedExecution(() => {
|
||||||
for (const tagId of Object.keys(this.orderedLists)) {
|
for (const tagId of Object.keys(this.orderedLists)) {
|
||||||
RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]);
|
RoomNotificationStateStore.instance.getListState(tagId).setRooms(this.orderedLists[tagId]);
|
||||||
|
@ -77,6 +79,15 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
RoomViewStore.addListener(() => this.handleRVSUpdate({}));
|
RoomViewStore.addListener(() => this.handleRVSUpdate({}));
|
||||||
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||||
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
this.algorithm.on(FILTER_CHANGED, this.onAlgorithmFilterUpdated);
|
||||||
|
this.setupWatchers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupWatchers() {
|
||||||
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
|
this.spaceWatcher = new SpaceWatcher(this);
|
||||||
|
} else {
|
||||||
|
this.tagWatcher = new TagWatcher(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get unfilteredLists(): ITagMap {
|
public get unfilteredLists(): ITagMap {
|
||||||
|
@ -92,9 +103,9 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// Intended for test usage
|
// Intended for test usage
|
||||||
public async resetStore() {
|
public async resetStore() {
|
||||||
await this.reset();
|
await this.reset();
|
||||||
this.tagWatcher = new TagWatcher(this);
|
|
||||||
this.filterConditions = [];
|
this.filterConditions = [];
|
||||||
this.initialListsGenerated = false;
|
this.initialListsGenerated = false;
|
||||||
|
this.setupWatchers();
|
||||||
|
|
||||||
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
|
||||||
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
|
this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
|
||||||
|
@ -554,8 +565,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
public async regenerateAllLists({trigger = true}) {
|
public async regenerateAllLists({trigger = true}) {
|
||||||
console.warn("Regenerating all room lists");
|
console.warn("Regenerating all room lists");
|
||||||
|
|
||||||
const rooms = this.matrixClient.getVisibleRooms()
|
const rooms = [
|
||||||
.filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
...this.matrixClient.getVisibleRooms(),
|
||||||
|
// also show space invites in the room list
|
||||||
|
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
|
||||||
|
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||||
|
|
||||||
const customTags = new Set<TagID>();
|
const customTags = new Set<TagID>();
|
||||||
if (this.state.tagsEnabled) {
|
if (this.state.tagsEnabled) {
|
||||||
for (const room of rooms) {
|
for (const room of rooms) {
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { RoomListStoreClass } from "./RoomListStore";
|
||||||
|
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||||
|
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||||
|
*/
|
||||||
|
export class SpaceWatcher {
|
||||||
|
private filter = new SpaceFilterCondition();
|
||||||
|
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||||
|
|
||||||
|
constructor(private store: RoomListStoreClass) {
|
||||||
|
this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state
|
||||||
|
store.addFilter(this.filter);
|
||||||
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSelectedSpaceUpdated = (activeSpace) => {
|
||||||
|
this.filter.updateSpace(this.activeSpace = activeSpace);
|
||||||
|
};
|
||||||
|
}
|
|
@ -186,6 +186,9 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doUpdateStickyRoom(val: Room) {
|
private async doUpdateStickyRoom(val: Room) {
|
||||||
|
// no-op sticky rooms
|
||||||
|
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
|
||||||
|
|
||||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||||
// otherwise we risk duplicating rooms.
|
// otherwise we risk duplicating rooms.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||||
|
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||||
|
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
||||||
|
import { setHasDiff } from "../../../utils/sets";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A filter condition for the room list which reveals rooms which
|
||||||
|
* are a member of a given space or if no space is selected shows:
|
||||||
|
* + Orphaned rooms (ones not in any space you are a part of)
|
||||||
|
* + All DMs
|
||||||
|
*/
|
||||||
|
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
|
||||||
|
private roomIds = new Set<Room>();
|
||||||
|
private space: Room = null;
|
||||||
|
|
||||||
|
public get relativePriority(): FilterPriority {
|
||||||
|
// Lowest priority so we can coarsely find rooms.
|
||||||
|
return FilterPriority.Lowest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isVisible(room: Room): boolean {
|
||||||
|
return this.roomIds.has(room.roomId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStoreUpdate = async (): Promise<void> => {
|
||||||
|
const beforeRoomIds = this.roomIds;
|
||||||
|
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
|
||||||
|
|
||||||
|
if (setHasDiff(beforeRoomIds, this.roomIds)) {
|
||||||
|
// XXX: Room List Store has a bug where rooms which are synced after the filter is set
|
||||||
|
// are excluded from the filter, this is a workaround for it.
|
||||||
|
this.emit(FILTER_CHANGED);
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emit(FILTER_CHANGED);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
|
||||||
|
|
||||||
|
public updateSpace(space: Room) {
|
||||||
|
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||||
|
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
||||||
|
this.onStoreUpdate(); // initial update from the change to the space
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue