From 82b55ffd7717b4256690f43eb6891b173fe3cec9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 Mar 2020 13:34:56 -0600 Subject: [PATCH 01/34] Add temporary timing functions to old RoomListStore This is to identify how bad of a state we're in to start with. --- src/stores/RoomListStore.js | 60 ++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 8 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 89edc9a8ef..e217f7ea38 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -58,7 +58,27 @@ export const ALGO_RECENT = "recent"; const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE]; -const getListAlgorithm = (listKey, settingAlgorithm) => { +function debugLog(...msg) { + console.log(`[RoomListStore:Debug] `, ...msg); +} + +const timers = {}; +let timerCounter = 0; +function startTimer(fnName) { + const id = `${fnName}_${(new Date()).getTime()}_${timerCounter++}`; + debugLog(`Started timer for ${fnName} with ID ${id}`); + timers[id] = {start: (new Date()).getTime(), fnName}; + return id; +} + +function endTimer(id) { + const timer = timers[id]; + delete timers[id]; + const diff = (new Date()).getTime() - timer.start; + debugLog(`${timer.fnName} took ${diff}ms (ID: ${id})`); +} + +function getListAlgorithm(listKey, settingAlgorithm) { // apply manual sorting only to m.favourite, otherwise respect the global setting // all the known tags are listed explicitly here to simplify future changes switch (listKey) { @@ -73,7 +93,7 @@ const getListAlgorithm = (listKey, settingAlgorithm) => { default: // custom-tags return ALGO_MANUAL; } -}; +} const knownLists = new Set([ "m.favourite", @@ -340,6 +360,7 @@ class RoomListStore extends Store { } _getRecommendedTagsForRoom(room) { + const timerId = startTimer(`_getRecommendedTagsForRoom(room:"${room.roomId}")`); const tags = []; const myMembership = room.getMyMembership(); @@ -365,11 +386,12 @@ class RoomListStore extends Store { tags.push("im.vector.fake.archived"); } - + endTimer(timerId); return tags; } _slotRoomIntoList(room, category, tag, existingEntries, newList, lastTimestampFn) { + const timerId = startTimer(`_slotRoomIntoList(room:"${room.roomId}", "${category}", "${tag}", existingEntries: "${existingEntries.length}", "${newList}", lastTimestampFn:"${lastTimestampFn !== null}")`); const targetCategoryIndex = CATEGORY_ORDER.indexOf(category); let categoryComparator = (a, b) => lastTimestampFn(a.room) >= lastTimestampFn(b.room); @@ -481,11 +503,16 @@ class RoomListStore extends Store { pushedEntry = true; } + endTimer(timerId); return pushedEntry; } _setRoomCategory(room, category) { - if (!room) return; // This should only happen in tests + const timerId = startTimer(`_setRoomCategory(room:"${room.roomId}", "${category}")`); + if (!room) { + endTimer(timerId); + return; // This should only happen in tests + } const listsClone = {}; @@ -582,9 +609,11 @@ class RoomListStore extends Store { } this._setState({lists: listsClone}); + endTimer(timerId); } _generateInitialRoomLists() { + const timerId = startTimer(`_generateInitialRoomLists()`); // Log something to show that we're throwing away the old results. This is for the inevitable // question of "why is 100% of my CPU going towards Riot?" - a quick look at the logs would reveal // that something is wrong with the RoomListStore. @@ -697,6 +726,7 @@ class RoomListStore extends Store { lists, ready: true, // Ready to receive updates to ordering }); + endTimer(timerId); } _eventTriggersRecentReorder(ev) { @@ -709,7 +739,9 @@ class RoomListStore extends Store { _tsOfNewestEvent(room) { // Apparently we can have rooms without timelines, at least under testing // environments. Just return MAX_INT when this happens. - if (!room || !room.timeline) return Number.MAX_SAFE_INTEGER; + if (!room || !room.timeline) { + return Number.MAX_SAFE_INTEGER; + } for (let i = room.timeline.length - 1; i >= 0; --i) { const ev = room.timeline[i]; @@ -729,23 +761,35 @@ class RoomListStore extends Store { } _calculateCategory(room) { + const timerId = startTimer(`_calculateCategory(room:"${room.roomId}")`); if (!this._state.orderImportantFirst) { // Effectively disable the categorization of rooms if we're supposed to // be sorting by more recent messages first. This triggers the timestamp // comparison bit of _setRoomCategory and _recentsComparator instead of // the category ordering. + endTimer(timerId); return CATEGORY_IDLE; } const mentions = room.getUnreadNotificationCount("highlight") > 0; - if (mentions) return CATEGORY_RED; + if (mentions) { + endTimer(timerId); + return CATEGORY_RED; + } let unread = room.getUnreadNotificationCount() > 0; - if (unread) return CATEGORY_GREY; + if (unread) { + endTimer(timerId); + return CATEGORY_GREY; + } unread = Unread.doesRoomHaveUnreadMessages(room); - if (unread) return CATEGORY_BOLD; + if (unread) { + endTimer(timerId); + return CATEGORY_BOLD; + } + endTimer(timerId); return CATEGORY_IDLE; } From 08419d195e365eab57e0ffe0e315a7de9264d041 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 20 Mar 2020 14:38:20 -0600 Subject: [PATCH 02/34] Initial breakout for room list rewrite This does a number of things (sorry): * Estimates the type changes needed to the dispatcher (later to be replaced by https://github.com/matrix-org/matrix-react-sdk/pull/4593) * Sets up the stack for a whole new room list store, and later components for usage. * Create a proxy class to ensure the app still functions as expected when the various stores are enabled/disabled * Demonstrates a possible structure for algorithms --- package.json | 1 + src/actions/RoomListActions.js | 17 +- src/components/structures/LeftPanel.js | 34 ++- src/components/structures/LoggedInView.tsx | 13 +- src/components/views/dialogs/InviteDialog.js | 9 +- src/components/views/rooms/RoomList.js | 15 +- src/components/views/rooms/RoomList2.tsx | 130 +++++++++++ src/dispatcher-types.ts | 28 +++ src/i18n/strings/en_EN.json | 2 + src/settings/Settings.js | 6 + src/stores/CustomRoomTagStore.js | 8 +- src/stores/RoomListStore.js | 17 ++ src/stores/room-list/RoomListStore2.ts | 213 ++++++++++++++++++ .../room-list/RoomListStoreTempProxy.ts | 49 ++++ .../room-list/algorithms/ChaoticAlgorithm.ts | 100 ++++++++ src/stores/room-list/algorithms/IAlgorithm.ts | 95 ++++++++ src/stores/room-list/algorithms/index.ts | 36 +++ src/stores/room-list/models.ts | 36 +++ test/components/views/rooms/RoomList-test.js | 16 +- tsconfig.json | 3 +- yarn.lock | 13 ++ 21 files changed, 794 insertions(+), 47 deletions(-) create mode 100644 src/components/views/rooms/RoomList2.tsx create mode 100644 src/dispatcher-types.ts create mode 100644 src/stores/room-list/RoomListStore2.ts create mode 100644 src/stores/room-list/RoomListStoreTempProxy.ts create mode 100644 src/stores/room-list/algorithms/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/IAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/index.ts create mode 100644 src/stores/room-list/models.ts diff --git a/package.json b/package.json index dda4a5a897..22ff071ba7 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "@babel/register": "^7.7.4", "@peculiar/webcrypto": "^1.0.22", "@types/classnames": "^2.2.10", + "@types/flux": "^3.1.9", "@types/modernizr": "^3.5.3", "@types/qrcode": "^1.3.4", "@types/react": "16.9", diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index 10a3848dda..072b1d9a86 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -15,11 +15,12 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; import * as sdk from '../index'; +import {RoomListStoreTempProxy} from "../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../stores/room-list/models"; const RoomListActions = {}; @@ -44,7 +45,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, // Is the tag ordered manually? if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const newList = [...lists[newTag]]; newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); @@ -73,11 +74,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === TAG_DM) || - (oldTag === TAG_DM && newTag === undefined) + if ((oldTag === undefined && newTag === DefaultTagID.DM) || + (oldTag === DefaultTagID.DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === TAG_DM, + room, newTag === DefaultTagID.DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -91,10 +92,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const hasChangedSubLists = oldTag !== newTag; // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with TAG_DM. + // but we avoid ever doing a request with DefaultTagID.DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== TAG_DM && + if (oldTag && oldTag !== DefaultTagID.DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( @@ -112,7 +113,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== TAG_DM && + if (newTag && newTag !== DefaultTagID.DM && (hasChangedSubLists || metaData) ) { // metaData is the body of the PUT to set the tag, so it must diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a9cd12199b..1993e2f419 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -26,6 +26,7 @@ import * as VectorConferenceHandler from '../../VectorConferenceHandler'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; +import RoomList2 from "../views/rooms/RoomList2"; const LeftPanel = createReactClass({ @@ -273,6 +274,29 @@ const LeftPanel = createReactClass({ breadcrumbs = (); } + let roomList = null; + if (SettingsStore.isFeatureEnabled("feature_new_room_list")) { + roomList = ; + } else { + roomList = ; + } + return (
{ tagPanelContainer } @@ -284,15 +308,7 @@ const LeftPanel = createReactClass({ { exploreButton } { searchBox }
- + {roomList} ); diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 9de2aac8e9..0950e52bba 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -31,7 +31,6 @@ import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStore from "../../stores/RoomListStore"; import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; @@ -42,6 +41,8 @@ import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import HomePage from "./HomePage"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PlatformPeg from "../../PlatformPeg"; +import { RoomListStoreTempProxy } from "../../stores/room-list/RoomListStoreTempProxy"; +import { DefaultTagID } from "../../stores/room-list/models"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -297,18 +298,18 @@ class LoggedInView extends React.PureComponent { }; onRoomStateEvents = (ev, state) => { - const roomLists = RoomListStore.getRoomLists(); - if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (roomLists[DefaultTagID.ServerNotice] && roomLists[DefaultTagID.ServerNotice].some(r => r.roomId === ev.getRoomId())) { this._updateServerNoticeEvents(); } }; _updateServerNoticeEvents = async () => { - const roomLists = RoomListStore.getRoomLists(); - if (!roomLists['m.server_notice']) return []; + const roomLists = RoomListStoreTempProxy.getRoomLists(); + if (!roomLists[DefaultTagID.ServerNotice]) return []; const pinnedEvents = []; - for (const room of roomLists['m.server_notice']) { + for (const room of roomLists[DefaultTagID.ServerNotice]) { const pinStateEvent = room.currentState.getStateEvents("m.room.pinned_events", ""); if (!pinStateEvent || !pinStateEvent.getContent().pinned) continue; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 7cbbf8ba64..e719c45f49 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -34,8 +34,9 @@ import {humanizeTime} from "../../../utils/humanize"; import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; import SettingsStore from '../../../settings/SettingsStore'; -import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore"; import {Key} from "../../../Keyboard"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -343,10 +344,10 @@ export default class InviteDialog extends React.PureComponent { _buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room - // Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the + // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const taggedRooms = RoomListStore.getRoomLists(); - const dmTaggedRooms = taggedRooms[TAG_DM]; + const taggedRooms = RoomListStoreTempProxy.getRoomLists(); + const dmTaggedRooms = taggedRooms[DefaultTagID.DM]; const myUserId = MatrixClientPeg.get().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId); diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 289a89a206..dc9c9238cd 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -29,7 +29,6 @@ import rate_limited_func from "../../../ratelimitedfunc"; import * as Rooms from '../../../Rooms'; import DMRoomMap from '../../../utils/DMRoomMap'; import TagOrderStore from '../../../stores/TagOrderStore'; -import RoomListStore, {TAG_DM} from '../../../stores/RoomListStore'; import CustomRoomTagStore from '../../../stores/CustomRoomTagStore'; import GroupStore from '../../../stores/GroupStore'; import RoomSubList from '../../structures/RoomSubList'; @@ -41,6 +40,8 @@ import * as Receipt from "../../../utils/Receipt"; import {Resizer} from '../../../resizer'; import {Layout, Distributor} from '../../../resizer/distributors/roomsublist2'; import {RovingTabIndexProvider} from "../../../accessibility/RovingTabIndex"; +import {RoomListStoreTempProxy} from "../../../stores/room-list/RoomListStoreTempProxy"; +import {DefaultTagID} from "../../../stores/room-list/models"; import * as Unread from "../../../Unread"; import RoomViewStore from "../../../stores/RoomViewStore"; @@ -161,7 +162,7 @@ export default createReactClass({ this.updateVisibleRooms(); }); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._delayedRefreshRoomList(); }); @@ -521,7 +522,7 @@ export default createReactClass({ }, getTagNameForRoomId: function(roomId) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); for (const tagName of Object.keys(lists)) { for (const room of lists[tagName]) { // Should be impossible, but guard anyways. @@ -541,7 +542,7 @@ export default createReactClass({ }, getRoomLists: function() { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const filteredLists = {}; @@ -773,10 +774,10 @@ export default createReactClass({ incomingCall: incomingCallIfTaggedAs('m.favourite'), }, { - list: this.state.lists[TAG_DM], + list: this.state.lists[DefaultTagID.DM], label: _t('Direct Messages'), - tagName: TAG_DM, - incomingCall: incomingCallIfTaggedAs(TAG_DM), + tagName: DefaultTagID.DM, + incomingCall: incomingCallIfTaggedAs(DefaultTagID.DM), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'});}, addRoomLabel: _t("Start chat"), }, diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx new file mode 100644 index 0000000000..1790fa8cf6 --- /dev/null +++ b/src/components/views/rooms/RoomList2.tsx @@ -0,0 +1,130 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 Vector Creations Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import { _t } from "../../../languageHandler"; +import { Layout } from '../../../resizer/distributors/roomsublist2'; +import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; +import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import RoomListStore from "../../../stores/room-list/RoomListStore2"; + +interface IProps { + onKeyDown: (ev: React.KeyboardEvent) => void; + onFocus: (ev: React.FocusEvent) => void; + onBlur: (ev: React.FocusEvent) => void; + resizeNotifier: ResizeNotifier; + collapsed: boolean; + searchFilter: string; +} + +interface IState { +} + +// TODO: Actually write stub +export class RoomSublist2 extends React.Component { + public setHeight(size: number) { + } +} + +export default class RoomList2 extends React.Component { + private sublistRefs: { [tagId: string]: React.RefObject } = {}; + private sublistSizes: { [tagId: string]: number } = {}; + private sublistCollapseStates: { [tagId: string]: boolean } = {}; + private unfilteredLayout: Layout; + private filteredLayout: Layout; + + constructor(props: IProps) { + super(props); + + this.loadSublistSizes(); + this.prepareLayouts(); + } + + public componentDidMount(): void { + RoomListStore.instance.addListener(() => { + console.log(RoomListStore.instance.orderedLists); + }); + } + + private loadSublistSizes() { + const sizesJson = window.localStorage.getItem("mx_roomlist_sizes"); + if (sizesJson) this.sublistSizes = JSON.parse(sizesJson); + + const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed"); + if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson); + } + + private saveSublistSizes() { + window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes)); + window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates)); + } + + private prepareLayouts() { + this.unfilteredLayout = new Layout((tagId: string, height: number) => { + const sublist = this.sublistRefs[tagId]; + if (sublist) sublist.current.setHeight(height); + + // TODO: Check overflow + + // Don't store a height for collapsed sublists + if (!this.sublistCollapseStates[tagId]) { + this.sublistSizes[tagId] = height; + this.saveSublistSizes(); + } + }, this.sublistSizes, this.sublistCollapseStates, { + allowWhitespace: false, + handleHeight: 1, + }); + + this.filteredLayout = new Layout((tagId: string, height: number) => { + const sublist = this.sublistRefs[tagId]; + if (sublist) sublist.current.setHeight(height); + }, null, null, { + allowWhitespace: false, + handleHeight: 0, + }); + } + + private collectSublistRef(tagId: string, ref: React.RefObject) { + if (!ref) { + delete this.sublistRefs[tagId]; + } else { + this.sublistRefs[tagId] = ref; + } + } + + public render() { + return ( + + {({onKeyDownHandler}) => ( +
{_t("TODO")}
+ )} +
+ ); + } +} diff --git a/src/dispatcher-types.ts b/src/dispatcher-types.ts new file mode 100644 index 0000000000..16fac0c849 --- /dev/null +++ b/src/dispatcher-types.ts @@ -0,0 +1,28 @@ +/* +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 * as flux from "flux"; +import dis from "./dispatcher"; + +// TODO: Merge this with the dispatcher and centralize types + +export interface ActionPayload { + [property: string]: any; // effectively "extends Object" + action: string; +} + +// For ease of reference in TypeScript classes +export const defaultDispatcher: flux.Dispatcher = dis; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index f16a0d7755..fd474f378c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,6 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", @@ -1116,6 +1117,7 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", + "TODO": "TODO", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 5c6d843349..554cf6b968 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -131,6 +131,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_new_room_list": { + isFeature: true, + displayName: _td("Use the improved room list component (refresh to apply changes)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_custom_themes": { isFeature: true, displayName: _td("Support adding custom themes"), diff --git a/src/stores/CustomRoomTagStore.js b/src/stores/CustomRoomTagStore.js index 909282c085..bf8e970535 100644 --- a/src/stores/CustomRoomTagStore.js +++ b/src/stores/CustomRoomTagStore.js @@ -15,10 +15,10 @@ limitations under the License. */ import dis from '../dispatcher'; import * as RoomNotifs from '../RoomNotifs'; -import RoomListStore from './RoomListStore'; import EventEmitter from 'events'; import { throttle } from "lodash"; import SettingsStore from "../settings/SettingsStore"; +import {RoomListStoreTempProxy} from "./room-list/RoomListStoreTempProxy"; const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/; @@ -60,7 +60,7 @@ class CustomRoomTagStore extends EventEmitter { trailing: true, }, ); - this._roomListStoreToken = RoomListStore.addListener(() => { + this._roomListStoreToken = RoomListStoreTempProxy.addListener(() => { this._setState({tags: this._getUpdatedTags()}); }); dis.register(payload => this._onDispatch(payload)); @@ -85,7 +85,7 @@ class CustomRoomTagStore extends EventEmitter { } getSortedTags() { - const roomLists = RoomListStore.getRoomLists(); + const roomLists = RoomListStoreTempProxy.getRoomLists(); const tagNames = Object.keys(this._state.tags).sort(); const prefixes = tagNames.map((name, i) => { @@ -140,7 +140,7 @@ class CustomRoomTagStore extends EventEmitter { return; } - const newTagNames = Object.keys(RoomListStore.getRoomLists()) + const newTagNames = Object.keys(RoomListStoreTempProxy.getRoomLists()) .filter((tagName) => { return !tagName.match(STANDARD_TAGS_REGEX); }).sort(); diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index e217f7ea38..ccccbcc313 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -112,11 +112,19 @@ class RoomListStore extends Store { constructor() { super(dis); + this._checkDisabled(); this._init(); this._getManualComparator = this._getManualComparator.bind(this); this._recentsComparator = this._recentsComparator.bind(this); } + _checkDisabled() { + this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.disabled) { + console.warn("DISABLING LEGACY ROOM LIST STORE"); + } + } + /** * Changes the sorting algorithm used by the RoomListStore. * @param {string} algorithm The new algorithm to use. Should be one of the ALGO_* constants. @@ -133,6 +141,8 @@ class RoomListStore extends Store { } _init() { + if (this.disabled) return; + // Initialise state const defaultLists = { "m.server_notice": [/* { room: js-sdk room, category: string } */], @@ -160,6 +170,8 @@ class RoomListStore extends Store { } _setState(newState) { + if (this.disabled) return; + // If we're changing the lists, transparently change the presentation lists (which // is given to requesting components). This dramatically simplifies our code elsewhere // while also ensuring we don't need to update all the calling components to support @@ -176,6 +188,8 @@ class RoomListStore extends Store { } __onDispatch(payload) { + if (this.disabled) return; + const logicallyReady = this._matrixClient && this._state.ready; switch (payload.action) { case 'setting_updated': { @@ -202,6 +216,9 @@ class RoomListStore extends Store { break; } + this._checkDisabled(); + if (this.disabled) return; + // Always ensure that we set any state needed for settings here. It is possible that // setting updates trigger on startup before we are ready to sync, so we want to make // sure that the right state is in place before we actually react to those changes. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts new file mode 100644 index 0000000000..70d6d4a598 --- /dev/null +++ b/src/stores/room-list/RoomListStore2.ts @@ -0,0 +1,213 @@ +/* +Copyright 2018, 2019 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Store} from 'flux/utils'; +import {Room} from "matrix-js-sdk/src/models/room"; +import {MatrixClient} from "matrix-js-sdk/src/client"; +import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; +import SettingsStore from "../../settings/SettingsStore"; +import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models"; +import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; +import TagOrderStore from "../TagOrderStore"; +import { getAlgorithmInstance } from "./algorithms"; + +interface IState { + tagsEnabled?: boolean; + + preferredSort?: SortAlgorithm; + preferredAlgorithm?: ListAlgorithm; +} + +class _RoomListStore extends Store { + private state: IState = {}; + private matrixClient: MatrixClient; + private initialListsGenerated = false; + private enabled = false; + private algorithm: IAlgorithm; + + private readonly watchedSettings = [ + 'RoomList.orderAlphabetically', + 'RoomList.orderByImportance', + 'feature_custom_tags', + ]; + + constructor() { + super(defaultDispatcher); + + this.checkEnabled(); + this.reset(); + for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); + } + + public get orderedLists(): ITagMap { + if (!this.algorithm) return {}; // No tags yet. + return this.algorithm.getOrderedRooms(); + } + + // TODO: Remove enabled flag when the old RoomListStore goes away + private checkEnabled() { + this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); + if (this.enabled) { + console.log("ENABLING NEW ROOM LIST STORE"); + } + } + + private reset(): void { + // We don't call setState() because it'll cause changes to emitted which could + // crash the app during logout/signin/etc. + this.state = {}; + } + + private readAndCacheSettingsFromStore() { + const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); + const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); + const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); + this.setState({ + tagsEnabled, + preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, + preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, + }); + this.setAlgorithmClass(); + } + + protected __onDispatch(payload: ActionPayload): void { + if (payload.action === 'MatrixActions.sync') { + // Filter out anything that isn't the first PREPARED sync. + if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { + return; + } + + this.checkEnabled(); + if (!this.enabled) return; + + this.matrixClient = payload.matrixClient; + + // Update any settings here, as some may have happened before we were logically ready. + this.readAndCacheSettingsFromStore(); + + // noinspection JSIgnoredPromiseFromCall + this.regenerateAllLists(); + } + + // TODO: Remove this once the RoomListStore becomes default + if (!this.enabled) return; + + if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { + // Reset state without causing updates as the client will have been destroyed + // and downstream code will throw NPE errors. + this.reset(); + this.matrixClient = null; + this.initialListsGenerated = false; // we'll want to regenerate them + } + + // Everything below here requires a MatrixClient or some sort of logical readiness. + const logicallyReady = this.matrixClient && this.initialListsGenerated; + if (!logicallyReady) return; + + if (payload.action === 'setting_updated') { + if (this.watchedSettings.includes(payload.settingName)) { + this.readAndCacheSettingsFromStore(); + + // noinspection JSIgnoredPromiseFromCall + this.regenerateAllLists(); // regenerate the lists now + } + } else if (payload.action === 'MatrixActions.Room.receipt') { + // First see if the receipt event is for our own user. If it was, trigger + // a room update (we probably read the room on a different device). + const myUserId = this.matrixClient.getUserId(); + for (const eventId of Object.keys(payload.event.getContent())) { + const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); + if (receiptUsers.includes(myUserId)) { + // TODO: Update room now that it's been read + return; + } + } + } else if (payload.action === 'MatrixActions.Room.tags') { + // TODO: Update room from tags + } else if (payload.action === 'MatrixActions.room.timeline') { + // TODO: Update room from new events + } else if (payload.action === 'MatrixActions.Event.decrypted') { + // TODO: Update room from decrypted event + } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { + // TODO: Update DMs + } else if (payload.action === 'MatrixActions.Room.myMembership') { + // TODO: Update room from membership change + } else if (payload.action === 'MatrixActions.room') { + // TODO: Update room from creation/join + } else if (payload.action === 'view_room') { + // TODO: Update sticky room + } + } + + private getSortAlgorithmFor(tagId: TagID): SortAlgorithm { + switch (tagId) { + case DefaultTagID.Invite: + case DefaultTagID.Untagged: + case DefaultTagID.Archived: + case DefaultTagID.LowPriority: + case DefaultTagID.DM: + return this.state.preferredSort; + case DefaultTagID.Favourite: + default: + return SortAlgorithm.Manual; + } + } + + private setState(newState: IState) { + if (!this.enabled) return; + + this.state = Object.assign(this.state, newState); + this.__emitChange(); + } + + private setAlgorithmClass() { + this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + } + + private async regenerateAllLists() { + console.log("REGEN"); + const tags: ITagSortingMap = {}; + for (const tagId of OrderedDefaultTagIDs) { + tags[tagId] = this.getSortAlgorithmFor(tagId); + } + + if (this.state.tagsEnabled) { + // TODO: Find a more reliable way to get tags + const roomTags = TagOrderStore.getOrderedTags() || []; + console.log("rtags", roomTags); + } + + await this.algorithm.populateTags(tags); + await this.algorithm.setKnownRooms(this.matrixClient.getRooms()); + + this.initialListsGenerated = true; + + // TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync? + } +} + +export default class RoomListStore { + private static internalInstance: _RoomListStore; + + public static get instance(): _RoomListStore { + if (!RoomListStore.internalInstance) { + RoomListStore.internalInstance = new _RoomListStore(); + } + + return RoomListStore.internalInstance; + } +} diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts new file mode 100644 index 0000000000..7b12602541 --- /dev/null +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -0,0 +1,49 @@ +/* +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 { TagID } from "./models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import SettingsStore from "../../settings/SettingsStore"; +import RoomListStore from "./RoomListStore2"; +import OldRoomListStore from "../RoomListStore"; + +/** + * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when + * it is available to everyone. + * + * TODO: Remove this when RoomListStore gets fully replaced. + */ +export class RoomListStoreTempProxy { + public static isUsingNewStore(): boolean { + return SettingsStore.isFeatureEnabled("feature_new_room_list"); + } + + public static addListener(handler: () => void) { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.addListener(handler); + } else { + return OldRoomListStore.addListener(handler); + } + } + + public static getRoomLists(): {[tagId in TagID]: Room[]} { + if (RoomListStoreTempProxy.isUsingNewStore()) { + return RoomListStore.instance.orderedLists; + } else { + return OldRoomListStore.getRoomLists(); + } + } +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..4fe5125a15 --- /dev/null +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -0,0 +1,100 @@ +/* +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 { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { DefaultTagID } from "../models"; + +/** + * A demonstration/temporary algorithm to verify the API surface works. + * TODO: Remove this before shipping + */ +export class ChaoticAlgorithm implements IAlgorithm { + + private cached: ITagMap = {}; + private sortAlgorithms: ITagSortingMap; + private rooms: Room[] = []; + + constructor(private representativeAlgorithm: ListAlgorithm) { + } + + getOrderedRooms(): ITagMap { + return this.cached; + } + + async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + this.setKnownRooms(this.rooms); // regenerate the room lists + } + + handleRoomUpdate(room): Promise { + return undefined; + } + + setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + this.cached = newTags; + return; + } + + // TODO: Remove logging + console.log('setting known rooms - regen in progress'); + console.log({alg: this.representativeAlgorithm}); + + // Step through each room and determine which tags it should be in. + // We don't care about ordering or sorting here - we're simply organizing things. + for (const room of rooms) { + const tags = room.tags; + let inTag = false; + for (const tagId in tags) { + // noinspection JSUnfilteredForInLoop + if (isNullOrUndefined(newTags[tagId])) { + // skip the tag if we don't know about it + continue; + } + + inTag = true; + + // noinspection JSUnfilteredForInLoop + newTags[tagId].push(room); + } + + // If the room wasn't pushed to a tag, push it to the untagged tag. + if (!inTag) { + newTags[DefaultTagID.Untagged].push(room); + } + } + + // TODO: Do sorting + + // Finally, assign the tags to our cache + this.cached = newTags; + } +} diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts new file mode 100644 index 0000000000..fbe2f7a27d --- /dev/null +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -0,0 +1,95 @@ +/* +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 { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export enum Category { + Red = "RED", + Grey = "GREY", + Bold = "BOLD", + Idle = "IDLE", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} + +// TODO: Convert IAlgorithm to an abstract class? +// TODO: Add locking support to avoid concurrent writes +// TODO: EventEmitter support + +/** + * Represents an algorithm for the RoomListStore to use + */ +export interface IAlgorithm { + /** + * Asks the Algorithm to regenerate all lists, using the tags given + * as reference for which lists to generate and which way to generate + * them. + * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @returns {Promise<*>} A promise which resolves when complete. + */ + populateTags(tagSortingMap: ITagSortingMap): Promise; + + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + getOrderedRooms(): ITagMap; + + /** + * Seeds the Algorithm with a set of rooms. The algorithm will discard all + * previously known information and instead use these rooms instead. + * @param {Room[]} rooms The rooms to force the algorithm to use. + * @returns {Promise<*>} A promise which resolves when complete. + */ + setKnownRooms(rooms: Room[]): Promise; + + /** + * Asks the Algorithm to update its knowledge of a room. For example, when + * a user tags a room, joins/creates a room, or leaves a room the Algorithm + * should be told that the room's info might have changed. The Algorithm + * may no-op this request if no changes are required. + * @param {Room} room The room which might have affected sorting. + * @returns {Promise} A promise which resolve to true or false + * depending on whether or not getOrderedRooms() should be called after + * processing. + */ + handleRoomUpdate(room: Room): Promise; +} diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts new file mode 100644 index 0000000000..cb67d42187 --- /dev/null +++ b/src/stores/room-list/algorithms/index.ts @@ -0,0 +1,36 @@ +/* +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 { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; +import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; + +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { + [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), + [ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance), +}; + +/** + * Gets an instance of the defined algorithm + * @param {ListAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm { + if (!ALGORITHM_FACTORIES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_FACTORIES[algorithm](); +} diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts new file mode 100644 index 0000000000..d1c915e035 --- /dev/null +++ b/src/stores/room-list/models.ts @@ -0,0 +1,36 @@ +/* +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 DefaultTagID { + Invite = "im.vector.fake.invite", + Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms + Archived = "im.vector.fake.archived", + LowPriority = "m.lowpriority", + Favourite = "m.favourite", + DM = "im.vector.fake.direct", + ServerNotice = "m.server_notice", +} +export const OrderedDefaultTagIDs = [ + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; + +export type TagID = string | DefaultTagID; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 8dc4647920..7bcd2a8ae3 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -14,7 +14,7 @@ import DMRoomMap from '../../../../src/utils/DMRoomMap.js'; import GroupStore from '../../../../src/stores/GroupStore.js'; import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk'; -import {TAG_DM} from "../../../../src/stores/RoomListStore"; +import {DefaultTagID} from "../../../../src/stores/room-list/models"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; @@ -153,7 +153,7 @@ describe('RoomList', () => { // Set up the room that will be moved such that it has the correct state for a room in // the section for oldTag if (['m.favourite', 'm.lowpriority'].includes(oldTag)) movingRoom.tags = {[oldTag]: {}}; - if (oldTag === TAG_DM) { + if (oldTag === DefaultTagID.DM) { // Mock inverse m.direct DMRoomMap.shared().roomToUser = { [movingRoom.roomId]: '@someotheruser:domain', @@ -180,7 +180,7 @@ describe('RoomList', () => { // TODO: Re-enable dragging tests when we support dragging again. describe.skip('does correct optimistic update when dragging from', () => { it('rooms to people', () => { - expectCorrectMove(undefined, TAG_DM); + expectCorrectMove(undefined, DefaultTagID.DM); }); it('rooms to favourites', () => { @@ -195,15 +195,15 @@ describe('RoomList', () => { // Whe running the app live, it updates when some other event occurs (likely the // m.direct arriving) that these tests do not fire. xit('people to rooms', () => { - expectCorrectMove(TAG_DM, undefined); + expectCorrectMove(DefaultTagID.DM, undefined); }); it('people to favourites', () => { - expectCorrectMove(TAG_DM, 'm.favourite'); + expectCorrectMove(DefaultTagID.DM, 'm.favourite'); }); it('people to lowpriority', () => { - expectCorrectMove(TAG_DM, 'm.lowpriority'); + expectCorrectMove(DefaultTagID.DM, 'm.lowpriority'); }); it('low priority to rooms', () => { @@ -211,7 +211,7 @@ describe('RoomList', () => { }); it('low priority to people', () => { - expectCorrectMove('m.lowpriority', TAG_DM); + expectCorrectMove('m.lowpriority', DefaultTagID.DM); }); it('low priority to low priority', () => { @@ -223,7 +223,7 @@ describe('RoomList', () => { }); it('favourites to people', () => { - expectCorrectMove('m.favourite', TAG_DM); + expectCorrectMove('m.favourite', DefaultTagID.DM); }); it('favourites to low priority', () => { diff --git a/tsconfig.json b/tsconfig.json index b87f640734..8a01ca335e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "jsx": "react", "types": [ "node", - "react" + "react", + "flux" ] }, "include": [ diff --git a/yarn.lock b/yarn.lock index b0d3816dc4..6375c745fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1218,6 +1218,19 @@ resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== +"@types/fbemitter@*": + version "2.0.32" + resolved "https://registry.yarnpkg.com/@types/fbemitter/-/fbemitter-2.0.32.tgz#8ed204da0f54e9c8eaec31b1eec91e25132d082c" + integrity sha1-jtIE2g9U6cjq7DGx7skeJRMtCCw= + +"@types/flux@^3.1.9": + version "3.1.9" + resolved "https://registry.yarnpkg.com/@types/flux/-/flux-3.1.9.tgz#ddfc9641ee2e2e6cb6cd730c6a48ef82e2076711" + integrity sha512-bSbDf4tTuN9wn3LTGPnH9wnSSLtR5rV7UPWFpM00NJ1pSTBwCzeZG07XsZ9lBkxwngrqjDtM97PLt5IuIdCQUA== + dependencies: + "@types/fbemitter" "*" + "@types/react" "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" From 861268d39f4dd612f1ad2e02a7f1097d1f590be4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 27 Apr 2020 15:25:04 -0600 Subject: [PATCH 03/34] Invent an AsyncStore and use it for room lists This is to get around the problem of a slow dispatch loop. Instead of slowing the whole app down to deal with room lists, we'll just raise events to say we're ready. Based upon the EventEmitter class. --- package.json | 1 + src/components/views/rooms/RoomList2.tsx | 6 +- src/stores/AsyncStore.ts | 105 ++++++++++++++++++ src/stores/room-list/RoomListStore2.ts | 55 +++++---- .../room-list/RoomListStoreTempProxy.ts | 6 +- .../room-list/algorithms/ChaoticAlgorithm.ts | 1 - yarn.lock | 5 + 7 files changed, 144 insertions(+), 35 deletions(-) create mode 100644 src/stores/AsyncStore.ts diff --git a/package.json b/package.json index 22ff071ba7..2dfb366972 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@babel/runtime": "^7.8.3", + "await-lock": "^2.0.1", "blueimp-canvas-to-blob": "^3.5.0", "browser-encrypt-attachment": "^0.3.0", "browser-request": "^0.3.3", diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 1790fa8cf6..f97f599ae3 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -21,7 +21,7 @@ import { _t } from "../../../languageHandler"; import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; -import RoomListStore from "../../../stores/room-list/RoomListStore2"; +import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -56,8 +56,8 @@ export default class RoomList2 extends React.Component { } public componentDidMount(): void { - RoomListStore.instance.addListener(() => { - console.log(RoomListStore.instance.orderedLists); + RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { + console.log("new lists", store.orderedLists); }); } diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts new file mode 100644 index 0000000000..5e19e17248 --- /dev/null +++ b/src/stores/AsyncStore.ts @@ -0,0 +1,105 @@ +/* +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 AwaitLock from 'await-lock'; +import { ActionPayload } from "../dispatcher-types"; +import { Dispatcher } from "flux"; + +/** + * The event/channel to listen for in an AsyncStore. + */ +export const UPDATE_EVENT = "update"; + +/** + * Represents a minimal store which works similar to Flux stores. Instead + * of everything needing to happen in a dispatch cycle, everything can + * happen async to that cycle. + * + * The store's core principle is Object.assign(), therefore it is recommended + * to break out your state to be as safe as possible. The state mutations are + * also locked, preventing concurrent writes. + * + * All updates to the store happen on the UPDATE_EVENT event channel with the + * one argument being the instance of the store. + * + * To update the state, use updateState() and preferably await the result to + * help prevent lock conflicts. + */ +export abstract class AsyncStore extends EventEmitter { + private storeState: T = {}; + private lock = new AwaitLock(); + private readonly dispatcherRef: string; + + /** + * Creates a new AsyncStore using the given dispatcher. + * @param {Dispatcher} dispatcher The dispatcher to rely upon. + */ + protected constructor(private dispatcher: Dispatcher) { + super(); + + this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this)); + } + + /** + * The current state of the store. Cannot be mutated. + */ + protected get state(): T { + return Object.freeze(this.storeState); + } + + /** + * Stops the store's listening functions, such as the listener to the dispatcher. + */ + protected stop() { + if (this.dispatcherRef) this.dispatcher.unregister(this.dispatcherRef); + } + + /** + * Updates the state of the store. + * @param {T|*} newState The state to update in the store using Object.assign() + */ + protected async updateState(newState: T | Object) { + await this.lock.acquireAsync(); + try { + this.storeState = Object.assign({}, this.storeState, newState); + this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Resets the store's to the provided state or an empty object. + * @param {T|*} newState The new state of the store. + * @param {boolean} quiet If true, the function will not raise an UPDATE_EVENT. + */ + protected async reset(newState: T | Object = null, quiet = false) { + await this.lock.acquireAsync(); + try { + this.storeState = (newState || {}); + if (!quiet) this.emit(UPDATE_EVENT, this); + } finally { + await this.lock.release(); + } + } + + /** + * Called when the dispatcher broadcasts a dispatch event. + * @param {ActionPayload} payload The event being dispatched. + */ + protected abstract onDispatch(payload: ActionPayload); +} diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 70d6d4a598..7fab8c7ff9 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -15,15 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Store} from 'flux/utils'; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; -import { OrderedDefaultTagIDs, DefaultTagID, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; import TagOrderStore from "../TagOrderStore"; import { getAlgorithmInstance } from "./algorithms"; +import { AsyncStore } from "../AsyncStore"; interface IState { tagsEnabled?: boolean; @@ -32,8 +31,13 @@ interface IState { preferredAlgorithm?: ListAlgorithm; } -class _RoomListStore extends Store { - private state: IState = {}; +/** + * The event/channel which is called when the room lists have been changed. Raised + * with one argument: the instance of the store. + */ +export const LISTS_UPDATE_EVENT = "lists_update"; + +class _RoomListStore extends AsyncStore { private matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; @@ -49,7 +53,6 @@ class _RoomListStore extends Store { super(defaultDispatcher); this.checkEnabled(); - this.reset(); for (const settingName of this.watchedSettings) SettingsStore.monitorSetting(settingName, null); } @@ -66,17 +69,11 @@ class _RoomListStore extends Store { } } - private reset(): void { - // We don't call setState() because it'll cause changes to emitted which could - // crash the app during logout/signin/etc. - this.state = {}; - } - - private readAndCacheSettingsFromStore() { + private async readAndCacheSettingsFromStore() { const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); const orderByImportance = SettingsStore.getValue("RoomList.orderByImportance"); const orderAlphabetically = SettingsStore.getValue("RoomList.orderAlphabetically"); - this.setState({ + await this.updateState({ tagsEnabled, preferredSort: orderAlphabetically ? SortAlgorithm.Alphabetic : SortAlgorithm.Recent, preferredAlgorithm: orderByImportance ? ListAlgorithm.Importance : ListAlgorithm.Natural, @@ -84,23 +81,23 @@ class _RoomListStore extends Store { this.setAlgorithmClass(); } - protected __onDispatch(payload: ActionPayload): void { + protected async onDispatch(payload: ActionPayload) { if (payload.action === 'MatrixActions.sync') { // Filter out anything that isn't the first PREPARED sync. if (!(payload.prevState === 'PREPARED' && payload.state !== 'PREPARED')) { return; } + // TODO: Remove this once the RoomListStore becomes default this.checkEnabled(); if (!this.enabled) return; this.matrixClient = payload.matrixClient; // Update any settings here, as some may have happened before we were logically ready. - this.readAndCacheSettingsFromStore(); - - // noinspection JSIgnoredPromiseFromCall - this.regenerateAllLists(); + console.log("Regenerating room lists: Startup"); + await this.readAndCacheSettingsFromStore(); + await this.regenerateAllLists(); } // TODO: Remove this once the RoomListStore becomes default @@ -109,7 +106,7 @@ class _RoomListStore extends Store { if (payload.action === 'on_client_not_viable' || payload.action === 'on_logged_out') { // Reset state without causing updates as the client will have been destroyed // and downstream code will throw NPE errors. - this.reset(); + this.reset(null, true); this.matrixClient = null; this.initialListsGenerated = false; // we'll want to regenerate them } @@ -120,14 +117,15 @@ class _RoomListStore extends Store { if (payload.action === 'setting_updated') { if (this.watchedSettings.includes(payload.settingName)) { - this.readAndCacheSettingsFromStore(); + console.log("Regenerating room lists: Settings changed"); + await this.readAndCacheSettingsFromStore(); - // noinspection JSIgnoredPromiseFromCall - this.regenerateAllLists(); // regenerate the lists now + await this.regenerateAllLists(); // regenerate the lists now } } else if (payload.action === 'MatrixActions.Room.receipt') { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). + // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle const myUserId = this.matrixClient.getUserId(); for (const eventId of Object.keys(payload.event.getContent())) { const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); @@ -167,11 +165,10 @@ class _RoomListStore extends Store { } } - private setState(newState: IState) { + protected async updateState(newState: IState) { if (!this.enabled) return; - this.state = Object.assign(this.state, newState); - this.__emitChange(); + await super.updateState(newState); } private setAlgorithmClass() { @@ -179,7 +176,7 @@ class _RoomListStore extends Store { } private async regenerateAllLists() { - console.log("REGEN"); + console.warn("Regenerating all room lists"); const tags: ITagSortingMap = {}; for (const tagId of OrderedDefaultTagIDs) { tags[tagId] = this.getSortAlgorithmFor(tagId); @@ -196,7 +193,7 @@ class _RoomListStore extends Store { this.initialListsGenerated = true; - // TODO: How do we asynchronously update the store's state? or do we just give in and make it all sync? + this.emit(LISTS_UPDATE_EVENT, this); } } diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 7b12602541..88171c0809 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,6 +19,8 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; +import { ITagMap } from "./algorithms/IAlgorithm"; +import { UPDATE_EVENT } from "../AsyncStore"; /** * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when @@ -33,13 +35,13 @@ export class RoomListStoreTempProxy { public static addListener(handler: () => void) { if (RoomListStoreTempProxy.isUsingNewStore()) { - return RoomListStore.instance.addListener(handler); + return RoomListStore.instance.on(UPDATE_EVENT, handler); } else { return OldRoomListStore.addListener(handler); } } - public static getRoomLists(): {[tagId in TagID]: Room[]} { + public static getRoomLists(): ITagMap { if (RoomListStoreTempProxy.isUsingNewStore()) { return RoomListStore.instance.orderedLists; } else { diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 4fe5125a15..f72adb3aa8 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -65,7 +65,6 @@ export class ChaoticAlgorithm implements IAlgorithm { } // TODO: Remove logging - console.log('setting known rooms - regen in progress'); console.log({alg: this.representativeAlgorithm}); // Step through each room and determine which tags it should be in. diff --git a/yarn.lock b/yarn.lock index 6375c745fc..80c7e581a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1834,6 +1834,11 @@ autoprefixer@^9.0.0: postcss "^7.0.27" postcss-value-parser "^4.0.3" +await-lock@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.0.1.tgz#b3f65fdf66e08f7538260f79b46c15bcfc18cadd" + integrity sha512-ntLi9fzlMT/vWjC1wwVI11/cSRJ3nTS35qVekNc9WnaoMOP2eWH0RvIqwLQkDjX4a4YynsKEv+Ere2VONp9wxg== + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" From becaddeb80c54014473604b73d3f99d452a57916 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 14:12:58 -0600 Subject: [PATCH 04/34] Categorize rooms by effective membership --- .../room-list/algorithms/ChaoticAlgorithm.ts | 4 +- src/stores/room-list/membership.ts | 73 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 src/stores/room-list/membership.ts diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index f72adb3aa8..9dfe6f6205 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -18,6 +18,7 @@ import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { DefaultTagID } from "../models"; +import { splitRoomsByMembership } from "../membership"; /** * A demonstration/temporary algorithm to verify the API surface works. @@ -65,7 +66,8 @@ export class ChaoticAlgorithm implements IAlgorithm { } // TODO: Remove logging - console.log({alg: this.representativeAlgorithm}); + const memberships = splitRoomsByMembership(rooms); + console.log({alg: this.representativeAlgorithm, memberships}); // Step through each room and determine which tags it should be in. // We don't care about ordering or sorting here - we're simply organizing things. diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts new file mode 100644 index 0000000000..884e2a4a04 --- /dev/null +++ b/src/stores/room-list/membership.ts @@ -0,0 +1,73 @@ +/* +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 {Room} from "matrix-js-sdk/src/models/room"; +import {Event} from "matrix-js-sdk/src/models/event"; + +/** + * Approximation of a membership status for a given room. + */ +export enum EffectiveMembership { + /** + * The user is effectively joined to the room. For example, actually joined + * or knocking on the room (when that becomes possible). + */ + Join = "JOIN", + + /** + * The user is effectively invited to the room. Currently this is a direct map + * to the invite membership as no other membership states are effectively + * invites. + */ + Invite = "INVITE", + + /** + * The user is effectively no longer in the room. For example, kicked, + * banned, or voluntarily left. + */ + Leave = "LEAVE", +} + +export interface MembershipSplit { + // @ts-ignore - TS wants this to be a string key, but we know better. + [state: EffectiveMembership]: Room[]; +} + +export function splitRoomsByMembership(rooms: Room[]): MembershipSplit { + const split: MembershipSplit = { + [EffectiveMembership.Invite]: [], + [EffectiveMembership.Join]: [], + [EffectiveMembership.Leave]: [], + }; + + for (const room of rooms) { + split[getEffectiveMembership(room.getMyMembership())].push(room); + } + + return split; +} + +export function getEffectiveMembership(membership: string): EffectiveMembership { + if (membership === 'invite') { + return EffectiveMembership.Invite; + } else if (membership === 'join') { + // TODO: Do the same for knock? Update docs as needed in the enum. + return EffectiveMembership.Join; + } else { + // Probably a leave, kick, or ban + return EffectiveMembership.Leave; + } +} From 00d400b516ef35c2121132ecdee657355783d3a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 20:36:42 -0600 Subject: [PATCH 05/34] Possible framework for a proof of concept This is the fruits of about 3 attempts to write code that works. None of those attempts are here, but how edition 4 could work is at least documented now. --- src/stores/room-list/README.md | 117 +++++++++++ src/stores/room-list/TagManager.ts | 32 +++ .../room-list/algorithms/ChaoticAlgorithm.ts | 1 + src/stores/room-list/algorithms/IAlgorithm.ts | 9 +- .../algorithms/ImportanceAlgorithm.ts | 189 ++++++++++++++++++ src/stores/room-list/algorithms/index.ts | 3 +- 6 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 src/stores/room-list/README.md create mode 100644 src/stores/room-list/TagManager.ts create mode 100644 src/stores/room-list/algorithms/ImportanceAlgorithm.ts diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md new file mode 100644 index 0000000000..ed13210420 --- /dev/null +++ b/src/stores/room-list/README.md @@ -0,0 +1,117 @@ +# Room list sorting + +It's so complicated it needs its own README. + +## Algorithms involved + +There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. +Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting +Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting +algorithm determines how individual tags (lists of rooms, sometimes called sublists) are ordered. + +Behaviour of the room list takes the shape of default sorting on tags in most cases, though it can +override what is happening at the tag level depending on the algorithm used (as is the case with the +importance algorithm, described later). + +Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm +the power to decide when and how to apply the tag sorting, if at all. + +### Tag sorting algorithm: Alphabetical + +When used, rooms in a given tag will be sorted alphabetically, where the alphabet is determined by a +simple string comparison operation (essentially giving the browser the problem of figuring out if A +comes before Z). + +### Tag sorting algorithm: Manual + +Manual sorting makes use of the `order` property present on all tags for a room, per the +[Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values +of `order` cause rooms to appear closer to the top of the list. + +### Tag sorting algorithm: Recent + +Rooms are ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +in the room list system which determines whether an event type is capable of bubbling up in the room list. +Normally events like room messages, stickers, and room security changes will be considered useful enough +to cause a shift in time. + +Note that this is reliant on the event timestamps of the most recent message. Because Matrix is eventually +consistent this means that from time to time a room might plummet or skyrocket across the tag due to the +timestamp contained within the event (generated server-side by the sender's server). + +### List ordering algorithm: Natural + +This is the easiest of the algorithms to understand because it does essentially nothing. It imposes no +behavioural changes over the tag sorting algorithm and is by far the simplest way to order a room list. +Historically, it's been the only option in Riot and extremely common in most chat applications due to +its relative deterministic behaviour. + +### List ordering algorithm: Importance + +On the other end of the spectrum, this is the most complicated algorithm which exists. There's major +behavioural changes and the tag sorting algorithm is selectively applied depending on circumstances. + +Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags +simply get the manual sorting algorithm applied to them with no further involvement from the importance +algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off +relative (perceived) importance to the user: + +* **Red**: The room has unread mentions waiting for the user. +* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread + messages which cause a push notification or badge count. Typically this is the default as rooms are + set to 'All Messages'. +* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without + a badge/notification count (or 'Mentions Only'/'Muted'). +* **Idle**: No relevant activity has occurred in the room since the user last read it. + +Conveniently, each tag is ordered by those categories as presented: red rooms appear above grey, grey +above idle, etc. + +Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm +is applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other and the grey rooms sorted amongst each other, but +collectively the tag will be sorted into categories with red being at the top. + +The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. +The sticky room will remain in position on the room list regardless of other factors going on as typically +clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms +above the selected room at all times where N is the number of rooms above the selected rooms when it was +selected. + +For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one +room above their selection at all times. If they receive another notification and the tag ordering is set +to Recent, they'll see the new notification go to the top position and the one that was previously there +fall behind the sticky room. + +The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the +tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another +room, the previous sticky room is recalculated to determine which category it needs to be in as the user +could have been scrolled up while new messages were received. + +Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what +kinds of rooms move around their selection. An example would be the user having 4 red rooms, the user +selecting the third room (leaving 2 above it), and then having the rooms above it read on another device. +This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain +2 rooms above the sticky room. + +An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement +exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain +the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +put the sticky room in a position where it's had to decrease N will not increase N. + +## Responsibilities of the store + +The store is responsible for the ordering, upkeep, and tracking of all rooms. The component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +all kinds of filtering. + +## Class breakdowns + +The `RoomListStore` is the major coordinator of various `IAlgorithm` implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. A `TagManager` is responsible for figuring +out which tags get which rooms, as Matrix specifies them as a reverse map: tags are defined on rooms and +are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific +utilities are also included, though they are expected to move somewhere more general when needed. For +example, the `membership` utilities could easily be moved elsewhere as needed. diff --git a/src/stores/room-list/TagManager.ts b/src/stores/room-list/TagManager.ts new file mode 100644 index 0000000000..368c4e2c21 --- /dev/null +++ b/src/stores/room-list/TagManager.ts @@ -0,0 +1,32 @@ +/* +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"; + +// TODO: Docs on what this is +export class TagManager extends EventEmitter { + constructor() { + super(); + } + + // TODO: Implementation. + // This will need to track where rooms belong in tags, and which tags they + // should be tracked within. This is algorithm independent because all the + // algorithms need to know what each tag contains. + // + // This will likely make use of the effective membership to determine the + // invite+historical sections. +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 9dfe6f6205..1b640669c0 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -31,6 +31,7 @@ export class ChaoticAlgorithm implements IAlgorithm { private rooms: Room[] = []; constructor(private representativeAlgorithm: ListAlgorithm) { + console.log("Constructed a ChaoticAlgorithm"); } getOrderedRooms(): ITagMap { diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts index fbe2f7a27d..ab5c4742df 100644 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -32,13 +32,6 @@ export enum ListAlgorithm { Natural = "NATURAL", } -export enum Category { - Red = "RED", - Grey = "GREY", - Bold = "BOLD", - Idle = "IDLE", -} - export interface ITagSortingMap { // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. [tagId: TagID]: SortAlgorithm; @@ -50,7 +43,7 @@ export interface ITagMap { } // TODO: Convert IAlgorithm to an abstract class? -// TODO: Add locking support to avoid concurrent writes +// TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support /** diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts new file mode 100644 index 0000000000..0a2184eb43 --- /dev/null +++ b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts @@ -0,0 +1,189 @@ +/* +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 { IAlgorithm, ITagMap, ITagSortingMap } from "./IAlgorithm"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { DefaultTagID, TagID } from "../models"; +import { splitRoomsByMembership } from "../membership"; + +/** + * The determined category of a room. + */ +export enum Category { + /** + * The room has unread mentions within. + */ + Red = "RED", + /** + * The room has unread notifications within. Note that these are not unread + * mentions - they are simply messages which the user has asked to cause a + * badge count update or push notification. + */ + Grey = "GREY", + /** + * The room has unread messages within (grey without the badge). + */ + Bold = "BOLD", + /** + * The room has no relevant unread messages within. + */ + Idle = "IDLE", +} + +/** + * An implementation of the "importance" algorithm for room list sorting. Where + * the tag sorting algorithm does not interfere, rooms will be ordered into + * categories of varying importance to the user. Alphabetical sorting does not + * interfere with this algorithm, however manual ordering does. + * + * The importance of a room is defined by the kind of notifications, if any, are + * present on the room. These are classified internally as Red, Grey, Bold, and + * Idle. Red rooms have mentions, grey have unread messages, bold is a less noisy + * version of grey, and idle means all activity has been seen by the user. + * + * The algorithm works by monitoring all room changes, including new messages in + * tracked rooms, to determine if it needs a new category or different placement + * within the same category. For more information, see the comments contained + * within the class. + */ +export class ImportanceAlgorithm implements IAlgorithm { + + // HOW THIS WORKS + // -------------- + // + // This block of comments assumes you've read the README one level higher. + // You should do that if you haven't already. + // + // Tags are fed into the algorithmic functions from the TagManager changes, + // which cause subsequent updates to the room list itself. Categories within + // those tags are tracked as index numbers within the array (zero = top), with + // each sticky room being tracked separately. Internally, the category index + // can be found from `this.indices[tag][category]` and the sticky room information + // from `this.stickyRooms[tag]`. + // + // Room categories are constantly re-evaluated and tracked in the `this.categorized` + // object. Note that this doesn't track rooms by category but instead by room ID. + // The theory is that by knowing the previous position, new desired position, and + // category indices we can avoid tracking multiple complicated maps in memory. + // + // The room list store is always provided with the `this.cached` results, which are + // updated as needed and not recalculated often. For example, when a room needs to + // move within a tag, the array in `this.cached` will be spliced instead of iterated. + + private cached: ITagMap = {}; + private sortAlgorithms: ITagSortingMap; + private rooms: Room[] = []; + private indices: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + // @ts-ignore - TS wants this to be a string but we know better than it + [category: Category]: number; // integer + }; + } = {}; + private stickyRooms: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + room?: Room; + nAbove: number; // integer + }; + } = {}; + private categorized: { + // @ts-ignore - TS wants this to be a string but we know better than it + [tag: TagID]: { + // TODO: Remove note + // Note: Should in theory be able to only track this by room ID as we'll know + // the indices of each category and can determine if a category needs changing + // in the cached list. Could potentially save a bunch of time if we can figure + // out where a room is supposed to be using offsets, some math, and leaving the + // array generally alone. + [roomId: string]: { + room: Room; + category: Category; + }; + }; + } = {}; + + constructor() { + console.log("Constructed an ImportanceAlgorithm"); + } + + getOrderedRooms(): ITagMap { + return this.cached; + } + + async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + this.setKnownRooms(this.rooms); // regenerate the room lists + } + + handleRoomUpdate(room): Promise { + return undefined; + } + + setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + this.cached = newTags; + return; + } + + // TODO: Remove logging + const memberships = splitRoomsByMembership(rooms); + console.log({memberships}); + + // Step through each room and determine which tags it should be in. + // We don't care about ordering or sorting here - we're simply organizing things. + for (const room of rooms) { + const tags = room.tags; + let inTag = false; + for (const tagId in tags) { + // noinspection JSUnfilteredForInLoop + if (isNullOrUndefined(newTags[tagId])) { + // skip the tag if we don't know about it + continue; + } + + inTag = true; + + // noinspection JSUnfilteredForInLoop + newTags[tagId].push(room); + } + + // If the room wasn't pushed to a tag, push it to the untagged tag. + if (!inTag) { + newTags[DefaultTagID.Untagged].push(room); + } + } + + // TODO: Do sorting + + // Finally, assign the tags to our cache + this.cached = newTags; + } +} diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts index cb67d42187..918b176f48 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/index.ts @@ -16,10 +16,11 @@ limitations under the License. import { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; +import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), - [ListAlgorithm.Importance]: () => new ChaoticAlgorithm(ListAlgorithm.Importance), + [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; /** From 9c0422691a551a6a92533bf6e34684a09b95568a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 28 Apr 2020 20:44:18 -0600 Subject: [PATCH 06/34] Add another thought Maybe we can speed up the algorithm if we know why we're doing the update. --- src/stores/room-list/algorithms/IAlgorithm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts index ab5c4742df..b49f0b10b2 100644 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ b/src/stores/room-list/algorithms/IAlgorithm.ts @@ -84,5 +84,5 @@ export interface IAlgorithm { * depending on whether or not getOrderedRooms() should be called after * processing. */ - handleRoomUpdate(room: Room): Promise; + handleRoomUpdate(room: Room): Promise; // TODO: Take a ReasonForChange to better predict the behaviour? } From e7fffee17568fba9b89bbe98bc0b9382e111ba1b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Apr 2020 16:19:10 -0600 Subject: [PATCH 07/34] Remove the need for a tag manager Instead putting the tag handling in the Algorithm class --- src/stores/room-list/README.md | 13 +- src/stores/room-list/RoomListStore2.ts | 4 +- .../room-list/RoomListStoreTempProxy.ts | 2 +- src/stores/room-list/TagManager.ts | 32 ---- src/stores/room-list/algorithms/Algorithm.ts | 178 ++++++++++++++++++ .../room-list/algorithms/ChaoticAlgorithm.ts | 80 +------- src/stores/room-list/algorithms/IAlgorithm.ts | 88 --------- .../algorithms/ImportanceAlgorithm.ts | 76 +------- src/stores/room-list/algorithms/index.ts | 8 +- 9 files changed, 212 insertions(+), 269 deletions(-) delete mode 100644 src/stores/room-list/TagManager.ts create mode 100644 src/stores/room-list/algorithms/Algorithm.ts delete mode 100644 src/stores/room-list/algorithms/IAlgorithm.ts diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index ed13210420..0dd6c104d8 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -109,9 +109,10 @@ all kinds of filtering. ## Class breakdowns -The `RoomListStore` is the major coordinator of various `IAlgorithm` implementations, which take care -of the various `ListAlgorithm` and `SortingAlgorithm` options. A `TagManager` is responsible for figuring -out which tags get which rooms, as Matrix specifies them as a reverse map: tags are defined on rooms and -are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific -utilities are also included, though they are expected to move somewhere more general when needed. For -example, the `membership` utilities could easily be moved elsewhere as needed. +The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also +responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: +tags are defined on rooms and are not defined as a collection of rooms (unlike how they are presented +to the user). Various list-specific utilities are also included, though they are expected to move +somewhere more general when needed. For example, the `membership` utilities could easily be moved +elsewhere as needed. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 7fab8c7ff9..dc1cb49cd6 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -19,7 +19,7 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; -import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/IAlgorithm"; +import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { getAlgorithmInstance } from "./algorithms"; import { AsyncStore } from "../AsyncStore"; @@ -41,7 +41,7 @@ class _RoomListStore extends AsyncStore { private matrixClient: MatrixClient; private initialListsGenerated = false; private enabled = false; - private algorithm: IAlgorithm; + private algorithm: Algorithm; private readonly watchedSettings = [ 'RoomList.orderAlphabetically', diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 88171c0809..8ad3c5d35e 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/IAlgorithm"; +import { ITagMap } from "./algorithms/Algorithm"; import { UPDATE_EVENT } from "../AsyncStore"; /** diff --git a/src/stores/room-list/TagManager.ts b/src/stores/room-list/TagManager.ts deleted file mode 100644 index 368c4e2c21..0000000000 --- a/src/stores/room-list/TagManager.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import {EventEmitter} from "events"; - -// TODO: Docs on what this is -export class TagManager extends EventEmitter { - constructor() { - super(); - } - - // TODO: Implementation. - // This will need to track where rooms belong in tags, and which tags they - // should be tracked within. This is algorithm independent because all the - // algorithms need to know what each tag contains. - // - // This will likely make use of the effective membership to determine the - // invite+historical sections. -} diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts new file mode 100644 index 0000000000..15fc208b21 --- /dev/null +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -0,0 +1,178 @@ +/* +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 { DefaultTagID, TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { EffectiveMembership, splitRoomsByMembership } from "../membership"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} + +// TODO: Add locking support to avoid concurrent writes? +// TODO: EventEmitter support? Might not be needed. + +export abstract class Algorithm { + protected cached: ITagMap = {}; + protected sortAlgorithms: ITagSortingMap; + protected rooms: Room[] = []; + protected roomsByTag: { + // @ts-ignore - TS wants this to be a string but we know better + [tagId: TagID]: Room[]; + } = {}; + + protected constructor() { + } + + /** + * Asks the Algorithm to regenerate all lists, using the tags given + * as reference for which lists to generate and which way to generate + * them. + * @param {ITagSortingMap} tagSortingMap The tags to generate. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async populateTags(tagSortingMap: ITagSortingMap): Promise { + if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); + this.sortAlgorithms = tagSortingMap; + return this.setKnownRooms(this.rooms); + } + + /** + * Gets an ordered set of rooms for the all known tags. + * @returns {ITagMap} The cached list of rooms, ordered, + * for each tag. May be empty, but never null/undefined. + */ + public getOrderedRooms(): ITagMap { + return this.cached; + } + + /** + * Seeds the Algorithm with a set of rooms. The algorithm will discard all + * previously known information and instead use these rooms instead. + * @param {Room[]} rooms The rooms to force the algorithm to use. + * @returns {Promise<*>} A promise which resolves when complete. + */ + public async setKnownRooms(rooms: Room[]): Promise { + if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); + if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); + + this.rooms = rooms; + + const newTags: ITagMap = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } + + // If we can avoid doing work, do so. + if (!rooms.length) { + await this.generateFreshTags(newTags); // just in case it wants to do something + this.cached = newTags; + return; + } + + // Split out the easy rooms first (leave and invite) + const memberships = splitRoomsByMembership(rooms); + for (const room of memberships[EffectiveMembership.Invite]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is an Invite`); + newTags[DefaultTagID.Invite].push(room); + } + for (const room of memberships[EffectiveMembership.Leave]) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Historical`); + newTags[DefaultTagID.Archived].push(room); + } + + // Now process all the joined rooms. This is a bit more complicated + for (const room of memberships[EffectiveMembership.Join]) { + const tags = Object.keys(room.tags || {}); + + let inTag = false; + if (tags.length > 0) { + for (const tag of tags) { + if (!isNullOrUndefined(newTags[tag])) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); + newTags[tag].push(room); + inTag = true; + } + } + } + + if (!inTag) { + // TODO: Determine if DM and push there instead + newTags[DefaultTagID.Untagged].push(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is Untagged`); + } + } + + await this.generateFreshTags(newTags); + + this.cached = newTags; + } + + /** + * Called when the Algorithm believes a complete regeneration of the existing + * lists is needed. + * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag + * will already have the rooms which belong to it - they just need ordering. Must + * be mutated in place. + * @returns {Promise<*>} A promise which resolves when complete. + */ + protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; + + /** + * Called when the Algorithm wants a whole tag to be reordered. Typically this will + * be done whenever the tag's scope changes (added/removed rooms). + * @param {TagID} tagId The tag ID which changed. + * @param {Room[]} rooms The rooms within the tag, unordered. + * @returns {Promise} Resolves to the ordered rooms in the tag. + */ + protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; + + /** + * Asks the Algorithm to update its knowledge of a room. For example, when + * a user tags a room, joins/creates a room, or leaves a room the Algorithm + * should be told that the room's info might have changed. The Algorithm + * may no-op this request if no changes are required. + * @param {Room} room The room which might have affected sorting. + * @returns {Promise} A promise which resolve to true or false + * depending on whether or not getOrderedRooms() should be called after + * processing. + */ + // TODO: Take a ReasonForChange to better predict the behaviour? + // TODO: Intercept here and handle tag changes automatically + public abstract handleRoomUpdate(room: Room): Promise; +} diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts index 1b640669c0..5d4177db8b 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/ChaoticAlgorithm.ts @@ -14,89 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ITagMap, ITagSortingMap, ListAlgorithm } from "./IAlgorithm"; -import { Room } from "matrix-js-sdk/src/models/room"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; +import { Algorithm, ITagMap } from "./Algorithm"; import { DefaultTagID } from "../models"; -import { splitRoomsByMembership } from "../membership"; /** * A demonstration/temporary algorithm to verify the API surface works. * TODO: Remove this before shipping */ -export class ChaoticAlgorithm implements IAlgorithm { +export class ChaoticAlgorithm extends Algorithm { - private cached: ITagMap = {}; - private sortAlgorithms: ITagSortingMap; - private rooms: Room[] = []; - - constructor(private representativeAlgorithm: ListAlgorithm) { + constructor() { + super(); console.log("Constructed a ChaoticAlgorithm"); } - getOrderedRooms(): ITagMap { - return this.cached; + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + return Promise.resolve(); } - async populateTags(tagSortingMap: ITagSortingMap): Promise { - if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); - this.sortAlgorithms = tagSortingMap; - this.setKnownRooms(this.rooms); // regenerate the room lists + protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { + return Promise.resolve(rooms); } - handleRoomUpdate(room): Promise { - return undefined; - } - - setKnownRooms(rooms: Room[]): Promise { - if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); - if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - - this.rooms = rooms; - - const newTags = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } - - // If we can avoid doing work, do so. - if (!rooms.length) { - this.cached = newTags; - return; - } - - // TODO: Remove logging - const memberships = splitRoomsByMembership(rooms); - console.log({alg: this.representativeAlgorithm, memberships}); - - // Step through each room and determine which tags it should be in. - // We don't care about ordering or sorting here - we're simply organizing things. - for (const room of rooms) { - const tags = room.tags; - let inTag = false; - for (const tagId in tags) { - // noinspection JSUnfilteredForInLoop - if (isNullOrUndefined(newTags[tagId])) { - // skip the tag if we don't know about it - continue; - } - - inTag = true; - - // noinspection JSUnfilteredForInLoop - newTags[tagId].push(room); - } - - // If the room wasn't pushed to a tag, push it to the untagged tag. - if (!inTag) { - newTags[DefaultTagID.Untagged].push(room); - } - } - - // TODO: Do sorting - - // Finally, assign the tags to our cache - this.cached = newTags; + public async handleRoomUpdate(room): Promise { + return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/IAlgorithm.ts b/src/stores/room-list/algorithms/IAlgorithm.ts deleted file mode 100644 index b49f0b10b2..0000000000 --- a/src/stores/room-list/algorithms/IAlgorithm.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { TagID } from "../models"; -import { Room } from "matrix-js-sdk/src/models/room"; - - -export enum SortAlgorithm { - Manual = "MANUAL", - Alphabetic = "ALPHABETIC", - Recent = "RECENT", -} - -export enum ListAlgorithm { - // Orders Red > Grey > Bold > Idle - Importance = "IMPORTANCE", - - // Orders however the SortAlgorithm decides - Natural = "NATURAL", -} - -export interface ITagSortingMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: SortAlgorithm; -} - -export interface ITagMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: Room[]; -} - -// TODO: Convert IAlgorithm to an abstract class? -// TODO: Add locking support to avoid concurrent writes? -// TODO: EventEmitter support - -/** - * Represents an algorithm for the RoomListStore to use - */ -export interface IAlgorithm { - /** - * Asks the Algorithm to regenerate all lists, using the tags given - * as reference for which lists to generate and which way to generate - * them. - * @param {ITagSortingMap} tagSortingMap The tags to generate. - * @returns {Promise<*>} A promise which resolves when complete. - */ - populateTags(tagSortingMap: ITagSortingMap): Promise; - - /** - * Gets an ordered set of rooms for the all known tags. - * @returns {ITagMap} The cached list of rooms, ordered, - * for each tag. May be empty, but never null/undefined. - */ - getOrderedRooms(): ITagMap; - - /** - * Seeds the Algorithm with a set of rooms. The algorithm will discard all - * previously known information and instead use these rooms instead. - * @param {Room[]} rooms The rooms to force the algorithm to use. - * @returns {Promise<*>} A promise which resolves when complete. - */ - setKnownRooms(rooms: Room[]): Promise; - - /** - * Asks the Algorithm to update its knowledge of a room. For example, when - * a user tags a room, joins/creates a room, or leaves a room the Algorithm - * should be told that the room's info might have changed. The Algorithm - * may no-op this request if no changes are required. - * @param {Room} room The room which might have affected sorting. - * @returns {Promise} A promise which resolve to true or false - * depending on whether or not getOrderedRooms() should be called after - * processing. - */ - handleRoomUpdate(room: Room): Promise; // TODO: Take a ReasonForChange to better predict the behaviour? -} diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts index 0a2184eb43..1a7a73a9d5 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/ImportanceAlgorithm.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ITagMap, ITagSortingMap } from "./IAlgorithm"; +import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { DefaultTagID, TagID } from "../models"; @@ -60,7 +60,7 @@ export enum Category { * within the same category. For more information, see the comments contained * within the class. */ -export class ImportanceAlgorithm implements IAlgorithm { +export class ImportanceAlgorithm extends Algorithm { // HOW THIS WORKS // -------------- @@ -68,7 +68,7 @@ export class ImportanceAlgorithm implements IAlgorithm { // This block of comments assumes you've read the README one level higher. // You should do that if you haven't already. // - // Tags are fed into the algorithmic functions from the TagManager changes, + // Tags are fed into the algorithmic functions from the Algorithm superclass, // which cause subsequent updates to the room list itself. Categories within // those tags are tracked as index numbers within the array (zero = top), with // each sticky room being tracked separately. Internally, the category index @@ -84,9 +84,6 @@ export class ImportanceAlgorithm implements IAlgorithm { // updated as needed and not recalculated often. For example, when a room needs to // move within a tag, the array in `this.cached` will be spliced instead of iterated. - private cached: ITagMap = {}; - private sortAlgorithms: ITagSortingMap; - private rooms: Room[] = []; private indices: { // @ts-ignore - TS wants this to be a string but we know better than it [tag: TagID]: { @@ -118,72 +115,19 @@ export class ImportanceAlgorithm implements IAlgorithm { } = {}; constructor() { + super(); console.log("Constructed an ImportanceAlgorithm"); } - getOrderedRooms(): ITagMap { - return this.cached; + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + return Promise.resolve(); } - async populateTags(tagSortingMap: ITagSortingMap): Promise { - if (!tagSortingMap) throw new Error(`Map cannot be null or empty`); - this.sortAlgorithms = tagSortingMap; - this.setKnownRooms(this.rooms); // regenerate the room lists + protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { + return Promise.resolve(rooms); } - handleRoomUpdate(room): Promise { - return undefined; - } - - setKnownRooms(rooms: Room[]): Promise { - if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); - if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - - this.rooms = rooms; - - const newTags = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } - - // If we can avoid doing work, do so. - if (!rooms.length) { - this.cached = newTags; - return; - } - - // TODO: Remove logging - const memberships = splitRoomsByMembership(rooms); - console.log({memberships}); - - // Step through each room and determine which tags it should be in. - // We don't care about ordering or sorting here - we're simply organizing things. - for (const room of rooms) { - const tags = room.tags; - let inTag = false; - for (const tagId in tags) { - // noinspection JSUnfilteredForInLoop - if (isNullOrUndefined(newTags[tagId])) { - // skip the tag if we don't know about it - continue; - } - - inTag = true; - - // noinspection JSUnfilteredForInLoop - newTags[tagId].push(room); - } - - // If the room wasn't pushed to a tag, push it to the untagged tag. - if (!inTag) { - newTags[DefaultTagID.Untagged].push(room); - } - } - - // TODO: Do sorting - - // Finally, assign the tags to our cache - this.cached = newTags; + public async handleRoomUpdate(room): Promise { + return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/index.ts index 918b176f48..1277b66ac9 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/index.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { IAlgorithm, ListAlgorithm } from "./IAlgorithm"; +import { Algorithm, ListAlgorithm } from "./Algorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; -const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = { +const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; @@ -26,9 +26,9 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => IAlgorithm } = /** * Gets an instance of the defined algorithm * @param {ListAlgorithm} algorithm The algorithm to get an instance of. - * @returns {IAlgorithm} The algorithm instance. + * @returns {Algorithm} The algorithm instance. */ -export function getAlgorithmInstance(algorithm: ListAlgorithm): IAlgorithm { +export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } From d244eeb5d585f3101b7450d220cbd91cba014b33 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 29 Apr 2020 16:57:06 -0600 Subject: [PATCH 08/34] Break up algorithms and use the new layering Sorting and ordering has now been split apart. The ImportanceAlgorithm also finally makes use of the sorting. So far metrics look okay at 3ms for a simple account, though this could potentially get worse due to the multiple loops involved (one for tags, one for categories, one for ordering). We might be able to feed a whole list of rooms into the thing and have it regenerate the lists on demand. --- src/stores/room-list/RoomListStore2.ts | 7 +- .../room-list/RoomListStoreTempProxy.ts | 2 +- .../{ => list_ordering}/Algorithm.ts | 37 +++------ .../{ => list_ordering}/ChaoticAlgorithm.ts | 5 +- .../ImportanceAlgorithm.ts | 81 +++++++++++++++++-- .../algorithms/{ => list_ordering}/index.ts | 7 +- src/stores/room-list/algorithms/models.ts | 42 ++++++++++ .../tag_sorting/ChaoticAlgorithm.ts | 29 +++++++ .../algorithms/tag_sorting/IAlgorithm.ts | 31 +++++++ .../algorithms/tag_sorting/ManualAlgorithm.ts | 31 +++++++ .../room-list/algorithms/tag_sorting/index.ts | 52 ++++++++++++ 11 files changed, 283 insertions(+), 41 deletions(-) rename src/stores/room-list/algorithms/{ => list_ordering}/Algorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ChaoticAlgorithm.ts (90%) rename src/stores/room-list/algorithms/{ => list_ordering}/ImportanceAlgorithm.ts (63%) rename src/stores/room-list/algorithms/{ => list_ordering}/index.ts (84%) create mode 100644 src/stores/room-list/algorithms/models.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/tag_sorting/index.ts diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index dc1cb49cd6..0b3f61e261 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -19,10 +19,11 @@ import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; -import { Algorithm, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/Algorithm"; +import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; -import { getAlgorithmInstance } from "./algorithms"; +import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; interface IState { tagsEnabled?: boolean; @@ -172,7 +173,7 @@ class _RoomListStore extends AsyncStore { } private setAlgorithmClass() { - this.algorithm = getAlgorithmInstance(this.state.preferredAlgorithm); + this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm); } private async regenerateAllLists() { diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 8ad3c5d35e..4edca2b9cd 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/Algorithm"; +import { ITagMap } from "./algorithms/models"; import { UPDATE_EVENT } from "../AsyncStore"; /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/Algorithm.ts rename to src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 15fc208b21..4c8c9e9c60 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -14,38 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, TagID } from "../models"; +import { DefaultTagID, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { EffectiveMembership, splitRoomsByMembership } from "../membership"; - -export enum SortAlgorithm { - Manual = "MANUAL", - Alphabetic = "ALPHABETIC", - Recent = "RECENT", -} - -export enum ListAlgorithm { - // Orders Red > Grey > Bold > Idle - Importance = "IMPORTANCE", - - // Orders however the SortAlgorithm decides - Natural = "NATURAL", -} - -export interface ITagSortingMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: SortAlgorithm; -} - -export interface ITagMap { - // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. - [tagId: TagID]: Room[]; -} +import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; +import { ITagMap, ITagSortingMap } from "../models"; // TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support? Might not be needed. +/** + * Represents a list ordering algorithm. This class will take care of tag + * management (which rooms go in which tags) and ask the implementation to + * deal with ordering mechanics. + */ export abstract class Algorithm { protected cached: ITagMap = {}; protected sortAlgorithms: ITagSortingMap; @@ -160,6 +142,7 @@ export abstract class Algorithm { * @param {Room[]} rooms The rooms within the tag, unordered. * @returns {Promise} Resolves to the ordered rooms in the tag. */ + // TODO: Do we need this? protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; /** @@ -173,6 +156,6 @@ export abstract class Algorithm { * processing. */ // TODO: Take a ReasonForChange to better predict the behaviour? - // TODO: Intercept here and handle tag changes automatically + // TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that. public abstract handleRoomUpdate(room: Room): Promise; } diff --git a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts similarity index 90% rename from src/stores/room-list/algorithms/ChaoticAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 5d4177db8b..7c1a0b1acc 100644 --- a/src/stores/room-list/algorithms/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -14,8 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap } from "./Algorithm"; -import { DefaultTagID } from "../models"; +import { Algorithm } from "./Algorithm"; +import { DefaultTagID } from "../../models"; +import { ITagMap } from "../models"; /** * A demonstration/temporary algorithm to verify the API surface works. diff --git a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts similarity index 63% rename from src/stores/room-list/algorithms/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 1a7a73a9d5..d73fdee930 100644 --- a/src/stores/room-list/algorithms/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -1,4 +1,5 @@ /* +Copyright 2018, 2019 New Vector Ltd Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ITagMap, ITagSortingMap } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import { DefaultTagID, TagID } from "../models"; -import { splitRoomsByMembership } from "../membership"; +import { DefaultTagID, TagID } from "../../models"; +import { ITagMap, SortAlgorithm } from "../models"; +import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting"; +import * as Unread from '../../../../Unread'; /** * The determined category of a room. @@ -44,6 +46,11 @@ export enum Category { Idle = "IDLE", } +interface ICategorizedRoomMap { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: Room[]; +} + /** * An implementation of the "importance" algorithm for room list sorting. Where * the tag sorting algorithm does not interfere, rooms will be ordered into @@ -119,8 +126,72 @@ export class ImportanceAlgorithm extends Algorithm { console.log("Constructed an ImportanceAlgorithm"); } + // noinspection JSMethodCanBeStatic + private categorizeRooms(rooms: Room[]): ICategorizedRoomMap { + const map: ICategorizedRoomMap = { + [Category.Red]: [], + [Category.Grey]: [], + [Category.Bold]: [], + [Category.Idle]: [], + }; + for (const room of rooms) { + const category = this.getRoomCategory(room); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`); + map[category].push(room); + } + return map; + } + + // noinspection JSMethodCanBeStatic + private getRoomCategory(room: Room): Category { + // Function implementation borrowed from old RoomListStore + + const mentions = room.getUnreadNotificationCount('highlight') > 0; + if (mentions) { + return Category.Red; + } + + let unread = room.getUnreadNotificationCount() > 0; + if (unread) { + return Category.Grey; + } + + unread = Unread.doesRoomHaveUnreadMessages(room); + if (unread) { + return Category.Bold; + } + + return Category.Idle; + } + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - return Promise.resolve(); + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + if (sortBy === SortAlgorithm.Manual) { + // Manual tags essentially ignore the importance algorithm, so don't do anything + // special about them. + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } else { + // Every other sorting type affects the categories, not the whole tag. + const categorized = this.categorizeRooms(unorderedRooms); + for (const category of Object.keys(categorized)) { + const roomsToOrder = categorized[category]; + categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); + } + + // TODO: Update positions of categories in cache + updatedTagMap[tagId] = [ + ...categorized[Category.Red], + ...categorized[Category.Grey], + ...categorized[Category.Bold], + ...categorized[Category.Idle], + ]; + } + } } protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { diff --git a/src/stores/room-list/algorithms/index.ts b/src/stores/room-list/algorithms/list_ordering/index.ts similarity index 84% rename from src/stores/room-list/algorithms/index.ts rename to src/stores/room-list/algorithms/list_ordering/index.ts index 1277b66ac9..35f4af14cf 100644 --- a/src/stores/room-list/algorithms/index.ts +++ b/src/stores/room-list/algorithms/list_ordering/index.ts @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Algorithm, ListAlgorithm } from "./Algorithm"; +import { Algorithm } from "./Algorithm"; import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; +import { ListAlgorithm } from "../models"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { - [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(ListAlgorithm.Natural), + [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; @@ -28,7 +29,7 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { * @param {ListAlgorithm} algorithm The algorithm to get an instance of. * @returns {Algorithm} The algorithm instance. */ -export function getAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { +export function getListAlgorithmInstance(algorithm: ListAlgorithm): Algorithm { if (!ALGORITHM_FACTORIES[algorithm]) { throw new Error(`${algorithm} is not a known algorithm`); } diff --git a/src/stores/room-list/algorithms/models.ts b/src/stores/room-list/algorithms/models.ts new file mode 100644 index 0000000000..284600a776 --- /dev/null +++ b/src/stores/room-list/algorithms/models.ts @@ -0,0 +1,42 @@ +/* +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 { TagID } from "../models"; +import { Room } from "matrix-js-sdk/src/models/room"; + +export enum SortAlgorithm { + Manual = "MANUAL", + Alphabetic = "ALPHABETIC", + Recent = "RECENT", +} + +export enum ListAlgorithm { + // Orders Red > Grey > Bold > Idle + Importance = "IMPORTANCE", + + // Orders however the SortAlgorithm decides + Natural = "NATURAL", +} + +export interface ITagSortingMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: SortAlgorithm; +} + +export interface ITagMap { + // @ts-ignore - TypeScript really wants this to be [tagId: string] but we know better. + [tagId: TagID]: Room[]; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts new file mode 100644 index 0000000000..31846d084a --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts @@ -0,0 +1,29 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * A demonstration to test the API surface. + * TODO: Remove this before landing + */ +export class ChaoticAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + return rooms; + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts new file mode 100644 index 0000000000..6c22ee0c9c --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; + +/** + * Represents a tag sorting algorithm. + */ +export interface IAlgorithm { + /** + * Sorts the given rooms according to the sorting rules of the algorithm. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag ID in which the rooms are being sorted. + * @returns {Promise} Resolves to the sorted rooms. + */ + sortRooms(rooms: Room[], tagId: TagID): Promise; +} diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts new file mode 100644 index 0000000000..b8c0357633 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts @@ -0,0 +1,31 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; + +/** + * Sorts rooms according to the tag's `order` property on the room. + */ +export class ManualAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + const getOrderProp = (r: Room) => r.tags[tagId].order || 0; + return rooms.sort((a, b) => { + return getOrderProp(a) - getOrderProp(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts new file mode 100644 index 0000000000..07f8f484d8 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -0,0 +1,52 @@ +/* +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 { ChaoticAlgorithm } from "./ChaoticAlgorithm"; +import { SortAlgorithm } from "../models"; +import { ManualAlgorithm } from "./ManualAlgorithm"; +import { IAlgorithm } from "./IAlgorithm"; +import { TagID } from "../../models"; +import {Room} from "matrix-js-sdk/src/models/room"; + +const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { + [SortAlgorithm.Recent]: new ChaoticAlgorithm(), + [SortAlgorithm.Alphabetic]: new ChaoticAlgorithm(), + [SortAlgorithm.Manual]: new ManualAlgorithm(), +}; + +/** + * Gets an instance of the defined algorithm + * @param {SortAlgorithm} algorithm The algorithm to get an instance of. + * @returns {IAlgorithm} The algorithm instance. + */ +export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorithm { + if (!ALGORITHM_INSTANCES[algorithm]) { + throw new Error(`${algorithm} is not a known algorithm`); + } + + return ALGORITHM_INSTANCES[algorithm]; +} + +/** + * Sorts rooms in a given tag according to the algorithm given. + * @param {Room[]} rooms The rooms to sort. + * @param {TagID} tagId The tag in which the sorting is occurring. + * @param {SortAlgorithm} algorithm The algorithm to use for sorting. + * @returns {Promise} Resolves to the sorted rooms. + */ +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { + return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); +} From ecf8090b750acf559f0550ba80a09745c0c18839 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Apr 2020 12:29:32 -0600 Subject: [PATCH 09/34] Handle DMs --- .../room-list/algorithms/list_ordering/Algorithm.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts index 4c8c9e9c60..fd98f34966 100644 --- a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; import { ITagMap, ITagSortingMap } from "../models"; +import DMRoomMap from "../../../../utils/DMRoomMap"; // TODO: Add locking support to avoid concurrent writes? // TODO: EventEmitter support? Might not be needed. @@ -100,13 +101,21 @@ export abstract class Algorithm { // Now process all the joined rooms. This is a bit more complicated for (const room of memberships[EffectiveMembership.Join]) { - const tags = Object.keys(room.tags || {}); + let tags = Object.keys(room.tags || {}); + + if (tags.length === 0) { + // Check to see if it's a DM if it isn't anything else + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + tags = [DefaultTagID.DM]; + } + } let inTag = false; if (tags.length > 0) { for (const tag of tags) { + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); if (!isNullOrUndefined(newTags[tag])) { - console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged as ${tag}`); + console.log(`[DEBUG] "${room.name}" (${room.roomId}) is tagged with VALID tag ${tag}`); newTags[tag].push(room); inTag = true; } From 09b7f39df8118a0c6979b7fc4091c4d583ee2782 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 30 Apr 2020 13:21:50 -0600 Subject: [PATCH 10/34] Simple rendering of the room list for visual aid This is largely meant to prove the algorithm works and nothing more. --- src/components/views/rooms/RoomList2.tsx | 130 +++++++++++++++++++- src/components/views/rooms/RoomSublist2.tsx | 61 +++++++++ 2 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 src/components/views/rooms/RoomSublist2.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index f97f599ae3..04cb8a4549 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -17,11 +17,18 @@ limitations under the License. */ import * as React from "react"; -import { _t } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import { Layout } from '../../../resizer/distributors/roomsublist2'; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { ResizeNotifier } from "../../../utils/ResizeNotifier"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2"; +import { ITagMap } from "../../../stores/room-list/algorithms/models"; +import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { Dispatcher } from "flux"; +import { ActionPayload } from "../../../dispatcher-types"; +import dis from "../../../dispatcher"; +import { RoomSublist2 } from "./RoomSublist2"; +import { isNullOrUndefined } from "matrix-js-sdk/lib/src/utils"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; @@ -33,14 +40,83 @@ interface IProps { } interface IState { + sublists: ITagMap; } -// TODO: Actually write stub -export class RoomSublist2 extends React.Component { - public setHeight(size: number) { - } +const TAG_ORDER: TagID[] = [ + // -- Community Invites Placeholder -- + + DefaultTagID.Invite, + DefaultTagID.Favourite, + DefaultTagID.DM, + DefaultTagID.Untagged, + + // -- Custom Tags Placeholder -- + + DefaultTagID.LowPriority, + DefaultTagID.ServerNotice, + DefaultTagID.Archived, +]; +const COMMUNITY_TAGS_BEFORE_TAG = DefaultTagID.Invite; +const CUSTOM_TAGS_BEFORE_TAG = DefaultTagID.LowPriority; +const ALWAYS_VISIBLE_TAGS: TagID[] = [ + DefaultTagID.DM, + DefaultTagID.Untagged, +]; + +interface ITagAesthetics { + sectionLabel: string; + addRoomLabel?: string; + onAddRoom?: (dispatcher: Dispatcher) => void; + isInvite: boolean; + defaultHidden: boolean; } +const TAG_AESTHETICS: { + // @ts-ignore - TS wants this to be a string but we know better + [tagId: TagID]: ITagAesthetics; +} = { + [DefaultTagID.Invite]: { + sectionLabel: _td("Invites"), + isInvite: true, + defaultHidden: false, + }, + [DefaultTagID.Favourite]: { + sectionLabel: _td("Favourites"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.DM]: { + sectionLabel: _td("Direct Messages"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Start chat"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_chat'}), + }, + [DefaultTagID.Untagged]: { + sectionLabel: _td("Rooms"), + isInvite: false, + defaultHidden: false, + addRoomLabel: _td("Create room"), + onAddRoom: (dispatcher: Dispatcher) => dispatcher.dispatch({action: 'view_create_room'}), + }, + [DefaultTagID.LowPriority]: { + sectionLabel: _td("Low priority"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.ServerNotice]: { + sectionLabel: _td("System Alerts"), + isInvite: false, + defaultHidden: false, + }, + [DefaultTagID.Archived]: { + sectionLabel: _td("Historical"), + isInvite: false, + defaultHidden: true, + }, +}; + export default class RoomList2 extends React.Component { private sublistRefs: { [tagId: string]: React.RefObject } = {}; private sublistSizes: { [tagId: string]: number } = {}; @@ -51,6 +127,7 @@ export default class RoomList2 extends React.Component { constructor(props: IProps) { super(props); + this.state = {sublists: {}}; this.loadSublistSizes(); this.prepareLayouts(); } @@ -58,6 +135,7 @@ export default class RoomList2 extends React.Component { public componentDidMount(): void { RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => { console.log("new lists", store.orderedLists); + this.setState({sublists: store.orderedLists}); }); } @@ -108,7 +186,47 @@ export default class RoomList2 extends React.Component { } } + private renderSublists(): React.ReactElement[] { + const components: React.ReactElement[] = []; + + for (const orderedTagId of TAG_ORDER) { + if (COMMUNITY_TAGS_BEFORE_TAG === orderedTagId) { + // Populate community invites if we have the chance + // TODO + } + if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) { + // Populate custom tags if needed + // TODO + } + + const orderedRooms = this.state.sublists[orderedTagId] || []; + if (orderedRooms.length === 0 && !ALWAYS_VISIBLE_TAGS.includes(orderedTagId)) { + continue; // skip tag - not needed + } + + const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; + if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); + + const onAddRoomFn = () => { + if (!aesthetics.onAddRoom) return; + aesthetics.onAddRoom(dis); + }; + components.push(); + } + + return components; + } + public render() { + const sublists = this.renderSublists(); return ( {({onKeyDownHandler}) => ( @@ -122,7 +240,7 @@ export default class RoomList2 extends React.Component { // Firefox sometimes makes this element focusable due to // overflow:scroll;, so force it out of tab order. tabIndex={-1} - >{_t("TODO")} + >{sublists} )} ); diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx new file mode 100644 index 0000000000..a3ca525b14 --- /dev/null +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017, 2018 Vector Creations Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +interface IProps { + forRooms: boolean; + rooms?: Room[]; + startAsHidden: boolean; + label: string; + onAddRoom?: () => void; + addRoomLabel: string; + isInvite: boolean; + + // TODO: Collapsed state + // TODO: Height + // TODO: Group invites + // TODO: Calls + // TODO: forceExpand? + // TODO: Header clicking + // TODO: Spinner support for historical +} + +interface IState { +} + +// TODO: Actually write stub +export class RoomSublist2 extends React.Component { + public setHeight(size: number) { + } + + public render() { + // TODO: Proper rendering + + const rooms = this.props.rooms.map(r => ( +
{r.name} ({r.roomId})
+ )); + return ( +
+

{this.props.label}

+ {rooms} +
+ ); + } +} From 5dda7f02cffb88e0621b68429fcc725e4dab8bb6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 4 May 2020 09:06:34 -0600 Subject: [PATCH 11/34] Early handling of dispatched events A possible approach to handling the various triggers for recategorizing rooms. --- src/components/views/rooms/RoomList2.tsx | 1 - src/stores/room-list/RoomListStore2.ts | 41 ++++++++++++++++--- .../algorithms/list_ordering/Algorithm.ts | 41 +++++++++++-------- .../list_ordering/ChaoticAlgorithm.ts | 7 +--- .../list_ordering/ImportanceAlgorithm.ts | 17 ++++---- src/stores/room-list/models.ts | 5 +++ 6 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 04cb8a4549..f7788f0003 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -28,7 +28,6 @@ import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; import { RoomSublist2 } from "./RoomSublist2"; -import { isNullOrUndefined } from "matrix-js-sdk/lib/src/utils"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 0b3f61e261..06f97ae53e 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -18,12 +18,14 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; -import { DefaultTagID, OrderedDefaultTagIDs, TagID } from "./models"; +import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; interface IState { tagsEnabled?: boolean; @@ -123,7 +125,14 @@ class _RoomListStore extends AsyncStore { await this.regenerateAllLists(); // regenerate the lists now } - } else if (payload.action === 'MatrixActions.Room.receipt') { + } + + if (!this.algorithm) { + // This shouldn't happen because `initialListsGenerated` implies we have an algorithm. + throw new Error("Room list store has no algorithm to process dispatcher update with"); + } + + if (payload.action === 'MatrixActions.Room.receipt') { // First see if the receipt event is for our own user. If it was, trigger // a room update (we probably read the room on a different device). // noinspection JSObjectNullOrUndefined - this.matrixClient can't be null by this point in the lifecycle @@ -132,23 +141,45 @@ class _RoomListStore extends AsyncStore { const receiptUsers = Object.keys(payload.event.getContent()[eventId]['m.read'] || {}); if (receiptUsers.includes(myUserId)) { // TODO: Update room now that it's been read + console.log(payload); return; } } } else if (payload.action === 'MatrixActions.Room.tags') { // TODO: Update room from tags - } else if (payload.action === 'MatrixActions.room.timeline') { - // TODO: Update room from new events + console.log(payload); + } else if (payload.action === 'MatrixActions.Room.timeline') { + const eventPayload = (payload); // TODO: Type out the dispatcher types + + // Ignore non-live events (backfill) + if (!eventPayload.isLiveEvent || !payload.isLiveUnfilteredRoomTimelineEvent) return; + + const roomId = eventPayload.event.getRoomId(); + const room = this.matrixClient.getRoom(roomId); + await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.Event.decrypted') { // TODO: Update room from decrypted event + console.log(payload); } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { // TODO: Update DMs + console.log(payload); } else if (payload.action === 'MatrixActions.Room.myMembership') { // TODO: Update room from membership change - } else if (payload.action === 'MatrixActions.room') { + console.log(payload); + } else if (payload.action === 'MatrixActions.Room') { // TODO: Update room from creation/join + console.log(payload); } else if (payload.action === 'view_room') { // TODO: Update sticky room + console.log(payload); + } + } + + private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); + if (shouldUpdate) { + console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`); + this.emit(LISTS_UPDATE_EVENT, this); } } diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts index fd98f34966..e154847847 100644 --- a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/Algorithm.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DefaultTagID, TagID } from "../../models"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EffectiveMembership, splitRoomsByMembership } from "../../membership"; @@ -33,9 +33,8 @@ export abstract class Algorithm { protected cached: ITagMap = {}; protected sortAlgorithms: ITagSortingMap; protected rooms: Room[] = []; - protected roomsByTag: { - // @ts-ignore - TS wants this to be a string but we know better - [tagId: TagID]: Room[]; + protected roomIdsToTags: { + [roomId: string]: TagID[]; } = {}; protected constructor() { @@ -132,6 +131,25 @@ export abstract class Algorithm { await this.generateFreshTags(newTags); this.cached = newTags; + this.updateTagsFromCache(); + } + + /** + * Updates the roomsToTags map + */ + protected updateTagsFromCache() { + const newMap = {}; + + const tags = Object.keys(this.cached); + for (const tagId of tags) { + const rooms = this.cached[tagId]; + for (const room of rooms) { + if (!newMap[room.roomId]) newMap[room.roomId] = []; + newMap[room.roomId].push(tagId); + } + } + + this.roomIdsToTags = newMap; } /** @@ -144,27 +162,16 @@ export abstract class Algorithm { */ protected abstract generateFreshTags(updatedTagMap: ITagMap): Promise; - /** - * Called when the Algorithm wants a whole tag to be reordered. Typically this will - * be done whenever the tag's scope changes (added/removed rooms). - * @param {TagID} tagId The tag ID which changed. - * @param {Room[]} rooms The rooms within the tag, unordered. - * @returns {Promise} Resolves to the ordered rooms in the tag. - */ - // TODO: Do we need this? - protected abstract regenerateTag(tagId: TagID, rooms: Room[]): Promise; - /** * Asks the Algorithm to update its knowledge of a room. For example, when * a user tags a room, joins/creates a room, or leaves a room the Algorithm * should be told that the room's info might have changed. The Algorithm * may no-op this request if no changes are required. * @param {Room} room The room which might have affected sorting. + * @param {RoomUpdateCause} cause The reason for the update being triggered. * @returns {Promise} A promise which resolve to true or false * depending on whether or not getOrderedRooms() should be called after * processing. */ - // TODO: Take a ReasonForChange to better predict the behaviour? - // TODO: Intercept here and handle tag changes automatically? May be best to let the impl do that. - public abstract handleRoomUpdate(room: Room): Promise; + public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; } diff --git a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts index 7c1a0b1acc..185fb606fb 100644 --- a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts @@ -15,7 +15,6 @@ limitations under the License. */ import { Algorithm } from "./Algorithm"; -import { DefaultTagID } from "../../models"; import { ITagMap } from "../models"; /** @@ -33,11 +32,7 @@ export class ChaoticAlgorithm extends Algorithm { return Promise.resolve(); } - protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { - return Promise.resolve(rooms); - } - - public async handleRoomUpdate(room): Promise { + public async handleRoomUpdate(room, cause): Promise { return Promise.resolve(false); } } diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index d73fdee930..d66f7cdc37 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -17,9 +17,9 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DefaultTagID, TagID } from "../../models"; +import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; -import { getSortingAlgorithmInstance, sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag_sorting"; import * as Unread from '../../../../Unread'; /** @@ -194,11 +194,12 @@ export class ImportanceAlgorithm extends Algorithm { } } - protected async regenerateTag(tagId: string | DefaultTagID, rooms: []): Promise<[]> { - return Promise.resolve(rooms); - } - - public async handleRoomUpdate(room): Promise { - return Promise.resolve(false); + public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + return false; } } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index d1c915e035..5294680773 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -34,3 +34,8 @@ export const OrderedDefaultTagIDs = [ ]; export type TagID = string | DefaultTagID; + +export enum RoomUpdateCause { + Timeline = "TIMELINE", + RoomRead = "ROOM_READ", +} From ea34bb3022cda77e96a67e6ae1f06ef881db24d2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 4 May 2020 09:13:35 -0600 Subject: [PATCH 12/34] Make component index happy --- src/components/views/rooms/RoomList2.tsx | 2 +- src/components/views/rooms/RoomSublist2.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index f7788f0003..7aefc21a49 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -27,7 +27,7 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; -import { RoomSublist2 } from "./RoomSublist2"; +import RoomSublist2 from "./RoomSublist2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index a3ca525b14..ed3740e8b3 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -41,7 +41,7 @@ interface IState { } // TODO: Actually write stub -export class RoomSublist2 extends React.Component { +export default class RoomSublist2 extends React.Component { public setHeight(size: number) { } From e1fab9a5b663a8c15398dfd15d2015a0b242e193 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 May 2020 16:34:22 -0600 Subject: [PATCH 13/34] Work out the new category index for each room update See comments within for details on what this means. --- .../list_ordering/ImportanceAlgorithm.ts | 173 ++++++++++++++---- 1 file changed, 136 insertions(+), 37 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index d66f7cdc37..76cdf4763f 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -17,7 +17,7 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; -import { DefaultTagID, RoomUpdateCause, TagID } from "../../models"; +import { RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; import { sortRoomsWithAlgorithm } from "../tag_sorting"; import * as Unread from '../../../../Unread'; @@ -51,6 +51,16 @@ interface ICategorizedRoomMap { [category: Category]: Room[]; } +interface ICategoryIndex { + // @ts-ignore - TS wants this to be a string, but we know better + [category: Category]: number; // integer +} + +// Caution: changing this means you'll need to update a bunch of assumptions and +// comments! Check the usage of Category carefully to figure out what needs changing +// if you're going to change this array's order. +const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idle]; + /** * An implementation of the "importance" algorithm for room list sorting. Where * the tag sorting algorithm does not interfere, rooms will be ordered into @@ -69,6 +79,7 @@ interface ICategorizedRoomMap { */ export class ImportanceAlgorithm extends Algorithm { + // TODO: Update documentation // HOW THIS WORKS // -------------- // @@ -80,7 +91,7 @@ export class ImportanceAlgorithm extends Algorithm { // those tags are tracked as index numbers within the array (zero = top), with // each sticky room being tracked separately. Internally, the category index // can be found from `this.indices[tag][category]` and the sticky room information - // from `this.stickyRooms[tag]`. + // from `this.stickyRoom`. // // Room categories are constantly re-evaluated and tracked in the `this.categorized` // object. Note that this doesn't track rooms by category but instead by room ID. @@ -93,33 +104,17 @@ export class ImportanceAlgorithm extends Algorithm { private indices: { // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - // @ts-ignore - TS wants this to be a string but we know better than it - [category: Category]: number; // integer - }; - } = {}; - private stickyRooms: { - // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - room?: Room; - nAbove: number; // integer - }; - } = {}; - private categorized: { - // @ts-ignore - TS wants this to be a string but we know better than it - [tag: TagID]: { - // TODO: Remove note - // Note: Should in theory be able to only track this by room ID as we'll know - // the indices of each category and can determine if a category needs changing - // in the cached list. Could potentially save a bunch of time if we can figure - // out where a room is supposed to be using offsets, some math, and leaving the - // array generally alone. - [roomId: string]: { - room: Room; - category: Category; - }; - }; + [tag: TagID]: ICategoryIndex; } = {}; + private stickyRoom: { + roomId: string; + tag: TagID; + fromTop: number; + } = { + roomId: null, + tag: null, + fromTop: 0, + }; constructor() { super(); @@ -136,7 +131,6 @@ export class ImportanceAlgorithm extends Algorithm { }; for (const room of rooms) { const category = this.getRoomCategory(room); - console.log(`[DEBUG] "${room.name}" (${room.roomId}) is a ${category} room`); map[category].push(room); } return map; @@ -183,13 +177,16 @@ export class ImportanceAlgorithm extends Algorithm { categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, tagId, sortBy); } - // TODO: Update positions of categories in cache - updatedTagMap[tagId] = [ - ...categorized[Category.Red], - ...categorized[Category.Grey], - ...categorized[Category.Bold], - ...categorized[Category.Idle], - ]; + const newlyOrganized: Room[] = []; + const newIndicies: ICategoryIndex = {}; + + for (const category of CATEGORY_ORDER) { + newIndicies[category] = newlyOrganized.length; + newlyOrganized.push(...categorized[category]); + } + + this.indices[tagId] = newIndicies; + updatedTagMap[tagId] = newlyOrganized; } } } @@ -200,6 +197,108 @@ export class ImportanceAlgorithm extends Algorithm { console.warn(`No tags known for "${room.name}" (${room.roomId})`); return false; } - return false; + const category = this.getRoomCategory(room); + let changed = false; + for (const tag of tags) { + if (this.sortAlgorithms[tag] === SortAlgorithm.Manual) { + continue; // Nothing to do here. + } + + const taggedRooms = this.cached[tag]; + const indicies = this.indices[tag]; + let roomIdx = taggedRooms.indexOf(room); + let inList = true; + if (roomIdx === -1) { + console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); + roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); + } + if (roomIdx === -1) { + console.warn(`Room ${room.roomId} has no index in ${tag} - assuming end of list`); + roomIdx = taggedRooms.length; + inList = false; // used so we don't splice the dead room out + } + + // Try to avoid doing array operations if we don't have to: only move rooms within + // the categories if we're jumping categories + const oldCategory = this.getCategoryFromIndicies(roomIdx, indicies); + if (oldCategory !== category) { + // Move the room and update the indicies + this.moveRoomIndexes(1, oldCategory, category, indicies); + taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) + taggedRooms.splice(indicies[category], 0, room); // splice in the new room (pre-adjusted) + // Note: if moveRoomIndexes() is called after the splice then the insert operation + // will happen in the wrong place. Because we would have already adjusted the index + // for the category, we don't need to determine how the room is moving in the list. + // If we instead tried to insert before updating the indicies, we'd have to determine + // whether the room was moving later (towards IDLE) or earlier (towards RED) from its + // current position, as it'll affect the category's start index after we remove the + // room from the array. + } + + // The room received an update, so take out the slice and sort it. This should be relatively + // quick because the room is inserted at the top of the category, and most popular sorting + // algorithms will deal with trying to keep the active room at the top/start of the category. + // For the few algorithms that will have to move the thing quite far (alphabetic with a Z room + // for example), the list should already be sorted well enough that it can rip through the + // array and slot the changed room in quickly. + const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] + ? Number.MAX_SAFE_INTEGER + : indicies[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = indicies[category]; + const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine + const unsortedSlice = taggedRooms.splice(startIdx, numSort); + const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); + taggedRooms.splice(startIdx, 0, ...sorted); + + // Finally, flag that we've done something + changed = true; + } + return changed; + } + + private getCategoryFromIndicies(index: number, indicies: ICategoryIndex): Category { + for (let i = 0; i < CATEGORY_ORDER.length; i++) { + const category = CATEGORY_ORDER[i]; + const isLast = i === (CATEGORY_ORDER.length - 1); + const startIdx = indicies[category]; + const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indicies[CATEGORY_ORDER[i + 1]]; + if (index >= startIdx && index < endIdx) { + return category; + } + } + + // "Should never happen" disclaimer goes here + throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); + } + + private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indicies: ICategoryIndex) { + // We have to update the index of the category *after* the from/toCategory variables + // in order to update the indicies correctly. Because the room is moving from/to those + // categories, the next category's index will change - not the category we're modifying. + // We also need to update subsequent categories as they'll all shift by nRooms, so we + // loop over the order to achieve that. + + for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indicies[nextCategory] -= nRooms; + } + + for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { + const nextCategory = CATEGORY_ORDER[i]; + indicies[nextCategory] += nRooms; + } + + // Do a quick check to see if we've completely broken the index + for (let i = 1; i <= CATEGORY_ORDER.length; i++) { + const lastCat = CATEGORY_ORDER[i - 1]; + const thisCat = CATEGORY_ORDER[i]; + + if (indicies[lastCat] > indicies[thisCat]) { + // "should never happen" disclaimer goes here + console.warn(`!! Room list index corruption: ${lastCat} (i:${indicies[lastCat]}) is greater than ${thisCat} (i:${indicies[thisCat]}) - category indicies are likely desynced from reality`); + + // TODO: Regenerate index when this happens + } + } } } From 4a0d14e32269fa8e4049d06173e7d9c4035a6499 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 7 May 2020 16:38:14 -0600 Subject: [PATCH 14/34] Make missing rooms throw instead For now at least. We shouldn't encounter this case until we get around to adding support for newly-joined rooms. --- .../algorithms/list_ordering/ImportanceAlgorithm.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 76cdf4763f..0ebdad1ed1 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -207,15 +207,12 @@ export class ImportanceAlgorithm extends Algorithm { const taggedRooms = this.cached[tag]; const indicies = this.indices[tag]; let roomIdx = taggedRooms.indexOf(room); - let inList = true; if (roomIdx === -1) { console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); roomIdx = taggedRooms.findIndex(r => r.roomId === room.roomId); } if (roomIdx === -1) { - console.warn(`Room ${room.roomId} has no index in ${tag} - assuming end of list`); - roomIdx = taggedRooms.length; - inList = false; // used so we don't splice the dead room out + throw new Error(`Room ${room.roomId} has no index in ${tag}`); } // Try to avoid doing array operations if we don't have to: only move rooms within From e88788f4e9c7804c9b78bcd670035bcfe14fae18 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 May 2020 11:59:03 -0600 Subject: [PATCH 15/34] Handle event decryption too --- src/stores/room-list/RoomListStore2.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 06f97ae53e..3a6d911dde 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -156,10 +156,21 @@ class _RoomListStore extends AsyncStore { const roomId = eventPayload.event.getRoomId(); const room = this.matrixClient.getRoom(roomId); + console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`); await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.Event.decrypted') { - // TODO: Update room from decrypted event - console.log(payload); + const eventPayload = (payload); // TODO: Type out the dispatcher types + const roomId = eventPayload.event.getRoomId(); + const room = this.matrixClient.getRoom(roomId); + if (!room) { + console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`); + return; + } + console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`); + // TODO: Check that e2e rooms are calculated correctly on initial load. + // It seems like when viewing the room the timeline is decrypted, rather than at startup. This could + // cause inaccuracies with the list ordering. We may have to decrypt the last N messages of every room :( + await this.handleRoomUpdate(room, RoomUpdateCause.Timeline); } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') { // TODO: Update DMs console.log(payload); From cb3d17ee28de8d0e96650b8f1dfb5a6fa698a388 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 8 May 2020 12:53:05 -0600 Subject: [PATCH 16/34] Bare minimum for rendering a room list This is non-interactive and missing most features users will expect to have --- src/components/views/rooms/RoomList2.tsx | 9 +- src/components/views/rooms/RoomSublist2.tsx | 174 +++++++++++++++++++- src/components/views/rooms/RoomTile2.tsx | 118 +++++++++++++ 3 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 src/components/views/rooms/RoomTile2.tsx diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 7aefc21a49..9b0d4579f0 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -156,7 +156,7 @@ export default class RoomList2 extends React.Component { const sublist = this.sublistRefs[tagId]; if (sublist) sublist.current.setHeight(height); - // TODO: Check overflow + // TODO: Check overflow (see old impl) // Don't store a height for collapsed sublists if (!this.sublistCollapseStates[tagId]) { @@ -178,6 +178,7 @@ export default class RoomList2 extends React.Component { } private collectSublistRef(tagId: string, ref: React.RefObject) { + // TODO: Is this needed? if (!ref) { delete this.sublistRefs[tagId]; } else { @@ -206,11 +207,9 @@ export default class RoomList2 extends React.Component { const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); - const onAddRoomFn = () => { - if (!aesthetics.onAddRoom) return; - aesthetics.onAddRoom(dis); - }; + const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; components.push( { + private headerButton = createRef(); + public setHeight(size: number) { + // TODO: Do a thing } - public render() { - // TODO: Proper rendering + private hasTiles(): boolean { + return this.numTiles > 0; + } + + private get numTiles(): number { + // TODO: Account for group invites + return (this.props.rooms || []).length; + } + + private onAddRoom = (e) => { + e.stopPropagation(); + if (this.props.onAddRoom) this.props.onAddRoom(); + }; + + private renderTiles(): React.ReactElement[] { + const tiles: React.ReactElement[] = []; + + if (this.props.rooms) { + for (const room of this.props.rooms) { + tiles.push(); + } + } + + return tiles; + } + + private renderHeader(): React.ReactElement { + const notifications = !this.props.isInvite + ? RoomNotifs.aggregateNotificationCount(this.props.rooms) + : {count: 0, highlight: true}; + const notifCount = notifications.count; + const notifHighlight = notifications.highlight; + + // TODO: Title on collapsed + // TODO: Incoming call box + + let chevron = null; + if (this.hasTiles()) { + const chevronClasses = classNames({ + 'mx_RoomSubList_chevron': true, + 'mx_RoomSubList_chevronRight': false, // isCollapsed + 'mx_RoomSubList_chevronDown': true, // !isCollapsed + }); + chevron = (
); + } - const rooms = this.props.rooms.map(r => ( -
{r.name} ({r.roomId})
- )); return ( -
-

{this.props.label}

- {rooms} + + {({onFocus, isActive, ref}) => { + const tabIndex = isActive ? 0 : -1; + + let badge; + if (true) { // !isCollapsed + const badgeClasses = classNames({ + 'mx_RoomSubList_badge': true, + 'mx_RoomSubList_badgeHighlight': notifHighlight, + }); + // Wrap the contents in a div and apply styles to the child div so that the browser default outline works + if (notifCount > 0) { + badge = ( + +
+ {FormattingUtils.formatCount(notifCount)} +
+
+ ); + } else if (this.props.isInvite && this.hasTiles()) { + // Render the `!` badge for invites + badge = ( + +
+ {FormattingUtils.formatCount(this.numTiles)} +
+
+ ); + } + } + + let addRoomButton = null; + if (!!this.props.onAddRoom) { + addRoomButton = ( + + ); + } + + // TODO: a11y + return ( +
+ + {chevron} + {this.props.label} + + {badge} + {addRoomButton} +
+ ); + }} +
+ ); + } + + public render(): React.ReactElement { + // TODO: Proper rendering + // TODO: Error boundary + + const tiles = this.renderTiles(); + + const classes = classNames({ + // TODO: Proper collapse support + 'mx_RoomSubList': true, + 'mx_RoomSubList_hidden': false, // len && isCollapsed + 'mx_RoomSubList_nonEmpty': this.hasTiles(), // len && !isCollapsed + }); + + let content = null; + if (tiles.length > 0) { + // TODO: Lazy list rendering + // TODO: Whatever scrolling magic needs to happen here + content = ( + + {tiles} + + ) + } + + // TODO: onKeyDown support + return ( +
+ {this.renderHeader()} + {content}
); } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx new file mode 100644 index 0000000000..cb9ce5cf1a --- /dev/null +++ b/src/components/views/rooms/RoomTile2.tsx @@ -0,0 +1,118 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd +Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019, 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, { createRef } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import classNames from "classnames"; +import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; +import AccessibleButton from "../../views/elements/AccessibleButton"; +import RoomAvatar from "../../views/avatars/RoomAvatar"; + +interface IProps { + room: Room; + + // TODO: Allow faslifying counts (for invites and stuff) + // TODO: Transparency? + // TODO: Incoming call? + // TODO: onClick +} + +interface IState { +} + +// TODO: Finish stub +export default class RoomTile2 extends React.Component { + private roomTile = createRef(); + + // TODO: Custom status + // TODO: Lock icon + // TODO: DM indicator + // TODO: Presence indicator + // TODO: e2e shields + // TODO: Handle changes to room aesthetics (name, join rules, etc) + // TODO: scrollIntoView? + // TODO: hover, badge, etc + // TODO: isSelected for hover effects + // TODO: Context menu + // TODO: a11y + + public render(): React.ReactElement { + // TODO: Collapsed state + // TODO: Invites + // TODO: a11y proper + // TODO: Render more than bare minimum + + const classes = classNames({ + 'mx_RoomTile': true, + // 'mx_RoomTile_selected': this.state.selected, + // 'mx_RoomTile_unread': this.props.unread, + // 'mx_RoomTile_unreadNotify': notifBadges, + // 'mx_RoomTile_highlight': mentionBadges, + // 'mx_RoomTile_invited': isInvite, + // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, + 'mx_RoomTile_noBadges': true, // !badges + // 'mx_RoomTile_transparent': this.props.transparent, + // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, + }); + + const avatarClasses = classNames({ + 'mx_RoomTile_avatar': true, + }); + + // TODO: the original RoomTile uses state for the room name. Do we need to? + let name = this.props.room.name; + if (typeof name !== 'string') name = ''; + name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon + + const nameClasses = classNames({ + 'mx_RoomTile_name': true, + 'mx_RoomTile_invite': false, + 'mx_RoomTile_badgeShown': false, + }); + + return ( + + + {({onFocus, isActive, ref}) => + +
+
+ +
+
+
+
+
+ {name} +
+
+
+
+ } +
+
+ ); + } +} From df3d5c415917324a3784535c23fc695f5e3ff1ff Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 14:25:30 -0600 Subject: [PATCH 17/34] Update i18n for room list --- src/i18n/strings/en_EN.json | 10 +++++----- src/settings/Settings.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index fd474f378c..d6ff5f70eb 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,7 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list component (refresh to apply changes)": "Use the improved room list component (refresh to apply changes)", + "Use the improved room list component (refresh to apply changes, in development)": "Use the improved room list component (refresh to apply changes, in development)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", @@ -1117,7 +1117,7 @@ "Low priority": "Low priority", "Historical": "Historical", "System Alerts": "System Alerts", - "TODO": "TODO", + "Create room": "Create room", "This room": "This room", "Joining room …": "Joining room …", "Loading …": "Loading …", @@ -1161,6 +1161,9 @@ "Securely back up your keys to avoid losing them. Learn more.": "Securely back up your keys to avoid losing them. Learn more.", "Not now": "Not now", "Don't ask me again": "Don't ask me again", + "Jump to first unread room.": "Jump to first unread room.", + "Jump to first invite.": "Jump to first invite.", + "Add room": "Add room", "Options": "Options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", "%(count)s unread messages including mentions.|one": "1 unread mention.", @@ -2052,9 +2055,6 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", - "Jump to first unread room.": "Jump to first unread room.", - "Jump to first invite.": "Jump to first invite.", - "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 554cf6b968..110fd4238b 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -133,7 +133,7 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list component (refresh to apply changes)"), + displayName: _td("Use the improved room list component (refresh to apply changes, in development)"), supportedLevels: LEVELS_FEATURE, default: false, }, From 9f0810240f6ae8d627708d8d51e4f058d0173f55 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:12:45 -0600 Subject: [PATCH 18/34] Clean up imports and other minor lints --- src/components/views/rooms/RoomList2.tsx | 2 +- src/stores/room-list/RoomListStore2.ts | 5 ++--- src/stores/room-list/RoomListStoreTempProxy.ts | 4 +--- src/stores/room-list/algorithms/tag_sorting/index.ts | 2 +- src/stores/room-list/membership.ts | 3 +-- src/stores/room-list/models.ts | 1 + 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 9b0d4579f0..402a7af014 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -27,7 +27,7 @@ import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; -import RoomSublist2 from "./RoomSublist2"; +import RoomSublist2 from "./RoomSublist2"; interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 3a6d911dde..8bbcfc3c8d 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -21,11 +21,10 @@ import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; -import { getListAlgorithmInstance } from "./algorithms/list_ordering"; import { AsyncStore } from "../AsyncStore"; -import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; +import { getListAlgorithmInstance } from "./algorithms/list_ordering"; interface IState { tagsEnabled?: boolean; diff --git a/src/stores/room-list/RoomListStoreTempProxy.ts b/src/stores/room-list/RoomListStoreTempProxy.ts index 4edca2b9cd..0268cf0a46 100644 --- a/src/stores/room-list/RoomListStoreTempProxy.ts +++ b/src/stores/room-list/RoomListStoreTempProxy.ts @@ -14,13 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { TagID } from "./models"; -import { Room } from "matrix-js-sdk/src/models/room"; import SettingsStore from "../../settings/SettingsStore"; import RoomListStore from "./RoomListStore2"; import OldRoomListStore from "../RoomListStore"; -import { ITagMap } from "./algorithms/models"; import { UPDATE_EVENT } from "../AsyncStore"; +import { ITagMap } from "./algorithms/models"; /** * Temporary RoomListStore proxy. Should be replaced with RoomListStore2 when diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts index 07f8f484d8..155c0f0118 100644 --- a/src/stores/room-list/algorithms/tag_sorting/index.ts +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -19,7 +19,7 @@ import { SortAlgorithm } from "../models"; import { ManualAlgorithm } from "./ManualAlgorithm"; import { IAlgorithm } from "./IAlgorithm"; import { TagID } from "../../models"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { [SortAlgorithm.Recent]: new ChaoticAlgorithm(), diff --git a/src/stores/room-list/membership.ts b/src/stores/room-list/membership.ts index 884e2a4a04..3cb4bf146c 100644 --- a/src/stores/room-list/membership.ts +++ b/src/stores/room-list/membership.ts @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Room} from "matrix-js-sdk/src/models/room"; -import {Event} from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; /** * Approximation of a membership status for a given room. diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 5294680773..428378a7aa 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -23,6 +23,7 @@ export enum DefaultTagID { DM = "im.vector.fake.direct", ServerNotice = "m.server_notice", } + export const OrderedDefaultTagIDs = [ DefaultTagID.Invite, DefaultTagID.Favourite, From 715dd7e1b6126ec0f858d80af1873917d9ebe714 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:20:26 -0600 Subject: [PATCH 19/34] Prepare tooltip for collapsed support --- src/components/views/rooms/RoomTile2.tsx | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index cb9ce5cf1a..21ba32ae75 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -23,6 +23,7 @@ import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; +import Tooltip from "../../views/elements/Tooltip"; interface IProps { room: Room; @@ -34,6 +35,7 @@ interface IProps { } interface IState { + hover: boolean; } // TODO: Finish stub @@ -52,6 +54,22 @@ export default class RoomTile2 extends React.Component { // TODO: Context menu // TODO: a11y + 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 { // TODO: Collapsed state // TODO: Invites @@ -86,6 +104,13 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_badgeShown': false, }); + let tooltip = null; + if (false) { // isCollapsed + if (this.state.hover) { + tooltip = + } + } + return ( @@ -95,6 +120,8 @@ export default class RoomTile2 extends React.Component { tabIndex={isActive ? 0 : -1} inputRef={ref} className={classes} + onMouseEnter={this.onTileMouseEnter} + onMouseLeave={this.onTileMouseLeave} role="treeitem" >
@@ -109,6 +136,7 @@ export default class RoomTile2 extends React.Component {
+ {tooltip} } From 6bdcbd0f3d2137caedc19f9659eecac0636632f9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 16:29:32 -0600 Subject: [PATCH 20/34] Support switching rooms --- src/components/views/rooms/RoomTile2.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 21ba32ae75..f32bd7924f 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -24,6 +24,8 @@ import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; import Tooltip from "../../views/elements/Tooltip"; +import dis from '../../../dispatcher'; +import { Key } from "../../../Keyboard"; interface IProps { room: Room; @@ -31,7 +33,6 @@ interface IProps { // TODO: Allow faslifying counts (for invites and stuff) // TODO: Transparency? // TODO: Incoming call? - // TODO: onClick } interface IState { @@ -70,6 +71,16 @@ export default class RoomTile2 extends React.Component { this.setState({hover: false}); }; + private onTileClick = (ev: React.KeyboardEvent) => { + dis.dispatch({ + action: 'view_room', + // TODO: Support show_room_tile in new room list + show_room_tile: true, // make sure the room is visible in the list + room_id: this.props.room.roomId, + clear_search: (ev && (ev.key === Key.ENTER || ev.key === Key.SPACE)), + }); + }; + public render(): React.ReactElement { // TODO: Collapsed state // TODO: Invites @@ -122,6 +133,7 @@ export default class RoomTile2 extends React.Component { className={classes} onMouseEnter={this.onTileMouseEnter} onMouseLeave={this.onTileMouseLeave} + onClick={this.onTileClick} role="treeitem" >
From e8c33161ec3871805403ef0e508cd79321570a8d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 11 May 2020 21:09:32 -0600 Subject: [PATCH 21/34] Initial work on badges This doesn't work for bold rooms --- src/components/views/rooms/RoomTile2.tsx | 68 +++++++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index f32bd7924f..c6c400ce52 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -26,6 +26,10 @@ import RoomAvatar from "../../views/avatars/RoomAvatar"; import Tooltip from "../../views/elements/Tooltip"; import dis from '../../../dispatcher'; import { Key } from "../../../Keyboard"; +import * as RoomNotifs from '../../../RoomNotifs'; +import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; +import * as Unread from '../../../Unread'; +import * as FormattingUtils from "../../../utils/FormattingUtils"; interface IProps { room: Room; @@ -35,7 +39,14 @@ interface IProps { // TODO: Incoming call? } -interface IState { +interface IBadgeState { + showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room + numUnread: number; // used only if showBadge or showBadgeHighlight is true + showBadgeHighlight: boolean; // make the badge red + isInvite: boolean; // show a `!` instead of a number +} + +interface IState extends IBadgeState { hover: boolean; } @@ -60,6 +71,35 @@ export default class RoomTile2 extends React.Component { this.state = { hover: false, + + ...this.getBadgeState(), + }; + } + + public componentWillUnmount() { + + } + + private updateBadgeCount() { + this.setState({...this.getBadgeState()}); + } + + private getBadgeState(): IBadgeState { + // TODO: Make this code path faster + const highlightCount = RoomNotifs.getUnreadNotificationCount(this.props.room, 'highlight'); + const numUnread = RoomNotifs.getUnreadNotificationCount(this.props.room); + const showBadge = Unread.doesRoomHaveUnreadMessages(this.props.room); + const myMembership = getEffectiveMembership(this.props.room.getMyMembership()); + const isInvite = myMembership === EffectiveMembership.Invite; + const notifState = RoomNotifs.getRoomNotifsState(this.props.room.roomId); + const shouldShowNotifBadge = RoomNotifs.shouldShowNotifBadge(notifState); + const shouldShowHighlightBadge = RoomNotifs.shouldShowMentionBadge(notifState); + + return { + showBadge: (showBadge && shouldShowNotifBadge) || isInvite, + numUnread, + showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite, + isInvite, }; } @@ -90,12 +130,12 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile': true, // 'mx_RoomTile_selected': this.state.selected, - // 'mx_RoomTile_unread': this.props.unread, - // 'mx_RoomTile_unreadNotify': notifBadges, - // 'mx_RoomTile_highlight': mentionBadges, - // 'mx_RoomTile_invited': isInvite, + 'mx_RoomTile_unread': this.state.numUnread > 0, + 'mx_RoomTile_unreadNotify': this.state.showBadge, + 'mx_RoomTile_highlight': this.state.showBadgeHighlight, + 'mx_RoomTile_invited': this.state.isInvite, // 'mx_RoomTile_menuDisplayed': isMenuDisplayed, - 'mx_RoomTile_noBadges': true, // !badges + 'mx_RoomTile_noBadges': !this.state.showBadge, // 'mx_RoomTile_transparent': this.props.transparent, // 'mx_RoomTile_hasSubtext': subtext && !this.props.collapsed, }); @@ -104,6 +144,17 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_avatar': true, }); + + let badge; + if (this.state.showBadge) { + const badgeClasses = classNames({ + 'mx_RoomTile_badge': true, + 'mx_RoomTile_badgeButton': false, // this.state.badgeHover || isMenuDisplayed + }); + const formattedCount = this.state.isInvite ? `!` : FormattingUtils.formatCount(this.state.numUnread); + badge =
{formattedCount}
; + } + // TODO: the original RoomTile uses state for the room name. Do we need to? let name = this.props.room.name; if (typeof name !== 'string') name = ''; @@ -111,8 +162,8 @@ export default class RoomTile2 extends React.Component { const nameClasses = classNames({ 'mx_RoomTile_name': true, - 'mx_RoomTile_invite': false, - 'mx_RoomTile_badgeShown': false, + 'mx_RoomTile_invite': this.state.isInvite, + 'mx_RoomTile_badgeShown': this.state.showBadge, }); let tooltip = null; @@ -147,6 +198,7 @@ export default class RoomTile2 extends React.Component { {name}
+ {badge} {tooltip} From c37352679d72aec70468d541b2f7cd9483406d30 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 09:34:31 -0600 Subject: [PATCH 22/34] Fix bold rooms not bolding --- src/components/views/rooms/RoomTile2.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index c6c400ce52..c4025fdb53 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -42,6 +42,7 @@ interface IProps { interface IBadgeState { showBadge: boolean; // if numUnread > 0 && !showBadge -> bold room numUnread: number; // used only if showBadge or showBadgeHighlight is true + hasUnread: number; // used to make the room bold showBadgeHighlight: boolean; // make the badge red isInvite: boolean; // show a `!` instead of a number } @@ -98,6 +99,7 @@ export default class RoomTile2 extends React.Component { return { showBadge: (showBadge && shouldShowNotifBadge) || isInvite, numUnread, + hasUnread: showBadge, showBadgeHighlight: (highlightCount > 0 && shouldShowHighlightBadge) || isInvite, isInvite, }; @@ -130,7 +132,7 @@ export default class RoomTile2 extends React.Component { const classes = classNames({ 'mx_RoomTile': true, // 'mx_RoomTile_selected': this.state.selected, - 'mx_RoomTile_unread': this.state.numUnread > 0, + 'mx_RoomTile_unread': this.state.numUnread > 0 || this.state.hasUnread, 'mx_RoomTile_unreadNotify': this.state.showBadge, 'mx_RoomTile_highlight': this.state.showBadgeHighlight, 'mx_RoomTile_invited': this.state.isInvite, From f8cbadaba564cf29e06a81261705882f2783c30f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 12:53:00 -0600 Subject: [PATCH 23/34] Clean up comments in skeleton components --- src/components/views/rooms/RoomList2.tsx | 19 ++++++++++--------- src/components/views/rooms/RoomSublist2.tsx | 16 +++++++++++++--- src/components/views/rooms/RoomTile2.tsx | 20 ++++++++++++++------ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 402a7af014..12a0117505 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -29,6 +29,15 @@ import { ActionPayload } from "../../../dispatcher-types"; import dis from "../../../dispatcher"; import RoomSublist2 from "./RoomSublist2"; +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + ******************************************************************* + */ + interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; onFocus: (ev: React.FocusEvent) => void; @@ -152,6 +161,7 @@ export default class RoomList2 extends React.Component { } private prepareLayouts() { + // TODO: Change layout engine for FTUE support this.unfilteredLayout = new Layout((tagId: string, height: number) => { const sublist = this.sublistRefs[tagId]; if (sublist) sublist.current.setHeight(height); @@ -177,15 +187,6 @@ export default class RoomList2 extends React.Component { }); } - private collectSublistRef(tagId: string, ref: React.RefObject) { - // TODO: Is this needed? - if (!ref) { - delete this.sublistRefs[tagId]; - } else { - this.sublistRefs[tagId] = ref; - } - } - private renderSublists(): React.ReactElement[] { const components: React.ReactElement[] = []; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 21e58abd12..4c3f65b323 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -29,6 +29,15 @@ import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButto import * as FormattingUtils from '../../../utils/FormattingUtils'; import RoomTile2 from "./RoomTile2"; +/******************************************************************* + * CAUTION * + ******************************************************************* + * This is a work in progress implementation and isn't complete or * + * even useful as a component. Please avoid using it until this * + * warning disappears. * + ******************************************************************* + */ + interface IProps { forRooms: boolean; rooms?: Room[]; @@ -50,12 +59,11 @@ interface IProps { interface IState { } -// TODO: Finish stub export default class RoomSublist2 extends React.Component { private headerButton = createRef(); public setHeight(size: number) { - // TODO: Do a thing + // TODO: Do a thing (maybe - height changes are different in FTUE) } private hasTiles(): boolean { @@ -107,8 +115,10 @@ export default class RoomSublist2 extends React.Component { return ( {({onFocus, isActive, ref}) => { + // TODO: Use onFocus const tabIndex = isActive ? 0 : -1; + // TODO: Collapsed state let badge; if (true) { // !isCollapsed const badgeClasses = classNames({ @@ -156,7 +166,7 @@ export default class RoomSublist2 extends React.Component { ); } - // TODO: a11y + // TODO: a11y (see old component) return (
{ private roomTile = createRef(); // TODO: Custom status // TODO: Lock icon - // TODO: DM indicator // TODO: Presence indicator // TODO: e2e shields // TODO: Handle changes to room aesthetics (name, join rules, etc) @@ -78,7 +85,7 @@ export default class RoomTile2 extends React.Component { } public componentWillUnmount() { - + // TODO: Listen for changes to the badge count and update as needed } private updateBadgeCount() { @@ -168,6 +175,7 @@ export default class RoomTile2 extends React.Component { 'mx_RoomTile_badgeShown': this.state.showBadge, }); + // TODO: Support collapsed state properly let tooltip = null; if (false) { // isCollapsed if (this.state.hover) { From aafbd7f2089446aa0c8a9ffc3e2e131ad7ef2e7d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:01:51 -0600 Subject: [PATCH 24/34] Update misc documentation and spell indices correctly --- src/stores/RoomListStore.js | 2 +- src/stores/room-list/RoomListStore2.ts | 4 +- .../list_ordering/ImportanceAlgorithm.ts | 49 +++++++++---------- src/stores/room-list/models.ts | 2 +- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index ccccbcc313..d452d1589e 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -121,7 +121,7 @@ class RoomListStore extends Store { _checkDisabled() { this.disabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); if (this.disabled) { - console.warn("DISABLING LEGACY ROOM LIST STORE"); + console.warn("👋 legacy room list store has been disabled"); } } diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 8bbcfc3c8d..c461aeab66 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -67,7 +67,7 @@ class _RoomListStore extends AsyncStore { private checkEnabled() { this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list"); if (this.enabled) { - console.log("ENABLING NEW ROOM LIST STORE"); + console.log("⚡ new room list store engaged"); } } @@ -225,7 +225,7 @@ class _RoomListStore extends AsyncStore { } if (this.state.tagsEnabled) { - // TODO: Find a more reliable way to get tags + // TODO: Find a more reliable way to get tags (this doesn't work) const roomTags = TagOrderStore.getOrderedTags() || []; console.log("rtags", roomTags); } diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts index 0ebdad1ed1..fd5d4c8163 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts @@ -79,7 +79,6 @@ const CATEGORY_ORDER = [Category.Red, Category.Grey, Category.Bold, Category.Idl */ export class ImportanceAlgorithm extends Algorithm { - // TODO: Update documentation // HOW THIS WORKS // -------------- // @@ -93,19 +92,17 @@ export class ImportanceAlgorithm extends Algorithm { // can be found from `this.indices[tag][category]` and the sticky room information // from `this.stickyRoom`. // - // Room categories are constantly re-evaluated and tracked in the `this.categorized` - // object. Note that this doesn't track rooms by category but instead by room ID. - // The theory is that by knowing the previous position, new desired position, and - // category indices we can avoid tracking multiple complicated maps in memory. - // // The room list store is always provided with the `this.cached` results, which are // updated as needed and not recalculated often. For example, when a room needs to // move within a tag, the array in `this.cached` will be spliced instead of iterated. + // The `indices` help track the positions of each category to make splicing easier. private indices: { // @ts-ignore - TS wants this to be a string but we know better than it [tag: TagID]: ICategoryIndex; } = {}; + + // TODO: Use this (see docs above) private stickyRoom: { roomId: string; tag: TagID; @@ -178,14 +175,14 @@ export class ImportanceAlgorithm extends Algorithm { } const newlyOrganized: Room[] = []; - const newIndicies: ICategoryIndex = {}; + const newIndices: ICategoryIndex = {}; for (const category of CATEGORY_ORDER) { - newIndicies[category] = newlyOrganized.length; + newIndices[category] = newlyOrganized.length; newlyOrganized.push(...categorized[category]); } - this.indices[tagId] = newIndicies; + this.indices[tagId] = newIndices; updatedTagMap[tagId] = newlyOrganized; } } @@ -205,7 +202,7 @@ export class ImportanceAlgorithm extends Algorithm { } const taggedRooms = this.cached[tag]; - const indicies = this.indices[tag]; + const indices = this.indices[tag]; let roomIdx = taggedRooms.indexOf(room); if (roomIdx === -1) { console.warn(`Degrading performance to find missing room in "${tag}": ${room.roomId}`); @@ -217,16 +214,16 @@ export class ImportanceAlgorithm extends Algorithm { // Try to avoid doing array operations if we don't have to: only move rooms within // the categories if we're jumping categories - const oldCategory = this.getCategoryFromIndicies(roomIdx, indicies); + const oldCategory = this.getCategoryFromIndices(roomIdx, indices); if (oldCategory !== category) { - // Move the room and update the indicies - this.moveRoomIndexes(1, oldCategory, category, indicies); + // Move the room and update the indices + this.moveRoomIndexes(1, oldCategory, category, indices); taggedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) - taggedRooms.splice(indicies[category], 0, room); // splice in the new room (pre-adjusted) + taggedRooms.splice(indices[category], 0, room); // splice in the new room (pre-adjusted) // Note: if moveRoomIndexes() is called after the splice then the insert operation // will happen in the wrong place. Because we would have already adjusted the index // for the category, we don't need to determine how the room is moving in the list. - // If we instead tried to insert before updating the indicies, we'd have to determine + // If we instead tried to insert before updating the indices, we'd have to determine // whether the room was moving later (towards IDLE) or earlier (towards RED) from its // current position, as it'll affect the category's start index after we remove the // room from the array. @@ -240,8 +237,8 @@ export class ImportanceAlgorithm extends Algorithm { // array and slot the changed room in quickly. const nextCategoryStartIdx = category === CATEGORY_ORDER[CATEGORY_ORDER.length - 1] ? Number.MAX_SAFE_INTEGER - : indicies[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; - const startIdx = indicies[category]; + : indices[CATEGORY_ORDER[CATEGORY_ORDER.indexOf(category) + 1]]; + const startIdx = indices[category]; const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine const unsortedSlice = taggedRooms.splice(startIdx, numSort); const sorted = await sortRoomsWithAlgorithm(unsortedSlice, tag, this.sortAlgorithms[tag]); @@ -253,12 +250,12 @@ export class ImportanceAlgorithm extends Algorithm { return changed; } - private getCategoryFromIndicies(index: number, indicies: ICategoryIndex): Category { + private getCategoryFromIndices(index: number, indices: ICategoryIndex): Category { for (let i = 0; i < CATEGORY_ORDER.length; i++) { const category = CATEGORY_ORDER[i]; const isLast = i === (CATEGORY_ORDER.length - 1); - const startIdx = indicies[category]; - const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indicies[CATEGORY_ORDER[i + 1]]; + const startIdx = indices[category]; + const endIdx = isLast ? Number.MAX_SAFE_INTEGER : indices[CATEGORY_ORDER[i + 1]]; if (index >= startIdx && index < endIdx) { return category; } @@ -268,21 +265,21 @@ export class ImportanceAlgorithm extends Algorithm { throw new Error("Programming error: somehow you've ended up with an index that isn't in a category"); } - private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indicies: ICategoryIndex) { + private moveRoomIndexes(nRooms: number, fromCategory: Category, toCategory: Category, indices: ICategoryIndex) { // We have to update the index of the category *after* the from/toCategory variables - // in order to update the indicies correctly. Because the room is moving from/to those + // in order to update the indices correctly. Because the room is moving from/to those // categories, the next category's index will change - not the category we're modifying. // We also need to update subsequent categories as they'll all shift by nRooms, so we // loop over the order to achieve that. for (let i = CATEGORY_ORDER.indexOf(fromCategory) + 1; i < CATEGORY_ORDER.length; i++) { const nextCategory = CATEGORY_ORDER[i]; - indicies[nextCategory] -= nRooms; + indices[nextCategory] -= nRooms; } for (let i = CATEGORY_ORDER.indexOf(toCategory) + 1; i < CATEGORY_ORDER.length; i++) { const nextCategory = CATEGORY_ORDER[i]; - indicies[nextCategory] += nRooms; + indices[nextCategory] += nRooms; } // Do a quick check to see if we've completely broken the index @@ -290,9 +287,9 @@ export class ImportanceAlgorithm extends Algorithm { const lastCat = CATEGORY_ORDER[i - 1]; const thisCat = CATEGORY_ORDER[i]; - if (indicies[lastCat] > indicies[thisCat]) { + if (indices[lastCat] > indices[thisCat]) { // "should never happen" disclaimer goes here - console.warn(`!! Room list index corruption: ${lastCat} (i:${indicies[lastCat]}) is greater than ${thisCat} (i:${indicies[thisCat]}) - category indicies are likely desynced from reality`); + console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`); // TODO: Regenerate index when this happens } diff --git a/src/stores/room-list/models.ts b/src/stores/room-list/models.ts index 428378a7aa..a0c2621077 100644 --- a/src/stores/room-list/models.ts +++ b/src/stores/room-list/models.ts @@ -38,5 +38,5 @@ export type TagID = string | DefaultTagID; export enum RoomUpdateCause { Timeline = "TIMELINE", - RoomRead = "ROOM_READ", + RoomRead = "ROOM_READ", // TODO: Use this. } From 9fbd489b3b1fb83e7f3710ee22fd56a2c8e533e2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:03:43 -0600 Subject: [PATCH 25/34] Update i18n --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d6ff5f70eb..c4912eb4d0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -406,7 +406,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Use the improved room list component (refresh to apply changes, in development)": "Use the improved room list component (refresh to apply changes, in development)", + "Use the improved room list (in development - refresh to apply changes)": "Use the improved room list (in development - refresh to apply changes)", "Support adding custom themes": "Support adding custom themes", "Enable cross-signing to verify per-user instead of per-session": "Enable cross-signing to verify per-user instead of per-session", "Show info about bridges in room settings": "Show info about bridges in room settings", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 110fd4238b..cd9ec430bf 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -133,7 +133,7 @@ export const SETTINGS = { }, "feature_new_room_list": { isFeature: true, - displayName: _td("Use the improved room list component (refresh to apply changes, in development)"), + displayName: _td("Use the improved room list (in development - refresh to apply changes)"), supportedLevels: LEVELS_FEATURE, default: false, }, From 8e047c3731a436e9cc660ad0b1078f3b971d8121 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:26:17 -0600 Subject: [PATCH 26/34] Update README for room list store --- src/stores/room-list/README.md | 49 +++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index 0dd6c104d8..020108878b 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -7,20 +7,21 @@ It's so complicated it needs its own README. There's two main kinds of algorithms involved in the room list store: list ordering and tag sorting. Throughout the code an intentional decision has been made to call them the List Algorithm and Sorting Algorithm respectively. The list algorithm determines the behaviour of the room list whereas the sorting -algorithm determines how individual tags (lists of rooms, sometimes called sublists) are ordered. +algorithm determines how rooms get ordered within tags affected by the list algorithm. -Behaviour of the room list takes the shape of default sorting on tags in most cases, though it can -override what is happening at the tag level depending on the algorithm used (as is the case with the -importance algorithm, described later). +Behaviour of the room list takes the shape of determining what features the room list supports, as well +as determining where and when to apply the sorting algorithm in a tag. The importance algorithm, which +is described later in this doc, is an example of an algorithm which makes heavy behavioural changes +to the room list. Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm the power to decide when and how to apply the tag sorting, if at all. ### Tag sorting algorithm: Alphabetical -When used, rooms in a given tag will be sorted alphabetically, where the alphabet is determined by a -simple string comparison operation (essentially giving the browser the problem of figuring out if A -comes before Z). +When used, rooms in a given tag will be sorted alphabetically, where the alphabet's order is a problem +for the browser. All we do is a simple string comparison and expect the browser to return something +useful. ### Tag sorting algorithm: Manual @@ -30,7 +31,7 @@ of `order` cause rooms to appear closer to the top of the list. ### Tag sorting algorithm: Recent -Rooms are ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm +Rooms get ordered by the timestamp of the most recent useful message. Usefulness is yet another algorithm in the room list system which determines whether an event type is capable of bubbling up in the room list. Normally events like room messages, stickers, and room security changes will be considered useful enough to cause a shift in time. @@ -49,7 +50,7 @@ its relative deterministic behaviour. ### List ordering algorithm: Importance On the other end of the spectrum, this is the most complicated algorithm which exists. There's major -behavioural changes and the tag sorting algorithm is selectively applied depending on circumstances. +behavioural changes, and the tag sorting algorithm gets selectively applied depending on circumstances. Each tag which is not manually ordered gets split into 4 sections or "categories". Manually ordered tags simply get the manual sorting algorithm applied to them with no further involvement from the importance @@ -58,34 +59,37 @@ relative (perceived) importance to the user: * **Red**: The room has unread mentions waiting for the user. * **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread - messages which cause a push notification or badge count. Typically this is the default as rooms are + messages which cause a push notification or badge count. Typically, this is the default as rooms get set to 'All Messages'. * **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without a badge/notification count (or 'Mentions Only'/'Muted'). -* **Idle**: No relevant activity has occurred in the room since the user last read it. +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user + last read it. -Conveniently, each tag is ordered by those categories as presented: red rooms appear above grey, grey -above idle, etc. +Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey +above bold, etc. Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm -is applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) -being sorted alphabetically amongst each other and the grey rooms sorted amongst each other, but +gets applied to each category in a sub-sub-list fashion. This should result in the red rooms (for example) +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top. + + The algorithm also has a concept of a 'sticky' room which is the room the user is currently viewing. The sticky room will remain in position on the room list regardless of other factors going on as typically clicking on a room will cause it to change categories into 'idle'. This is done by preserving N rooms -above the selected room at all times where N is the number of rooms above the selected rooms when it was +above the selected room at all times, where N is the number of rooms above the selected rooms when it was selected. For example, if the user has 3 red rooms and selects the middle room, they will always see exactly one -room above their selection at all times. If they receive another notification and the tag ordering is set -to Recent, they'll see the new notification go to the top position and the one that was previously there -fall behind the sticky room. +room above their selection at all times. If they receive another notification, and the tag ordering is +specified as Recent, they'll see the new notification go to the top position, and the one that was previously +there fall behind the sticky room. The sticky room's category is technically 'idle' while being viewed and is explicitly pulled out of the tag sorting algorithm's input as it must maintain its position in the list. When the user moves to another -room, the previous sticky room is recalculated to determine which category it needs to be in as the user +room, the previous sticky room gets recalculated to determine which category it needs to be in as the user could have been scrolled up while new messages were received. Further, the sticky room is not aware of category boundaries and thus the user can see a shift in what @@ -112,7 +116,10 @@ all kinds of filtering. The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` superclass is also responsible for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: -tags are defined on rooms and are not defined as a collection of rooms (unlike how they are presented +tags get defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the user). Various list-specific utilities are also included, though they are expected to move somewhere more general when needed. For example, the `membership` utilities could easily be moved elsewhere as needed. + +The various bits throughout the room list store should also have jsdoc of some kind to help describe +what they do and how they work. From 6cb1efc1a48b476891b488ace83108358883f0b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 13:45:17 -0600 Subject: [PATCH 27/34] Use the new TS dispatcher --- src/actions/RoomListActions.ts | 5 +++-- src/components/views/rooms/RoomList2.tsx | 4 ++-- src/components/views/rooms/RoomTile2.tsx | 2 +- src/dispatcher-types.ts | 28 ------------------------ src/stores/AsyncStore.ts | 2 +- src/stores/room-list/RoomListStore2.ts | 3 ++- 6 files changed, 9 insertions(+), 35 deletions(-) delete mode 100644 src/dispatcher-types.ts diff --git a/src/actions/RoomListActions.ts b/src/actions/RoomListActions.ts index eb9831ec47..e15e1b0c65 100644 --- a/src/actions/RoomListActions.ts +++ b/src/actions/RoomListActions.ts @@ -16,7 +16,7 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore, { TAG_DM } from '../stores/RoomListStore'; +import { TAG_DM } from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -24,6 +24,7 @@ import * as sdk from '../index'; import { MatrixClient } from "matrix-js-sdk/src/client"; import { Room } from "matrix-js-sdk/src/models/room"; import { AsyncActionPayload } from "../dispatcher/payloads"; +import { RoomListStoreTempProxy } from "../stores/room-list/RoomListStoreTempProxy"; export default class RoomListActions { /** @@ -51,7 +52,7 @@ export default class RoomListActions { // Is the tag ordered manually? if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { - const lists = RoomListStore.getRoomLists(); + const lists = RoomListStoreTempProxy.getRoomLists(); const newList = [...lists[newTag]]; newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index 12a0117505..a5d0175f01 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -25,9 +25,9 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/Roo import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { Dispatcher } from "flux"; -import { ActionPayload } from "../../../dispatcher-types"; -import dis from "../../../dispatcher"; +import dis from "../../../dispatcher/dispatcher"; import RoomSublist2 from "./RoomSublist2"; +import { ActionPayload } from "../../../dispatcher/payloads"; /******************************************************************* * CAUTION * diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 8f1b0a3f7a..53e56d7a1e 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -24,7 +24,7 @@ import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; import AccessibleButton from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; import Tooltip from "../../views/elements/Tooltip"; -import dis from '../../../dispatcher'; +import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import * as RoomNotifs from '../../../RoomNotifs'; import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership"; diff --git a/src/dispatcher-types.ts b/src/dispatcher-types.ts deleted file mode 100644 index 16fac0c849..0000000000 --- a/src/dispatcher-types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import * as flux from "flux"; -import dis from "./dispatcher"; - -// TODO: Merge this with the dispatcher and centralize types - -export interface ActionPayload { - [property: string]: any; // effectively "extends Object" - action: string; -} - -// For ease of reference in TypeScript classes -export const defaultDispatcher: flux.Dispatcher = dis; diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts index 5e19e17248..d79fd220b2 100644 --- a/src/stores/AsyncStore.ts +++ b/src/stores/AsyncStore.ts @@ -16,8 +16,8 @@ limitations under the License. import { EventEmitter } from 'events'; import AwaitLock from 'await-lock'; -import { ActionPayload } from "../dispatcher-types"; import { Dispatcher } from "flux"; +import { ActionPayload } from "../dispatcher/payloads"; /** * The event/channel to listen for in an AsyncStore. diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index c461aeab66..4f38a25d95 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -16,7 +16,6 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk/src/client"; -import { ActionPayload, defaultDispatcher } from "../../dispatcher-types"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { Algorithm } from "./algorithms/list_ordering/Algorithm"; @@ -25,6 +24,8 @@ import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { getListAlgorithmInstance } from "./algorithms/list_ordering"; +import { ActionPayload } from "../../dispatcher/payloads"; +import defaultDispatcher from "../../dispatcher/dispatcher"; interface IState { tagsEnabled?: boolean; From 91a997da1435134f1f42065b3a589a5d49b2ba07 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 14:06:48 -0600 Subject: [PATCH 28/34] Replace ChaoticAlgorithm for tag sorting with deterministic behaviour aka: implement the algorithms. --- ...ticAlgorithm.ts => AlphabeticAlgorithm.ts} | 11 ++- .../algorithms/tag_sorting/RecentAlgorithm.ts | 81 +++++++++++++++++++ .../room-list/algorithms/tag_sorting/index.ts | 7 +- 3 files changed, 92 insertions(+), 7 deletions(-) rename src/stores/room-list/algorithms/tag_sorting/{ChaoticAlgorithm.ts => AlphabeticAlgorithm.ts} (70%) create mode 100644 src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts similarity index 70% rename from src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts rename to src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts index 31846d084a..8d74ebd11e 100644 --- a/src/stores/room-list/algorithms/tag_sorting/ChaoticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts @@ -17,13 +17,16 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { TagID } from "../../models"; import { IAlgorithm } from "./IAlgorithm"; +import { MatrixClientPeg } from "../../../../MatrixClientPeg"; +import * as Unread from "../../../../Unread"; /** - * A demonstration to test the API surface. - * TODO: Remove this before landing + * Sorts rooms according to the browser's determination of alphabetic. */ -export class ChaoticAlgorithm implements IAlgorithm { +export class AlphabeticAlgorithm implements IAlgorithm { public async sortRooms(rooms: Room[], tagId: TagID): Promise { - return rooms; + return rooms.sort((a, b) => { + return a.name.localeCompare(b.name); + }); } } diff --git a/src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts new file mode 100644 index 0000000000..df84c051f0 --- /dev/null +++ b/src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts @@ -0,0 +1,81 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; +import { TagID } from "../../models"; +import { IAlgorithm } from "./IAlgorithm"; +import { MatrixClientPeg } from "../../../../MatrixClientPeg"; +import * as Unread from "../../../../Unread"; + +/** + * Sorts rooms according to the last event's timestamp in each room that seems + * useful to the user. + */ +export class RecentAlgorithm implements IAlgorithm { + public async sortRooms(rooms: Room[], tagId: TagID): Promise { + // We cache the timestamp lookup to avoid iterating forever on the timeline + // of events. This cache only survives a single sort though. + // We wouldn't need this if `.sort()` didn't constantly try and compare all + // of the rooms to each other. + + // TODO: We could probably improve the sorting algorithm here by finding changes. + // For example, if we spent a little bit of time to determine which elements have + // actually changed (probably needs to be done higher up?) then we could do an + // insertion sort or similar on the limited set of changes. + + const tsCache: { [roomId: string]: number } = {}; + const getLastTs = (r: Room) => { + if (tsCache[r.roomId]) { + return tsCache[r.roomId]; + } + + const ts = (() => { + // Apparently we can have rooms without timelines, at least under testing + // environments. Just return MAX_INT when this happens. + if (!r || !r.timeline) { + return Number.MAX_SAFE_INTEGER; + } + + for (let i = r.timeline.length - 1; i >= 0; --i) { + const ev = r.timeline[i]; + if (!ev.getTs()) continue; // skip events that don't have timestamps (tests only?) + + // TODO: Don't assume we're using the same client as the peg + if (ev.getSender() === MatrixClientPeg.get().getUserId() + || Unread.eventTriggersUnreadCount(ev)) { + return ev.getTs(); + } + } + + // we might only have events that don't trigger the unread indicator, + // in which case use the oldest event even if normally it wouldn't count. + // This is better than just assuming the last event was forever ago. + if (r.timeline.length && r.timeline[0].getTs()) { + return r.timeline[0].getTs(); + } else { + return Number.MAX_SAFE_INTEGER; + } + })(); + + tsCache[r.roomId] = ts; + return ts; + }; + + return rooms.sort((a, b) => { + return getLastTs(a) - getLastTs(b); + }); + } +} diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag_sorting/index.ts index 155c0f0118..c22865f5ba 100644 --- a/src/stores/room-list/algorithms/tag_sorting/index.ts +++ b/src/stores/room-list/algorithms/tag_sorting/index.ts @@ -14,16 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { SortAlgorithm } from "../models"; import { ManualAlgorithm } from "./ManualAlgorithm"; import { IAlgorithm } from "./IAlgorithm"; import { TagID } from "../../models"; import { Room } from "matrix-js-sdk/src/models/room"; +import { RecentAlgorithm } from "./RecentAlgorithm"; +import { AlphabeticAlgorithm } from "./AlphabeticAlgorithm"; const ALGORITHM_INSTANCES: { [algorithm in SortAlgorithm]: IAlgorithm } = { - [SortAlgorithm.Recent]: new ChaoticAlgorithm(), - [SortAlgorithm.Alphabetic]: new ChaoticAlgorithm(), + [SortAlgorithm.Recent]: new RecentAlgorithm(), + [SortAlgorithm.Alphabetic]: new AlphabeticAlgorithm(), [SortAlgorithm.Manual]: new ManualAlgorithm(), }; From b7ba9b3c413043ac759aacb15fe6650f2be2a73b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 14:16:26 -0600 Subject: [PATCH 29/34] Replace ChaoticAlgorithm with NaturalAlgorithm for list behaviour --- .../list_ordering/ChaoticAlgorithm.ts | 38 ------------- .../list_ordering/NaturalAlgorithm.ts | 56 +++++++++++++++++++ .../algorithms/list_ordering/index.ts | 4 +- 3 files changed, 58 insertions(+), 40 deletions(-) delete mode 100644 src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts create mode 100644 src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts diff --git a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts deleted file mode 100644 index 185fb606fb..0000000000 --- a/src/stores/room-list/algorithms/list_ordering/ChaoticAlgorithm.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2020 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { Algorithm } from "./Algorithm"; -import { ITagMap } from "../models"; - -/** - * A demonstration/temporary algorithm to verify the API surface works. - * TODO: Remove this before shipping - */ -export class ChaoticAlgorithm extends Algorithm { - - constructor() { - super(); - console.log("Constructed a ChaoticAlgorithm"); - } - - protected async generateFreshTags(updatedTagMap: ITagMap): Promise { - return Promise.resolve(); - } - - public async handleRoomUpdate(room, cause): Promise { - return Promise.resolve(false); - } -} diff --git a/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts new file mode 100644 index 0000000000..1265564352 --- /dev/null +++ b/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts @@ -0,0 +1,56 @@ +/* +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 { Algorithm } from "./Algorithm"; +import { ITagMap } from "../models"; +import { sortRoomsWithAlgorithm } from "../tag_sorting"; + +/** + * Uses the natural tag sorting algorithm order to determine tag ordering. No + * additional behavioural changes are present. + */ +export class NaturalAlgorithm extends Algorithm { + + constructor() { + super(); + console.log("Constructed a NaturalAlgorithm"); + } + + protected async generateFreshTags(updatedTagMap: ITagMap): Promise { + for (const tagId of Object.keys(updatedTagMap)) { + const unorderedRooms = updatedTagMap[tagId]; + + const sortBy = this.sortAlgorithms[tagId]; + if (!sortBy) throw new Error(`${tagId} does not have a sorting algorithm`); + + updatedTagMap[tagId] = await sortRoomsWithAlgorithm(unorderedRooms, tagId, sortBy); + } + } + + public async handleRoomUpdate(room, cause): Promise { + const tags = this.roomIdsToTags[room.roomId]; + if (!tags) { + console.warn(`No tags known for "${room.name}" (${room.roomId})`); + return false; + } + for (const tag of tags) { + // TODO: Optimize this loop to avoid useless operations + // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags + this.cached[tag] = await sortRoomsWithAlgorithm(this.cached[tag], tag, this.sortAlgorithms[tag]); + } + return true; // assume we changed something + } +} diff --git a/src/stores/room-list/algorithms/list_ordering/index.ts b/src/stores/room-list/algorithms/list_ordering/index.ts index 35f4af14cf..bcccd150cd 100644 --- a/src/stores/room-list/algorithms/list_ordering/index.ts +++ b/src/stores/room-list/algorithms/list_ordering/index.ts @@ -15,12 +15,12 @@ limitations under the License. */ import { Algorithm } from "./Algorithm"; -import { ChaoticAlgorithm } from "./ChaoticAlgorithm"; import { ImportanceAlgorithm } from "./ImportanceAlgorithm"; import { ListAlgorithm } from "../models"; +import { NaturalAlgorithm } from "./NaturalAlgorithm"; const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: () => Algorithm } = { - [ListAlgorithm.Natural]: () => new ChaoticAlgorithm(), + [ListAlgorithm.Natural]: () => new NaturalAlgorithm(), [ListAlgorithm.Importance]: () => new ImportanceAlgorithm(), }; From 5cfe29de668161ab4ed82e05e4890deee94b5893 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 14 May 2020 14:20:01 -0600 Subject: [PATCH 30/34] Update AsyncStore's docs to be slightly more clear --- src/stores/AsyncStore.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts index d79fd220b2..3519050078 100644 --- a/src/stores/AsyncStore.ts +++ b/src/stores/AsyncStore.ts @@ -29,9 +29,11 @@ export const UPDATE_EVENT = "update"; * of everything needing to happen in a dispatch cycle, everything can * happen async to that cycle. * - * The store's core principle is Object.assign(), therefore it is recommended - * to break out your state to be as safe as possible. The state mutations are - * also locked, preventing concurrent writes. + * The store operates by using Object.assign() to mutate state - it sends the + * state objects (current and new) through the function onto a new empty + * object. Because of this, it is recommended to break out your state to be as + * safe as possible. The state mutations are also locked, preventing concurrent + * writes. * * All updates to the store happen on the UPDATE_EVENT event channel with the * one argument being the instance of the store. From 21e471375ee2cb52d950c27c337530f389dbe0d7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 Mar 2020 13:34:56 -0600 Subject: [PATCH 31/34] Revert "Add temporary timing functions to old RoomListStore" This reverts commit 82b55ffd7717b4256690f43eb6891b173fe3cec9. --- src/stores/RoomListStore.js | 60 +++++-------------------------------- 1 file changed, 8 insertions(+), 52 deletions(-) diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index b0690e7e69..c19b2f8bc2 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -58,27 +58,7 @@ export const ALGO_RECENT = "recent"; const CATEGORY_ORDER = [CATEGORY_RED, CATEGORY_GREY, CATEGORY_BOLD, CATEGORY_IDLE]; -function debugLog(...msg) { - console.log(`[RoomListStore:Debug] `, ...msg); -} - -const timers = {}; -let timerCounter = 0; -function startTimer(fnName) { - const id = `${fnName}_${(new Date()).getTime()}_${timerCounter++}`; - debugLog(`Started timer for ${fnName} with ID ${id}`); - timers[id] = {start: (new Date()).getTime(), fnName}; - return id; -} - -function endTimer(id) { - const timer = timers[id]; - delete timers[id]; - const diff = (new Date()).getTime() - timer.start; - debugLog(`${timer.fnName} took ${diff}ms (ID: ${id})`); -} - -function getListAlgorithm(listKey, settingAlgorithm) { +const getListAlgorithm = (listKey, settingAlgorithm) => { // apply manual sorting only to m.favourite, otherwise respect the global setting // all the known tags are listed explicitly here to simplify future changes switch (listKey) { @@ -93,7 +73,7 @@ function getListAlgorithm(listKey, settingAlgorithm) { default: // custom-tags return ALGO_MANUAL; } -} +}; const knownLists = new Set([ "m.favourite", @@ -377,7 +357,6 @@ class RoomListStore extends Store { } _getRecommendedTagsForRoom(room) { - const timerId = startTimer(`_getRecommendedTagsForRoom(room:"${room.roomId}")`); const tags = []; const myMembership = room.getMyMembership(); @@ -403,12 +382,11 @@ class RoomListStore extends Store { tags.push("im.vector.fake.archived"); } - endTimer(timerId); + return tags; } _slotRoomIntoList(room, category, tag, existingEntries, newList, lastTimestampFn) { - const timerId = startTimer(`_slotRoomIntoList(room:"${room.roomId}", "${category}", "${tag}", existingEntries: "${existingEntries.length}", "${newList}", lastTimestampFn:"${lastTimestampFn !== null}")`); const targetCategoryIndex = CATEGORY_ORDER.indexOf(category); let categoryComparator = (a, b) => lastTimestampFn(a.room) >= lastTimestampFn(b.room); @@ -520,16 +498,11 @@ class RoomListStore extends Store { pushedEntry = true; } - endTimer(timerId); return pushedEntry; } _setRoomCategory(room, category) { - const timerId = startTimer(`_setRoomCategory(room:"${room.roomId}", "${category}")`); - if (!room) { - endTimer(timerId); - return; // This should only happen in tests - } + if (!room) return; // This should only happen in tests const listsClone = {}; @@ -626,11 +599,9 @@ class RoomListStore extends Store { } this._setState({lists: listsClone}); - endTimer(timerId); } _generateInitialRoomLists() { - const timerId = startTimer(`_generateInitialRoomLists()`); // Log something to show that we're throwing away the old results. This is for the inevitable // question of "why is 100% of my CPU going towards Riot?" - a quick look at the logs would reveal // that something is wrong with the RoomListStore. @@ -743,7 +714,6 @@ class RoomListStore extends Store { lists, ready: true, // Ready to receive updates to ordering }); - endTimer(timerId); } _eventTriggersRecentReorder(ev) { @@ -756,9 +726,7 @@ class RoomListStore extends Store { _tsOfNewestEvent(room) { // Apparently we can have rooms without timelines, at least under testing // environments. Just return MAX_INT when this happens. - if (!room || !room.timeline) { - return Number.MAX_SAFE_INTEGER; - } + if (!room || !room.timeline) return Number.MAX_SAFE_INTEGER; for (let i = room.timeline.length - 1; i >= 0; --i) { const ev = room.timeline[i]; @@ -778,35 +746,23 @@ class RoomListStore extends Store { } _calculateCategory(room) { - const timerId = startTimer(`_calculateCategory(room:"${room.roomId}")`); if (!this._state.orderImportantFirst) { // Effectively disable the categorization of rooms if we're supposed to // be sorting by more recent messages first. This triggers the timestamp // comparison bit of _setRoomCategory and _recentsComparator instead of // the category ordering. - endTimer(timerId); return CATEGORY_IDLE; } const mentions = room.getUnreadNotificationCount("highlight") > 0; - if (mentions) { - endTimer(timerId); - return CATEGORY_RED; - } + if (mentions) return CATEGORY_RED; let unread = room.getUnreadNotificationCount() > 0; - if (unread) { - endTimer(timerId); - return CATEGORY_GREY; - } + if (unread) return CATEGORY_GREY; unread = Unread.doesRoomHaveUnreadMessages(room); - if (unread) { - endTimer(timerId); - return CATEGORY_BOLD; - } + if (unread) return CATEGORY_BOLD; - endTimer(timerId); return CATEGORY_IDLE; } From 559dd98d01c0192f9a3d5c7ee56d77b9324d0ac6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:53:16 -0600 Subject: [PATCH 32/34] Fix comment style to be less bothersome --- src/components/views/rooms/RoomList2.tsx | 3 +-- src/components/views/rooms/RoomSublist2.tsx | 3 +-- src/components/views/rooms/RoomTile2.tsx | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx index a5d0175f01..d0c147c953 100644 --- a/src/components/views/rooms/RoomList2.tsx +++ b/src/components/views/rooms/RoomList2.tsx @@ -35,8 +35,7 @@ import { ActionPayload } from "../../../dispatcher/payloads"; * This is a work in progress implementation and isn't complete or * * even useful as a component. Please avoid using it until this * * warning disappears. * - ******************************************************************* - */ + *******************************************************************/ interface IProps { onKeyDown: (ev: React.KeyboardEvent) => void; diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 4c3f65b323..e2f489b959 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -35,8 +35,7 @@ import RoomTile2 from "./RoomTile2"; * This is a work in progress implementation and isn't complete or * * even useful as a component. Please avoid using it until this * * warning disappears. * - ******************************************************************* - */ + *******************************************************************/ interface IProps { forRooms: boolean; diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 53e56d7a1e..42b65cba87 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -37,8 +37,7 @@ import * as FormattingUtils from "../../../utils/FormattingUtils"; * This is a work in progress implementation and isn't complete or * * even useful as a component. Please avoid using it until this * * warning disappears. * - ******************************************************************* - */ + *******************************************************************/ interface IProps { room: Room; From a11985f239831290c4da4507c46535ea74f06838 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:54:38 -0600 Subject: [PATCH 33/34] Which component? The room list! --- src/stores/room-list/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/room-list/README.md b/src/stores/room-list/README.md index 020108878b..82a6e841db 100644 --- a/src/stores/room-list/README.md +++ b/src/stores/room-list/README.md @@ -106,7 +106,7 @@ put the sticky room in a position where it's had to decrease N will not increase ## Responsibilities of the store -The store is responsible for the ordering, upkeep, and tracking of all rooms. The component simply gets +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets an object containing the tags it needs to worry about and the rooms within. The room list component will decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering. From e3c0b4711686231c4704882dfbfbe72000911c05 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 21 May 2020 11:56:04 -0600 Subject: [PATCH 34/34] Hyphenize algorithm directories --- src/stores/room-list/RoomListStore2.ts | 4 ++-- .../algorithms/{list_ordering => list-ordering}/Algorithm.ts | 0 .../{list_ordering => list-ordering}/ImportanceAlgorithm.ts | 2 +- .../{list_ordering => list-ordering}/NaturalAlgorithm.ts | 2 +- .../algorithms/{list_ordering => list-ordering}/index.ts | 0 .../{tag_sorting => tag-sorting}/AlphabeticAlgorithm.ts | 0 .../algorithms/{tag_sorting => tag-sorting}/IAlgorithm.ts | 0 .../{tag_sorting => tag-sorting}/ManualAlgorithm.ts | 0 .../{tag_sorting => tag-sorting}/RecentAlgorithm.ts | 0 .../algorithms/{tag_sorting => tag-sorting}/index.ts | 0 10 files changed, 4 insertions(+), 4 deletions(-) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/Algorithm.ts (100%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/ImportanceAlgorithm.ts (99%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/NaturalAlgorithm.ts (97%) rename src/stores/room-list/algorithms/{list_ordering => list-ordering}/index.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/AlphabeticAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/IAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/ManualAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/RecentAlgorithm.ts (100%) rename src/stores/room-list/algorithms/{tag_sorting => tag-sorting}/index.ts (100%) diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts index 4f38a25d95..881b8fd3cf 100644 --- a/src/stores/room-list/RoomListStore2.ts +++ b/src/stores/room-list/RoomListStore2.ts @@ -18,12 +18,12 @@ limitations under the License. import { MatrixClient } from "matrix-js-sdk/src/client"; import SettingsStore from "../../settings/SettingsStore"; import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; -import { Algorithm } from "./algorithms/list_ordering/Algorithm"; +import { Algorithm } from "./algorithms/list-ordering/Algorithm"; import TagOrderStore from "../TagOrderStore"; import { AsyncStore } from "../AsyncStore"; import { Room } from "matrix-js-sdk/src/models/room"; import { ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; -import { getListAlgorithmInstance } from "./algorithms/list_ordering"; +import { getListAlgorithmInstance } from "./algorithms/list-ordering"; import { ActionPayload } from "../../dispatcher/payloads"; import defaultDispatcher from "../../dispatcher/dispatcher"; diff --git a/src/stores/room-list/algorithms/list_ordering/Algorithm.ts b/src/stores/room-list/algorithms/list-ordering/Algorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/list_ordering/Algorithm.ts rename to src/stores/room-list/algorithms/list-ordering/Algorithm.ts diff --git a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts similarity index 99% rename from src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts rename to src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index fd5d4c8163..c72cdc2e1c 100644 --- a/src/stores/room-list/algorithms/list_ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -19,7 +19,7 @@ import { Algorithm } from "./Algorithm"; import { Room } from "matrix-js-sdk/src/models/room"; import { RoomUpdateCause, TagID } from "../../models"; import { ITagMap, SortAlgorithm } from "../models"; -import { sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; import * as Unread from '../../../../Unread'; /** diff --git a/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts similarity index 97% rename from src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts rename to src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index 1265564352..44a501e592 100644 --- a/src/stores/room-list/algorithms/list_ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -16,7 +16,7 @@ limitations under the License. import { Algorithm } from "./Algorithm"; import { ITagMap } from "../models"; -import { sortRoomsWithAlgorithm } from "../tag_sorting"; +import { sortRoomsWithAlgorithm } from "../tag-sorting"; /** * Uses the natural tag sorting algorithm order to determine tag ordering. No diff --git a/src/stores/room-list/algorithms/list_ordering/index.ts b/src/stores/room-list/algorithms/list-ordering/index.ts similarity index 100% rename from src/stores/room-list/algorithms/list_ordering/index.ts rename to src/stores/room-list/algorithms/list-ordering/index.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/AlphabeticAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/IAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/ManualAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/RecentAlgorithm.ts rename to src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts diff --git a/src/stores/room-list/algorithms/tag_sorting/index.ts b/src/stores/room-list/algorithms/tag-sorting/index.ts similarity index 100% rename from src/stores/room-list/algorithms/tag_sorting/index.ts rename to src/stores/room-list/algorithms/tag-sorting/index.ts