{ visibleTiles }
@@ -739,12 +782,18 @@ export default class RoomSublist extends React.Component
{
// floats above the resize handle, if we have one present. If the user has all
// tiles visible, it becomes 'show less'.
let showNButton = null;
+ const hasMoreSlidingSync = (
+ this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
+ );
- if (maxTilesPx > this.state.height) {
+ if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
// the height of all the tiles is greater than the section height: we need a 'show more' button
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
- const numMissing = this.numTiles - amountFullyShown;
+ let numMissing = this.numTiles - amountFullyShown;
+ if (this.slidingSyncMode) {
+ numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
+ }
const label = _t("Show %(count)s more", { count: numMissing });
let showMoreText = (
diff --git a/src/hooks/useSlidingSyncRoomSearch.ts b/src/hooks/useSlidingSyncRoomSearch.ts
new file mode 100644
index 0000000000..6ba08dc1a7
--- /dev/null
+++ b/src/hooks/useSlidingSyncRoomSearch.ts
@@ -0,0 +1,86 @@
+/*
+Copyright 2022 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 { useCallback, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { MatrixClientPeg } from "../MatrixClientPeg";
+import { useLatestResult } from "./useLatestResult";
+import { SlidingSyncManager } from "../SlidingSyncManager";
+
+export interface SlidingSyncRoomSearchOpts {
+ limit: number;
+ query?: string;
+}
+
+export const useSlidingSyncRoomSearch = () => {
+ const [rooms, setRooms] = useState([]);
+
+ const [loading, setLoading] = useState(false);
+ const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
+
+ const [updateQuery, updateResult] = useLatestResult<{ term: string, limit?: number }, Room[]>(setRooms);
+
+ const search = useCallback(async ({
+ limit = 100,
+ query: term,
+ }: SlidingSyncRoomSearchOpts): Promise => {
+ const opts = { limit, term };
+ updateQuery(opts);
+
+ if (!term?.length) {
+ setRooms([]);
+ return true;
+ }
+
+ try {
+ setLoading(true);
+ await SlidingSyncManager.instance.ensureListRegistered(listIndex, {
+ ranges: [[0, limit]],
+ filters: {
+ room_name_like: term,
+ is_tombstoned: false,
+ },
+ });
+ const rooms = [];
+ const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex);
+ let i = 0;
+ while (roomIndexToRoomId[i]) {
+ const roomId = roomIndexToRoomId[i];
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ if (room) {
+ rooms.push(room);
+ }
+ i++;
+ }
+ updateResult(opts, rooms);
+ return true;
+ } catch (e) {
+ console.error("Could not fetch sliding sync rooms for params", { limit, term }, e);
+ updateResult(opts, []);
+ return false;
+ } finally {
+ setLoading(false);
+ // TODO: delete the list?
+ }
+ }, [updateQuery, updateResult, listIndex]);
+
+ return {
+ loading,
+ rooms,
+ search,
+ } as const;
+};
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 6afb549b16..c2ee03575d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -907,6 +907,7 @@
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
"Send read receipts": "Send read receipts",
+ "Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
"Use new session manager (under active development)": "Use new session manager (under active development)",
@@ -2856,6 +2857,14 @@
"Link to selected message": "Link to selected message",
"Link to room": "Link to room",
"Command Help": "Command Help",
+ "Checking...": "Checking...",
+ "Your server has native support": "Your server has native support",
+ "Your server lacks native support": "Your server lacks native support",
+ "Your server lacks native support, you must specify a proxy": "Your server lacks native support, you must specify a proxy",
+ "Sliding Sync configuration": "Sliding Sync configuration",
+ "To disable you will need to log out and back in, use with caution!": "To disable you will need to log out and back in, use with caution!",
+ "Proxy URL (optional)": "Proxy URL (optional)",
+ "Proxy URL": "Proxy URL",
"Sections to show": "Sections to show",
"This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.",
"Space settings": "Space settings",
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index e374de12d1..1675968257 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -41,6 +41,7 @@ import IncompatibleController from "./controllers/IncompatibleController";
import { ImageSize } from "./enums/ImageSize";
import { MetaSpace } from "../stores/spaces";
import SdkConfig from "../SdkConfig";
+import SlidingSyncController from './controllers/SlidingSyncController';
import ThreadBetaController from './controllers/ThreadBetaController';
import { FontWatcher } from "./watchers/FontWatcher";
@@ -406,6 +407,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Send read receipts"),
default: true,
},
+ "feature_sliding_sync": {
+ isFeature: true,
+ labsGroup: LabGroup.Developer,
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ displayName: _td('Sliding Sync mode (under active development, cannot be disabled)'),
+ default: false,
+ controller: new SlidingSyncController(),
+ },
+ "feature_sliding_sync_proxy_url": {
+ supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
+ default: "",
+ },
"feature_location_share_live": {
isFeature: true,
labsGroup: LabGroup.Messaging,
diff --git a/src/settings/controllers/SlidingSyncController.ts b/src/settings/controllers/SlidingSyncController.ts
new file mode 100644
index 0000000000..fdbea7bda0
--- /dev/null
+++ b/src/settings/controllers/SlidingSyncController.ts
@@ -0,0 +1,39 @@
+/*
+Copyright 2022 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 SettingController from "./SettingController";
+import PlatformPeg from "../../PlatformPeg";
+import { SettingLevel } from "../SettingLevel";
+import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
+import Modal from "../../Modal";
+import SettingsStore from "../SettingsStore";
+
+export default class SlidingSyncController extends SettingController {
+ public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise {
+ const { finished } = Modal.createDialog(SlidingSyncOptionsDialog);
+ const [value] = await finished;
+ return newValue === value; // abort the operation if we're already in the state the user chose via modal
+ }
+
+ public async onChange(): Promise {
+ PlatformPeg.get().reload();
+ }
+
+ public get settingDisabled(): boolean {
+ // Cannot be disabled once enabled, user has been warned and must log out and back in.
+ return SettingsStore.getValue("feature_sliding_sync");
+ }
+}
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index 4f0e7d5b13..c9324edc5d 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -47,6 +47,8 @@ import { JoinRoomErrorPayload } from "../dispatcher/payloads/JoinRoomErrorPayloa
import { ViewRoomErrorPayload } from "../dispatcher/payloads/ViewRoomErrorPayload";
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload";
+import SettingsStore from "../settings/SettingsStore";
+import { SlidingSyncManager } from "../SlidingSyncManager";
import { awaitRoomDownSync } from "../utils/RoomUpgrade";
const NUM_JOIN_RETRY = 5;
@@ -278,6 +280,32 @@ export class RoomViewStore extends Store {
activeSpace,
});
}
+ if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
+ if (this.state.roomId) {
+ // unsubscribe from this room, but don't await it as we don't care when this gets done.
+ SlidingSyncManager.instance.setRoomVisible(this.state.roomId, false);
+ }
+ this.setState({
+ roomId: payload.room_id,
+ initialEventId: null,
+ initialEventPixelOffset: null,
+ isInitialEventHighlighted: null,
+ initialEventScrollIntoView: true,
+ roomAlias: null,
+ roomLoading: true,
+ roomLoadError: null,
+ viaServers: payload.via_servers,
+ wasContextSwitch: payload.context_switch,
+ });
+ // set this room as the room subscription. We need to await for it as this will fetch
+ // all room state for this room, which is required before we get the state below.
+ await SlidingSyncManager.instance.setRoomVisible(payload.room_id, true);
+ // Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now
+ dis.dispatch({
+ ...payload,
+ });
+ return;
+ }
const newState = {
roomId: payload.room_id,
diff --git a/src/stores/room-list/Interface.ts b/src/stores/room-list/Interface.ts
index ab53870989..55de9dd3ad 100644
--- a/src/stores/room-list/Interface.ts
+++ b/src/stores/room-list/Interface.ts
@@ -23,6 +23,9 @@ import { IFilterCondition } from "./filters/IFilterCondition";
export enum RoomListStoreEvent {
// The event/channel which is called when the room lists have been changed.
ListsUpdate = "lists_update",
+ // The event which is called when the room list is loading.
+ // Called with the (tagId, bool) which is true when the list is loading, else false.
+ ListsLoading = "lists_loading",
}
export interface RoomListStore extends EventEmitter {
@@ -33,6 +36,15 @@ export interface RoomListStore extends EventEmitter {
*/
get orderedLists(): ITagMap;
+ /**
+ * Return the total number of rooms in this list. Prefer this method to
+ * RoomListStore.orderedLists[tagId].length because the client may not
+ * be aware of all the rooms in this list (e.g in Sliding Sync).
+ * @param tagId the tag to get the room count for.
+ * @returns the number of rooms in this list, or 0 if the list is unknown.
+ */
+ getCount(tagId: TagID): number;
+
/**
* Set the sort algorithm for the specified tag.
* @param tagId the tag to set the algorithm for
diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts
index f80839f66f..c74a58494a 100644
--- a/src/stores/room-list/RoomListStore.ts
+++ b/src/stores/room-list/RoomListStore.ts
@@ -38,12 +38,14 @@ import { VisibilityProvider } from "./filters/VisibilityProvider";
import { SpaceWatcher } from "./SpaceWatcher";
import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators";
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
+import { SlidingRoomListStoreClass } from "./SlidingRoomListStore";
interface IState {
// state is tracked in underlying classes
}
export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
+export const LISTS_LOADING_EVENT = RoomListStoreEvent.ListsLoading; // unused; used by SlidingRoomListStore
export class RoomListStoreClass extends AsyncStoreWithClient implements Interface {
/**
@@ -585,6 +587,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient implements
return algorithmTags;
}
+ public getCount(tagId: TagID): number {
+ // The room list store knows about all the rooms, so just return the length.
+ return this.orderedLists[tagId].length || 0;
+ }
+
/**
* Manually update a room with a given cause. This should only be used if the
* room list store would otherwise be incapable of doing the update itself. Note
@@ -602,10 +609,17 @@ export default class RoomListStore {
private static internalInstance: Interface;
public static get instance(): Interface {
- if (!this.internalInstance) {
- const instance = new RoomListStoreClass();
- instance.start();
- this.internalInstance = instance;
+ if (!RoomListStore.internalInstance) {
+ if (SettingsStore.getValue("feature_sliding_sync")) {
+ logger.info("using SlidingRoomListStoreClass");
+ const instance = new SlidingRoomListStoreClass();
+ instance.start();
+ RoomListStore.internalInstance = instance;
+ } else {
+ const instance = new RoomListStoreClass();
+ instance.start();
+ RoomListStore.internalInstance = instance;
+ }
}
return this.internalInstance;
diff --git a/src/stores/room-list/SlidingRoomListStore.ts b/src/stores/room-list/SlidingRoomListStore.ts
new file mode 100644
index 0000000000..ccf4946886
--- /dev/null
+++ b/src/stores/room-list/SlidingRoomListStore.ts
@@ -0,0 +1,386 @@
+/*
+Copyright 2022 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 { logger } from "matrix-js-sdk/src/logger";
+import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
+
+import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models";
+import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
+import { ActionPayload } from "../../dispatcher/payloads";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { IFilterCondition } from "./filters/IFilterCondition";
+import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
+import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
+import { SlidingSyncManager } from "../../SlidingSyncManager";
+import SpaceStore from "../spaces/SpaceStore";
+import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces";
+import { LISTS_LOADING_EVENT } from "./RoomListStore";
+import { RoomViewStore } from "../RoomViewStore";
+
+interface IState {
+ // state is tracked in underlying classes
+}
+
+export const SlidingSyncSortToFilter: Record = {
+ [SortAlgorithm.Alphabetic]: ["by_name", "by_recency"],
+ [SortAlgorithm.Recent]: ["by_highlight_count", "by_notification_count", "by_recency"],
+ [SortAlgorithm.Manual]: ["by_recency"],
+};
+
+const filterConditions: Record = {
+ [DefaultTagID.Invite]: {
+ is_invite: true,
+ },
+ [DefaultTagID.Favourite]: {
+ tags: ["m.favourite"],
+ is_tombstoned: false,
+ },
+ // TODO https://github.com/vector-im/element-web/issues/23207
+ // DefaultTagID.SavedItems,
+ [DefaultTagID.DM]: {
+ is_dm: true,
+ is_invite: false,
+ is_tombstoned: false,
+ },
+ [DefaultTagID.Untagged]: {
+ is_dm: false,
+ is_invite: false,
+ is_tombstoned: false,
+ not_room_types: ["m.space"],
+ not_tags: ["m.favourite", "m.lowpriority"],
+ // spaces filter added dynamically
+ },
+ [DefaultTagID.LowPriority]: {
+ tags: ["m.lowpriority"],
+ is_tombstoned: false,
+ },
+ // TODO https://github.com/vector-im/element-web/issues/23207
+ // DefaultTagID.ServerNotice,
+ // DefaultTagID.Suggested,
+ // DefaultTagID.Archived,
+};
+
+export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
+
+export class SlidingRoomListStoreClass extends AsyncStoreWithClient implements Interface {
+ private tagIdToSortAlgo: Record = {};
+ private tagMap: ITagMap = {};
+ private counts: Record = {};
+ private stickyRoomId: string | null;
+
+ public constructor() {
+ super(defaultDispatcher);
+ this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
+ }
+
+ public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
+ logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
+ this.tagIdToSortAlgo[tagId] = sort;
+ const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
+ switch (sort) {
+ case SortAlgorithm.Alphabetic:
+ await SlidingSyncManager.instance.ensureListRegistered(
+ slidingSyncIndex, {
+ sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
+ },
+ );
+ break;
+ case SortAlgorithm.Recent:
+ await SlidingSyncManager.instance.ensureListRegistered(
+ slidingSyncIndex, {
+ sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
+ },
+ );
+ break;
+ case SortAlgorithm.Manual:
+ logger.error("cannot enable manual sort in sliding sync mode");
+ break;
+ default:
+ logger.error("unknown sort mode: ", sort);
+ }
+ }
+
+ public getTagSorting(tagId: TagID): SortAlgorithm {
+ let algo = this.tagIdToSortAlgo[tagId];
+ if (!algo) {
+ logger.warn("SlidingRoomListStore.getTagSorting: no sort algorithm for tag ", tagId);
+ algo = SortAlgorithm.Recent; // why not, we have to do something..
+ }
+ return algo;
+ }
+
+ public getCount(tagId: TagID): number {
+ return this.counts[tagId] || 0;
+ }
+
+ public setListOrder(tagId: TagID, order: ListAlgorithm) {
+ // TODO: https://github.com/vector-im/element-web/issues/23207
+ }
+
+ public getListOrder(tagId: TagID): ListAlgorithm {
+ // TODO: handle unread msgs first? https://github.com/vector-im/element-web/issues/23207
+ return ListAlgorithm.Natural;
+ }
+
+ /**
+ * Adds a filter condition to the room list store. Filters may be applied async,
+ * and thus might not cause an update to the store immediately.
+ * @param {IFilterCondition} filter The filter condition to add.
+ */
+ public async addFilter(filter: IFilterCondition): Promise {
+ // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear
+ // in the room list. We do not support arbitrary code for filters in sliding sync.
+ }
+
+ /**
+ * Removes a filter condition from the room list store. If the filter was
+ * not previously added to the room list store, this will no-op. The effects
+ * of removing a filter may be applied async and therefore might not cause
+ * an update right away.
+ * @param {IFilterCondition} filter The filter condition to remove.
+ */
+ public removeFilter(filter: IFilterCondition): void {
+ // Do nothing, the filters are only used by SpaceWatcher to see if a room should appear
+ // in the room list. We do not support arbitrary code for filters in sliding sync.
+ }
+
+ /**
+ * Gets the tags for a room identified by the store. The returned set
+ * should never be empty, and will contain DefaultTagID.Untagged if
+ * the store is not aware of any tags.
+ * @param room The room to get the tags for.
+ * @returns The tags for the room.
+ */
+ public getTagsForRoom(room: Room): TagID[] {
+ // check all lists for each tag we know about and see if the room is there
+ const tags: TagID[] = [];
+ for (const tagId in this.tagIdToSortAlgo) {
+ const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
+ const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
+ for (const roomIndex in roomIndexToRoomId) {
+ const roomId = roomIndexToRoomId[roomIndex];
+ if (roomId === room.roomId) {
+ tags.push(tagId);
+ break;
+ }
+ }
+ }
+ return tags;
+ }
+
+ /**
+ * Manually update a room with a given cause. This should only be used if the
+ * room list store would otherwise be incapable of doing the update itself. Note
+ * that this may race with the room list's regular operation.
+ * @param {Room} room The room to update.
+ * @param {RoomUpdateCause} cause The cause to update for.
+ */
+ public async manualRoomUpdate(room: Room, cause: RoomUpdateCause) {
+ // TODO: this is only used when you forget a room, not that important for now.
+ }
+
+ public get orderedLists(): ITagMap {
+ return this.tagMap;
+ }
+
+ private refreshOrderedLists(tagId: string, roomIndexToRoomId: Record): void {
+ const tagMap = this.tagMap;
+
+ // this room will not move due to it being viewed: it is sticky. This can be null to indicate
+ // no sticky room if you aren't viewing a room.
+ this.stickyRoomId = RoomViewStore.instance.getRoomId();
+ let stickyRoomNewIndex = -1;
+ const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => {
+ return room.roomId === this.stickyRoomId;
+ });
+
+ // order from low to high
+ const orderedRoomIndexes = Object.keys(roomIndexToRoomId).map((numStr) => {
+ return Number(numStr);
+ }).sort((a, b) => {
+ return a-b;
+ });
+ const seenRoomIds = new Set();
+ const orderedRoomIds = orderedRoomIndexes.map((i) => {
+ const rid = roomIndexToRoomId[i];
+ if (seenRoomIds.has(rid)) {
+ logger.error("room " + rid + " already has an index position: duplicate room!");
+ }
+ seenRoomIds.add(rid);
+ if (!rid) {
+ throw new Error("index " + i + " has no room ID: Map => " + JSON.stringify(roomIndexToRoomId));
+ }
+ if (rid === this.stickyRoomId) {
+ stickyRoomNewIndex = i;
+ }
+ return rid;
+ });
+ logger.debug(
+ `SlidingRoomListStore.refreshOrderedLists ${tagId} sticky: ${this.stickyRoomId}`,
+ `${stickyRoomOldIndex} -> ${stickyRoomNewIndex}`,
+ "rooms:",
+ orderedRoomIds.length < 30 ? orderedRoomIds : orderedRoomIds.length,
+ );
+
+ if (this.stickyRoomId && stickyRoomOldIndex >= 0 && stickyRoomNewIndex >= 0) {
+ // this update will move this sticky room from old to new, which we do not want.
+ // Instead, keep the sticky room ID index position as it is, swap it with
+ // whatever was in its place.
+ // Some scenarios with sticky room S and bump room B (other letters unimportant):
+ // A, S, C, B S, A, B
+ // B, A, S, C <---- without sticky rooms ---> B, S, A
+ // B, S, A, C <- with sticky rooms applied -> S, B, A
+ // In other words, we need to swap positions to keep it locked in place.
+ const inWayRoomId = orderedRoomIds[stickyRoomOldIndex];
+ orderedRoomIds[stickyRoomOldIndex] = this.stickyRoomId;
+ orderedRoomIds[stickyRoomNewIndex] = inWayRoomId;
+ }
+
+ // now set the rooms
+ const rooms = orderedRoomIds.map((roomId) => {
+ return this.matrixClient.getRoom(roomId);
+ });
+ tagMap[tagId] = rooms;
+ this.tagMap = tagMap;
+ }
+
+ private onSlidingSyncListUpdate(listIndex: number, joinCount: number, roomIndexToRoomId: Record) {
+ const tagId = SlidingSyncManager.instance.listIdForIndex(listIndex);
+ this.counts[tagId]= joinCount;
+ this.refreshOrderedLists(tagId, roomIndexToRoomId);
+ // let the UI update
+ this.emit(LISTS_UPDATE_EVENT);
+ }
+
+ private onRoomViewStoreUpdated() {
+ // we only care about this to know when the user has clicked on a room to set the stickiness value
+ if (RoomViewStore.instance.getRoomId() === this.stickyRoomId) {
+ return;
+ }
+
+ let hasUpdatedAnyList = false;
+
+ // every list with the OLD sticky room ID needs to be resorted because it now needs to take
+ // its proper place as it is no longer sticky. The newly sticky room can remain the same though,
+ // as we only actually care about its sticky status when we get list updates.
+ const oldStickyRoom = this.stickyRoomId;
+ // it's not safe to check the data in slidingSync as it is tracking the server's view of the
+ // room list. There's an edge case whereby the sticky room has gone outside the window and so
+ // would not be present in the roomIndexToRoomId map anymore, and hence clicking away from it
+ // will make it disappear eventually. We need to check orderedLists as that is the actual
+ // sorted renderable list of rooms which sticky rooms apply to.
+ for (const tagId in this.orderedLists) {
+ const list = this.orderedLists[tagId];
+ const room = list.find((room) => {
+ return room.roomId === oldStickyRoom;
+ });
+ if (room) {
+ // resort it based on the slidingSync view of the list. This may cause this old sticky
+ // room to cease to exist.
+ const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
+ const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
+ this.refreshOrderedLists(tagId, roomIndexToRoomId);
+ hasUpdatedAnyList = true;
+ }
+ }
+ // in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID.
+ this.stickyRoomId = RoomViewStore.instance.getRoomId();
+
+ if (hasUpdatedAnyList) {
+ this.emit(LISTS_UPDATE_EVENT);
+ }
+ }
+
+ protected async onReady(): Promise {
+ logger.info("SlidingRoomListStore.onReady");
+ // permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation.
+ SlidingSyncManager.instance.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this));
+ RoomViewStore.instance.addListener(this.onRoomViewStoreUpdated.bind(this));
+ SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this));
+ if (SpaceStore.instance.activeSpace) {
+ this.onSelectedSpaceUpdated(SpaceStore.instance.activeSpace, false);
+ }
+
+ // sliding sync has an initial response for spaces. Now request all the lists.
+ // We do the spaces list _first_ to avoid potential flickering on DefaultTagID.Untagged list
+ // which would be caused by initially having no `spaces` filter set, and then suddenly setting one.
+ OrderedDefaultTagIDs.forEach((tagId) => {
+ const filter = filterConditions[tagId];
+ if (!filter) {
+ logger.info("SlidingRoomListStore.onReady unsupported list ", tagId);
+ return; // we do not support this list yet.
+ }
+ const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config
+ this.tagIdToSortAlgo[tagId] = sort;
+ this.emit(LISTS_LOADING_EVENT, tagId, true);
+ const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
+ SlidingSyncManager.instance.ensureListRegistered(index, {
+ filters: filter,
+ sort: SlidingSyncSortToFilter[sort],
+ }).then(() => {
+ this.emit(LISTS_LOADING_EVENT, tagId, false);
+ });
+ });
+ }
+
+ private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean) => {
+ logger.info("SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace);
+ // update the untagged filter
+ const tagId = DefaultTagID.Untagged;
+ const filters = filterConditions[tagId];
+ const oldSpace = filters.spaces?.[0];
+ filters.spaces = (activeSpace && activeSpace != MetaSpace.Home) ? [activeSpace] : undefined;
+ if (oldSpace !== activeSpace) {
+ this.emit(LISTS_LOADING_EVENT, tagId, true);
+ SlidingSyncManager.instance.ensureListRegistered(
+ SlidingSyncManager.instance.getOrAllocateListIndex(tagId),
+ {
+ filters: filters,
+ },
+ ).then(() => {
+ this.emit(LISTS_LOADING_EVENT, tagId, false);
+ });
+ }
+ };
+
+ // Intended for test usage
+ public async resetStore() {
+ // Test function
+ }
+
+ /**
+ * Regenerates the room whole room list, discarding any previous results.
+ *
+ * Note: This is only exposed externally for the tests. Do not call this from within
+ * the app.
+ * @param trigger Set to false to prevent a list update from being sent. Should only
+ * be used if the calling code will manually trigger the update.
+ */
+ public regenerateAllLists({ trigger = true }) {
+ // Test function
+ }
+
+ protected async onNotReady(): Promise {
+ await this.resetStore();
+ }
+
+ protected async onAction(payload: ActionPayload) {
+ }
+
+ protected async onDispatchAsync(payload: ActionPayload) {
+ }
+}
diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
index af46a921cb..31513b3de2 100644
--- a/src/stores/room-list/SpaceWatcher.ts
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { RoomListStoreClass } from "./RoomListStore";
+import { RoomListStore as Interface } from "./Interface";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
import SpaceStore from "../spaces/SpaceStore";
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
@@ -28,7 +28,7 @@ export class SpaceWatcher {
private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
- constructor(private store: RoomListStoreClass) {
+ constructor(private store: Interface) {
if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
this.updateFilter();
store.addFilter(this.filter);
diff --git a/test/stores/RoomViewStore-test.tsx b/test/stores/RoomViewStore-test.tsx
index 1513783559..b505fc6e6d 100644
--- a/test/stores/RoomViewStore-test.tsx
+++ b/test/stores/RoomViewStore-test.tsx
@@ -41,6 +41,7 @@ describe('RoomViewStore', function() {
joinRoom: jest.fn(),
getRoom: jest.fn(),
getRoomIdForAlias: jest.fn(),
+ isGuest: jest.fn(),
});
const room = new Room('!room:server', mockClient, userId);
@@ -49,6 +50,7 @@ describe('RoomViewStore', function() {
mockClient.credentials = { userId: "@test:example.com" };
mockClient.joinRoom.mockResolvedValue(room);
mockClient.getRoom.mockReturnValue(room);
+ mockClient.isGuest.mockReturnValue(false);
// Reset the state of the store
RoomViewStore.instance.reset();