Merge branches 'develop' and 't3chguy/room-list/3' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/3
Conflicts: src/components/structures/ContextMenu.tsx src/components/views/rooms/RoomSublist2.tsx
This commit is contained in:
commit
404009c8cb
36 changed files with 998 additions and 457 deletions
|
@ -49,6 +49,7 @@
|
||||||
@import "./views/auth/_ServerTypeSelector.scss";
|
@import "./views/auth/_ServerTypeSelector.scss";
|
||||||
@import "./views/auth/_Welcome.scss";
|
@import "./views/auth/_Welcome.scss";
|
||||||
@import "./views/avatars/_BaseAvatar.scss";
|
@import "./views/avatars/_BaseAvatar.scss";
|
||||||
|
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
@import "./views/context_menus/_RoomTileContextMenu.scss";
|
||||||
|
|
|
@ -70,7 +70,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations
|
||||||
|
|
||||||
.mx_LeftPanel2_breadcrumbsContainer {
|
.mx_LeftPanel2_breadcrumbsContainer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow-y: hidden;
|
||||||
|
overflow-x: scroll;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
34
res/css/views/avatars/_DecoratedRoomAvatar.scss
Normal file
34
res/css/views/avatars/_DecoratedRoomAvatar.scss
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// XXX: We shouldn't be using TemporaryTile anywhere - delete it.
|
||||||
|
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.mx_RoomTileIcon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NotificationBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,7 +55,7 @@ limitations under the License.
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
|
box-shadow: 4px 4px 12px 0 $menu-box-shadow-color;
|
||||||
background-color: $menu-bg-color;
|
background-color: $menu-bg-color;
|
||||||
z-index: 4000; // Higher than dialogs so tooltips can be used in dialogs
|
z-index: 6000; // Higher than context menu so tooltips can be used everywhere
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
line-height: $font-14px;
|
line-height: $font-14px;
|
||||||
|
|
|
@ -51,3 +51,18 @@ limitations under the License.
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomBreadcrumbs2_Tooltip {
|
||||||
|
margin-left: -42px;
|
||||||
|
margin-top: -42px;
|
||||||
|
|
||||||
|
&.mx_Tooltip {
|
||||||
|
background-color: $tagpanel-bg-color;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
border: 0;
|
||||||
|
|
||||||
|
.mx_Tooltip_chevron {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -389,3 +389,7 @@ limitations under the License.
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist2_addRoomTooltip {
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
||||||
|
|
|
@ -23,27 +23,20 @@ limitations under the License.
|
||||||
|
|
||||||
// The tile is also a flexbox row itself
|
// The tile is also a flexbox row itself
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
|
&.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen {
|
||||||
background-color: $roomtile2-selected-bg-color;
|
background-color: $roomtile2-selected-bg-color;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_avatarContainer {
|
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.mx_RoomTileIcon {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_nameContainer {
|
.mx_RoomTile2_nameContainer {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
max-width: calc(100% - 58px); // 32px avatar, 18px badge area, 8px margin on avatar
|
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
|
// Create a new column layout flexbox for the name parts
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -81,23 +74,37 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_badgeContainer {
|
.mx_RoomTile2_menuButton {
|
||||||
width: 18px;
|
margin-left: 4px; // spacing between buttons
|
||||||
height: 32px;
|
}
|
||||||
|
|
||||||
// Create another flexbox row because it's super easy to position the badge at
|
.mx_RoomTile2_badgeContainer {
|
||||||
// the end this way.
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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
|
// The context menu buttons are hidden by default
|
||||||
.mx_RoomTile2_menuButton,
|
.mx_RoomTile2_menuButton,
|
||||||
.mx_RoomTile2_notificationsButton {
|
.mx_RoomTile2_notificationsButton {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
min-width: 20px; // yay flex
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin: auto 0 auto 8px;
|
margin: auto 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
@ -130,7 +137,7 @@ limitations under the License.
|
||||||
.mx_RoomTile2_badgeContainer {
|
.mx_RoomTile2_badgeContainer {
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
visibility: hidden;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_notificationsButton,
|
.mx_RoomTile2_notificationsButton,
|
||||||
|
@ -145,16 +152,9 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.mx_RoomTile2_avatarContainer {
|
.mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile2_badgeContainer {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -160,6 +160,13 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Prevent clicks on the background from going through to the component which opened the menu.
|
||||||
|
_onFinished = (ev: InputEvent) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
ev.preventDefault();
|
||||||
|
if (this.props.onFinished) this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
_onMoveFocus = (element, up) => {
|
_onMoveFocus = (element, up) => {
|
||||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||||
|
|
||||||
|
@ -262,7 +269,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
position.bottom = props.bottom;
|
position.bottom = props.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
let chevronFace: IProps["chevronFace"];
|
let chevronFace: ChevronFace;
|
||||||
if (props.left) {
|
if (props.left) {
|
||||||
position.left = props.left;
|
position.left = props.left;
|
||||||
chevronFace = ChevronFace.Left;
|
chevronFace = ChevronFace.Left;
|
||||||
|
@ -349,7 +356,7 @@ export class ContextMenu extends React.PureComponent<IProps, IState> {
|
||||||
<div
|
<div
|
||||||
className="mx_ContextualMenu_background"
|
className="mx_ContextualMenu_background"
|
||||||
style={wrapperStyle}
|
style={wrapperStyle}
|
||||||
onClick={props.onFinished}
|
onClick={this._onFinished}
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -30,6 +30,7 @@ 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, { RoomListStore2, LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomListStore2";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -69,6 +70,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||||
});
|
});
|
||||||
|
@ -81,6 +83,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
||||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +154,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
let breadcrumbs;
|
let breadcrumbs;
|
||||||
if (this.state.showBreadcrumbs) {
|
if (this.state.showBreadcrumbs) {
|
||||||
breadcrumbs = (
|
breadcrumbs = (
|
||||||
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
|
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer mx_AutoHideScrollbar">
|
||||||
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
|
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -205,6 +208,11 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const roomListClasses = classNames(
|
||||||
|
"mx_LeftPanel2_actualRoomListContainer",
|
||||||
|
"mx_AutoHideScrollbar",
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{tagPanel}
|
{tagPanel}
|
||||||
|
@ -212,7 +220,7 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchExplore()}
|
||||||
<div
|
<div
|
||||||
className="mx_LeftPanel2_actualRoomListContainer"
|
className={roomListClasses}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
ref={this.listContainerRef}
|
ref={this.listContainerRef}
|
||||||
>{roomList}</div>
|
>{roomList}</div>
|
||||||
|
|
|
@ -23,7 +23,6 @@ import * as Matrix from "matrix-js-sdk";
|
||||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { isCryptoAvailable } from 'matrix-js-sdk/src/crypto';
|
|
||||||
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
// focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss
|
||||||
import 'focus-visible';
|
import 'focus-visible';
|
||||||
// what-input helps improve keyboard accessibility
|
// what-input helps improve keyboard accessibility
|
||||||
|
|
|
@ -1819,6 +1819,7 @@ export default createReactClass({
|
||||||
);
|
);
|
||||||
|
|
||||||
const showRoomRecoveryReminder = (
|
const showRoomRecoveryReminder = (
|
||||||
|
this.context.isCryptoEnabled() &&
|
||||||
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
SettingsStore.getValue("showRoomRecoveryReminder") &&
|
||||||
this.context.isRoomEncrypted(this.state.room.roomId) &&
|
this.context.isRoomEncrypted(this.state.room.roomId) &&
|
||||||
this.context.getKeyBackupEnabled() === false
|
this.context.getKeyBackupEnabled() === false
|
||||||
|
|
|
@ -37,6 +37,7 @@ import {OwnProfileStore} from "../../stores/OwnProfileStore";
|
||||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
import BaseAvatar from '../views/avatars/BaseAvatar';
|
import BaseAvatar from '../views/avatars/BaseAvatar';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -230,7 +231,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
{MatrixClientPeg.get().getUserId()}
|
{MatrixClientPeg.get().getUserId()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<AccessibleTooltipButton
|
||||||
className="mx_UserMenu_contextMenu_themeButton"
|
className="mx_UserMenu_contextMenu_themeButton"
|
||||||
onClick={this.onSwitchThemeClick}
|
onClick={this.onSwitchThemeClick}
|
||||||
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
title={this.state.isDarkTheme ? _t("Switch to light mode") : _t("Switch to dark mode")}
|
||||||
|
@ -240,7 +241,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
alt={_t("Switch theme")}
|
alt={_t("Switch theme")}
|
||||||
width={16}
|
width={16}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AccessibleTooltipButton>
|
||||||
</div>
|
</div>
|
||||||
{hostingLink}
|
{hostingLink}
|
||||||
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
<div className="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst">
|
||||||
|
|
65
src/components/views/avatars/DecoratedRoomAvatar.tsx
Normal file
65
src/components/views/avatars/DecoratedRoomAvatar.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
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 { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { TagID } from '../../../stores/room-list/models';
|
||||||
|
import RoomAvatar from "./RoomAvatar";
|
||||||
|
import RoomTileIcon from "../rooms/RoomTileIcon";
|
||||||
|
import NotificationBadge from '../rooms/NotificationBadge';
|
||||||
|
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||||
|
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
room: Room;
|
||||||
|
avatarSize: number;
|
||||||
|
tag: TagID;
|
||||||
|
displayBadge?: boolean;
|
||||||
|
forceCount?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
notificationState?: INotificationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
notificationState: new TagSpecificNotificationState(this.props.room, this.props.tag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
|
let badge: React.ReactNode;
|
||||||
|
if (this.props.displayBadge) {
|
||||||
|
badge = <NotificationBadge
|
||||||
|
notification={this.state.notificationState}
|
||||||
|
forceCount={this.props.forceCount}
|
||||||
|
roomId={this.props.room.roomId}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_DecoratedRoomAvatar">
|
||||||
|
<RoomAvatar room={this.props.room} width={this.props.avatarSize} height={this.props.avatarSize} />
|
||||||
|
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
|
||||||
|
{badge}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,21 +16,28 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import * as sdk from "../../../index";
|
import {IProps} from "./AccessibleButton";
|
||||||
|
import Tooltip from './Tooltip';
|
||||||
|
|
||||||
export default class AccessibleTooltipButton extends React.PureComponent {
|
interface ITooltipProps extends IProps {
|
||||||
static propTypes = {
|
title: string;
|
||||||
...AccessibleButton.propTypes,
|
tooltipClassName?: string;
|
||||||
// The tooltip to render on hover
|
}
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
interface IState {
|
||||||
hover: false,
|
hover: boolean;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default class AccessibleTooltipButton extends React.PureComponent<ITooltipProps, IState> {
|
||||||
|
constructor(props: ITooltipProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onMouseOver = () => {
|
onMouseOver = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -45,14 +52,15 @@ export default class AccessibleTooltipButton extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
const {title, children, ...props} = this.props;
|
const {title, children, ...props} = this.props;
|
||||||
|
const tooltipClassName = classnames(
|
||||||
|
"mx_AccessibleTooltipButton_tooltip",
|
||||||
|
this.props.tooltipClassName,
|
||||||
|
);
|
||||||
|
|
||||||
const tip = this.state.hover ? <Tooltip
|
const tip = this.state.hover ? <Tooltip
|
||||||
className="mx_AccessibleTooltipButton_container"
|
className="mx_AccessibleTooltipButton_container"
|
||||||
tooltipClassName="mx_AccessibleTooltipButton_tooltip"
|
tooltipClassName={tooltipClassName}
|
||||||
label={title}
|
label={title}
|
||||||
/> : <div />;
|
/> : <div />;
|
||||||
return (
|
return (
|
|
@ -17,35 +17,13 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import * as RoomNotifs from '../../../RoomNotifs';
|
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
|
|
||||||
import * as Unread from '../../../Unread';
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { arrayDiff } from "../../../utils/arrays";
|
|
||||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
export const NOTIFICATION_STATE_UPDATE = "update";
|
import { XOR } from "../../../@types/common";
|
||||||
|
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState";
|
||||||
export enum NotificationColor {
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
// Inverted (None -> Red) because we do integer comparisons on this
|
|
||||||
None, // nothing special
|
|
||||||
// TODO: Remove bold with notifications: https://github.com/vector-im/riot-web/issues/14227
|
|
||||||
Bold, // no badge, show as unread
|
|
||||||
Grey, // unread notified messages
|
|
||||||
Red, // unread pings
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INotificationState extends EventEmitter {
|
|
||||||
symbol?: string;
|
|
||||||
count: number;
|
|
||||||
color: NotificationColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
notification: INotificationState;
|
notification: INotificationState;
|
||||||
|
@ -62,11 +40,18 @@ interface IProps {
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
|
||||||
|
/**
|
||||||
|
* If specified will return an AccessibleButton instead of a div.
|
||||||
|
*/
|
||||||
|
onClick?(ev: React.MouseEvent);
|
||||||
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
showCounts: boolean; // whether or not to show counts. Independent of props.forceCount
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class NotificationBadge extends React.PureComponent<IProps, IState> {
|
export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
|
||||||
private countWatcherRef: string;
|
private countWatcherRef: string;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
|
@ -109,20 +94,25 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
// Don't show a badge if we don't need to
|
const {notification, forceCount, roomId, onClick, ...props} = this.props;
|
||||||
if (this.props.notification.color <= NotificationColor.None) return null;
|
|
||||||
|
|
||||||
const hasNotif = this.props.notification.color >= NotificationColor.Red;
|
// Don't show a badge if we don't need to
|
||||||
const hasCount = this.props.notification.color >= NotificationColor.Grey;
|
if (notification.color <= NotificationColor.None) return null;
|
||||||
const hasUnread = this.props.notification.color >= NotificationColor.Bold;
|
|
||||||
const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif;
|
// TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261
|
||||||
let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount);
|
// As of writing, that is "if red, show count always" and "optionally show counts instead of dots".
|
||||||
if (this.props.forceCount) {
|
// See git diff for what that boolean state looks like.
|
||||||
|
// XXX: We ignore this.state.showCounts (the setting which controls counts vs dots).
|
||||||
|
const hasNotif = notification.color >= NotificationColor.Red;
|
||||||
|
const hasCount = notification.color >= NotificationColor.Grey;
|
||||||
|
const hasAnySymbol = notification.symbol || notification.count > 0;
|
||||||
|
let isEmptyBadge = !hasAnySymbol || !hasCount;
|
||||||
|
if (forceCount) {
|
||||||
isEmptyBadge = false;
|
isEmptyBadge = false;
|
||||||
if (!hasCount) return null; // Can't render a badge
|
if (!hasCount) return null; // Can't render a badge
|
||||||
}
|
}
|
||||||
|
|
||||||
let symbol = this.props.notification.symbol || formatMinimalBadgeCount(this.props.notification.count);
|
let symbol = notification.symbol || formatMinimalBadgeCount(notification.count);
|
||||||
if (isEmptyBadge) symbol = "";
|
if (isEmptyBadge) symbol = "";
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -134,6 +124,14 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
||||||
'mx_NotificationBadge_3char': symbol.length > 2,
|
'mx_NotificationBadge_3char': symbol.length > 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (onClick) {
|
||||||
|
return (
|
||||||
|
<AccessibleButton {...props} className={classes} onClick={onClick}>
|
||||||
|
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<div className={classes}>
|
||||||
<span className="mx_NotificationBadge_count">{symbol}</span>
|
<span className="mx_NotificationBadge_count">{symbol}</span>
|
||||||
|
@ -141,242 +139,3 @@ export default class NotificationBadge extends React.PureComponent<IProps, IStat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Clean up these state classes: https://github.com/vector-im/riot-web/issues/14153
|
|
||||||
|
|
||||||
export class StaticNotificationState extends EventEmitter implements INotificationState {
|
|
||||||
constructor(public symbol: string, public count: number, public color: NotificationColor) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
|
|
||||||
return new StaticNotificationState(null, count, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static forSymbol(symbol: string, color: NotificationColor): StaticNotificationState {
|
|
||||||
return new StaticNotificationState(symbol, 0, color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RoomNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
|
||||||
private _symbol: string;
|
|
||||||
private _count: number;
|
|
||||||
private _color: NotificationColor;
|
|
||||||
|
|
||||||
constructor(private room: Room) {
|
|
||||||
super();
|
|
||||||
this.room.on("Room.receipt", this.handleReadReceipt);
|
|
||||||
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
|
||||||
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
|
||||||
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
|
||||||
this.updateNotificationState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get symbol(): string {
|
|
||||||
return this._symbol;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get count(): number {
|
|
||||||
return this._count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get color(): NotificationColor {
|
|
||||||
return this._color;
|
|
||||||
}
|
|
||||||
|
|
||||||
private get roomIsInvite(): boolean {
|
|
||||||
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
|
||||||
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
|
||||||
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
|
||||||
if (MatrixClientPeg.get()) {
|
|
||||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
|
||||||
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
|
||||||
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
|
||||||
this.updateNotificationState();
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
|
||||||
const roomId = event.getRoomId();
|
|
||||||
|
|
||||||
if (roomId !== this.room.roomId) return; // ignore - not for us
|
|
||||||
this.updateNotificationState();
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateNotificationState() {
|
|
||||||
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
|
||||||
|
|
||||||
if (this.roomIsInvite) {
|
|
||||||
this._color = NotificationColor.Red;
|
|
||||||
this._symbol = "!";
|
|
||||||
this._count = 1; // not used, technically
|
|
||||||
} else {
|
|
||||||
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
|
|
||||||
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
|
|
||||||
|
|
||||||
// For a 'true count' we pick the grey notifications first because they include the
|
|
||||||
// red notifications. If we don't have a grey count for some reason we use the red
|
|
||||||
// count. If that count is broken for some reason, assume zero. This avoids us showing
|
|
||||||
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
|
|
||||||
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
|
|
||||||
|
|
||||||
// Note: we only set the symbol if we have an actual count. We don't want to show
|
|
||||||
// zero on badges.
|
|
||||||
|
|
||||||
if (redNotifs > 0) {
|
|
||||||
this._color = NotificationColor.Red;
|
|
||||||
this._count = trueCount;
|
|
||||||
this._symbol = null; // symbol calculated by component
|
|
||||||
} else if (greyNotifs > 0) {
|
|
||||||
this._color = NotificationColor.Grey;
|
|
||||||
this._count = trueCount;
|
|
||||||
this._symbol = null; // symbol calculated by component
|
|
||||||
} else {
|
|
||||||
// We don't have any notified messages, but we might have unread messages. Let's
|
|
||||||
// find out.
|
|
||||||
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);
|
|
||||||
if (hasUnread) {
|
|
||||||
this._color = NotificationColor.Bold;
|
|
||||||
} else {
|
|
||||||
this._color = NotificationColor.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// no symbol or count for this state
|
|
||||||
this._count = 0;
|
|
||||||
this._symbol = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally, publish an update if needed
|
|
||||||
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
|
||||||
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
||||||
this.emit(NOTIFICATION_STATE_UPDATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class 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,
|
|
||||||
} = {
|
|
||||||
[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ListNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
|
||||||
private _count: number;
|
|
||||||
private _color: NotificationColor;
|
|
||||||
private rooms: Room[] = [];
|
|
||||||
private states: { [roomId: string]: RoomNotificationState } = {};
|
|
||||||
|
|
||||||
constructor(private byTileCount = false, private tagId: TagID) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get symbol(): string {
|
|
||||||
return null; // This notification state doesn't support symbols
|
|
||||||
}
|
|
||||||
|
|
||||||
public get count(): number {
|
|
||||||
return this._count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get color(): NotificationColor {
|
|
||||||
return this._color;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setRooms(rooms: Room[]) {
|
|
||||||
// If we're only concerned about the tile count, don't bother setting up listeners.
|
|
||||||
if (this.byTileCount) {
|
|
||||||
this.rooms = rooms;
|
|
||||||
this.calculateTotalState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldRooms = this.rooms;
|
|
||||||
const diff = arrayDiff(oldRooms, rooms);
|
|
||||||
this.rooms = rooms;
|
|
||||||
for (const oldRoom of diff.removed) {
|
|
||||||
const state = this.states[oldRoom.roomId];
|
|
||||||
if (!state) continue; // We likely just didn't have a badge (race condition)
|
|
||||||
delete this.states[oldRoom.roomId];
|
|
||||||
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
|
||||||
state.destroy();
|
|
||||||
}
|
|
||||||
for (const newRoom of diff.added) {
|
|
||||||
const state = new TagSpecificNotificationState(newRoom, this.tagId);
|
|
||||||
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
|
||||||
if (this.states[newRoom.roomId]) {
|
|
||||||
// "Should never happen" disclaimer.
|
|
||||||
console.warn("Overwriting notification state for room:", newRoom.roomId);
|
|
||||||
this.states[newRoom.roomId].destroy();
|
|
||||||
}
|
|
||||||
this.states[newRoom.roomId] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calculateTotalState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getForRoom(room: Room) {
|
|
||||||
const state = this.states[room.roomId];
|
|
||||||
if (!state) throw new Error("Unknown room for notification state");
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy() {
|
|
||||||
for (const state of Object.values(this.states)) {
|
|
||||||
state.destroy();
|
|
||||||
}
|
|
||||||
this.states = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomNotificationStateUpdate = () => {
|
|
||||||
this.calculateTotalState();
|
|
||||||
};
|
|
||||||
|
|
||||||
private calculateTotalState() {
|
|
||||||
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
|
||||||
|
|
||||||
if (this.byTileCount) {
|
|
||||||
this._color = NotificationColor.Red;
|
|
||||||
this._count = this.rooms.length;
|
|
||||||
} else {
|
|
||||||
this._count = 0;
|
|
||||||
this._color = NotificationColor.None;
|
|
||||||
for (const state of Object.values(this.states)) {
|
|
||||||
this._count += state.count;
|
|
||||||
this._color = Math.max(this.color, state.color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finally, publish an update if needed
|
|
||||||
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
|
||||||
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
|
||||||
this.emit(NOTIFICATION_STATE_UPDATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,13 +17,16 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
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 { DefaultTagID } from "../../../stores/room-list/models";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -93,15 +96,25 @@ export default class RoomBreadcrumbs2 extends React.PureComponent<IProps, IState
|
||||||
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
|
// TODO: Scrolling: https://github.com/vector-im/riot-web/issues/14040
|
||||||
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
|
// TODO: Tooltips: https://github.com/vector-im/riot-web/issues/14040
|
||||||
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
const tiles = BreadcrumbsStore.instance.rooms.map((r, i) => {
|
||||||
|
const roomTags = RoomListStore.instance.getTagsForRoom(r);
|
||||||
|
const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0];
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
className="mx_RoomBreadcrumbs2_crumb"
|
className="mx_RoomBreadcrumbs2_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}
|
||||||
|
tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"}
|
||||||
>
|
>
|
||||||
<RoomAvatar room={r} width={32} height={32}/>
|
<DecoratedRoomAvatar
|
||||||
</AccessibleButton>
|
room={r}
|
||||||
|
avatarSize={32}
|
||||||
|
tag={roomTag}
|
||||||
|
displayBadge={true}
|
||||||
|
forceCount={true}
|
||||||
|
/>
|
||||||
|
</AccessibleTooltipButton>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,16 @@ 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 { Dispatcher } from "flux";
|
import { Dispatcher } from "flux";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import RoomSublist2 from "./RoomSublist2";
|
import RoomSublist2 from "./RoomSublist2";
|
||||||
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 { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import GroupAvatar from "../avatars/GroupAvatar";
|
||||||
|
import TemporaryTile from "./TemporaryTile";
|
||||||
|
import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -160,16 +166,57 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
const newLists = store.orderedLists;
|
this.updateLists(); // trigger the first update
|
||||||
console.log("new lists", newLists);
|
}
|
||||||
|
|
||||||
const layoutMap = new Map<TagID, ListLayout>();
|
public componentWillUnmount() {
|
||||||
for (const tagId of Object.keys(newLists)) {
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
||||||
layoutMap.set(tagId, new ListLayout(tagId));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({sublists: newLists, layouts: layoutMap});
|
private updateLists = () => {
|
||||||
|
const newLists = RoomListStore.instance.orderedLists;
|
||||||
|
console.log("new lists", newLists);
|
||||||
|
|
||||||
|
const layoutMap = new Map<TagID, ListLayout>();
|
||||||
|
for (const tagId of Object.keys(newLists)) {
|
||||||
|
layoutMap.set(tagId, new ListLayout(tagId));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({sublists: newLists, layouts: layoutMap});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderCommunityInvites(): React.ReactElement[] {
|
||||||
|
// TODO: Put community invites in a more sensible place (not in the room list)
|
||||||
|
return MatrixClientPeg.get().getGroups().filter(g => {
|
||||||
|
if (g.myMembership !== 'invite') return false;
|
||||||
|
return !this.searchFilter || this.searchFilter.matches(g.name);
|
||||||
|
}).map(g => {
|
||||||
|
const avatar = (
|
||||||
|
<GroupAvatar
|
||||||
|
groupId={g.groupId}
|
||||||
|
groupName={g.name}
|
||||||
|
groupAvatarUrl={g.avatarUrl}
|
||||||
|
width={32} height={32} resizeMethod='crop'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const openGroup = () => {
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: 'view_group',
|
||||||
|
group_id: g.groupId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<TemporaryTile
|
||||||
|
isMinimized={this.props.isMinimized}
|
||||||
|
isSelected={false}
|
||||||
|
displayName={g.name}
|
||||||
|
avatar={avatar}
|
||||||
|
notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)}
|
||||||
|
onClick={openGroup}
|
||||||
|
key={`temporaryGroupTile_${g.groupId}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +242,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
||||||
|
|
||||||
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
|
||||||
|
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
|
||||||
components.push(
|
components.push(
|
||||||
<RoomSublist2
|
<RoomSublist2
|
||||||
key={`sublist-${orderedTagId}`}
|
key={`sublist-${orderedTagId}`}
|
||||||
|
@ -208,6 +256,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
|
||||||
isInvite={aesthetics.isInvite}
|
isInvite={aesthetics.isInvite}
|
||||||
layout={this.state.layouts.get(orderedTagId)}
|
layout={this.state.layouts.get(orderedTagId)}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,21 +17,26 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { createRef } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
import { 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 RoomTile2 from "./RoomTile2";
|
||||||
import {ResizableBox, ResizeCallbackData} from "react-resizable";
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
||||||
import {ListLayout} from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import NotificationBadge, {ListNotificationState} from "./NotificationBadge";
|
import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import {ChevronFace, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
||||||
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 NotificationBadge from "./NotificationBadge";
|
||||||
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
||||||
|
import Tooltip from "../elements/Tooltip";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -61,6 +66,10 @@ interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
tagId: TagID;
|
tagId: TagID;
|
||||||
|
|
||||||
|
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
||||||
|
// You should feel bad if you use this.
|
||||||
|
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
||||||
|
|
||||||
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,8 +94,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numTiles(): number {
|
private get numTiles(): number {
|
||||||
// TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179
|
return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length;
|
||||||
return (this.props.rooms || []).length;
|
}
|
||||||
|
|
||||||
|
private get numVisibleTiles(): number {
|
||||||
|
if (!this.props.layout) return 0;
|
||||||
|
const nVisible = Math.floor(this.props.layout.visibleTiles);
|
||||||
|
return Math.min(nVisible, this.numTiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
|
@ -105,7 +119,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
|
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
|
||||||
const direction = e.movementY < 0 ? -1 : +1;
|
const direction = e.movementY < 0 ? -1 : +1;
|
||||||
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
|
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
|
||||||
this.props.layout.visibleTiles += tileDiff;
|
this.props.layout.setVisibleTilesWithin(tileDiff, this.numTiles);
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -165,6 +179,30 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onBadgeClick = (ev: React.MouseEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
let room;
|
||||||
|
if (this.props.tagId === DefaultTagID.Invite) {
|
||||||
|
// switch to first room as that'll be the top of the list for the user
|
||||||
|
room = this.props.rooms && this.props.rooms[0];
|
||||||
|
} else {
|
||||||
|
// find the first room with a count of the same colour as the badge count
|
||||||
|
room = this.props.rooms.find((r: Room) => {
|
||||||
|
const notifState = this.state.notificationState.getForRoom(r);
|
||||||
|
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: room.roomId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
||||||
let target = ev.target as HTMLDivElement;
|
let target = ev.target as HTMLDivElement;
|
||||||
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
|
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
|
||||||
|
@ -184,13 +222,21 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderTiles(): React.ReactElement[] {
|
private renderVisibleTiles(): React.ReactElement[] {
|
||||||
if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering
|
if (this.props.layout && this.props.layout.isCollapsed) {
|
||||||
|
// don't waste time on rendering
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const tiles: React.ReactElement[] = [];
|
const tiles: React.ReactElement[] = [];
|
||||||
|
|
||||||
|
if (this.props.extraBadTilesThatShouldntExist) {
|
||||||
|
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.rooms) {
|
if (this.props.rooms) {
|
||||||
for (const room of this.props.rooms) {
|
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
||||||
|
for (const room of visibleRooms) {
|
||||||
tiles.push(
|
tiles.push(
|
||||||
<RoomTile2
|
<RoomTile2
|
||||||
room={room}
|
room={room}
|
||||||
|
@ -203,6 +249,14 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We only have to do this because of the extra tiles. We do it conditionally
|
||||||
|
// to avoid spending cycles on slicing. It's generally fine to do this though
|
||||||
|
// as users are unlikely to have more than a handful of tiles when the extra
|
||||||
|
// tiles are used.
|
||||||
|
if (tiles.length > this.numVisibleTiles) {
|
||||||
|
return tiles.slice(0, this.numVisibleTiles);
|
||||||
|
}
|
||||||
|
|
||||||
return tiles;
|
return tiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,16 +340,25 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
// TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
|
// TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180
|
||||||
const tabIndex = isActive ? 0 : -1;
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
const badge = <NotificationBadge forceCount={true} notification={this.state.notificationState}/>;
|
const badge = (
|
||||||
|
<NotificationBadge
|
||||||
|
forceCount={true}
|
||||||
|
notification={this.state.notificationState}
|
||||||
|
onClick={this.onBadgeClick}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
let addRoomButton = null;
|
let addRoomButton = null;
|
||||||
if (!!this.props.onAddRoom) {
|
if (!!this.props.onAddRoom) {
|
||||||
addRoomButton = (
|
addRoomButton = (
|
||||||
<AccessibleButton
|
<AccessibleTooltipButton
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
onClick={this.onAddRoom}
|
onClick={this.onAddRoom}
|
||||||
className="mx_RoomSublist2_auxButton"
|
className="mx_RoomSublist2_auxButton"
|
||||||
aria-label={this.props.addRoomLabel || _t("Add room")}
|
aria-label={this.props.addRoomLabel || _t("Add room")}
|
||||||
|
title={this.props.addRoomLabel}
|
||||||
|
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -354,7 +417,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
public render(): React.ReactElement {
|
public render(): React.ReactElement {
|
||||||
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
|
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
|
||||||
|
|
||||||
const tiles = this.renderTiles();
|
const visibleTiles = this.renderVisibleTiles();
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
'mx_RoomSublist2': true,
|
'mx_RoomSublist2': true,
|
||||||
|
@ -363,13 +426,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
if (tiles.length > 0) {
|
if (visibleTiles.length > 0) {
|
||||||
const layout = this.props.layout; // to shorten calls
|
const layout = this.props.layout; // to shorten calls
|
||||||
|
|
||||||
const nVisible = Math.floor(layout.visibleTiles);
|
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
|
||||||
const visibleTiles = tiles.slice(0, nVisible);
|
|
||||||
|
|
||||||
const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length);
|
|
||||||
const showMoreBtnClasses = classNames({
|
const showMoreBtnClasses = classNames({
|
||||||
'mx_RoomSublist2_showNButton': true,
|
'mx_RoomSublist2_showNButton': true,
|
||||||
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
||||||
|
@ -379,9 +439,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
// floats above the resize handle, if we have one present. If the user has all
|
// floats above the resize handle, if we have one present. If the user has all
|
||||||
// tiles visible, it becomes 'show less'.
|
// tiles visible, it becomes 'show less'.
|
||||||
let showNButton = null;
|
let showNButton = null;
|
||||||
if (tiles.length > nVisible) {
|
if (this.numTiles > visibleTiles.length) {
|
||||||
// we have a cutoff condition - add the button to show all
|
// we have a cutoff condition - add the button to show all
|
||||||
const numMissing = tiles.length - visibleTiles.length;
|
const numMissing = this.numTiles - visibleTiles.length;
|
||||||
let showMoreText = (
|
let showMoreText = (
|
||||||
<span className='mx_RoomSublist2_showNButtonText'>
|
<span className='mx_RoomSublist2_showNButtonText'>
|
||||||
{_t("Show %(count)s more", {count: numMissing})}
|
{_t("Show %(count)s more", {count: numMissing})}
|
||||||
|
@ -396,7 +456,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
{showMoreText}
|
{showMoreText}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (tiles.length <= nVisible && tiles.length > this.props.layout.defaultVisibleTiles) {
|
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.props.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_RoomSublist2_showNButtonText'>
|
||||||
|
@ -416,7 +476,7 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
// Figure out if we need a handle
|
// Figure out if we need a handle
|
||||||
let handles = ['s'];
|
let handles = ['s'];
|
||||||
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
|
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
||||||
handles = []; // no handles, we're at a minimum
|
handles = []; // no handles, we're at a minimum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -435,9 +495,9 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
|
||||||
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
|
if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
|
||||||
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
|
padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
|
||||||
|
|
||||||
const relativeTiles = layout.tilesWithPadding(tiles.length, padding);
|
const relativeTiles = layout.tilesWithPadding(this.numTiles, padding);
|
||||||
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
|
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
|
||||||
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
|
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding);
|
||||||
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
||||||
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
||||||
|
|
||||||
|
|
|
@ -17,28 +17,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createRef } from "react";
|
import React from "react";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton";
|
||||||
import RoomAvatar from "../../views/avatars/RoomAvatar";
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { Key } from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
import ActiveRoomObserver from "../../../ActiveRoomObserver";
|
||||||
import NotificationBadge, {
|
|
||||||
INotificationState,
|
|
||||||
NotificationColor,
|
|
||||||
TagSpecificNotificationState
|
|
||||||
} from "./NotificationBadge";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import {ChevronFace, ContextMenu, ContextMenuButton, MenuItemRadio} from "../../structures/ContextMenu";
|
import {ChevronFace, ContextMenu, ContextMenuButton, MenuItemRadio} from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||||
import RoomTileIcon from "./RoomTileIcon";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { setRoomNotifsState } from "../../../RoomNotifs";
|
import { setRoomNotifsState } from "../../../RoomNotifs";
|
||||||
|
import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState";
|
||||||
|
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||||
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
||||||
|
@ -121,6 +119,10 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get showContextMenu(): boolean {
|
||||||
|
return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite;
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
if (this.props.room) {
|
if (this.props.room) {
|
||||||
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate);
|
||||||
|
@ -170,6 +172,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onContextMenu = (ev: React.MouseEvent) => {
|
private onContextMenu = (ev: React.MouseEvent) => {
|
||||||
|
// If we don't have a context menu to show, ignore the action.
|
||||||
|
if (!this.showContextMenu) return;
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -237,7 +242,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
private onClickMute = ev => this.saveNotifState(ev, MUTE);
|
||||||
|
|
||||||
private renderNotificationsMenu(): React.ReactElement {
|
private renderNotificationsMenu(): React.ReactElement {
|
||||||
if (this.props.isMinimized || MatrixClientPeg.get().isGuest() || this.props.tag === DefaultTagID.Invite) {
|
if (MatrixClientPeg.get().isGuest() || !this.showContextMenu) {
|
||||||
// the menu makes no sense in these cases so do not show one
|
// the menu makes no sense in these cases so do not show one
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -282,12 +287,14 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const classes = classNames("mx_RoomTile2_notificationsButton", {
|
const classes = classNames("mx_RoomTile2_notificationsButton", {
|
||||||
// Show bell icon for the default case too.
|
// Show bell icon for the default case too.
|
||||||
mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES,
|
mx_RoomTile2_iconBell: state === state === ALL_MESSAGES,
|
||||||
mx_RoomTile2_iconBellDot: state === MENTIONS_ONLY,
|
mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD,
|
||||||
|
mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY,
|
||||||
mx_RoomTile2_iconBellCrossed: state === MUTE,
|
mx_RoomTile2_iconBellCrossed: state === MUTE,
|
||||||
// XXX: RoomNotifs assumes ALL_MESSAGES is default, this is wrong,
|
|
||||||
// but cannot be fixed until FTUE Notifications lands.
|
// Only show the icon by default if the room is overridden to muted.
|
||||||
mx_RoomTile2_notificationsButton_show: state !== ALL_MESSAGES,
|
// TODO: [FTUE Notifications] Probably need to detect global mute state
|
||||||
|
mx_RoomTile2_notificationsButton_show: state === MUTE,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -304,12 +311,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderGeneralMenu(): React.ReactElement {
|
private renderGeneralMenu(): React.ReactElement {
|
||||||
if (this.props.isMinimized) return null; // no menu when minimized
|
if (!this.showContextMenu) return null; // no menu to show
|
||||||
|
|
||||||
// TODO: Get a proper invite context menu, or take invites out of the room list.
|
// TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
|
||||||
if (this.props.tag === DefaultTagID.Invite) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.generalMenuPosition) {
|
if (this.state.generalMenuPosition) {
|
||||||
|
@ -361,13 +365,25 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
'mx_RoomTile2_minimized': this.props.isMinimized,
|
'mx_RoomTile2_minimized': this.props.isMinimized,
|
||||||
});
|
});
|
||||||
|
|
||||||
const badge = (
|
const roomAvatar = <DecoratedRoomAvatar
|
||||||
<NotificationBadge
|
room={this.props.room}
|
||||||
notification={this.state.notificationState}
|
avatarSize={32}
|
||||||
forceCount={false}
|
tag={this.props.tag}
|
||||||
roomId={this.props.room.roomId}
|
displayBadge={this.props.isMinimized}
|
||||||
/>
|
/>;
|
||||||
);
|
|
||||||
|
let badge: React.ReactNode;
|
||||||
|
if (!this.props.isMinimized) {
|
||||||
|
badge = (
|
||||||
|
<div className="mx_RoomTile2_badgeContainer">
|
||||||
|
<NotificationBadge
|
||||||
|
notification={this.state.notificationState}
|
||||||
|
forceCount={false}
|
||||||
|
roomId={this.props.room.roomId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
// TODO: the original RoomTile uses state for the room name. Do we need to?
|
||||||
let name = this.props.room.name;
|
let name = this.props.room.name;
|
||||||
|
@ -405,7 +421,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
if (this.props.isMinimized) nameContainer = null;
|
if (this.props.isMinimized) nameContainer = null;
|
||||||
|
|
||||||
const avatarSize = 32;
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<RovingTabIndexWrapper>
|
<RovingTabIndexWrapper>
|
||||||
|
@ -421,14 +436,9 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
onContextMenu={this.onContextMenu}
|
onContextMenu={this.onContextMenu}
|
||||||
>
|
>
|
||||||
<div className="mx_RoomTile2_avatarContainer">
|
{roomAvatar}
|
||||||
<RoomAvatar room={this.props.room} width={avatarSize} height={avatarSize} />
|
|
||||||
<RoomTileIcon room={this.props.room} tag={this.props.tag} />
|
|
||||||
</div>
|
|
||||||
{nameContainer}
|
{nameContainer}
|
||||||
<div className="mx_RoomTile2_badgeContainer">
|
{badge}
|
||||||
{badge}
|
|
||||||
</div>
|
|
||||||
{this.renderNotificationsMenu()}
|
{this.renderNotificationsMenu()}
|
||||||
{this.renderGeneralMenu()}
|
{this.renderGeneralMenu()}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
116
src/components/views/rooms/TemporaryTile.tsx
Normal file
116
src/components/views/rooms/TemporaryTile.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
||||||
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
|
import { INotificationState } from "../../../stores/notifications/INotificationState";
|
||||||
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
isMinimized: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
displayName: string;
|
||||||
|
avatar: React.ReactElement;
|
||||||
|
notificationState: INotificationState;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
hover: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TemporaryTile extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hover: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onTileMouseEnter = () => {
|
||||||
|
this.setState({hover: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
private onTileMouseLeave = () => {
|
||||||
|
this.setState({hover: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): React.ReactElement {
|
||||||
|
// XXX: We copy classes because it's easier
|
||||||
|
const classes = classNames({
|
||||||
|
'mx_RoomTile2': true,
|
||||||
|
'mx_TemporaryTile': true,
|
||||||
|
'mx_RoomTile2_selected': this.props.isSelected,
|
||||||
|
'mx_RoomTile2_minimized': this.props.isMinimized,
|
||||||
|
});
|
||||||
|
|
||||||
|
const badge = (
|
||||||
|
<NotificationBadge
|
||||||
|
notification={this.props.notificationState}
|
||||||
|
forceCount={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
let name = this.props.displayName;
|
||||||
|
if (typeof name !== 'string') name = '';
|
||||||
|
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||||
|
|
||||||
|
const nameClasses = classNames({
|
||||||
|
"mx_RoomTile2_name": true,
|
||||||
|
"mx_RoomTile2_nameHasUnreadEvents": this.props.notificationState.color >= NotificationColor.Bold,
|
||||||
|
});
|
||||||
|
|
||||||
|
let nameContainer = (
|
||||||
|
<div className="mx_RoomTile2_nameContainer">
|
||||||
|
<div title={name} className={nameClasses} tabIndex={-1} dir="auto">
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
if (this.props.isMinimized) nameContainer = null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<RovingTabIndexWrapper>
|
||||||
|
{({onFocus, isActive, ref}) =>
|
||||||
|
<AccessibleButton
|
||||||
|
onFocus={onFocus}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
inputRef={ref}
|
||||||
|
className={classes}
|
||||||
|
onMouseEnter={this.onTileMouseEnter}
|
||||||
|
onMouseLeave={this.onTileMouseLeave}
|
||||||
|
onClick={this.props.onClick}
|
||||||
|
role="treeitem"
|
||||||
|
>
|
||||||
|
<div className="mx_RoomTile2_avatarContainer">
|
||||||
|
{this.props.avatar}
|
||||||
|
</div>
|
||||||
|
{nameContainer}
|
||||||
|
<div className="mx_RoomTile2_badgeContainer">
|
||||||
|
{badge}
|
||||||
|
</div>
|
||||||
|
</AccessibleButton>
|
||||||
|
}
|
||||||
|
</RovingTabIndexWrapper>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -150,7 +150,7 @@ export const SETTINGS = {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td("Use the improved room list (will refresh to apply changes)"),
|
displayName: _td("Use the improved room list (will refresh to apply changes)"),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: true,
|
||||||
controller: new ReloadOnChangeController(),
|
controller: new ReloadOnChangeController(),
|
||||||
},
|
},
|
||||||
"feature_custom_themes": {
|
"feature_custom_themes": {
|
||||||
|
|
|
@ -51,7 +51,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get visible(): boolean {
|
public get visible(): boolean {
|
||||||
return this.state.enabled;
|
return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload) {
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
|
26
src/stores/notifications/INotificationState.ts
Normal file
26
src/stores/notifications/INotificationState.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
|
||||||
|
export const NOTIFICATION_STATE_UPDATE = "update";
|
||||||
|
|
||||||
|
export interface INotificationState extends EventEmitter {
|
||||||
|
symbol?: string;
|
||||||
|
count: number;
|
||||||
|
color: NotificationColor;
|
||||||
|
}
|
120
src/stores/notifications/ListNotificationState.ts
Normal file
120
src/stores/notifications/ListNotificationState.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "./INotificationState";
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
import { TagID } from "../room-list/models";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { arrayDiff } from "../../utils/arrays";
|
||||||
|
import { RoomNotificationState } from "./RoomNotificationState";
|
||||||
|
import { TagSpecificNotificationState } from "./TagSpecificNotificationState";
|
||||||
|
|
||||||
|
export class ListNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||||
|
private _count: number;
|
||||||
|
private _color: NotificationColor;
|
||||||
|
private rooms: Room[] = [];
|
||||||
|
private states: { [roomId: string]: RoomNotificationState } = {};
|
||||||
|
|
||||||
|
constructor(private byTileCount = false, private tagId: TagID) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get symbol(): string {
|
||||||
|
return null; // This notification state doesn't support symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
public get count(): number {
|
||||||
|
return this._count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get color(): NotificationColor {
|
||||||
|
return this._color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setRooms(rooms: Room[]) {
|
||||||
|
// If we're only concerned about the tile count, don't bother setting up listeners.
|
||||||
|
if (this.byTileCount) {
|
||||||
|
this.rooms = rooms;
|
||||||
|
this.calculateTotalState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldRooms = this.rooms;
|
||||||
|
const diff = arrayDiff(oldRooms, rooms);
|
||||||
|
this.rooms = rooms;
|
||||||
|
for (const oldRoom of diff.removed) {
|
||||||
|
const state = this.states[oldRoom.roomId];
|
||||||
|
if (!state) continue; // We likely just didn't have a badge (race condition)
|
||||||
|
delete this.states[oldRoom.roomId];
|
||||||
|
state.off(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||||
|
state.destroy();
|
||||||
|
}
|
||||||
|
for (const newRoom of diff.added) {
|
||||||
|
const state = new TagSpecificNotificationState(newRoom, this.tagId);
|
||||||
|
state.on(NOTIFICATION_STATE_UPDATE, this.onRoomNotificationStateUpdate);
|
||||||
|
if (this.states[newRoom.roomId]) {
|
||||||
|
// "Should never happen" disclaimer.
|
||||||
|
console.warn("Overwriting notification state for room:", newRoom.roomId);
|
||||||
|
this.states[newRoom.roomId].destroy();
|
||||||
|
}
|
||||||
|
this.states[newRoom.roomId] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.calculateTotalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getForRoom(room: Room) {
|
||||||
|
const state = this.states[room.roomId];
|
||||||
|
if (!state) throw new Error("Unknown room for notification state");
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
for (const state of Object.values(this.states)) {
|
||||||
|
state.destroy();
|
||||||
|
}
|
||||||
|
this.states = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRoomNotificationStateUpdate = () => {
|
||||||
|
this.calculateTotalState();
|
||||||
|
};
|
||||||
|
|
||||||
|
private calculateTotalState() {
|
||||||
|
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
||||||
|
|
||||||
|
if (this.byTileCount) {
|
||||||
|
this._color = NotificationColor.Red;
|
||||||
|
this._count = this.rooms.length;
|
||||||
|
} else {
|
||||||
|
this._count = 0;
|
||||||
|
this._color = NotificationColor.None;
|
||||||
|
for (const state of Object.values(this.states)) {
|
||||||
|
this._count += state.count;
|
||||||
|
this._color = Math.max(this.color, state.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, publish an update if needed
|
||||||
|
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
||||||
|
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
||||||
|
this.emit(NOTIFICATION_STATE_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
24
src/stores/notifications/NotificationColor.ts
Normal file
24
src/stores/notifications/NotificationColor.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum NotificationColor {
|
||||||
|
// Inverted (None -> Red) because we do integer comparisons on this
|
||||||
|
None, // nothing special
|
||||||
|
// TODO: Remove bold with notifications: https://github.com/vector-im/riot-web/issues/14227
|
||||||
|
Bold, // no badge, show as unread
|
||||||
|
Grey, // unread notified messages
|
||||||
|
Red, // unread pings
|
||||||
|
}
|
144
src/stores/notifications/RoomNotificationState.ts
Normal file
144
src/stores/notifications/RoomNotificationState.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { INotificationState, NOTIFICATION_STATE_UPDATE } from "./INotificationState";
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
import { IDestroyable } from "../../utils/IDestroyable";
|
||||||
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
import { EffectiveMembership, getEffectiveMembership } from "../room-list/membership";
|
||||||
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import * as RoomNotifs from '../../RoomNotifs';
|
||||||
|
import * as Unread from '../../Unread';
|
||||||
|
|
||||||
|
export class RoomNotificationState extends EventEmitter implements IDestroyable, INotificationState {
|
||||||
|
private _symbol: string;
|
||||||
|
private _count: number;
|
||||||
|
private _color: NotificationColor;
|
||||||
|
|
||||||
|
constructor(private room: Room) {
|
||||||
|
super();
|
||||||
|
this.room.on("Room.receipt", this.handleReadReceipt);
|
||||||
|
this.room.on("Room.timeline", this.handleRoomEventUpdate);
|
||||||
|
this.room.on("Room.redaction", this.handleRoomEventUpdate);
|
||||||
|
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
|
||||||
|
MatrixClientPeg.get().on("accountData", this.handleAccountDataUpdate);
|
||||||
|
this.updateNotificationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get symbol(): string {
|
||||||
|
return this._symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get count(): number {
|
||||||
|
return this._count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get color(): NotificationColor {
|
||||||
|
return this._color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get roomIsInvite(): boolean {
|
||||||
|
return getEffectiveMembership(this.room.getMyMembership()) === EffectiveMembership.Invite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.room.removeListener("Room.receipt", this.handleReadReceipt);
|
||||||
|
this.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
|
||||||
|
this.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
|
||||||
|
if (MatrixClientPeg.get()) {
|
||||||
|
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
|
||||||
|
MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleReadReceipt = (event: MatrixEvent, room: Room) => {
|
||||||
|
if (!readReceiptChangeIsFor(event, MatrixClientPeg.get())) return; // not our own - ignore
|
||||||
|
if (room.roomId !== this.room.roomId) return; // not for us - ignore
|
||||||
|
this.updateNotificationState();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleRoomEventUpdate = (event: MatrixEvent) => {
|
||||||
|
const roomId = event.getRoomId();
|
||||||
|
|
||||||
|
if (roomId !== this.room.roomId) return; // ignore - not for us
|
||||||
|
this.updateNotificationState();
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleAccountDataUpdate = (ev: MatrixEvent) => {
|
||||||
|
if (ev.getType() === "m.push_rules") {
|
||||||
|
this.updateNotificationState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateNotificationState() {
|
||||||
|
const before = {count: this.count, symbol: this.symbol, color: this.color};
|
||||||
|
|
||||||
|
if (RoomNotifs.getRoomNotifsState(this.room.roomId) === RoomNotifs.MUTE) {
|
||||||
|
// When muted we suppress all notification states, even if we have context on them.
|
||||||
|
this._color = NotificationColor.None;
|
||||||
|
this._symbol = null;
|
||||||
|
this._count = 0;
|
||||||
|
} else if (this.roomIsInvite) {
|
||||||
|
this._color = NotificationColor.Red;
|
||||||
|
this._symbol = "!";
|
||||||
|
this._count = 1; // not used, technically
|
||||||
|
} else {
|
||||||
|
const redNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'highlight');
|
||||||
|
const greyNotifs = RoomNotifs.getUnreadNotificationCount(this.room, 'total');
|
||||||
|
|
||||||
|
// For a 'true count' we pick the grey notifications first because they include the
|
||||||
|
// red notifications. If we don't have a grey count for some reason we use the red
|
||||||
|
// count. If that count is broken for some reason, assume zero. This avoids us showing
|
||||||
|
// a badge for 'NaN' (which formats as 'NaNB' for NaN Billion).
|
||||||
|
const trueCount = greyNotifs ? greyNotifs : (redNotifs ? redNotifs : 0);
|
||||||
|
|
||||||
|
// Note: we only set the symbol if we have an actual count. We don't want to show
|
||||||
|
// zero on badges.
|
||||||
|
|
||||||
|
if (redNotifs > 0) {
|
||||||
|
this._color = NotificationColor.Red;
|
||||||
|
this._count = trueCount;
|
||||||
|
this._symbol = null; // symbol calculated by component
|
||||||
|
} else if (greyNotifs > 0) {
|
||||||
|
this._color = NotificationColor.Grey;
|
||||||
|
this._count = trueCount;
|
||||||
|
this._symbol = null; // symbol calculated by component
|
||||||
|
} else {
|
||||||
|
// We don't have any notified messages, but we might have unread messages. Let's
|
||||||
|
// find out.
|
||||||
|
const hasUnread = Unread.doesRoomHaveUnreadMessages(this.room);
|
||||||
|
if (hasUnread) {
|
||||||
|
this._color = NotificationColor.Bold;
|
||||||
|
} else {
|
||||||
|
this._color = NotificationColor.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no symbol or count for this state
|
||||||
|
this._count = 0;
|
||||||
|
this._symbol = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, publish an update if needed
|
||||||
|
const after = {count: this.count, symbol: this.symbol, color: this.color};
|
||||||
|
if (JSON.stringify(before) !== JSON.stringify(after)) {
|
||||||
|
this.emit(NOTIFICATION_STATE_UPDATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/stores/notifications/StaticNotificationState.ts
Normal file
33
src/stores/notifications/StaticNotificationState.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { INotificationState } from "./INotificationState";
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
|
||||||
|
export class StaticNotificationState extends EventEmitter implements INotificationState {
|
||||||
|
constructor(public symbol: string, public count: number, public color: NotificationColor) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static forCount(count: number, color: NotificationColor): StaticNotificationState {
|
||||||
|
return new StaticNotificationState(null, count, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static forSymbol(symbol: string, color: NotificationColor): StaticNotificationState {
|
||||||
|
return new StaticNotificationState(symbol, 0, color);
|
||||||
|
}
|
||||||
|
}
|
46
src/stores/notifications/TagSpecificNotificationState.ts
Normal file
46
src/stores/notifications/TagSpecificNotificationState.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,9 @@ import { TagID } from "./models";
|
||||||
|
|
||||||
const TILE_HEIGHT_PX = 44;
|
const TILE_HEIGHT_PX = 44;
|
||||||
|
|
||||||
// the .65 comes from the CSS where the show more button is
|
// this comes from the CSS where the show more button is
|
||||||
// mathematically 65% of a tile when floating.
|
// mathematically this percent of a tile when floating.
|
||||||
const RESIZER_BOX_FACTOR = 0.65;
|
const RESIZER_BOX_FACTOR = 0.78;
|
||||||
|
|
||||||
interface ISerializedListLayout {
|
interface ISerializedListLayout {
|
||||||
numTiles: number;
|
numTiles: number;
|
||||||
|
@ -85,10 +85,16 @@ export class ListLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
public get defaultVisibleTiles(): number {
|
public get defaultVisibleTiles(): number {
|
||||||
// TODO: Remove dogfood flag: https://github.com/vector-im/riot-web/issues/14231
|
// 10 is what "feels right", and mostly subject to design's opinion.
|
||||||
// TODO: Resolve dogfooding: https://github.com/vector-im/riot-web/issues/14137
|
return 10 + RESIZER_BOX_FACTOR;
|
||||||
const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4);
|
}
|
||||||
return val + RESIZER_BOX_FACTOR;
|
|
||||||
|
public setVisibleTilesWithin(diff: number, maxPossible: number) {
|
||||||
|
if (this.visibleTiles > maxPossible) {
|
||||||
|
this.visibleTiles = maxPossible + diff;
|
||||||
|
} else {
|
||||||
|
this.visibleTiles += diff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
|
public calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number {
|
||||||
|
@ -122,6 +128,10 @@ export class ListLayout {
|
||||||
return px / this.tileHeight;
|
return px / this.tileHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reset() {
|
||||||
|
localStorage.removeItem(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
private save() {
|
private save() {
|
||||||
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
localStorage.setItem(this.key, JSON.stringify(this.serialize()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||||
import TagOrderStore from "../TagOrderStore";
|
import TagOrderStore from "../TagOrderStore";
|
||||||
import { AsyncStore } from "../AsyncStore";
|
import { AsyncStore } from "../AsyncStore";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
@ -30,6 +30,7 @@ import { TagWatcher } from "./TagWatcher";
|
||||||
import RoomViewStore from "../RoomViewStore";
|
import RoomViewStore from "../RoomViewStore";
|
||||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "./membership";
|
import { EffectiveMembership, getEffectiveMembership } from "./membership";
|
||||||
|
import { ListLayout } from "./ListLayout";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -100,8 +101,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
|
||||||
this.algorithm.stickyRoom = null;
|
this.algorithm.stickyRoom = null;
|
||||||
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
} else if (activeRoom !== this.algorithm.stickyRoom) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`Changing sticky room to ${activeRoomId}`);
|
|
||||||
this.algorithm.stickyRoom = activeRoom;
|
this.algorithm.stickyRoom = activeRoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +184,8 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
const room = this.matrixClient.getRoom(roomId);
|
const room = this.matrixClient.getRoom(roomId);
|
||||||
const tryUpdate = async (updatedRoom: Room) => {
|
const tryUpdate = async (updatedRoom: Room) => {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
|
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
|
||||||
|
` in ${updatedRoom.roomId}`);
|
||||||
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
|
if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
||||||
console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
|
console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
|
||||||
|
@ -297,8 +297,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
|
||||||
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
|
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -365,8 +363,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAlgorithmListUpdated = () => {
|
private onAlgorithmListUpdated = () => {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log("Underlying algorithm has triggered a list update - refiring");
|
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -396,9 +392,16 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
this.emit(LISTS_UPDATE_EVENT, this);
|
this.emit(LISTS_UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: this primarily exists for debugging, and isn't really intended to be used by anything.
|
||||||
|
public async resetLayouts() {
|
||||||
|
console.warn("Resetting layouts for room list");
|
||||||
|
for (const tagId of Object.keys(this.orderedLists)) {
|
||||||
|
new ListLayout(tagId).reset();
|
||||||
|
}
|
||||||
|
await this.regenerateAllLists();
|
||||||
|
}
|
||||||
|
|
||||||
public addFilter(filter: IFilterCondition): void {
|
public addFilter(filter: IFilterCondition): void {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log("Adding filter condition:", filter);
|
|
||||||
this.filterConditions.push(filter);
|
this.filterConditions.push(filter);
|
||||||
if (this.algorithm) {
|
if (this.algorithm) {
|
||||||
this.algorithm.addFilterCondition(filter);
|
this.algorithm.addFilterCondition(filter);
|
||||||
|
@ -406,8 +409,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFilter(filter: IFilterCondition): void {
|
public removeFilter(filter: IFilterCondition): void {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log("Removing filter condition:", filter);
|
|
||||||
const idx = this.filterConditions.indexOf(filter);
|
const idx = this.filterConditions.indexOf(filter);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
this.filterConditions.splice(idx, 1);
|
this.filterConditions.splice(idx, 1);
|
||||||
|
@ -417,6 +418,19 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the tags for a room identified by the store. The returned set
|
||||||
|
* should never be empty, and will contain DefaultTagID.Untagged if
|
||||||
|
* the store is not aware of any tags.
|
||||||
|
* @param room The room to get the tags for.
|
||||||
|
* @returns The tags for the room.
|
||||||
|
*/
|
||||||
|
public getTagsForRoom(room: Room): TagID[] {
|
||||||
|
const algorithmTags = this.algorithm.getTagsForRoom(room);
|
||||||
|
if (!algorithmTags) return [DefaultTagID.Untagged];
|
||||||
|
return algorithmTags;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomListStore {
|
export default class RoomListStore {
|
||||||
|
|
|
@ -272,9 +272,6 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newMap[tagId] = allowedRoomsInThisTag;
|
newMap[tagId] = allowedRoomsInThisTag;
|
||||||
|
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
||||||
|
@ -310,9 +307,6 @@ export class Algorithm extends EventEmitter {
|
||||||
if (filteredRooms.length > 0) {
|
if (filteredRooms.length > 0) {
|
||||||
this.filteredRooms[tagId] = filteredRooms;
|
this.filteredRooms[tagId] = filteredRooms;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
|
protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
|
||||||
|
@ -351,8 +345,6 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._cachedStickyRooms || !updatedTag) {
|
if (!this._cachedStickyRooms || !updatedTag) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`Generating clone of cached rooms for sticky room handling`);
|
|
||||||
const stickiedTagMap: ITagMap = {};
|
const stickiedTagMap: ITagMap = {};
|
||||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||||
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
|
stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
|
||||||
|
@ -363,8 +355,6 @@ export class Algorithm extends EventEmitter {
|
||||||
if (updatedTag) {
|
if (updatedTag) {
|
||||||
// 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.
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,8 +363,6 @@ export class Algorithm extends EventEmitter {
|
||||||
// we might have updated from the cache is also our sticky room.
|
// we might have updated from the cache is also our sticky room.
|
||||||
const sticky = this._stickyRoom;
|
const sticky = this._stickyRoom;
|
||||||
if (!updatedTag || updatedTag === sticky.tag) {
|
if (!updatedTag || updatedTag === sticky.tag) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -466,13 +454,9 @@ export class Algorithm extends EventEmitter {
|
||||||
// Split out the easy rooms first (leave and invite)
|
// Split out the easy rooms first (leave and invite)
|
||||||
const memberships = splitRoomsByMembership(rooms);
|
const memberships = splitRoomsByMembership(rooms);
|
||||||
for (const room of memberships[EffectiveMembership.Invite]) {
|
for (const room of memberships[EffectiveMembership.Invite]) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`);
|
|
||||||
newTags[DefaultTagID.Invite].push(room);
|
newTags[DefaultTagID.Invite].push(room);
|
||||||
}
|
}
|
||||||
for (const room of memberships[EffectiveMembership.Leave]) {
|
for (const room of memberships[EffectiveMembership.Leave]) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`);
|
|
||||||
newTags[DefaultTagID.Archived].push(room);
|
newTags[DefaultTagID.Archived].push(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,11 +467,7 @@ export class Algorithm extends EventEmitter {
|
||||||
let inTag = false;
|
let inTag = false;
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`);
|
|
||||||
if (!isNullOrUndefined(newTags[tag])) {
|
if (!isNullOrUndefined(newTags[tag])) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`);
|
|
||||||
newTags[tag].push(room);
|
newTags[tag].push(room);
|
||||||
inTag = true;
|
inTag = true;
|
||||||
}
|
}
|
||||||
|
@ -497,9 +477,6 @@ export class Algorithm extends EventEmitter {
|
||||||
if (!inTag) {
|
if (!inTag) {
|
||||||
// TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236
|
// TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236
|
||||||
newTags[DefaultTagID.Untagged].push(room);
|
newTags[DefaultTagID.Untagged].push(room);
|
||||||
|
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,7 +501,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTagsForRoom(room: Room): TagID[] {
|
public getTagsForRoom(room: Room): TagID[] {
|
||||||
// XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
|
// XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
|
||||||
// different use case and therefore different performance curve
|
// different use case and therefore different performance curve
|
||||||
|
|
||||||
|
|
|
@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
|
|
||||||
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||||
super(tagId, initialSortingAlgorithm);
|
super(tagId, initialSortingAlgorithm);
|
||||||
|
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[RoomListDebug] Constructed an ImportanceAlgorithm for ${tagId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// noinspection JSMethodCanBeStatic
|
// noinspection JSMethodCanBeStatic
|
||||||
|
|
|
@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
|
||||||
|
|
||||||
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
|
||||||
super(tagId, initialSortingAlgorithm);
|
super(tagId, initialSortingAlgorithm);
|
||||||
|
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log(`[RoomListDebug] Constructed a NaturalAlgorithm for ${tagId}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setRooms(rooms: Room[]): Promise<any> {
|
public async setRooms(rooms: Room[]): Promise<any> {
|
||||||
|
|
|
@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
|
||||||
const beforeRoomIds = this.roomIds;
|
const beforeRoomIds = this.roomIds;
|
||||||
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId);
|
||||||
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
if (arrayHasDiff(beforeRoomIds, this.roomIds)) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log("Updating filter for group: ", this.community.groupId);
|
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
|
|
||||||
public set search(val: string) {
|
public set search(val: string) {
|
||||||
this._search = val;
|
this._search = val;
|
||||||
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
|
|
||||||
console.log("Updating filter for room name search:", this._search);
|
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +58,15 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
|
|
||||||
if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name
|
if (!room.name) return false; // should realistically not happen: the js-sdk always calculates a name
|
||||||
|
|
||||||
|
return this.matches(room.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public matches(val: string): boolean {
|
||||||
// Note: we have to match the filter with the removeHiddenChars() room name because the
|
// Note: we have to match the filter with the removeHiddenChars() room name because the
|
||||||
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
||||||
// We also doubly convert to lowercase to work around oddities of the library.
|
// We also doubly convert to lowercase to work around oddities of the library.
|
||||||
const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase();
|
const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase();
|
||||||
const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase();
|
const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase();
|
||||||
return noSecretsName.includes(noSecretsFilter);
|
return noSecretsName.includes(noSecretsFilter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@ export class MessageEventPreview implements IPreview {
|
||||||
eventContent = event.getContent()['m.new_content'];
|
eventContent = event.getContent()['m.new_content'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!eventContent || !eventContent['body']) return null; // invalid for our purposes
|
||||||
|
|
||||||
let body = (eventContent['body'] || '').trim();
|
let body = (eventContent['body'] || '').trim();
|
||||||
const msgtype = eventContent['msgtype'];
|
const msgtype = eventContent['msgtype'];
|
||||||
if (!body || !msgtype) return null; // invalid event, no preview
|
if (!body || !msgtype) return null; // invalid event, no preview
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue