diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss
index 633c33feea..47de95dfc7 100644
--- a/res/css/views/rooms/_RoomSublist2.scss
+++ b/res/css/views/rooms/_RoomSublist2.scss
@@ -47,12 +47,6 @@ limitations under the License.
         padding-bottom: 8px;
         height: 24px;
 
-        // Hide the header container if the contained element is stickied.
-        // We don't use display:none as that causes the header to go away too.
-        &.mx_RoomSublist2_headerContainer_hasSticky {
-            height: 0;
-        }
-
         .mx_RoomSublist2_stickable {
             flex: 1;
             max-width: 100%;
@@ -180,6 +174,15 @@ limitations under the License.
         }
     }
 
+    // In the general case, we leave height of headers alone even if sticky, so
+    // that the sublists below them do not jump. However, that leaves a gap
+    // when scrolled to the top above the first sublist (whose header can only
+    // ever stick to top), so we force height to 0 for only that first header.
+    // See also https://github.com/vector-im/riot-web/issues/14429.
+    &:first-child .mx_RoomSublist2_headerContainer {
+        height: 0;
+    }
+
     .mx_RoomSublist2_resizeBox {
         position: relative;
 
@@ -196,6 +199,8 @@ limitations under the License.
             // as the box model should be top aligned. Happens in both FF and Chromium
             display: flex;
             flex-direction: column;
+
+            mask-image: linear-gradient(0deg, transparent, black 3px);
         }
 
         .mx_RoomSublist2_resizerHandles_showNButton {
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 3f970ea8c3..fc52296d8b 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -37,9 +37,6 @@ declare global {
         mx_RoomListStore2: RoomListStore2;
         mx_RoomListLayoutStore: RoomListLayoutStore;
         mxPlatformPeg: PlatformPeg;
-
-        // TODO: Remove flag before launch: https://github.com/vector-im/riot-web/issues/14231
-        mx_QuietRoomListLogging: boolean;
     }
 
     // workaround for https://github.com/microsoft/TypeScript/issues/30933
diff --git a/src/components/structures/LeftPanel2.tsx b/src/components/structures/LeftPanel2.tsx
index 3c8994b1c0..d41a7278d9 100644
--- a/src/components/structures/LeftPanel2.tsx
+++ b/src/components/structures/LeftPanel2.tsx
@@ -35,17 +35,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomLi
 import {Key} from "../../Keyboard";
 import IndicatorScrollbar from "../structures/IndicatorScrollbar";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
 
-/*******************************************************************
- *   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 {
     isMinimized: boolean;
     resizeNotifier: ResizeNotifier;
@@ -111,6 +102,10 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
         const newVal = BreadcrumbsStore.instance.visible;
         if (newVal !== this.state.showBreadcrumbs) {
             this.setState({showBreadcrumbs: newVal});
+
+            // Update the sticky headers too as the breadcrumbs will be popping in or out.
+            if (!this.listContainerRef.current) return; // ignore: no headers to sticky
+            this.handleStickyHeaders(this.listContainerRef.current);
         }
     };
 
@@ -170,7 +165,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
         // layout updates.
         for (const header of targetStyles.keys()) {
             const style = targetStyles.get(header);
-            const headerContainer = header.parentElement; // .mx_RoomSublist2_headerContainer
 
             if (style.makeInvisible) {
                 // we will have already removed the 'display: none', so add it back.
@@ -187,19 +181,29 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
                 if (header.style.top !== newTop) {
                     header.style.top = newTop;
                 }
-            } else if (style.stickyBottom) {
+            } else {
+                if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
+                    header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
+                }
+                if (header.style.top) {
+                    header.style.removeProperty('top');
+                }
+            }
+
+            if (style.stickyBottom) {
                 if (!header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
                     header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
                 }
+            } else {
+                if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
+                    header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
+                }
             }
 
             if (style.stickyTop || style.stickyBottom) {
                 if (!header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
                     header.classList.add("mx_RoomSublist2_headerContainer_sticky");
                 }
-                if (!headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
-                    headerContainer.classList.add("mx_RoomSublist2_headerContainer_hasSticky");
-                }
 
                 const newWidth = `${headerStickyWidth}px`;
                 if (header.style.width !== newWidth) {
@@ -209,21 +213,9 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
                 if (header.classList.contains("mx_RoomSublist2_headerContainer_sticky")) {
                     header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
                 }
-                if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyTop")) {
-                    header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
-                }
-                if (header.classList.contains("mx_RoomSublist2_headerContainer_stickyBottom")) {
-                    header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
-                }
-                if (headerContainer.classList.contains("mx_RoomSublist2_headerContainer_hasSticky")) {
-                    headerContainer.classList.remove("mx_RoomSublist2_headerContainer_hasSticky");
-                }
                 if (header.style.width) {
                     header.style.removeProperty('width');
                 }
-                if (header.style.top) {
-                    header.style.removeProperty('top');
-                }
             }
         }
 
@@ -242,7 +234,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
         }
     }
 
-    // TODO: Improve header reliability: https://github.com/vector-im/riot-web/issues/14232
     private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
         const list = ev.target as HTMLDivElement;
         this.handleStickyHeaders(list);
@@ -383,8 +374,6 @@ export default class LeftPanel2 extends React.Component<IProps, IState> {
             onResize={this.onResize}
         />;
 
-        // TODO: Conference handling / calls: https://github.com/vector-im/riot-web/issues/14177
-
         const containerClasses = classNames({
             "mx_LeftPanel2": true,
             "mx_LeftPanel2_hasTagPanel": !!tagPanel,
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 231bd92ddf..517a5f2580 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -25,16 +25,6 @@ import { Key } from "../../Keyboard";
 import AccessibleButton from "../views/elements/AccessibleButton";
 import { Action } from "../../dispatcher/actions";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
-
-/*******************************************************************
- *   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 {
     onQueryUpdate: (newQuery: string) => void;
     isMinimized: boolean;
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index a6eabe25f7..842e11b516 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -170,6 +170,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
         ev.stopPropagation();
 
         // TODO: Archived room view: https://github.com/vector-im/riot-web/issues/14038
+        // Note: You'll need to uncomment the button too.
         console.log("TODO: Show archived rooms");
     };
 
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index 941a057927..d215df9126 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -18,8 +18,6 @@ import React from "react";
 import classNames from "classnames";
 import { formatMinimalBadgeCount } from "../../../utils/FormattingUtils";
 import SettingsStore from "../../../settings/SettingsStore";
-import { DefaultTagID, TagID } from "../../../stores/room-list/models";
-import { readReceiptChangeIsFor } from "../../../utils/read-receipts";
 import AccessibleButton from "../elements/AccessibleButton";
 import { XOR } from "../../../@types/common";
 import { NOTIFICATION_STATE_UPDATE, NotificationState } from "../../../stores/notifications/NotificationState";
diff --git a/src/components/views/rooms/RoomBreadcrumbs2.tsx b/src/components/views/rooms/RoomBreadcrumbs2.tsx
index 7d0584ef66..619ad6da4d 100644
--- a/src/components/views/rooms/RoomBreadcrumbs2.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs2.tsx
@@ -27,17 +27,8 @@ import RoomListStore from "../../../stores/room-list/RoomListStore2";
 import { DefaultTagID } from "../../../stores/room-list/models";
 import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
 
-/*******************************************************************
- *   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 {
 }
 
diff --git a/src/components/views/rooms/RoomList2.tsx b/src/components/views/rooms/RoomList2.tsx
index 67787963a3..f4a6a88950 100644
--- a/src/components/views/rooms/RoomList2.tsx
+++ b/src/components/views/rooms/RoomList2.tsx
@@ -41,17 +41,8 @@ import { Action } from "../../../dispatcher/actions";
 import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
 import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
 
-/*******************************************************************
- *   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;
@@ -218,19 +209,14 @@ export default class RoomList2 extends React.Component<IProps, IState> {
     };
 
     private updateLists = () => {
-        const newLists = RoomListStore.instance.orderedLists;
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log("new lists", newLists);
-        }
-
-        this.setState({sublists: newLists}, () => {
+        this.setState({sublists: RoomListStore.instance.orderedLists}, () => {
             this.props.onResize();
         });
     };
 
     private renderCommunityInvites(): React.ReactElement[] {
         // TODO: Put community invites in a more sensible place (not in the room list)
+        // See https://github.com/vector-im/riot-web/issues/14456
         return MatrixClientPeg.get().getGroups().filter(g => {
            if (g.myMembership !== 'invite') return false;
            return !this.searchFilter || this.searchFilter.matches(g.name || "");
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx
index c22e6cd807..68b7a585b5 100644
--- a/src/components/views/rooms/RoomSublist2.tsx
+++ b/src/components/views/rooms/RoomSublist2.tsx
@@ -17,7 +17,7 @@ limitations under the License.
 */
 
 import * as React from "react";
-import {createRef, UIEventHandler} from "react";
+import {createRef} from "react";
 import { Room } from "matrix-js-sdk/src/models/room";
 import classNames from 'classnames';
 import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
@@ -48,17 +48,8 @@ import { polyfillTouchEvent } from "../../../@types/polyfill";
 import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
 import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
 
-/*******************************************************************
- *   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.                                             *
- *******************************************************************/
-
 const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
 const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
 export const HEADER_HEIGHT = 32; // As defined by CSS
@@ -137,9 +128,10 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
         let padding = RESIZE_HANDLE_HEIGHT;
         // this is used for calculating the max height of the whole container,
         // and takes into account whether there should be room reserved for the show less button
-        // when fully expanded. Note that the show more button might still be shown when not fully expanded,
-        // but in this case it will take the space of a tile and we don't need to reserve space for it.
-        if (this.numTiles > this.layout.defaultVisibleTiles) {
+        // when fully expanded. We cannot check against the layout's defaultVisible tile count
+        // because there are conditions in which we need to know that the 'show more' button
+        // is present while well under the default tile limit.
+        if (this.numTiles > this.numVisibleTiles) {
             padding += SHOW_N_BUTTON_HEIGHT;
         }
         return padding;
@@ -236,10 +228,13 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
     };
 
     private onShowAllClick = () => {
+        // read number of visible tiles before we mutate it
+        const numVisibleTiles = this.numVisibleTiles;
         const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
         this.applyHeightChange(newHeight);
         this.setState({height: newHeight}, () => {
-            this.focusRoomTile(this.numTiles - 1);
+            // focus the top-most new room
+            this.focusRoomTile(numVisibleTiles);
         });
     };
 
@@ -606,8 +601,6 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
     }
 
     public render(): React.ReactElement {
-        // TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
-
         const visibleTiles = this.renderVisibleTiles();
         const classes = classNames({
             'mx_RoomSublist2': true,
@@ -623,11 +616,15 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
             const showMoreAtMinHeight = minTiles < this.numTiles;
             const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
             const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
-            const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
+            let maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
             const showMoreBtnClasses = classNames({
                 'mx_RoomSublist2_showNButton': true,
             });
 
+            if (this.numTiles > this.layout.defaultVisibleTiles) {
+                maxTilesPx += SHOW_N_BUTTON_HEIGHT;
+            }
+
             // If we're hiding rooms, show a 'show more' button to the user. This button
             // floats above the resize handle, if we have one present. If the user has all
             // tiles visible, it becomes 'show less'.
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index ca2f8865f9..fe6a19f2ed 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -55,17 +55,8 @@ import {ActionPayload} from "../../../dispatcher/payloads";
 import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
 import { NotificationState } from "../../../stores/notifications/NotificationState";
 
-// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14367
 // TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14367
 
-/*******************************************************************
- *   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 {
     room: Room;
     showMessagePreview: boolean;
@@ -124,7 +115,6 @@ const NotifOption: React.FC<INotifOptionProps> = ({active, onClick, iconClassNam
 export default class RoomTile2 extends React.Component<IProps, IState> {
     private dispatcherRef: string;
     private roomTileRef = createRef<HTMLDivElement>();
-    // TODO: a11y: https://github.com/vector-im/riot-web/issues/14180
 
     constructor(props: IProps) {
         super(props);
@@ -310,7 +300,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
             await setRoomNotifsState(this.props.room.roomId, newState);
         } catch (error) {
             // TODO: some form of error notification to the user to inform them that their state change failed.
-            // https://github.com/vector-im/riot-web/issues/14281
+            // See https://github.com/vector-im/riot-web/issues/14281
             console.error(error);
         }
 
@@ -398,8 +388,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
     private renderGeneralMenu(): React.ReactElement {
         if (!this.showContextMenu) return null; // no menu to show
 
-        // TODO: We could do with a proper invite context menu, unlike what showContextMenu suggests
-
         const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
 
         const isFavorite = roomTags.includes(DefaultTagID.Favourite);
@@ -465,8 +453,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
     }
 
     public render(): React.ReactElement {
-        // TODO: Invites: https://github.com/vector-im/riot-web/issues/14198
-
         const classes = classNames({
             'mx_RoomTile2': true,
             'mx_RoomTile2_selected': this.state.selected,
@@ -495,7 +481,6 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
             );
         }
 
-        // 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
diff --git a/src/components/views/rooms/TemporaryTile.tsx b/src/components/views/rooms/TemporaryTile.tsx
index a3ee7eb5bd..9baaba817d 100644
--- a/src/components/views/rooms/TemporaryTile.tsx
+++ b/src/components/views/rooms/TemporaryTile.tsx
@@ -34,6 +34,7 @@ interface IState {
     hover: boolean;
 }
 
+// TODO: Remove with community invites in the room list: https://github.com/vector-im/riot-web/issues/14456
 export default class TemporaryTile extends React.Component<IProps, IState> {
     constructor(props: IProps) {
         super(props);
diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts
index 48ef75cb59..5639a9104c 100644
--- a/src/stores/BreadcrumbsStore.ts
+++ b/src/stores/BreadcrumbsStore.ts
@@ -21,6 +21,7 @@ import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
 import defaultDispatcher from "../dispatcher/dispatcher";
 import { arrayHasDiff } from "../utils/arrays";
 import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
+import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 
 const MAX_ROOMS = 20; // arbitrary
 const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up
@@ -51,7 +52,11 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
     }
 
     public get visible(): boolean {
-        return this.state.enabled && this.matrixClient.getVisibleRooms().length >= 20;
+        return this.state.enabled && this.meetsRoomRequirement;
+    }
+
+    private get meetsRoomRequirement(): boolean {
+        return this.matrixClient.getVisibleRooms().length >= 20;
     }
 
     protected async onAction(payload: ActionPayload) {
@@ -99,8 +104,9 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
     }
 
     private onMyMembership = async (room: Room) => {
-        // We turn on breadcrumbs by default once the user has at least 1 room to show.
-        if (!this.state.enabled) {
+        // Only turn on breadcrumbs is the user hasn't explicitly turned it off again.
+        const settingValueRaw = SettingsStore.getValue("breadcrumbs", null, /*excludeDefault=*/true);
+        if (this.meetsRoomRequirement && isNullOrUndefined(settingValueRaw)) {
             await SettingsStore.setValue("breadcrumbs", null, SettingLevel.ACCOUNT, true);
         }
     };
diff --git a/src/stores/notifications/RoomNotificationState.ts b/src/stores/notifications/RoomNotificationState.ts
index ab354c0e93..dc38f8bf0f 100644
--- a/src/stores/notifications/RoomNotificationState.ts
+++ b/src/stores/notifications/RoomNotificationState.ts
@@ -17,7 +17,7 @@ limitations under the License.
 import { NotificationColor } from "./NotificationColor";
 import { IDestroyable } from "../../utils/IDestroyable";
 import { MatrixClientPeg } from "../../MatrixClientPeg";
-import { EffectiveMembership, getEffectiveMembership } from "../room-list/membership";
+import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
 import { readReceiptChangeIsFor } from "../../utils/read-receipts";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import { Room } from "matrix-js-sdk/src/models/room";
diff --git a/src/stores/room-list/RoomListStore2.ts b/src/stores/room-list/RoomListStore2.ts
index f3a77e765b..1e3c4a18eb 100644
--- a/src/stores/room-list/RoomListStore2.ts
+++ b/src/stores/room-list/RoomListStore2.ts
@@ -19,7 +19,6 @@ import { MatrixClient } from "matrix-js-sdk/src/client";
 import SettingsStore from "../../settings/SettingsStore";
 import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
 import TagOrderStore from "../TagOrderStore";
-import { AsyncStore } from "../AsyncStore";
 import { Room } from "matrix-js-sdk/src/models/room";
 import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
 import { ActionPayload } from "../../dispatcher/payloads";
@@ -29,10 +28,11 @@ import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
 import { TagWatcher } from "./TagWatcher";
 import RoomViewStore from "../RoomViewStore";
 import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
-import { EffectiveMembership, getEffectiveMembership } from "./membership";
+import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
 import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
 import RoomListLayoutStore from "./RoomListLayoutStore";
 import { MarkedExecution } from "../../utils/MarkedExecution";
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
 
 interface IState {
     tagsEnabled?: boolean;
@@ -44,14 +44,13 @@ interface IState {
  */
 export const LISTS_UPDATE_EVENT = "lists_update";
 
-export class RoomListStore2 extends AsyncStore<ActionPayload> {
+export class RoomListStore2 extends AsyncStoreWithClient<ActionPayload> {
     /**
      * Set to true if you're running tests on the store. Should not be touched in
      * any other environment.
      */
     public static TEST_MODE = false;
 
-    private _matrixClient: MatrixClient;
     private initialListsGenerated = false;
     private enabled = false;
     private algorithm = new Algorithm();
@@ -79,7 +78,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     public get matrixClient(): MatrixClient {
-        return this._matrixClient;
+        return super.matrixClient;
     }
 
     // Intended for test usage
@@ -88,23 +87,28 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         this.tagWatcher = new TagWatcher(this);
         this.filterConditions = [];
         this.initialListsGenerated = false;
-        this._matrixClient = null;
 
         this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
         this.algorithm.off(FILTER_CHANGED, this.onAlgorithmListUpdated);
         this.algorithm = new Algorithm();
         this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
         this.algorithm.on(FILTER_CHANGED, this.onAlgorithmListUpdated);
+
+        // Reset state without causing updates as the client will have been destroyed
+        // and downstream code will throw NPE errors.
+        await this.reset(null, true);
     }
 
     // Public for test usage. Do not call this.
-    public async makeReady(client: MatrixClient) {
+    public async makeReady(forcedClient?: MatrixClient) {
+        if (forcedClient) {
+            super.matrixClient = forcedClient;
+        }
+
         // TODO: Remove with https://github.com/vector-im/riot-web/issues/14367
         this.checkEnabled();
         if (!this.enabled) return;
 
-        this._matrixClient = client;
-
         // Update any settings here, as some may have happened before we were logically ready.
         // Update any settings here, as some may have happened before we were logically ready.
         console.log("Regenerating room lists: Startup");
@@ -150,10 +154,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                 console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`);
                 await this.algorithm.setStickyRoom(null);
             } else if (activeRoom !== this.algorithm.stickyRoom) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`Changing sticky room to ${activeRoomId}`);
-                }
                 await this.algorithm.setStickyRoom(activeRoom);
             }
         }
@@ -161,7 +161,15 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
         if (trigger) this.updateFn.trigger();
     }
 
-    protected async onDispatch(payload: ActionPayload) {
+    protected async onReady(): Promise<any> {
+        await this.makeReady();
+    }
+
+    protected async onNotReady(): Promise<any> {
+        await this.resetStore();
+    }
+
+    protected async onAction(payload: ActionPayload) {
         // When we're running tests we can't reliably use setImmediate out of timing concerns.
         // As such, we use a more synchronous model.
         if (RoomListStore2.TEST_MODE) {
@@ -175,29 +183,10 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     protected async onDispatchAsync(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;
-            }
-
-            await this.makeReady(payload.matrixClient);
-
-            return; // no point in running the next conditions - they won't match
-        }
-
         // 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.
-            await this.reset(null, true);
-            this._matrixClient = null;
-            this.initialListsGenerated = false; // we'll want to regenerate them
-        }
-
-        // Everything below here requires a MatrixClient or some sort of logical readiness.
+        // Everything here requires a MatrixClient or some sort of logical readiness.
         const logicallyReady = this.matrixClient && this.initialListsGenerated;
         if (!logicallyReady) return;
 
@@ -225,20 +214,12 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                     console.warn(`Own read receipt was in unknown room ${room.roomId}`);
                     return;
                 }
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Got own read receipt in ${room.roomId}`);
-                }
                 await this.handleRoomUpdate(room, RoomUpdateCause.ReadReceipt);
                 this.updateFn.trigger();
                 return;
             }
         } else if (payload.action === 'MatrixActions.Room.tags') {
             const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Got tag change in ${roomPayload.room.roomId}`);
-            }
             await this.handleRoomUpdate(roomPayload.room, RoomUpdateCause.PossibleTagChange);
             this.updateFn.trigger();
         } else if (payload.action === 'MatrixActions.Room.timeline') {
@@ -250,16 +231,7 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
             const roomId = eventPayload.event.getRoomId();
             const room = this.matrixClient.getRoom(roomId);
             const tryUpdate = async (updatedRoom: Room) => {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()}` +
-                        ` in ${updatedRoom.roomId}`);
-                }
                 if (eventPayload.event.getType() === 'm.room.tombstone' && eventPayload.event.getStateKey() === '') {
-                    if (!window.mx_QuietRoomListLogging) {
-                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                        console.log(`[RoomListDebug] Got tombstone event - trying to remove now-dead room`);
-                    }
                     const newRoom = this.matrixClient.getRoom(eventPayload.event.getContent()['replacement_room']);
                     if (newRoom) {
                         // If we have the new room, then the new room check will have seen the predecessor
@@ -289,18 +261,10 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
                 console.warn(`Event ${eventPayload.event.getId()} was decrypted in an unknown room ${roomId}`);
                 return;
             }
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Decrypted timeline event ${eventPayload.event.getId()} in ${roomId}`);
-            }
             await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
             this.updateFn.trigger();
         } else if (payload.action === 'MatrixActions.accountData' && payload.event_type === 'm.direct') {
             const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Received updated DM map`);
-            }
             const dmMap = eventPayload.event.getContent();
             for (const userId of Object.keys(dmMap)) {
                 const roomIds = dmMap[userId];
@@ -324,54 +288,29 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
             const oldMembership = getEffectiveMembership(membershipPayload.oldMembership);
             const newMembership = getEffectiveMembership(membershipPayload.membership);
             if (oldMembership !== EffectiveMembership.Join && newMembership === EffectiveMembership.Join) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
-                }
-
                 // If we're joining an upgraded room, we'll want to make sure we don't proliferate
                 // the dead room in the list.
                 const createEvent = membershipPayload.room.currentState.getStateEvents("m.room.create", "");
                 if (createEvent && createEvent.getContent()['predecessor']) {
-                    if (!window.mx_QuietRoomListLogging) {
-                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                        console.log(`[RoomListDebug] Room has a predecessor`);
-                    }
                     const prevRoom = this.matrixClient.getRoom(createEvent.getContent()['predecessor']['room_id']);
                     if (prevRoom) {
                         const isSticky = this.algorithm.stickyRoom === prevRoom;
                         if (isSticky) {
-                            if (!window.mx_QuietRoomListLogging) {
-                                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                                console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`);
-                            }
                             await this.algorithm.setStickyRoom(null);
                         }
 
                         // Note: we hit the algorithm instead of our handleRoomUpdate() function to
                         // avoid redundant updates.
-                        if (!window.mx_QuietRoomListLogging) {
-                            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                            console.log(`[RoomListDebug] Removing previous room from room list`);
-                        }
                         await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved);
                     }
                 }
 
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Adding new room to room list`);
-                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
                 this.updateFn.trigger();
                 return;
             }
 
             if (oldMembership !== EffectiveMembership.Invite && newMembership === EffectiveMembership.Invite) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Handling invite to ${membershipPayload.room.roomId}`);
-                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
                 this.updateFn.trigger();
                 return;
@@ -379,10 +318,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
 
             // If it's not a join, it's transitioning into a different list (possibly historical)
             if (oldMembership !== newMembership) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`[RoomListDebug] Handling membership change in ${membershipPayload.room.roomId}`);
-                }
                 await this.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.PossibleTagChange);
                 this.updateFn.trigger();
                 return;
@@ -393,10 +328,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     private async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<any> {
         const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause);
         if (shouldUpdate) {
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[DEBUG] Room "${room.name}" (${room.roomId}) triggered by ${cause} requires list update`);
-            }
             this.updateFn.mark();
         }
     }
@@ -516,10 +447,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     private onAlgorithmListUpdated = () => {
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log("Underlying algorithm has triggered a list update - marking");
-        }
         this.updateFn.mark();
     };
 
@@ -566,10 +493,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     public addFilter(filter: IFilterCondition): void {
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log("Adding filter condition:", filter);
-        }
         this.filterConditions.push(filter);
         if (this.algorithm) {
             this.algorithm.addFilterCondition(filter);
@@ -578,10 +501,6 @@ export class RoomListStore2 extends AsyncStore<ActionPayload> {
     }
 
     public removeFilter(filter: IFilterCondition): void {
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log("Removing filter condition:", filter);
-        }
         const idx = this.filterConditions.indexOf(filter);
         if (idx >= 0) {
             this.filterConditions.splice(idx, 1);
diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts
index 17e8283c74..19ca240a63 100644
--- a/src/stores/room-list/algorithms/Algorithm.ts
+++ b/src/stores/room-list/algorithms/Algorithm.ts
@@ -30,12 +30,10 @@ import {
     SortAlgorithm
 } from "./models";
 import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
-import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../membership";
+import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
 import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
 import { getListAlgorithmInstance } from "./list-ordering";
 
-// TODO: Add locking support to avoid concurrent writes? https://github.com/vector-im/riot-web/issues/14235
-
 /**
  * Fired when the Algorithm has determined a list has been updated.
  */
@@ -321,11 +319,6 @@ export class Algorithm extends EventEmitter {
                 }
             }
             newMap[tagId] = allowedRoomsInThisTag;
-
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[DEBUG] ${newMap[tagId].length}/${rooms.length} rooms filtered into ${tagId}`);
-            }
         }
 
         const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
@@ -337,10 +330,6 @@ export class Algorithm extends EventEmitter {
     protected recalculateFilteredRoomsForTag(tagId: TagID): void {
         if (!this.hasFilters) return; // don't bother doing work if there's nothing to do
 
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`Recalculating filtered rooms for ${tagId}`);
-        }
         delete this.filteredRooms[tagId];
         const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
         this.tryInsertStickyRoomToFilterSet(rooms, tagId);
@@ -348,11 +337,6 @@ export class Algorithm extends EventEmitter {
         if (filteredRooms.length > 0) {
             this.filteredRooms[tagId] = filteredRooms;
         }
-
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[DEBUG] ${filteredRooms.length}/${rooms.length} rooms filtered into ${tagId}`);
-        }
     }
 
     protected tryInsertStickyRoomToFilterSet(rooms: Room[], tagId: TagID) {
@@ -391,10 +375,6 @@ export class Algorithm extends EventEmitter {
         }
 
         if (!this._cachedStickyRooms || !updatedTag) {
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`Generating clone of cached rooms for sticky room handling`);
-            }
             const stickiedTagMap: ITagMap = {};
             for (const tagId of Object.keys(this.cachedRooms)) {
                 stickiedTagMap[tagId] = this.cachedRooms[tagId].map(r => r); // shallow clone
@@ -405,10 +385,6 @@ export class Algorithm extends EventEmitter {
         if (updatedTag) {
             // Update the tag indicated by the caller, if possible. This is mostly to ensure
             // our cache is up to date.
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`Replacing cached sticky rooms for ${updatedTag}`);
-            }
             this._cachedStickyRooms[updatedTag] = this.cachedRooms[updatedTag].map(r => r); // shallow clone
         }
 
@@ -417,10 +393,6 @@ export class Algorithm extends EventEmitter {
         // we might have updated from the cache is also our sticky room.
         const sticky = this._stickyRoom;
         if (!updatedTag || updatedTag === sticky.tag) {
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`);
-            }
             this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
         }
 
@@ -645,10 +617,6 @@ export class Algorithm extends EventEmitter {
      * processing.
      */
     public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise<boolean> {
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`Handle room update for ${room.roomId} called with cause ${cause}`);
-        }
         if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from");
 
         // Note: check the isSticky against the room ID just in case the reference is wrong
@@ -698,27 +666,24 @@ export class Algorithm extends EventEmitter {
             }
         }
 
+        let didTagChange = false;
         if (cause === RoomUpdateCause.PossibleTagChange) {
-            let didTagChange = false;
             const oldTags = this.roomIdsToTags[room.roomId] || [];
             const newTags = this.getTagsForRoom(room);
             const diff = arrayDiff(oldTags, newTags);
             if (diff.removed.length > 0 || diff.added.length > 0) {
                 for (const rmTag of diff.removed) {
-                    if (!window.mx_QuietRoomListLogging) {
-                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                        console.log(`Removing ${room.roomId} from ${rmTag}`);
-                    }
                     const algorithm: OrderingAlgorithm = this.algorithms[rmTag];
                     if (!algorithm) throw new Error(`No algorithm for ${rmTag}`);
                     await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved);
                     this.cachedRooms[rmTag] = algorithm.orderedRooms;
+
+                    // Later on we won't update the filtered rooms or sticky room for removed
+                    // tags, so do so now.
+                    this.recalculateFilteredRoomsForTag(rmTag);
+                    this.recalculateStickyRoom(rmTag);
                 }
                 for (const addTag of diff.added) {
-                    if (!window.mx_QuietRoomListLogging) {
-                        // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                        console.log(`Adding ${room.roomId} to ${addTag}`);
-                    }
                     const algorithm: OrderingAlgorithm = this.algorithms[addTag];
                     if (!algorithm) throw new Error(`No algorithm for ${addTag}`);
                     await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom);
@@ -728,17 +693,9 @@ export class Algorithm extends EventEmitter {
                 // Update the tag map so we don't regen it in a moment
                 this.roomIdsToTags[room.roomId] = newTags;
 
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`Changing update cause for ${room.roomId} to Timeline to sort rooms`);
-                }
                 cause = RoomUpdateCause.Timeline;
                 didTagChange = true;
             } else {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.log(`Received no-op update for ${room.roomId} - changing to Timeline update`);
-                }
                 cause = RoomUpdateCause.Timeline;
             }
 
@@ -764,28 +721,15 @@ export class Algorithm extends EventEmitter {
         // as the sticky room relies on this.
         if (cause !== RoomUpdateCause.NewRoom && cause !== RoomUpdateCause.RoomRemoved) {
             if (this.stickyRoom === room) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.warn(`[RoomListDebug] Received ${cause} update for sticky room ${room.roomId} - ignoring`);
-                }
                 return false;
             }
         }
 
         if (!this.roomIdsToTags[room.roomId]) {
             if (CAUSES_REQUIRING_ROOM.includes(cause)) {
-                if (!window.mx_QuietRoomListLogging) {
-                    // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                    console.warn(`Skipping tag update for ${room.roomId} because we don't know about the room`);
-                }
                 return false;
             }
 
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Updating tags for room ${room.roomId} (${room.name})`);
-            }
-
             // Get the tags for the room and populate the cache
             const roomTags = this.getTagsForRoom(room).filter(t => !isNullOrUndefined(this.cachedRooms[t]));
 
@@ -794,16 +738,6 @@ export class Algorithm extends EventEmitter {
             if (!roomTags.length) throw new Error(`Tags cannot be determined for ${room.roomId}`);
 
             this.roomIdsToTags[room.roomId] = roomTags;
-
-            if (!window.mx_QuietRoomListLogging) {
-                // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-                console.log(`[RoomListDebug] Updated tags for ${room.roomId}:`, roomTags);
-            }
-        }
-
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Reached algorithmic handling for ${room.roomId} and cause ${cause}`);
         }
 
         const tags = this.roomIdsToTags[room.roomId];
@@ -812,7 +746,7 @@ export class Algorithm extends EventEmitter {
             return false;
         }
 
-        let changed = false;
+        let changed = didTagChange;
         for (const tag of tags) {
             const algorithm: OrderingAlgorithm = this.algorithms[tag];
             if (!algorithm) throw new Error(`No algorithm for ${tag}`);
@@ -826,10 +760,6 @@ export class Algorithm extends EventEmitter {
             changed = true;
         }
 
-        if (!window.mx_QuietRoomListLogging) {
-            // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14035
-            console.log(`[RoomListDebug] Finished handling ${room.roomId} with cause ${cause} (changed=${changed})`);
-        }
         return changed;
     }
 }
diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
index ae1a2c98f6..a1c9e4eb85 100644
--- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
+++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts
@@ -55,7 +55,7 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
                 }
             }
 
-            // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14035
+            // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14457
             // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
             this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm);
 
diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts
index 8d74ebd11e..d909fb6288 100644
--- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts
+++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts
@@ -17,8 +17,6 @@ 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 browser's determination of alphabetic.
diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
index 154fd40b69..09182f3bfb 100644
--- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
+++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts
@@ -19,7 +19,7 @@ import { TagID } from "../../models";
 import { IAlgorithm } from "./IAlgorithm";
 import { MatrixClientPeg } from "../../../../MatrixClientPeg";
 import * as Unread from "../../../../Unread";
-import { EffectiveMembership, getEffectiveMembership } from "../../membership";
+import { EffectiveMembership, getEffectiveMembership } from "../../../../utils/membership";
 
 /**
  * Sorts rooms according to the last event's timestamp in each room that seems
@@ -33,12 +33,13 @@ export class RecentAlgorithm implements IAlgorithm {
         // of the rooms to each other.
 
         // TODO: We could probably improve the sorting algorithm here by finding changes.
-        // See https://github.com/vector-im/riot-web/issues/14035
+        // See https://github.com/vector-im/riot-web/issues/14459
         // 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.
 
         // TODO: Don't assume we're using the same client as the peg
+        // See https://github.com/vector-im/riot-web/issues/14458
         let myUserId = '';
         if (MatrixClientPeg.get()) {
             myUserId = MatrixClientPeg.get().getUserId();
diff --git a/src/stores/room-list/membership.ts b/src/utils/membership.ts
similarity index 100%
rename from src/stores/room-list/membership.ts
rename to src/utils/membership.ts