From 393eaf84c385d9208aad5f188cba5d747bab9c4e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 13:34:44 -0600 Subject: [PATCH 01/45] Move notification states out of the NotificationBadge component Fixes https://github.com/vector-im/riot-web/issues/14153 --- .../views/rooms/NotificationBadge.tsx | 269 +----------------- src/components/views/rooms/RoomSublist2.tsx | 3 +- src/components/views/rooms/RoomTile2.tsx | 9 +- .../notifications/INotificationState.ts | 26 ++ .../notifications/ListNotificationState.ts | 120 ++++++++ src/stores/notifications/NotificationColor.ts | 24 ++ .../notifications/RoomNotificationState.ts | 131 +++++++++ .../notifications/StaticNotificationState.ts | 33 +++ .../TagSpecificNotificationState.ts | 45 +++ 9 files changed, 387 insertions(+), 273 deletions(-) create mode 100644 src/stores/notifications/INotificationState.ts create mode 100644 src/stores/notifications/ListNotificationState.ts create mode 100644 src/stores/notifications/NotificationColor.ts create mode 100644 src/stores/notifications/RoomNotificationState.ts create mode 100644 src/stores/notifications/StaticNotificationState.ts create mode 100644 src/stores/notifications/TagSpecificNotificationState.ts diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2111310555..1bb72d02c3 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -17,35 +17,9 @@ 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 { INotificationState, NOTIFICATION_STATE_UPDATE } from "../../../stores/notifications/INotificationState"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IProps { notification: INotificationState; @@ -141,242 +115,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/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 58ebf54bf7..fadecbd0d6 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -26,13 +26,14 @@ 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 { 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 { TagID } from "../../../stores/room-list/models"; +import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; +import NotificationBadge from "./NotificationBadge"; // 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 diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 1284728855..4c9147e005 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,16 +26,15 @@ 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 { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import RoomTileIcon from "./RoomTileIcon"; +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 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..b9bc3f3492 --- /dev/null +++ b/src/stores/notifications/RoomNotificationState.ts @@ -0,0 +1,131 @@ +/* +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); + 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); + } + } +} 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..02d8717fee --- /dev/null +++ b/src/stores/notifications/TagSpecificNotificationState.ts @@ -0,0 +1,45 @@ +/* +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 { DefaultTagID, 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, + } = { + [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; + } +} From 782a555e44de49626837604020b05d1ad47b5a25 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 13:45:10 -0600 Subject: [PATCH 02/45] Make badges represent old list behaviour Fixes https://github.com/vector-im/riot-web/issues/14160 --- src/components/views/rooms/NotificationBadge.tsx | 9 ++++++--- src/stores/notifications/TagSpecificNotificationState.ts | 5 +++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 1bb72d02c3..dfc02176c6 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -86,11 +86,14 @@ 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); + const hasAnySymbol = this.props.notification.symbol || this.props.notification.count > 0; + let isEmptyBadge = !hasAnySymbol || !hasCount; if (this.props.forceCount) { isEmptyBadge = false; if (!hasCount) return null; // Can't render a badge diff --git a/src/stores/notifications/TagSpecificNotificationState.ts b/src/stores/notifications/TagSpecificNotificationState.ts index 02d8717fee..b443f4633b 100644 --- a/src/stores/notifications/TagSpecificNotificationState.ts +++ b/src/stores/notifications/TagSpecificNotificationState.ts @@ -16,7 +16,7 @@ limitations under the License. import { NotificationColor } from "./NotificationColor"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DefaultTagID, TagID } from "../room-list/models"; +import { TagID } from "../room-list/models"; import { RoomNotificationState } from "./RoomNotificationState"; export class TagSpecificNotificationState extends RoomNotificationState { @@ -24,7 +24,8 @@ export class TagSpecificNotificationState extends RoomNotificationState { // @ts-ignore - TS wants this to be a string key, but we know better [tagId: TagID]: NotificationColor, } = { - [DefaultTagID.DM]: NotificationColor.Red, + // TODO: Update for FTUE Notifications: https://github.com/vector-im/riot-web/issues/14261 + //[DefaultTagID.DM]: NotificationColor.Red, }; private readonly colorWhenNotIdle?: NotificationColor; From dcd51b5be3369cf87a89d937c5eddb94669b2d58 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:24:46 +0100 Subject: [PATCH 03/45] Implement breadcrumb notifications and scrolling --- res/css/_components.scss | 1 + res/css/structures/_LeftPanel2.scss | 3 +- .../views/avatars/_DecoratedRoomAvatar.scss | 33 ++++++++++ res/css/views/rooms/_RoomTile2.scss | 17 +---- src/components/structures/LeftPanel2.tsx | 2 +- .../views/avatars/DecoratedRoomAvatar.tsx | 65 +++++++++++++++++++ .../views/rooms/RoomBreadcrumbs2.tsx | 14 +++- src/components/views/rooms/RoomTile2.tsx | 24 ++++--- src/stores/room-list/RoomListStore2.ts | 18 ++++- src/stores/room-list/algorithms/Algorithm.ts | 11 ++++ 10 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 res/css/views/avatars/_DecoratedRoomAvatar.scss create mode 100644 src/components/views/avatars/DecoratedRoomAvatar.tsx 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..984fa0ce9a --- /dev/null +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -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. +*/ + +.mx_DecoratedRoomAvatar { + 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/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 2845068de3..e4e6a3eac1 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -30,15 +30,8 @@ limitations under the License. border-radius: 32px; } - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar { margin-right: 8px; - position: relative; - - .mx_RoomTileIcon { - position: absolute; - bottom: 0; - right: 0; - } } .mx_RoomTile2_nameContainer { @@ -145,16 +138,10 @@ limitations under the License. align-items: center; position: relative; - .mx_RoomTile2_avatarContainer { + .mx_DecoratedRoomAvatar { margin-right: 0; } - .mx_RoomTile2_badgeContainer { - position: absolute; - top: 0; - right: 0; - height: 18px; - } } } diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 0f614435e5..b4ec897561 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -151,7 +151,7 @@ export default class LeftPanel2 extends React.Component { let breadcrumbs; if (this.state.showBreadcrumbs) { breadcrumbs = ( -
+
{this.props.isMinimized ? null : }
); diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx new file mode 100644 index 0000000000..af1cdc779c --- /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, { INotificationState, TagSpecificNotificationState } from '../rooms/NotificationBadge'; + +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 { + console.log({tag: this.props.tag}) + + let badge: React.ReactNode; + if (this.props.displayBadge) { + badge = ; + } + + return
+ + + {badge} +
+ } +} \ No newline at end of file diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index bd12ced6ee..2f2b815002 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -17,13 +17,15 @@ 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"; // 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,6 +95,8 @@ 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})} > - + ); }); diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 1284728855..f8c46ee85a 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -22,7 +22,6 @@ 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"; @@ -35,7 +34,7 @@ import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } 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"; // 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 @@ -233,13 +232,22 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const badge = ( - + + 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; @@ -277,7 +285,6 @@ export default class RoomTile2 extends React.Component { ); if (this.props.isMinimized) nameContainer = null; - const avatarSize = 32; return ( @@ -292,10 +299,7 @@ export default class RoomTile2 extends React.Component { onClick={this.onTileClick} role="treeitem" > -
- - -
+ {roomAvatar} {nameContainer}
{badge} diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 497b8e5530..b4d96becc4 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"; @@ -187,7 +187,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`); @@ -422,6 +423,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..d4615356da 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -670,4 +670,15 @@ export class Algorithm extends EventEmitter { return true; } + + /** + * Returns the tags for a given room as known by the algorithm. May be null or + * empty. + * @param room The room to get known tags for. + * @returns The known tags for the room. + */ + public getTagsForRoom(room: Room): TagID[] { + if (!room) throw new Error("A room is required"); + return this.roomIdsToTags[room.roomId]; + } } From 0904ae8c7a27c6417f355a68010399754f48e350 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:35:59 +0100 Subject: [PATCH 04/45] Bug fixes --- .../views/avatars/DecoratedRoomAvatar.tsx | 2 +- src/stores/room-list/algorithms/Algorithm.ts | 13 +------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index af1cdc779c..5fb3287980 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -58,7 +58,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent - + {badge}
} diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index d4615356da..36abf86975 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -524,7 +524,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 @@ -670,15 +670,4 @@ export class Algorithm extends EventEmitter { return true; } - - /** - * Returns the tags for a given room as known by the algorithm. May be null or - * empty. - * @param room The room to get known tags for. - * @returns The known tags for the room. - */ - public getTagsForRoom(room: Room): TagID[] { - if (!room) throw new Error("A room is required"); - return this.roomIdsToTags[room.roomId]; - } } From 2379ec577cbfa6e3e83ab5f06b339a5ef36ed06f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:39:25 +0100 Subject: [PATCH 05/45] Lint semicolons --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 6 ++---- src/components/views/rooms/RoomBreadcrumbs2.tsx | 2 +- src/components/views/rooms/RoomTile2.tsx | 6 +++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 5fb3287980..1156c80313 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -41,12 +41,10 @@ export default class DecoratedRoomAvatar extends React.PureComponent {badge} -
+ ; } } \ No newline at end of file diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index 2f2b815002..c0417fc592 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -95,7 +95,7 @@ export default class RoomBreadcrumbs2 extends React.PureComponent { - const roomTags = RoomListStore.instance.getTagsForRoom(r) + const roomTags = RoomListStore.instance.getTagsForRoom(r); const roomTag = roomTags.includes(DefaultTagID.DM) ? DefaultTagID.DM : roomTags[0]; return ( { avatarSize={avatarSize} tag={this.props.tag} displayBadge={this.props.isMinimized} - /> + />; - let badge: React.ReactNode; + let badge: React.ReactNode; if (!this.props.isMinimized) { badge = + />; } // TODO: the original RoomTile uses state for the room name. Do we need to? From b1e0b35758cd28dd7e967c48dc181b5c7af12aeb Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:40:24 +0100 Subject: [PATCH 06/45] Lint style --- res/css/views/rooms/_RoomTile2.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index e4e6a3eac1..0a425d890f 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -141,7 +141,6 @@ limitations under the License. .mx_DecoratedRoomAvatar { margin-right: 0; } - } } From d2fb30a2116313c70943eb0e7bc37e644c2a7e7f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Tue, 30 Jun 2020 23:52:13 +0100 Subject: [PATCH 07/45] Hide scrollbar without pixel jumping --- src/components/structures/LeftPanel2.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index 0f614435e5..d34986f981 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import * as React from "react"; +import classnames from 'classnames'; import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; @@ -205,6 +206,11 @@ export default class LeftPanel2 extends React.Component { "mx_LeftPanel2_minimized": this.props.isMinimized, }); + const className = classnames( + "mx_LeftPanel2_actualRoomListContainer", + "mx_AutoHideScrollbar", + ); + return (
{tagPanel} @@ -212,7 +218,7 @@ export default class LeftPanel2 extends React.Component { {this.renderHeader()} {this.renderSearchExplore()}
{roomList}
From aab372c6484fc59d7ea30c55aadf4df8e7aee1aa Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 01:50:31 +0100 Subject: [PATCH 08/45] Add tooltips --- res/css/views/elements/_Tooltip.scss | 2 +- res/css/views/rooms/_RoomSublist2.scss | 4 ++ src/components/structures/UserMenu.tsx | 5 ++- .../views/elements/AccessibleButton.tsx | 2 +- ...pButton.js => AccessibleTooltipButton.tsx} | 38 +++++++++++-------- src/components/views/rooms/RoomSublist2.tsx | 6 ++- 6 files changed, 37 insertions(+), 20 deletions(-) rename src/components/views/elements/{AccessibleTooltipButton.js => AccessibleTooltipButton.tsx} (70%) 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/_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/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 8c06a06852..df8c777aeb 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; @@ -218,7 +219,7 @@ export default class UserMenu extends React.Component { {MatrixClientPeg.get().getUserId()}
-
{ alt={_t("Switch theme")} width={16} /> -
+ {hostingLink}
diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 01a27d9522..040147bb16 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -27,7 +27,7 @@ export type ButtonEvent = React.MouseEvent | React.KeyboardEvent { +export interface IProps extends React.InputHTMLAttributes { inputRef?: React.Ref; element?: string; // The kind of button, similar to how Bootstrap works. 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..1c0e18c399 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/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 58ebf54bf7..a6b2e72b6a 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -33,6 +33,8 @@ import StyledRadioButton from "../elements/StyledRadioButton"; import RoomListStore from "../../../stores/room-list/RoomListStore2"; import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models"; import { TagID } from "../../../stores/room-list/models"; +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 @@ -277,11 +279,13 @@ export default class RoomSublist2 extends React.Component { let addRoomButton = null; if (!!this.props.onAddRoom) { addRoomButton = ( - ); } From f935303eeb65f96d0822a7ac9ed3d316c6ee16f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 18:51:59 -0600 Subject: [PATCH 09/45] Change default number of rooms visible to 10 Fixes https://github.com/vector-im/riot-web/issues/14266 --- src/stores/room-list/ListLayout.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 56f94ccd9a..2cc8eda510 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -85,10 +85,8 @@ 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 calculateTilesToPixelsMin(maxTiles: number, n: number, possiblePadding: number): number { From 8cfe12b817995be90ad68083a49a5263fc6fe0fd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 18:52:13 -0600 Subject: [PATCH 10/45] Add a layout reset function For https://github.com/vector-im/riot-web/issues/14265 Intended to be accessed via `mx_RoomListStore2.resetLayout()` --- src/stores/room-list/ListLayout.ts | 4 ++++ src/stores/room-list/RoomListStore2.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 2cc8eda510..d8564bf947 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -120,6 +120,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 497b8e5530..ac2324295e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -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; @@ -401,6 +402,15 @@ 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); From 7674030c6eb32d7d9ede6f92e64be697f4b37bb1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 19:14:36 -0600 Subject: [PATCH 11/45] Show 'show more' when there are less tiles than the default For example, if you only have 3/10 rooms required for the default then resize smaller, we should have a 'show more' button. This works by changing the rendering to be slightly more efficient and only looping over what is seen (renderVisibleTiles(), using this.numTiles in place of tiles.length) and using a new setVisibleTilesWithin() function on the layout. Previously resizing the 3/10 case would be setting visibleTiles to ~8 instead of ~1 like it should (because the getter returns a default). --- src/components/views/rooms/RoomSublist2.tsx | 39 ++++++++++++--------- src/stores/room-list/ListLayout.ts | 8 +++++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 58ebf54bf7..6682527254 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -91,6 +91,12 @@ export default class RoomSublist2 extends React.Component { 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() { this.state.notificationState.setRooms(this.props.rooms); } @@ -107,7 +113,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 }; @@ -173,13 +179,17 @@ 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.rooms) { - for (const room of this.props.rooms) { + const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); + for (const room of visibleRooms) { tiles.push( { 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, @@ -347,13 +357,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, @@ -363,9 +370,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 > this.numVisibleTiles) { // 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})} @@ -380,7 +387,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText}
); - } else if (tiles.length <= nVisible && tiles.length > this.props.layout.defaultVisibleTiles) { + } else if (this.numTiles <= this.numVisibleTiles && this.numTiles > this.props.layout.defaultVisibleTiles) { // we have all tiles visible - add a button to show less let showLessText = ( @@ -400,7 +407,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 } @@ -419,9 +426,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/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index d8564bf947..528276e801 100644 --- a/src/stores/room-list/ListLayout.ts +++ b/src/stores/room-list/ListLayout.ts @@ -89,6 +89,14 @@ export class ListLayout { 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 { // Only apply the padding if we're about to use maxTiles as we need to // plan for the padding. If we're using n, the padding is already accounted From 8cfbfd4221f962f6ae36a154b9a5210b43c2e31e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Jun 2020 19:20:11 -0600 Subject: [PATCH 12/45] Increase RESIZER_BOX_FACTOR to account for overlap from handle Fixes https://github.com/vector-im/riot-web/issues/14136 The resizer handle wasn't being considered in this. 78% is both verified through mathematics and playing with it manually. --- src/stores/room-list/ListLayout.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/room-list/ListLayout.ts b/src/stores/room-list/ListLayout.ts index 528276e801..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; From 1889ee202bc77441e0219968550e68601fd53ac8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 12:23:27 +0100 Subject: [PATCH 13/45] Add tooltips for breadcrumbs --- res/css/views/rooms/_RoomBreadcrumbs2.scss | 15 +++++++++++++++ src/components/views/rooms/RoomBreadcrumbs2.tsx | 7 +++++-- 2 files changed, 20 insertions(+), 2 deletions(-) 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/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx index c0417fc592..687f4dd73e 100644 --- a/src/components/views/rooms/RoomBreadcrumbs2.tsx +++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx @@ -26,6 +26,7 @@ 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 @@ -98,11 +99,13 @@ export default class RoomBreadcrumbs2 extends React.PureComponent this.viewRoom(r, i)} aria-label={_t("Room %(name)s", {name: r.name})} + title={r.name} + tooltipClassName={"mx_RoomBreadcrumbs2_Tooltip"} > - + ); }); From de7df7dcf9bafbe286c25a643cbe69785bb147d8 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 12:28:00 +0100 Subject: [PATCH 14/45] Lint --- src/components/views/elements/AccessibleTooltipButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AccessibleTooltipButton.tsx b/src/components/views/elements/AccessibleTooltipButton.tsx index 1c0e18c399..f4d63136e1 100644 --- a/src/components/views/elements/AccessibleTooltipButton.tsx +++ b/src/components/views/elements/AccessibleTooltipButton.tsx @@ -33,7 +33,7 @@ interface IState { export default class AccessibleTooltipButton extends React.PureComponent { constructor(props: ITooltipProps) { - super(props) + super(props); this.state = { hover: false, }; From 1dd9c1eea3b420fba4dc34e034e265c90a6af9bf Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 12:28:32 +0100 Subject: [PATCH 15/45] Use avatar sisze inplace --- src/components/views/rooms/RoomTile2.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c36f504409..ed0044bacb 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -232,10 +232,9 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile2_minimized': this.props.isMinimized, }); - const avatarSize = 32; const roomAvatar = ; From ad27dbbfab6e7d2c3cdc07fc48bbad237a9b0465 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 15:15:18 +0100 Subject: [PATCH 16/45] Clean up classnames --- src/components/structures/LeftPanel2.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index d34986f981..ab117d55ed 100644 --- a/src/components/structures/LeftPanel2.tsx +++ b/src/components/structures/LeftPanel2.tsx @@ -15,7 +15,6 @@ limitations under the License. */ import * as React from "react"; -import classnames from 'classnames'; import { createRef } from "react"; import TagPanel from "./TagPanel"; import classNames from "classnames"; @@ -206,7 +205,7 @@ export default class LeftPanel2 extends React.Component { "mx_LeftPanel2_minimized": this.props.isMinimized, }); - const className = classnames( + const roomListClasses = classNames( "mx_LeftPanel2_actualRoomListContainer", "mx_AutoHideScrollbar", ); @@ -218,7 +217,7 @@ export default class LeftPanel2 extends React.Component { {this.renderHeader()} {this.renderSearchExplore()}
{roomList}
From 2162517a37cae3f5ea6a422d14c5d2f1429b6939 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 16:05:27 +0100 Subject: [PATCH 17/45] Display breadcrumbs only after 20 rooms have been joined --- src/components/structures/LeftPanel2.tsx | 3 +++ src/stores/BreadcrumbsStore.ts | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx index b4ec897561..fb07c9c601 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); } diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 1c47075cbb..43bef8a538 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -21,6 +21,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; +import _reduce from 'lodash/reduce'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -51,7 +52,10 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } public get visible(): boolean { - return this.state.enabled; + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0) + console.log(`calculating roomlist size: ${roomCount}`) + return roomCount >= 20; } protected async onAction(payload: ActionPayload) { From d203943b7fd9b7877767db3de3684a6e671a2d84 Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Wed, 1 Jul 2020 16:07:27 +0100 Subject: [PATCH 18/45] lint semis --- src/stores/BreadcrumbsStore.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 43bef8a538..9905dd4345 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -53,8 +53,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { public get visible(): boolean { // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0) - console.log(`calculating roomlist size: ${roomCount}`) + const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0); return roomCount >= 20; } From 946fde5cc5fd24daa1ea7b66385112b17fad36a5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 1 Jul 2020 11:59:32 -0600 Subject: [PATCH 19/45] Be consistent in visible tiles usage --- src/components/views/rooms/RoomSublist2.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 6682527254..fb955fcd57 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -370,7 +370,7 @@ 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 (this.numTiles > this.numVisibleTiles) { + if (this.numTiles > visibleTiles.length) { // we have a cutoff condition - add the button to show all const numMissing = this.numTiles - visibleTiles.length; let showMoreText = ( @@ -387,7 +387,7 @@ export default class RoomSublist2 extends React.Component { {showMoreText}
); - } else if (this.numTiles <= this.numVisibleTiles && this.numTiles > 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 = ( From 992349944a911a686e2992aecf4ea0df772d5a42 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 12:18:48 +0100 Subject: [PATCH 20/45] Fix room list 2's room tile wrapping wrongly --- res/css/views/rooms/_RoomTile2.scss | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 44c5b6ee17..144a5ccf86 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -23,7 +23,6 @@ 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; @@ -43,7 +42,8 @@ limitations under the License. .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,8 +81,20 @@ limitations under the License. } } + //.mx_RoomTile2_badgeContainer, + .mx_RoomTile2_menuButton, + .mx_RoomTile2_notificationsButton { + width: 20px; + min-width: 20px; // yay flex + height: 20px; + margin: auto 0; + } + + .mx_RoomTile2_menuButton { + margin-left: 4px; // spacing between buttons + } + .mx_RoomTile2_badgeContainer { - width: 18px; height: 32px; // Create another flexbox row because it's super easy to position the badge at @@ -90,14 +102,15 @@ limitations under the License. display: flex; align-items: center; justify-content: center; + + .mx_NotificationBadge { + margin-right: 2px; + } } // The context menu buttons are hidden by default .mx_RoomTile2_menuButton, .mx_RoomTile2_notificationsButton { - width: 20px; - height: 20px; - margin: auto 0 auto 8px; position: relative; display: none; @@ -130,7 +143,7 @@ limitations under the License. .mx_RoomTile2_badgeContainer { width: 0; height: 0; - visibility: hidden; + display: none; } .mx_RoomTile2_notificationsButton, From c259408d71faab9b788174572b5a7bbf5f6e4d61 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 12:35:06 +0100 Subject: [PATCH 21/45] fix alignment of dot and simplify CSS rules Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomTile2.scss | 31 +++++++++++++---------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 144a5ccf86..6241b3d0ba 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -81,36 +81,33 @@ limitations under the License. } } - //.mx_RoomTile2_badgeContainer, - .mx_RoomTile2_menuButton, - .mx_RoomTile2_notificationsButton { - width: 20px; - min-width: 20px; // yay flex - height: 20px; - margin: auto 0; - } - .mx_RoomTile2_menuButton { margin-left: 4px; // spacing between buttons } .mx_RoomTile2_badgeContainer { - height: 32px; - - // Create another flexbox row because it's super easy to position the badge at - // the end this way. - display: flex; - align-items: center; - justify-content: center; + height: 16px; + // don't set width so that it takes no space when there is no badge to show + margin: auto 0; // vertically align .mx_NotificationBadge { - margin-right: 2px; + 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; position: relative; display: none; From b5c94acbe6d5541c3fd94c7b1622a48f3e22bbe6 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 2 Jul 2020 13:17:51 +0100 Subject: [PATCH 22/45] Remove unused crypto import --- src/components/structures/MatrixChat.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 79bdf743ce..9e3e112a28 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 From a928785f723356c839ce00d4974a654825945d7c Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 2 Jul 2020 13:19:27 +0100 Subject: [PATCH 23/45] Check whether crypto is enabled in room recovery reminder This avoids a soft crash that may occur otherwise. Fixes https://github.com/vector-im/riot-web/issues/14289 --- src/components/structures/RoomView.js | 1 + 1 file changed, 1 insertion(+) 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 From 1c0d46b6e1a15caa5efd5084583cf2715c11055f Mon Sep 17 00:00:00 2001 From: Jorik Schellekens Date: Thu, 2 Jul 2020 15:26:51 +0100 Subject: [PATCH 24/45] Make breadcrumbs respsect setting --- src/stores/BreadcrumbsStore.ts | 5 +---- src/stores/room-list/RoomListStore2.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index 9905dd4345..c78f15c3b4 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -21,7 +21,6 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy"; -import _reduce from 'lodash/reduce'; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -52,9 +51,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } public get visible(): boolean { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - const roomCount = _reduce(RoomListStoreTempProxy.getRoomLists(), (result, rooms) => result + rooms.length, 0); - return roomCount >= 20; + return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20; } protected async onAction(payload: ActionPayload) { diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index b4d96becc4..73256e4de4 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -188,7 +188,7 @@ export class RoomListStore2 extends AsyncStore { 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}`); + ` 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`); From b7aa8203b683d51ceeb1017cc435955bf9679ddd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 09:04:38 -0600 Subject: [PATCH 25/45] Wedge community invites into the new room list Fixes https://github.com/vector-im/riot-web/issues/14179 Disclaimer: this is all of the horrible because it's not meant to be here. Invites in general are likely to move out of the room list, which means this is temporary. Additionally, the communities rework will take care of this more correctly. For now, we support the absolute bare minimum to have them shown. --- src/components/views/rooms/RoomList2.tsx | 40 ++++++ src/components/views/rooms/RoomSublist2.tsx | 19 ++- src/components/views/rooms/TemporaryTile.tsx | 114 ++++++++++++++++++ .../room-list/filters/NameFilterCondition.ts | 8 +- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/components/views/rooms/TemporaryTile.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index a1298e107b..606f2d60e9 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -25,10 +25,15 @@ 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 { NotificationColor, StaticNotificationState } from "./NotificationBadge"; // 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 @@ -173,6 +178,39 @@ export default class RoomList2 extends React.Component { }); } + 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 ( + + ); + }); + } + private renderSublists(): React.ReactElement[] { const components: React.ReactElement[] = []; @@ -195,6 +233,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 c64d62ebea..87796924e8 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -62,6 +62,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 } @@ -87,8 +91,7 @@ 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 { @@ -187,6 +190,10 @@ export default class RoomSublist2 extends React.Component { const tiles: React.ReactElement[] = []; + if (this.props.extraBadTilesThatShouldntExist) { + tiles.push(...this.props.extraBadTilesThatShouldntExist); + } + if (this.props.rooms) { const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles); for (const room of visibleRooms) { @@ -202,6 +209,14 @@ export default class RoomSublist2 extends React.Component { } } + // 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; } diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx new file mode 100644 index 0000000000..676969cade --- /dev/null +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -0,0 +1,114 @@ +/* +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 NotificationBadge, { INotificationState, NotificationColor } from "./NotificationBadge"; + +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_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; + + const avatarSize = 32; + return ( + + + {({onFocus, isActive, ref}) => + +
+ {this.props.avatar} +
+ {nameContainer} +
+ {badge} +
+
+ } +
+
+ ); + } +} diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 8625cd932c..12f147990d 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -60,11 +60,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); } } From 32642d592c9599f54853d3f850fb021eac3c40f2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 09:27:42 -0600 Subject: [PATCH 26/45] Add a key --- src/components/views/rooms/RoomList2.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 606f2d60e9..5d7b8ad2c6 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -206,6 +206,7 @@ export default class RoomList2 extends React.Component { avatar={avatar} notificationState={StaticNotificationState.forSymbol("!", NotificationColor.Red)} onClick={openGroup} + key={`temporaryGroupTile_${g.groupId}`} /> ); }); From a6586120785f345a31d4ce7ca3a6c383cfe3b6bf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 19:48:06 +0100 Subject: [PATCH 27/45] Add click-to-jump on badge in the room sublist header Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/NotificationBadge.tsx | 35 ++++++++++++++----- src/components/views/rooms/RoomSublist2.tsx | 33 ++++++++++++++++- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 2111310555..31f1ea2021 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -29,6 +29,8 @@ import { IDestroyable } from "../../../utils/IDestroyable"; import SettingsStore from "../../../settings/SettingsStore"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { readReceiptChangeIsFor } from "../../../utils/read-receipts"; +import AccessibleButton from "../elements/AccessibleButton"; +import { XOR } from "../../../@types/common"; export const NOTIFICATION_STATE_UPDATE = "update"; @@ -62,11 +64,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 +118,22 @@ 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; + // Don't show a badge if we don't need to + if (notification.color <= NotificationColor.None) return null; + + const hasNotif = notification.color >= NotificationColor.Red; + const hasCount = notification.color >= NotificationColor.Grey; + const hasUnread = notification.color >= NotificationColor.Bold; const couldBeEmpty = (!this.state.showCounts || hasUnread) && !hasNotif; let isEmptyBadge = couldBeEmpty && (!this.state.showCounts || !hasCount); - if (this.props.forceCount) { + 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 +145,14 @@ export default class NotificationBadge extends React.PureComponent 2, }); + if (onClick) { + return ( + + {symbol} + + ); + } + return (
{symbol} diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index c64d62ebea..dfd6cdaefa 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -33,6 +33,7 @@ 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 dis from "../../../dispatcher/dispatcher"; // 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,6 +161,29 @@ 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 in sortedList as that'll be the top of the list for the user + room = this.props.rooms && this.props.rooms[0]; + } else { + 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')) { @@ -287,7 +311,14 @@ 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) { From ae2a6ebc07a4f3ed02f673322a7d65bf277c08c7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 19:56:41 +0100 Subject: [PATCH 28/45] improve comments Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomSublist2.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index dfd6cdaefa..5584b8a521 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -167,9 +167,10 @@ export default class RoomSublist2 extends React.Component { let room; if (this.props.tagId === DefaultTagID.Invite) { - // switch to first room in sortedList as that'll be the top of the list for the user + // 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; From b65972d44f6e21ad59b4639b12c0cce779060106 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:23:20 -0600 Subject: [PATCH 29/45] Fix indentation --- src/stores/room-list/RoomListStore2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index d0ef88949c..e5205f6051 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -187,7 +187,7 @@ export class RoomListStore2 extends AsyncStore { 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}`); + ` 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`); From 45f4a2a980f553cf801f80953d63a03616d4a28f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:28:06 -0600 Subject: [PATCH 30/45] Fix imports for NotificationStates --- src/components/views/avatars/DecoratedRoomAvatar.tsx | 6 ++++-- src/components/views/rooms/RoomList2.tsx | 3 ++- src/components/views/rooms/TemporaryTile.tsx | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx index 1156c80313..e0ad3202b8 100644 --- a/src/components/views/avatars/DecoratedRoomAvatar.tsx +++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx @@ -20,7 +20,9 @@ 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, { INotificationState, TagSpecificNotificationState } from '../rooms/NotificationBadge'; +import NotificationBadge from '../rooms/NotificationBadge'; +import { INotificationState } from "../../../stores/notifications/INotificationState"; +import { TagSpecificNotificationState } from "../../../stores/notifications/TagSpecificNotificationState"; interface IProps { room: Room; @@ -60,4 +62,4 @@ export default class DecoratedRoomAvatar extends React.PureComponent; } -} \ No newline at end of file +} diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 5d7b8ad2c6..db7a095118 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -33,7 +33,8 @@ import { ListLayout } from "../../../stores/room-list/ListLayout"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import GroupAvatar from "../avatars/GroupAvatar"; import TemporaryTile from "./TemporaryTile"; -import { NotificationColor, StaticNotificationState } from "./NotificationBadge"; +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 diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx index 676969cade..3087e738f1 100644 --- a/src/components/views/rooms/TemporaryTile.tsx +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -18,7 +18,9 @@ import React from "react"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; -import NotificationBadge, { INotificationState, NotificationColor } from "./NotificationBadge"; +import { INotificationState } from "../../../stores/notifications/INotificationState"; +import NotificationBadge from "./NotificationBadge"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; interface IProps { isMinimized: boolean; From 349c3f70909b14bb9d32d893399c519250d20e24 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:33:06 -0600 Subject: [PATCH 31/45] Only show mute notification icon on rooms, not all notif icons --- src/components/views/rooms/RoomTile2.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 2647a24412..3060be8423 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -284,9 +284,10 @@ export default class RoomTile2 extends React.Component { mx_RoomTile2_iconBell: state === ALL_MESSAGES_LOUD || state === ALL_MESSAGES, mx_RoomTile2_iconBellDot: 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 ( From a5001e50aa45439aec777b5d92e8590d70ea036d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:33:24 -0600 Subject: [PATCH 32/45] Disable all unread decorations on muted rooms --- src/stores/notifications/RoomNotificationState.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index b9bc3f3492..a73c503453 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -81,7 +81,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, private updateNotificationState() { const before = {count: this.count, symbol: this.symbol, color: this.color}; - if (this.roomIsInvite) { + 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 From 7ea3164507354c950082832afddf9c6f826808b6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:39:20 -0600 Subject: [PATCH 33/45] Fix alignment of dot badges in new room list --- res/css/views/rooms/_RoomTile2.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index b11eb47d1c..3d2f791c01 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -83,6 +83,10 @@ limitations under the License. // don't set width so that it takes no space when there is no badge to show margin: auto 0; // vertically align + // Create a flexbox to make aligning dot badges easier + display: flex; + align-items: center; + .mx_NotificationBadge { margin-right: 2px; // centering } From c3ad8548683601b634dd40be797b82d1ae2d30cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:50:25 -0600 Subject: [PATCH 34/45] Fix alignment of avatars on community invites --- res/css/views/rooms/_RoomTile2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index b11eb47d1c..38c30cf320 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -29,7 +29,7 @@ limitations under the License. border-radius: 32px; } - .mx_DecoratedRoomAvatar { + .mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer { margin-right: 8px; } From e51f9d24920f4f1a2af0777c6ee7de7d2cfaba25 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:53:38 -0600 Subject: [PATCH 35/45] Fix closing the context menu causing the tile to be selected Fixes https://github.com/vector-im/riot-web/issues/14293 --- src/components/views/rooms/RoomTile2.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 2647a24412..920137ba88 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -17,7 +17,7 @@ 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"; @@ -30,7 +30,6 @@ import { ContextMenu, ContextMenuButton, MenuItemRadio } from "../../structures/ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import RoomTileIcon from "./RoomTileIcon"; import { getRoomNotifsState, ALL_MESSAGES, ALL_MESSAGES_LOUD, MENTIONS_ONLY, MUTE } from "../../../RoomNotifs"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { setRoomNotifsState } from "../../../RoomNotifs"; @@ -157,7 +156,9 @@ export default class RoomTile2 extends React.Component { this.setState({notificationsMenuPosition: target.getBoundingClientRect()}); }; - private onCloseNotificationsMenu = () => { + private onCloseNotificationsMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); this.setState({notificationsMenuPosition: null}); }; @@ -179,7 +180,9 @@ export default class RoomTile2 extends React.Component { }); }; - private onCloseGeneralMenu = () => { + private onCloseGeneralMenu = (ev: InputEvent) => { + ev.preventDefault(); + ev.stopPropagation(); this.setState({generalMenuPosition: null}); }; From aa702514ce2be1d124866e70553fb2b616d7217e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 13:59:28 -0600 Subject: [PATCH 36/45] Don't try and show context menus if we don't have one Fixes https://github.com/vector-im/riot-web/issues/14295 --- src/components/views/rooms/RoomTile2.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 920137ba88..7dbe9f1f10 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -119,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 +174,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({ @@ -239,7 +246,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; } @@ -306,12 +313,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) { From e08512020f93cd78701ebc310e6ea9963e9809ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 14:05:31 -0600 Subject: [PATCH 37/45] Fix a couple badge alignment issues with community invites --- res/css/views/avatars/_DecoratedRoomAvatar.scss | 3 ++- res/css/views/rooms/_RoomTile2.scss | 2 +- src/components/views/rooms/TemporaryTile.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/avatars/_DecoratedRoomAvatar.scss b/res/css/views/avatars/_DecoratedRoomAvatar.scss index 984fa0ce9a..b500d44a43 100644 --- a/res/css/views/avatars/_DecoratedRoomAvatar.scss +++ b/res/css/views/avatars/_DecoratedRoomAvatar.scss @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_DecoratedRoomAvatar { +// XXX: We shouldn't be using TemporaryTile anywhere - delete it. +.mx_DecoratedRoomAvatar, .mx_TemporaryTile { position: relative; .mx_RoomTileIcon { diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index 38c30cf320..d366e1b226 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -148,7 +148,7 @@ limitations under the License. align-items: center; position: relative; - .mx_DecoratedRoomAvatar { + .mx_DecoratedRoomAvatar, .mx_RoomTile2_avatarContainer { margin-right: 0; } } diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx index 3087e738f1..b6c165ecda 100644 --- a/src/components/views/rooms/TemporaryTile.tsx +++ b/src/components/views/rooms/TemporaryTile.tsx @@ -56,6 +56,7 @@ export default class TemporaryTile extends React.Component { // 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, }); @@ -85,7 +86,6 @@ export default class TemporaryTile extends React.Component { ); if (this.props.isMinimized) nameContainer = null; - const avatarSize = 32; return ( From 0d9ce0721f7777758482773ac2593f4444c64fe1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 14:11:31 -0600 Subject: [PATCH 38/45] Don't include empty badge container in minimized view Fixes https://github.com/vector-im/riot-web/issues/14294 It takes up space, and it won't hold anything anyways. --- src/components/views/rooms/RoomTile2.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 2647a24412..92c69e54e7 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -369,11 +369,15 @@ export default class RoomTile2 extends React.Component { let badge: React.ReactNode; if (!this.props.isMinimized) { - badge = ; + badge = ( +
+ +
+ ); } // TODO: the original RoomTile uses state for the room name. Do we need to? @@ -429,9 +433,7 @@ export default class RoomTile2 extends React.Component { > {roomAvatar} {nameContainer} -
- {badge} -
+ {badge} {this.renderNotificationsMenu()} {this.renderGeneralMenu()} From 1b782ce5f27365871103aacb2ee7058fe7ad43ac Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 14:23:56 -0600 Subject: [PATCH 39/45] Enable the new room list by default and trigger an initial render We have to trigger an initial render because during the login process the user will have started syncing (causing lists to generate) but the RoomList component won't be mounted & listening and therefore won't receive the initial lists. By generating them on mount, we ensure that the lists are present once the user gets through the login process. --- src/components/views/rooms/RoomList2.tsx | 29 +++++++++++++++--------- src/settings/Settings.js | 2 +- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index db7a095118..b0bb70c9a0 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -166,19 +166,26 @@ 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); - - const layoutMap = new Map(); - for (const tagId of Object.keys(newLists)) { - layoutMap.set(tagId, new ListLayout(tagId)); - } - - this.setState({sublists: newLists, layouts: layoutMap}); - }); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); + this.updateLists(); // trigger the first update } + public componentWillUnmount() { + RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); + } + + 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 => { 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": { From 547690374eade938bf4044e2a092d3571c6d7ce2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 14:53:21 -0600 Subject: [PATCH 40/45] Wrap event stoppage in null checks Some of the code paths (particularly onFinished) do not have events, but the code paths we care about to prevent the room selection do have events - we can stop those without stopping further menus. --- src/components/views/rooms/RoomTile2.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 7dbe9f1f10..6f686dbac3 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -160,9 +160,11 @@ export default class RoomTile2 extends React.Component { this.setState({notificationsMenuPosition: target.getBoundingClientRect()}); }; - private onCloseNotificationsMenu = (ev: InputEvent) => { - ev.preventDefault(); - ev.stopPropagation(); + private onCloseNotificationsMenu = (ev?: InputEvent) => { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } this.setState({notificationsMenuPosition: null}); }; @@ -187,9 +189,11 @@ export default class RoomTile2 extends React.Component { }); }; - private onCloseGeneralMenu = (ev: InputEvent) => { - ev.preventDefault(); - ev.stopPropagation(); + private onCloseGeneralMenu = (ev?: InputEvent) => { + if (ev) { + ev.preventDefault(); + ev.stopPropagation(); + } this.setState({generalMenuPosition: null}); }; From a6e0799b57c4830a61d09dc749510722691de973 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 15:05:01 -0600 Subject: [PATCH 41/45] Handle push rule changes in the RoomNotificationState --- src/stores/notifications/RoomNotificationState.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts index a73c503453..f9b19fcbcb 100644 --- a/src/stores/notifications/RoomNotificationState.ts +++ b/src/stores/notifications/RoomNotificationState.ts @@ -37,6 +37,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, 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(); } @@ -62,6 +63,7 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, this.room.removeListener("Room.redaction", this.handleRoomEventUpdate); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate); + MatrixClientPeg.get().removeListener("accountData", this.handleAccountDataUpdate); } } @@ -78,6 +80,12 @@ export class RoomNotificationState extends EventEmitter implements IDestroyable, 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}; From 3847dc91c06f8796ddfb2aa97440ccc40cc7b165 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 15:15:33 -0600 Subject: [PATCH 42/45] Move the stoppage to somewhere more generic --- src/components/structures/ContextMenu.js | 9 ++++++++- src/components/views/rooms/RoomTile2.tsx | 12 ++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index 5ba2662796..e43b0d1431 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -140,6 +140,13 @@ export class ContextMenu extends React.Component { 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? @@ -326,7 +333,7 @@ export class ContextMenu extends React.Component { let background; if (hasBackground) { background = ( -
+
); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 6f686dbac3..1024198560 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -160,11 +160,7 @@ export default class RoomTile2 extends React.Component { this.setState({notificationsMenuPosition: target.getBoundingClientRect()}); }; - private onCloseNotificationsMenu = (ev?: InputEvent) => { - if (ev) { - ev.preventDefault(); - ev.stopPropagation(); - } + private onCloseNotificationsMenu = () => { this.setState({notificationsMenuPosition: null}); }; @@ -189,11 +185,7 @@ export default class RoomTile2 extends React.Component { }); }; - private onCloseGeneralMenu = (ev?: InputEvent) => { - if (ev) { - ev.preventDefault(); - ev.stopPropagation(); - } + private onCloseGeneralMenu = () => { this.setState({generalMenuPosition: null}); }; From 5c5482a8ae530a9d06ca4fe356d9a57a8f0efc22 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2020 23:20:16 +0100 Subject: [PATCH 43/45] I've got 99 problems and this badge mismatch is no longer one Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile2.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 577387948c..25b60b7898 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -287,8 +287,9 @@ 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, // Only show the icon by default if the room is overridden to muted. From ae076a7439a854104db2f1e2fe555e6ad0941529 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 16:23:33 -0600 Subject: [PATCH 44/45] Add a null guard for message event previews --- src/stores/room-list/previews/MessageEventPreview.ts | 2 ++ 1 file changed, 2 insertions(+) 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 From 98ce1dafee5e009844a0d0bc3bbb82c08ad5a46a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jul 2020 16:36:07 -0600 Subject: [PATCH 45/45] Remove a bunch of noisy logging from the room list None of these logs are actually needed for troubleshooting anymore. --- src/stores/room-list/RoomListStore2.ts | 10 -------- src/stores/room-list/algorithms/Algorithm.ts | 23 ------------------- .../list-ordering/ImportanceAlgorithm.ts | 3 --- .../list-ordering/NaturalAlgorithm.ts | 3 --- .../filters/CommunityFilterCondition.ts | 2 -- .../room-list/filters/NameFilterCondition.ts | 2 -- 6 files changed, 43 deletions(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index e5205f6051..d4aec93035 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -101,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; } } @@ -299,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); } } @@ -367,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); }; @@ -408,8 +402,6 @@ export class RoomListStore2 extends AsyncStore { } 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); @@ -417,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); diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 36abf86975..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`); } } 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 12f147990d..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); }