diff --git a/res/css/_components.scss b/res/css/_components.scss index afc40ca0d6..8288cf34f6 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -49,6 +49,7 @@ @import "./views/auth/_ServerTypeSelector.scss"; @import "./views/auth/_Welcome.scss"; @import "./views/avatars/_BaseAvatar.scss"; +@import "./views/avatars/_DecoratedRoomAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_RoomTileContextMenu.scss"; diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 40babaa9ca..bdaada0d15 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -70,7 +70,8 @@ $tagPanelWidth: 70px; // only applies in this file, used for calculations .mx_LeftPanel2_breadcrumbsContainer { width: 100%; - overflow: hidden; + overflow-y: hidden; + overflow-x: scroll; margin-top: 8px; } } diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss new file mode 100644 index 0000000000..b500d44a43 --- /dev/null +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -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; + } +} diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 73ac9b3558..d67928bf83 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -55,7 +55,7 @@ limitations under the License. border-radius: 4px; box-shadow: 4px 4px 12px 0 $menu-box-shadow-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; pointer-events: none; line-height: $font-14px; diff --git a/res/css/views/rooms/_RoomBreadcrumbs2.scss b/res/css/views/rooms/_RoomBreadcrumbs2.scss index dd9581069c..6e5a5fbb16 100644 --- a/res/css/views/rooms/_RoomBreadcrumbs2.scss +++ b/res/css/views/rooms/_RoomBreadcrumbs2.scss @@ -51,3 +51,18 @@ limitations under the License. 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; + } + } +} diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index ffb96cf600..e86bc83cc8 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -389,3 +389,7 @@ limitations under the License. margin-top: 8px; } } + +.mx_RoomSublist2_addRoomTooltip { + margin-top: -3px; +} diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 44c5b6ee17..ba8d315d5a 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -23,27 +23,20 @@ limitations under the License. // The tile is also a flexbox row itself display: flex; - flex-wrap: wrap; &.mx_RoomTile2_selected, &:hover, &.mx_RoomTile2_hasMenuOpen { background-color: $roomtile2-selected-bg-color; border-radius: 32px; } - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer { margin-right: 8px; - position: relative; - - .mx_RoomTileIcon { - position: absolute; - bottom: 0; - right: 0; - } } .mx_RoomTile2_nameContainer { 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 display: flex; @@ -81,23 +74,37 @@ limitations under the License. } } - .mx_RoomTile2_badgeContainer { - width: 18px; - height: 32px; + .mx_RoomTile2_menuButton { + margin-left: 4px; // spacing between buttons + } - // Create another flexbox row because it's super easy to position the badge at - // the end this way. + .mx_RoomTile2_badgeContainer { + height: 16px; + // don't set width so that it takes no space when there is no badge to show + margin: auto 0; // vertically align + + // Create a flexbox to make aligning dot badges easier display: flex; align-items: center; - 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 .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { width: 20px; + min-width: 20px; // yay flex height: 20px; - margin: auto 0 auto 8px; + margin: auto 0; position: relative; display: none; @@ -130,7 +137,7 @@ limitations under the License. .mx_RoomTile2_badgeContainer { width: 0; height: 0; - visibility: hidden; + display: none; } .mx_RoomTile2_notificationsButton, @@ -145,16 +152,9 @@ limitations under the License. align-items: center; position: relative; - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer { margin-right: 0; } - - .mx_RoomTile2_badgeContainer { - position: absolute; - top: 0; - right: 0; - height: 18px; - } } } diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index cec924c022..c76bbe6133 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -160,6 +160,13 @@ export class ContextMenu extends React.PureComponent { 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) => { let descending = false; // are we currently descending or ascending through the DOM tree? @@ -262,7 +269,7 @@ export class ContextMenu extends React.PureComponent { position.bottom = props.bottom; } - let chevronFace: IProps["chevronFace"]; + let chevronFace: ChevronFace; if (props.left) { position.left = props.left; chevronFace = ChevronFace.Left; @@ -349,7 +356,7 @@ export class ContextMenu extends React.PureComponent {
); diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 0f614435e5..83d5b9e138 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -30,6 +30,7 @@ import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; import ResizeNotifier from "../../utils/ResizeNotifier"; 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -69,6 +70,7 @@ export default class LeftPanel2 extends React.Component { }; BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")}); }); @@ -81,6 +83,7 @@ export default class LeftPanel2 extends React.Component { public componentWillUnmount() { SettingsStore.unwatchSetting(this.tagPanelWatcherRef); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); } @@ -151,7 +154,7 @@ export default class LeftPanel2 extends React.Component { let breadcrumbs; if (this.state.showBreadcrumbs) { breadcrumbs = ( -
+
{this.props.isMinimized ? null : }
); @@ -205,6 +208,11 @@ export default class LeftPanel2 extends React.Component { "mx_LeftPanel2_minimized": this.props.isMinimized, }); + const roomListClasses = classNames( + "mx_LeftPanel2_actualRoomListContainer", + "mx_AutoHideScrollbar", + ); + return (
{tagPanel} @@ -212,7 +220,7 @@ export default class LeftPanel2 extends React.Component { {this.renderHeader()} {this.renderSearchExplore()}
{roomList}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 02f08211b9..315c648e15 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -23,7 +23,6 @@ import * as Matrix from "matrix-js-sdk"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; 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 import 'focus-visible'; // what-input helps improve keyboard accessibility diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 19f1cccebd..519c4c1f8e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1819,6 +1819,7 @@ export default createReactClass({ ); const showRoomRecoveryReminder = ( + this.context.isCryptoEnabled() && SettingsStore.getValue("showRoomRecoveryReminder") && this.context.isRoomEncrypted(this.state.room.roomId) && this.context.getKeyBackupEnabled() === false diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index aba0a3f053..26da0dce83 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -37,6 +37,7 @@ import {OwnProfileStore} from "../../stores/OwnProfileStore"; import {UPDATE_EVENT} from "../../stores/AsyncStore"; import BaseAvatar from '../views/avatars/BaseAvatar'; import classNames from "classnames"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; interface IProps { isMinimized: boolean; @@ -230,7 +231,7 @@ export default class UserMenu extends React.Component { {MatrixClientPeg.get().getUserId()}
-
{ alt={_t("Switch theme")} width={16} /> -
+
{hostingLink}
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx new file mode 100644 index 0000000000..e0ad3202b8 --- /dev/null +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -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 { + + 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 = ; + } + + return
+ + + {badge} +
; + } +} diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.tsx similarity index 70% rename from src/components/views/elements/AccessibleTooltipButton.js rename to src/components/views/elements/AccessibleTooltipButton.tsx index 6c84c6ab7e..f4d63136e1 100644 --- a/src/components/views/elements/AccessibleTooltipButton.js +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -16,21 +16,28 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import classnames from 'classnames'; import AccessibleButton from "./AccessibleButton"; -import * as sdk from "../../../index"; +import {IProps} from "./AccessibleButton"; +import Tooltip from './Tooltip'; -export default class AccessibleTooltipButton extends React.PureComponent { - static propTypes = { - ...AccessibleButton.propTypes, - // The tooltip to render on hover - title: PropTypes.string.isRequired, - }; +interface ITooltipProps extends IProps { + title: string; + tooltipClassName?: string; +} - state = { - hover: false, - }; +interface IState { + hover: boolean; +} + +export default class AccessibleTooltipButton extends React.PureComponent { + constructor(props: ITooltipProps) { + super(props); + this.state = { + hover: false, + }; + } onMouseOver = () => { this.setState({ @@ -45,14 +52,15 @@ export default class AccessibleTooltipButton extends React.PureComponent { }; render() { - const Tooltip = sdk.getComponent("elements.Tooltip"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const {title, children, ...props} = this.props; + const tooltipClassName = classnames( + "mx_AccessibleTooltipButton_tooltip", + this.props.tooltipClassName, + ); const tip = this.state.hover ? :
; return ( diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2111310555..829b05fbfc 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -17,35 +17,13 @@ limitations under the License. import React from "react"; import classNames from "classnames"; import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils"; -import { Room } from "matrix-js-sdk/src/models/room"; -import * 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 { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { readReceiptChangeIsFor } from "../../../utils/read-receipts"; - -export const NOTIFICATION_STATE_UPDATE = "update"; - -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 -} - -export interface INotificationState extends EventEmitter { - symbol?: string; - count: number; - color: NotificationColor; -} +import AccessibleButton from "../elements/AccessibleButton"; +import { XOR } from "../../../@types/common"; +import { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IProps { notification: INotificationState; @@ -62,11 +40,18 @@ interface IProps { roomId?: string; } +interface IClickableProps extends IProps, React.InputHTMLAttributes { + /** + * If specified will return an AccessibleButton instead of a div. + */ + onClick?(ev: React.MouseEvent); +} + interface IState { showCounts: boolean; // whether or not to show counts. Independent of props.forceCount } -export default class NotificationBadge extends React.PureComponent { +export default class NotificationBadge extends React.PureComponent, IState> { private countWatcherRef: string; constructor(props: IProps) { @@ -109,20 +94,25 @@ export default class NotificationBadge extends React.PureComponent= NotificationColor.Red; - const hasCount = this.props.notification.color >= NotificationColor.Grey; - const hasUnread = this.props.notification.color >= NotificationColor.Bold; - const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif; - let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); - if (this.props.forceCount) { + // Don't show a badge if we don't need to + if (notification.color <= NotificationColor.None) return null; + + // TODO: Update these booleans for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261 + // As of writing, that is "if red, show count always" and "optionally show counts instead of dots". + // 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; 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 = ""; const classes = classNames({ @@ -134,6 +124,14 @@ export default class NotificationBadge extends React.PureComponent 2, }); + if (onClick) { + return ( + + {symbol} + + ); + } + return (
{symbol} @@ -141,242 +139,3 @@ export default class NotificationBadge extends React.PureComponent { - 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); - } - } -} diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index bd12ced6ee..687f4dd73e 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -17,13 +17,16 @@ limitations under the License. import React from "react"; import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; import AccessibleButton from "../elements/AccessibleButton"; -import RoomAvatar from "../avatars/RoomAvatar"; +import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import { _t } from "../../../languageHandler"; import { Room } from "matrix-js-sdk/src/models/room"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import Analytics from "../../../Analytics"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -93,15 +96,25 @@ export default class RoomBreadcrumbs2 extends React.PureComponent { + const roomTags = RoomListStore.instance.getTagsForRoom(r); + const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( - this.viewRoom(r, i)} aria-label={_t("Room %(name)s", {name: r.name})} + title={r.name} + tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"} > - - + + ); }); diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index a1298e107b..b0bb70c9a0 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -25,10 +25,16 @@ import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import dis from "../../../dispatcher/dispatcher"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; import { ActionPayload } from "../../../dispatcher/payloads"; import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition"; 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -160,16 +166,57 @@ export default class RoomList2 extends React.Component { } public componentDidMount(): void { - RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => { - const newLists = store.orderedLists; - console.log("new lists", newLists); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); + this.updateLists(); // trigger the first update + } - const layoutMap = new Map(); - for (const tagId of Object.keys(newLists)) { - layoutMap.set(tagId, new ListLayout(tagId)); - } + public componentWillUnmount() { + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); + } - this.setState({sublists: newLists, layouts: layoutMap}); + private updateLists = () => { + const newLists = RoomListStore.instance.orderedLists; + console.log("new lists", newLists); + + const layoutMap = new Map(); + 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 = ( + + ); + const openGroup = () => { + defaultDispatcher.dispatch({ + action: 'view_group', + group_id: g.groupId, + }); + }; + return ( + + ); }); } @@ -195,6 +242,7 @@ export default class RoomList2 extends React.Component { if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; + const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null; components.push( { isInvite={aesthetics.isInvite} layout={this.state.layouts.get(orderedTagId)} isMinimized={this.props.isMinimized} + extraBadTilesThatShouldntExist={extraTiles} /> ); } diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index fb3ebf4728..41f97192b9 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -17,21 +17,26 @@ limitations under the License. */ 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 {RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex"; -import {_t} from "../../../languageHandler"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import { _t } from "../../../languageHandler"; import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomTile2 from "./RoomTile2"; -import {ResizableBox, ResizeCallbackData} from "react-resizable"; -import {ListLayout} from "../../../stores/room-list/ListLayout"; -import NotificationBadge, {ListNotificationState} from "./NotificationBadge"; -import {ChevronFace, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; +import { ResizableBox, ResizeCallbackData } from "react-resizable"; +import { ListLayout } from "../../../stores/room-list/ListLayout"; +import { ChevronFace, ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import StyledCheckbox from "../elements/StyledCheckbox"; import StyledRadioButton from "../elements/StyledRadioButton"; import RoomListStore from "../../../stores/room-list/RoomListStore2"; -import {ListAlgorithm, SortAlgorithm} from "../../../stores/room-list/algorithms/models"; -import {DefaultTagID, TagID} from "../../../stores/room-list/models"; +import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/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: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -61,6 +66,10 @@ interface IProps { isMinimized: boolean; 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 } @@ -85,8 +94,13 @@ export default class RoomSublist2 extends React.Component { } private get numTiles(): number { - // TODO: Account for group invites: https://github.com/vector-im/riot-web/issues/14179 - return (this.props.rooms || []).length; + return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).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() { @@ -105,7 +119,7 @@ export default class RoomSublist2 extends React.Component { private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => { const direction = e.movementY < 0 ? -1 : +1; 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 }; @@ -165,6 +179,30 @@ export default class RoomSublist2 extends React.Component { 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) => { let target = ev.target as HTMLDivElement; if (!target.classList.contains('mx_RoomSublist2_headerText')) { @@ -184,13 +222,21 @@ export default class RoomSublist2 extends React.Component { } }; - private renderTiles(): React.ReactElement[] { - if (this.props.layout && this.props.layout.isCollapsed) return []; // don't waste time on rendering + private renderVisibleTiles(): React.ReactElement[] { + if (this.props.layout && this.props.layout.isCollapsed) { + // don't waste time on rendering + return []; + } const tiles: React.ReactElement[] = []; + if (this.props.extraBadTilesThatShouldntExist) { + tiles.push(...this.props.extraBadTilesThatShouldntExist); + } + 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( { } } + // 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; } @@ -286,16 +340,25 @@ export default class RoomSublist2 extends React.Component { // TODO: Use onFocus: https://github.com/vector-im/riot-web/issues/14180 const tabIndex = isActive ? 0 : -1; - const badge = ; + const badge = ( + + ); let addRoomButton = null; if (!!this.props.onAddRoom) { addRoomButton = ( - ); } @@ -354,7 +417,7 @@ export default class RoomSublist2 extends React.Component { public render(): React.ReactElement { // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185 - const tiles = this.renderTiles(); + const visibleTiles = this.renderVisibleTiles(); const classes = classNames({ 'mx_RoomSublist2': true, @@ -363,13 +426,10 @@ export default class RoomSublist2 extends React.Component { }); let content = null; - if (tiles.length > 0) { + if (visibleTiles.length > 0) { const layout = this.props.layout; // to shorten calls - const nVisible = Math.floor(layout.visibleTiles); - const visibleTiles = tiles.slice(0, nVisible); - - const maxTilesFactored = layout.tilesWithResizerBoxFactor(tiles.length); + const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles); const showMoreBtnClasses = classNames({ 'mx_RoomSublist2_showNButton': true, 'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored, @@ -379,9 +439,9 @@ export default class RoomSublist2 extends React.Component { // floats above the resize handle, if we have one present. If the user has all // tiles visible, it becomes 'show less'. let showNButton = null; - if (tiles.length > nVisible) { + if (this.numTiles > visibleTiles.length) { // 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 = ( {_t("Show %(count)s more", {count: numMissing})} @@ -396,7 +456,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText}
); - } 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 let showLessText = ( @@ -416,7 +476,7 @@ export default class RoomSublist2 extends React.Component { // Figure out if we need a handle 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 } @@ -435,9 +495,9 @@ export default class RoomSublist2 extends React.Component { if (showNButton) padding += SHOW_N_BUTTON_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 maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding); + const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding); const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles); const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding); diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 5f535392c0..b4009de752 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -17,28 +17,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton, { ButtonEvent } from "../../views/elements/AccessibleButton"; -import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import ActiveRoomObserver from "../../../ActiveRoomObserver"; -import NotificationBadge, { - INotificationState, - NotificationColor, - TagSpecificNotificationState -} from "./NotificationBadge"; import { _t } from "../../../languageHandler"; import {ChevronFace, ContextMenu, ContextMenuButton, MenuItemRadio} from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; 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 { MatrixClientPeg } from "../../../MatrixClientPeg"; 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: Rename on launch: https://github.com/vector-im/riot-web/issues/14231 @@ -121,6 +119,10 @@ export default class RoomTile2 extends React.Component { ActiveRoomObserver.addListener(this.props.room.roomId, this.onActiveRoomUpdate); } + private get showContextMenu(): boolean { + return !this.props.isMinimized && this.props.tag !== DefaultTagID.Invite; + } + public componentWillUnmount() { if (this.props.room) { ActiveRoomObserver.removeListener(this.props.room.roomId, this.onActiveRoomUpdate); @@ -170,6 +172,9 @@ export default class RoomTile2 extends React.Component { }; 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.stopPropagation(); this.setState({ @@ -237,7 +242,7 @@ export default class RoomTile2 extends React.Component { private onClickMute = ev => this.saveNotifState(ev, MUTE); 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 return null; } @@ -282,12 +287,14 @@ export default class RoomTile2 extends React.Component { const classes = classNames("mx_RoomTile2_notificationsButton", { // Show bell icon for the default case too. - mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES, - mx_RoomTile2_iconBellDot: state === MENTIONS_ONLY, + mx_RoomTile2_iconBell: state === state === ALL_MESSAGES, + mx_RoomTile2_iconBellDot: state === ALL_MESSAGES_LOUD, + mx_RoomTile2_iconBellMentions: state === MENTIONS_ONLY, mx_RoomTile2_iconBellCrossed: state === MUTE, - // XXX: RoomNotifs assumes ALL_MESSAGES is default, this is wrong, - // but cannot be fixed until FTUE Notifications lands. - mx_RoomTile2_notificationsButton_show: state !== ALL_MESSAGES, + + // Only show the icon by default if the room is overridden to muted. + // TODO: [FTUE Notifications] Probably need to detect global mute state + mx_RoomTile2_notificationsButton_show: state === MUTE, }); return ( @@ -304,12 +311,9 @@ export default class RoomTile2 extends React.Component { } 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. - if (this.props.tag === DefaultTagID.Invite) { - return null; - } + // TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests let contextMenu = null; if (this.state.generalMenuPosition) { @@ -361,13 +365,25 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ( - - ); + const roomAvatar = ; + + let badge: React.ReactNode; + if (!this.props.isMinimized) { + badge = ( +
+ +
+ ); + } // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; @@ -405,7 +421,6 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; - const avatarSize = 32; return ( @@ -421,14 +436,9 @@ export default class RoomTile2 extends React.Component { role="treeitem" onContextMenu={this.onContextMenu} > -
- - -
+ {roomAvatar} {nameContainer} -
- {badge} -
+ {badge} {this.renderNotificationsMenu()} {this.renderGeneralMenu()} diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx new file mode 100644 index 0000000000..b6c165ecda --- /dev/null +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -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 { + 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 = ( + + ); + + 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 = ( +
+
+ {name} +
+
+ ); + if (this.props.isMinimized) nameContainer = null; + + return ( + + + {({onFocus, isActive, ref}) => + +
+ {this.props.avatar} +
+ {nameContainer} +
+ {badge} +
+
+ } +
+
+ ); + } +} diff --git a/src/settings/Settings.js b/src/settings/Settings.js index cc45bbb4c7..218e151ef4 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -150,7 +150,7 @@ export const SETTINGS = { isFeature: true, displayName: _td("Use the improved room list (will refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, - default: false, + default: true, controller: new ReloadOnChangeController(), }, "feature_custom_themes": { diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 1c47075cbb..c78f15c3b4 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -51,7 +51,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } public get visible(): boolean { - return this.state.enabled; + return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20; } protected async onAction(payload: ActionPayload) { diff --git a/src/stores/notifications/INotificationState.ts b/src/stores/notifications/INotificationState.ts new file mode 100644 index 0000000000..65bd7b7957 --- /dev/null +++ b/src/stores/notifications/INotificationState.ts @@ -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; +} diff --git a/src/stores/notifications/ListNotificationState.ts b/src/stores/notifications/ListNotificationState.ts new file mode 100644 index 0000000000..5773693b47 --- /dev/null +++ b/src/stores/notifications/ListNotificationState.ts @@ -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); + } + } +} + diff --git a/src/stores/notifications/NotificationColor.ts b/src/stores/notifications/NotificationColor.ts new file mode 100644 index 0000000000..aa2384b3df --- /dev/null +++ b/src/stores/notifications/NotificationColor.ts @@ -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 +} diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts new file mode 100644 index 0000000000..f9b19fcbcb --- /dev/null +++ b/src/stores/notifications/RoomNotificationState.ts @@ -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); + } + } +} diff --git a/src/stores/notifications/StaticNotificationState.ts b/src/stores/notifications/StaticNotificationState.ts new file mode 100644 index 0000000000..51902688fe --- /dev/null +++ b/src/stores/notifications/StaticNotificationState.ts @@ -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); + } +} diff --git a/src/stores/notifications/TagSpecificNotificationState.ts b/src/stores/notifications/TagSpecificNotificationState.ts new file mode 100644 index 0000000000..b443f4633b --- /dev/null +++ b/src/stores/notifications/TagSpecificNotificationState.ts @@ -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; + } +} diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 56f94ccd9a..efb0c4bdfb 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -18,9 +18,9 @@ import { TagID } from "./models"; const TILE_HEIGHT_PX = 44; -// the .65 comes from the CSS where the show more button is -// mathematically 65% of a tile when floating. -const RESIZER_BOX_FACTOR = 0.65; +// this comes from the CSS where the show more button is +// mathematically this percent of a tile when floating. +const RESIZER_BOX_FACTOR = 0.78; interface ISerializedListLayout { numTiles: number; @@ -85,10 +85,16 @@ export class ListLayout { } public get defaultVisibleTiles(): number { - // TODO: Remove dogfood flag: https://github.com/vector-im/riot-web/issues/14231 - // TODO: Resolve dogfooding: https://github.com/vector-im/riot-web/issues/14137 - const val = Number(localStorage.getItem("mx_dogfood_rl_defTiles") || 4); - return val + RESIZER_BOX_FACTOR; + // 10 is what "feels right", and mostly subject to design's opinion. + return 10 + 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 { @@ -122,6 +128,10 @@ export class ListLayout { return px / this.tileHeight; } + public reset() { + localStorage.removeItem(this.key); + } + private save() { localStorage.setItem(this.key, JSON.stringify(this.serialize())); } diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 9edfef654c..d4aec93035 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -17,7 +17,7 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; -import { OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; @@ -30,6 +30,7 @@ import { TagWatcher } from "./TagWatcher"; import RoomViewStore from "../RoomViewStore"; import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm"; import { EffectiveMembership, getEffectiveMembership } from "./membership"; +import { ListLayout } from "./ListLayout"; interface IState { tagsEnabled?: boolean; @@ -100,8 +101,6 @@ export class RoomListStore2 extends AsyncStore { console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`); this.algorithm.stickyRoom = null; } 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; } } @@ -185,7 +184,8 @@ export class RoomListStore2 extends AsyncStore { const room = this.matrixClient.getRoom(roomId); const tryUpdate = async (updatedRoom: Room) => { // 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() === '') { // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`); @@ -297,8 +297,6 @@ export class RoomListStore2 extends AsyncStore { private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); 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); } } @@ -365,8 +363,6 @@ export class RoomListStore2 extends AsyncStore { } 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); }; @@ -396,9 +392,16 @@ export class RoomListStore2 extends AsyncStore { 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 { - // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035 - console.log("Adding filter condition:", filter); this.filterConditions.push(filter); if (this.algorithm) { this.algorithm.addFilterCondition(filter); @@ -406,8 +409,6 @@ export class RoomListStore2 extends AsyncStore { } 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); if (idx >= 0) { this.filterConditions.splice(idx, 1); @@ -417,6 +418,19 @@ export class RoomListStore2 extends AsyncStore { } } } + + /** + * 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 { diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 8215d2ef57..80ca4656af 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -272,9 +272,6 @@ export class Algorithm extends EventEmitter { } } 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; }, []); @@ -310,9 +307,6 @@ export class Algorithm extends EventEmitter { if (filteredRooms.length > 0) { 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) { @@ -351,8 +345,6 @@ export class Algorithm extends EventEmitter { } 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 = {}; for (const tagId of Object.keys(this.cachedRooms)) { stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone @@ -363,8 +355,6 @@ export class Algorithm extends EventEmitter { if (updatedTag) { // Update the tag indicated by the caller, if possible. This is mostly to ensure // 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 } @@ -373,8 +363,6 @@ export class Algorithm extends EventEmitter { // we might have updated from the cache is also our sticky room. const sticky = this._stickyRoom; 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); } @@ -466,13 +454,9 @@ export class Algorithm extends EventEmitter { // Split out the easy rooms first (leave and invite) const memberships = splitRoomsByMembership(rooms); 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); } 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); } @@ -483,11 +467,7 @@ export class Algorithm extends EventEmitter { let inTag = false; if (tags.length > 0) { 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])) { - // 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); inTag = true; } @@ -497,9 +477,6 @@ export class Algorithm extends EventEmitter { if (!inTag) { // TODO: Determine if DM and push there instead: https://github.com/vector-im/riot-web/issues/14236 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 // different use case and therefore different performance curve diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index e95f92f985..71b6e89df3 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -87,9 +87,6 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { 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 diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index f74329cb4d..1a75d8cf06 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -28,9 +28,6 @@ export class NaturalAlgorithm extends OrderingAlgorithm { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { 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 { diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts index 9f7d8daaa3..45e65fb4f4 100644 --- a/src/stores/room-list/filters/CommunityFilterCondition.ts +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -52,8 +52,6 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon const beforeRoomIds = this.roomIds; this.roomIds = (await GroupStore.getGroupRooms(this.community.groupId)).map(r => r.roomId); 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); } }; diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 8625cd932c..6014a122f8 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -41,8 +41,6 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio public set search(val: string) { 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); } @@ -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 + return this.matches(room.name); + } + + public matches(val: string): boolean { // 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). // We also doubly convert to lowercase to work around oddities of the library. - const noSecretsFilter = removeHiddenChars(lcFilter).toLowerCase(); - const noSecretsName = removeHiddenChars(room.name.toLowerCase()).toLowerCase(); + const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase(); + const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase(); return noSecretsName.includes(noSecretsFilter); } } diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 6f0dc14a58..86ec4c539b 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -30,6 +30,8 @@ export class MessageEventPreview implements IPreview { eventContent = event.getContent()['m.new_content']; } + if (!eventContent || !eventContent['body']) return null; // invalid for our purposes + let body = (eventContent['body'] || '').trim(); const msgtype = eventContent['msgtype']; if (!body || !msgtype) return null; // invalid event, no preview