Merge 473d86120b
into 8dff758153
This commit is contained in:
commit
c9126daa6c
13 changed files with 214 additions and 1220 deletions
|
@ -41,6 +41,7 @@ import PlatformPeg from "./PlatformPeg";
|
||||||
import { formatList } from "./utils/FormattingUtils";
|
import { formatList } from "./utils/FormattingUtils";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
|
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
|
||||||
|
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
||||||
|
|
||||||
export interface IMatrixClientCreds {
|
export interface IMatrixClientCreds {
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
|
@ -297,10 +298,16 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||||
opts.threadSupport = true;
|
opts.threadSupport = true;
|
||||||
|
|
||||||
|
/* TODO: Uncomment before PR lands
|
||||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
||||||
} else {
|
} else {
|
||||||
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
||||||
|
} */
|
||||||
|
// TODO: remove before PR lands. Defaults to SSS if the server entered supports it.
|
||||||
|
await SlidingSyncManager.instance.checkSupport(this.matrixClient);
|
||||||
|
if (SlidingSyncController.serverSupportsSlidingSync) {
|
||||||
|
opts.slidingSync = await SlidingSyncManager.instance.setup(this.matrixClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect the matrix client to the dispatcher and setting handlers
|
// Connect the matrix client to the dispatcher and setting handlers
|
||||||
|
|
|
@ -36,45 +36,53 @@ Please see LICENSE files in the repository root for full details.
|
||||||
* list ops)
|
* list ops)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixClient, EventType, AutoDiscovery, Method, timeoutSignal } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, EventType, ClientEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
MSC3575Filter,
|
MSC3575Filter,
|
||||||
MSC3575List,
|
MSC3575List,
|
||||||
|
MSC3575SlidingSyncResponse,
|
||||||
MSC3575_STATE_KEY_LAZY,
|
MSC3575_STATE_KEY_LAZY,
|
||||||
MSC3575_STATE_KEY_ME,
|
MSC3575_STATE_KEY_ME,
|
||||||
MSC3575_WILDCARD,
|
MSC3575_WILDCARD,
|
||||||
SlidingSync,
|
SlidingSync,
|
||||||
|
SlidingSyncEvent,
|
||||||
|
SlidingSyncState,
|
||||||
} from "matrix-js-sdk/src/sliding-sync";
|
} from "matrix-js-sdk/src/sliding-sync";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
import { defer, sleep } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
import SlidingSyncController from "./settings/controllers/SlidingSyncController";
|
||||||
|
|
||||||
// how long to long poll for
|
// how long to long poll for
|
||||||
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
||||||
|
|
||||||
|
// The state events we will get for every single room/space/old room/etc
|
||||||
|
// This list is only augmented when a direct room subscription is made. (e.g you view a room)
|
||||||
|
const REQUIRED_STATE_LIST = [
|
||||||
|
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
||||||
|
[EventType.RoomAvatar, ""], // any room avatar
|
||||||
|
[EventType.RoomCanonicalAlias, ""], // for room name calculations
|
||||||
|
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
||||||
|
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
||||||
|
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
||||||
|
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
||||||
|
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
||||||
|
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
||||||
|
];
|
||||||
|
|
||||||
// the things to fetch when a user clicks on a room
|
// the things to fetch when a user clicks on a room
|
||||||
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
||||||
timeline_limit: 50,
|
timeline_limit: 50,
|
||||||
// missing required_state which will change depending on the kind of room
|
// missing required_state which will change depending on the kind of room
|
||||||
include_old_rooms: {
|
include_old_rooms: {
|
||||||
timeline_limit: 0,
|
timeline_limit: 0,
|
||||||
required_state: [
|
required_state: REQUIRED_STATE_LIST,
|
||||||
// state needed to handle space navigation and tombstone chains
|
|
||||||
[EventType.RoomCreate, ""],
|
|
||||||
[EventType.RoomTombstone, ""],
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD],
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD],
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// lazy load room members so rooms like Matrix HQ don't take forever to load
|
// lazy load room members so rooms like Matrix HQ don't take forever to load
|
||||||
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
|
const UNENCRYPTED_SUBSCRIPTION_NAME = "unencrypted";
|
||||||
const UNENCRYPTED_SUBSCRIPTION = {
|
const UNENCRYPTED_SUBSCRIPTION = {
|
||||||
required_state: [
|
required_state: [
|
||||||
[MSC3575_WILDCARD, MSC3575_WILDCARD], // all events
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
|
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // except for m.room.members, get our own membership
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
|
[EventType.RoomMember, MSC3575_STATE_KEY_LAZY], // ...and lazy load the rest.
|
||||||
],
|
],
|
||||||
|
@ -90,6 +98,72 @@ const ENCRYPTED_SUBSCRIPTION = {
|
||||||
...DEFAULT_ROOM_SUBSCRIPTION_INFO,
|
...DEFAULT_ROOM_SUBSCRIPTION_INFO,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// the complete set of lists made in SSS. The manager will spider all of these lists depending
|
||||||
|
// on the count for each one.
|
||||||
|
const sssLists: Record<string, MSC3575List> = {
|
||||||
|
spaces: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 0, // we don't care about the most recent message for spaces
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
room_types: ["m.space"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invites: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
is_invite: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
favourites: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
tags: ["m.favourite"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dms: {
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
is_dm: true,
|
||||||
|
is_invite: false,
|
||||||
|
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
||||||
|
not_tags: ["m.favourite", "m.lowpriority"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
untagged: {
|
||||||
|
// SSS will dupe suppress invites/dms from here, so we don't need "not dms, not invites"
|
||||||
|
ranges: [[0, 10]],
|
||||||
|
timeline_limit: 1, // most recent message display
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
include_old_rooms: {
|
||||||
|
timeline_limit: 0,
|
||||||
|
required_state: REQUIRED_STATE_LIST,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export type PartialSlidingSyncRequest = {
|
export type PartialSlidingSyncRequest = {
|
||||||
filters?: MSC3575Filter;
|
filters?: MSC3575Filter;
|
||||||
sort?: string[];
|
sort?: string[];
|
||||||
|
@ -116,48 +190,17 @@ export class SlidingSyncManager {
|
||||||
return SlidingSyncManager.internalInstance;
|
return SlidingSyncManager.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
private configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
// create the set of lists we will use.
|
||||||
|
const lists = new Map();
|
||||||
|
for (const listName in sssLists) {
|
||||||
|
lists.set(listName, sssLists[listName]);
|
||||||
|
}
|
||||||
// by default use the encrypted subscription as that gets everything, which is a safer
|
// by default use the encrypted subscription as that gets everything, which is a safer
|
||||||
// default than potentially missing member events.
|
// default than potentially missing member events.
|
||||||
this.slidingSync = new SlidingSync(
|
this.slidingSync = new SlidingSync(proxyUrl, lists, ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
|
||||||
proxyUrl,
|
|
||||||
new Map(),
|
|
||||||
ENCRYPTED_SUBSCRIPTION,
|
|
||||||
client,
|
|
||||||
SLIDING_SYNC_TIMEOUT_MS,
|
|
||||||
);
|
|
||||||
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
||||||
// set the space list
|
|
||||||
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
|
|
||||||
ranges: [[0, 20]],
|
|
||||||
sort: ["by_name"],
|
|
||||||
slow_get_all_rooms: true,
|
|
||||||
timeline_limit: 0,
|
|
||||||
required_state: [
|
|
||||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
|
||||||
[EventType.RoomAvatar, ""], // any room avatar
|
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
|
||||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
|
||||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
|
||||||
],
|
|
||||||
include_old_rooms: {
|
|
||||||
timeline_limit: 0,
|
|
||||||
required_state: [
|
|
||||||
[EventType.RoomCreate, ""],
|
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
|
||||||
[EventType.SpaceChild, MSC3575_WILDCARD], // all space children
|
|
||||||
[EventType.SpaceParent, MSC3575_WILDCARD], // all space parents
|
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
|
||||||
],
|
|
||||||
},
|
|
||||||
filters: {
|
|
||||||
room_types: ["m.space"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.configureDefer.resolve();
|
this.configureDefer.resolve();
|
||||||
return this.slidingSync;
|
return this.slidingSync;
|
||||||
}
|
}
|
||||||
|
@ -229,90 +272,100 @@ export class SlidingSyncManager {
|
||||||
subscriptions.delete(roomId);
|
subscriptions.delete(roomId);
|
||||||
}
|
}
|
||||||
const room = this.client?.getRoom(roomId);
|
const room = this.client?.getRoom(roomId);
|
||||||
let shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
|
||||||
if (!room) {
|
|
||||||
// default to safety: request all state if we can't work it out. This can happen if you
|
// default to safety: request all state if we can't work it out. This can happen if you
|
||||||
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
// refresh the app whilst viewing a room: we call setRoomVisible before we know anything
|
||||||
// about the room.
|
// about the room.
|
||||||
shouldLazyLoad = false;
|
let shouldLazyLoad = false;
|
||||||
|
if (room) {
|
||||||
|
// do not lazy load encrypted rooms as we need the entire member list.
|
||||||
|
shouldLazyLoad = !(await this.client?.getCrypto()?.isEncryptionEnabledInRoom(roomId));
|
||||||
}
|
}
|
||||||
logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad);
|
logger.log("SlidingSync setRoomVisible:", roomId, visible, "shouldLazyLoad:", shouldLazyLoad);
|
||||||
if (shouldLazyLoad) {
|
if (shouldLazyLoad) {
|
||||||
// lazy load this room
|
// lazy load this room
|
||||||
this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME);
|
this.slidingSync!.useCustomSubscription(roomId, UNENCRYPTED_SUBSCRIPTION_NAME);
|
||||||
}
|
}
|
||||||
const p = this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
this.slidingSync!.modifyRoomSubscriptions(subscriptions);
|
||||||
if (room) {
|
if (room) {
|
||||||
return roomId; // we have data already for this room, show immediately e.g it's in a list
|
return roomId; // we have data already for this room, show immediately e.g it's in a list
|
||||||
}
|
}
|
||||||
try {
|
// wait until we know about this room. This may take a little while.
|
||||||
// wait until the next sync before returning as RoomView may need to know the current state
|
return new Promise((resolve) => {
|
||||||
await p;
|
logger.log(`SlidingSync setRoomVisible room ${roomId} not found, waiting for ClientEvent.Room`);
|
||||||
} catch {
|
const waitForRoom = (r: Room): void => {
|
||||||
logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction");
|
if (r.roomId === roomId) {
|
||||||
|
this.client?.off(ClientEvent.Room, waitForRoom);
|
||||||
|
logger.log(`SlidingSync room ${roomId} found, resolving setRoomVisible`);
|
||||||
|
resolve(roomId);
|
||||||
}
|
}
|
||||||
return roomId;
|
};
|
||||||
|
this.client?.on(ClientEvent.Room, waitForRoom);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all rooms on the user's account. Used for pre-populating the local search cache.
|
* Retrieve all rooms on the user's account. Retrieval is gradual over time.
|
||||||
* Retrieval is gradual over time.
|
* This function MUST be called BEFORE the first sync request goes out.
|
||||||
* @param batchSize The number of rooms to return in each request.
|
* @param batchSize The number of rooms to return in each request.
|
||||||
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
||||||
*/
|
*/
|
||||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
|
private async startSpidering(
|
||||||
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
|
slidingSync: SlidingSync,
|
||||||
let startIndex = batchSize;
|
batchSize: number,
|
||||||
let hasMore = true;
|
gapBetweenRequestsMs: number,
|
||||||
let firstTime = true;
|
): Promise<void> {
|
||||||
while (hasMore) {
|
// The manager has created several lists (see `sssLists` in this file), all of which will be spidered simultaneously.
|
||||||
const endIndex = startIndex + batchSize - 1;
|
// There are multiple lists to ensure that we can populate invites/favourites/DMs sections immediately, rather than
|
||||||
try {
|
// potentially waiting minutes if they are all very old rooms (and hence are returned last by the server). In this
|
||||||
const ranges = [
|
// way, the lists are effectively priority requests. We don't actually care which room goes into which list at this
|
||||||
[0, batchSize - 1],
|
// point, as the RoomListStore will calculate this based on the returned data.
|
||||||
[startIndex, endIndex],
|
|
||||||
];
|
// copy the initial set of list names and ranges, we'll keep this map updated.
|
||||||
if (firstTime) {
|
const listToUpperBound = new Map(
|
||||||
await this.slidingSync!.setList(SlidingSyncManager.ListSearch, {
|
Object.keys(sssLists).map((listName) => {
|
||||||
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
|
return [listName, sssLists[listName].ranges[0][1]];
|
||||||
// any changes to the list whilst spidering are caught.
|
}),
|
||||||
ranges: ranges,
|
);
|
||||||
sort: [
|
console.log("startSpidering:", listToUpperBound);
|
||||||
"by_recency", // this list isn't shown on the UI so just sorting by timestamp is enough
|
|
||||||
],
|
// listen for a response from the server. ANY 200 OK will do here, as we assume that it is ACKing
|
||||||
timeline_limit: 0, // we only care about the room details, not messages in the room
|
// the request change we have sent out. TODO: this may not be true if you concurrently subscribe to a room :/
|
||||||
required_state: [
|
// but in that case, for spidering at least, it isn't the end of the world as request N+1 includes all indexes
|
||||||
[EventType.RoomJoinRules, ""], // the public icon on the room list
|
// from request N.
|
||||||
[EventType.RoomAvatar, ""], // any room avatar
|
const lifecycle = async (
|
||||||
[EventType.RoomTombstone, ""], // lets JS SDK hide rooms which are dead
|
state: SlidingSyncState,
|
||||||
[EventType.RoomEncryption, ""], // lets rooms be configured for E2EE correctly
|
_: MSC3575SlidingSyncResponse | null,
|
||||||
[EventType.RoomCreate, ""], // for isSpaceRoom checks
|
err?: Error,
|
||||||
[EventType.RoomMember, MSC3575_STATE_KEY_ME], // lets the client calculate that we are in fact in the room
|
): Promise<void> => {
|
||||||
],
|
if (state !== SlidingSyncState.Complete) {
|
||||||
// we don't include_old_rooms here in an effort to reduce the impact of spidering all rooms
|
return;
|
||||||
// on the user's account. This means some data in the search dialog results may be inaccurate
|
}
|
||||||
// e.g membership of space, but this will be corrected when the user clicks on the room
|
await sleep(gapBetweenRequestsMs); // don't tightloop; even on errors
|
||||||
// as the direct room subscription does include old room iterations.
|
if (err) {
|
||||||
filters: {
|
return;
|
||||||
// we get spaces via a different list, so filter them out
|
}
|
||||||
not_room_types: ["m.space"],
|
|
||||||
},
|
// for all lists with total counts > range => increase the range
|
||||||
|
let hasSetRanges = false;
|
||||||
|
listToUpperBound.forEach((currentUpperBound, listName) => {
|
||||||
|
const totalCount = slidingSync.getListData(listName)?.joinedCount || 0;
|
||||||
|
if (currentUpperBound < totalCount) {
|
||||||
|
// increment the upper bound
|
||||||
|
const newUpperBound = currentUpperBound + batchSize;
|
||||||
|
console.log(`startSpidering: ${listName} ${currentUpperBound} => ${newUpperBound}`);
|
||||||
|
listToUpperBound.set(listName, newUpperBound);
|
||||||
|
// make the next request. This will only send the request when this callback has finished, so if
|
||||||
|
// we set all the list ranges at once we will only send 1 new request.
|
||||||
|
slidingSync.setListRanges(listName, [[0, newUpperBound]]);
|
||||||
|
hasSetRanges = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
if (!hasSetRanges) {
|
||||||
await this.slidingSync!.setListRanges(SlidingSyncManager.ListSearch, ranges);
|
// finish spidering
|
||||||
}
|
slidingSync.off(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||||
} catch {
|
|
||||||
// do nothing, as we reject only when we get interrupted but that's fine as the next
|
|
||||||
// request will include our data
|
|
||||||
} finally {
|
|
||||||
// gradually request more over time, even on errors.
|
|
||||||
await sleep(gapBetweenRequestsMs);
|
|
||||||
}
|
|
||||||
const listData = this.slidingSync!.getListData(SlidingSyncManager.ListSearch)!;
|
|
||||||
hasMore = endIndex + 1 < listData.joinedCount;
|
|
||||||
startIndex += batchSize;
|
|
||||||
firstTime = false;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
slidingSync.on(SlidingSyncEvent.Lifecycle, lifecycle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -325,42 +378,10 @@ export class SlidingSyncManager {
|
||||||
* @returns A working Sliding Sync or undefined
|
* @returns A working Sliding Sync or undefined
|
||||||
*/
|
*/
|
||||||
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
public async setup(client: MatrixClient): Promise<SlidingSync | undefined> {
|
||||||
const baseUrl = client.baseUrl;
|
const slidingSync = this.configure(client, client.baseUrl);
|
||||||
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
logger.info("Simplified Sliding Sync activated at", client.baseUrl);
|
||||||
const wellKnownProxyUrl = await this.getProxyFromWellKnown(client);
|
this.startSpidering(slidingSync, 50, 50); // 50 rooms at a time, 50ms apart
|
||||||
|
return slidingSync;
|
||||||
const slidingSyncEndpoint = proxyUrl || wellKnownProxyUrl || baseUrl;
|
|
||||||
|
|
||||||
this.configure(client, slidingSyncEndpoint);
|
|
||||||
logger.info("Sliding sync activated at", slidingSyncEndpoint);
|
|
||||||
this.startSpidering(100, 50); // 100 rooms at a time, 50ms apart
|
|
||||||
|
|
||||||
return this.slidingSync;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the sliding sync proxy URL from the client well known
|
|
||||||
* @param client The MatrixClient to use
|
|
||||||
* @return The proxy url
|
|
||||||
*/
|
|
||||||
public async getProxyFromWellKnown(client: MatrixClient): Promise<string | undefined> {
|
|
||||||
let proxyUrl: string | undefined;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const clientDomain = await client.getDomain();
|
|
||||||
if (clientDomain === null) {
|
|
||||||
throw new RangeError("Homeserver domain is null");
|
|
||||||
}
|
|
||||||
const clientWellKnown = await AutoDiscovery.findClientConfig(clientDomain);
|
|
||||||
proxyUrl = clientWellKnown?.["org.matrix.msc3575.proxy"]?.url;
|
|
||||||
} catch {
|
|
||||||
// Either client.getDomain() is null so we've shorted out, or is invalid so `AutoDiscovery.findClientConfig` has thrown
|
|
||||||
}
|
|
||||||
|
|
||||||
if (proxyUrl != undefined) {
|
|
||||||
logger.log("getProxyFromWellKnown: client well-known declares sliding sync proxy at", proxyUrl);
|
|
||||||
}
|
|
||||||
return proxyUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -371,9 +392,9 @@ export class SlidingSyncManager {
|
||||||
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
public async nativeSlidingSyncSupport(client: MatrixClient): Promise<boolean> {
|
||||||
// Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561
|
// Per https://github.com/matrix-org/matrix-spec-proposals/pull/3575/files#r1589542561
|
||||||
// `client` can be undefined/null in tests for some reason.
|
// `client` can be undefined/null in tests for some reason.
|
||||||
const support = await client?.doesServerSupportUnstableFeature("org.matrix.msc3575");
|
const support = await client?.doesServerSupportUnstableFeature("org.matrix.simplified_msc3575");
|
||||||
if (support) {
|
if (support) {
|
||||||
logger.log("nativeSlidingSyncSupport: sliding sync advertised as unstable");
|
logger.log("nativeSlidingSyncSupport: org.matrix.simplified_msc3575 sliding sync advertised as unstable");
|
||||||
}
|
}
|
||||||
return support;
|
return support;
|
||||||
}
|
}
|
||||||
|
@ -390,17 +411,6 @@ export class SlidingSyncManager {
|
||||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
SlidingSyncController.serverSupportsSlidingSync = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
SlidingSyncController.serverSupportsSlidingSync = false;
|
||||||
const proxyUrl = await this.getProxyFromWellKnown(client);
|
|
||||||
if (proxyUrl != undefined) {
|
|
||||||
const response = await fetch(new URL("/client/server.json", proxyUrl), {
|
|
||||||
method: Method.Get,
|
|
||||||
signal: timeoutSignal(10 * 1000), // 10s
|
|
||||||
});
|
|
||||||
if (response.status === 200) {
|
|
||||||
logger.log("checkSupport: well-known sliding sync proxy is up at", proxyUrl);
|
|
||||||
SlidingSyncController.serverSupportsSlidingSync = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -710,7 +710,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
if (!Thread.hasServerSideSupport && this.context.timelineRenderingType === TimelineRenderingType.Thread) {
|
||||||
if (toStartOfTimeline && !this.state.canBackPaginate) {
|
if (toStartOfTimeline && !this.state.canBackPaginate) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1541,7 +1540,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
const onLoaded = (): void => {
|
const onLoaded = (): void => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted) return;
|
||||||
|
|
||||||
// clear the timeline min-height when (re)loading the timeline
|
// clear the timeline min-height when (re)loading the timeline
|
||||||
this.messagePanel.current?.onTimelineReset();
|
this.messagePanel.current?.onTimelineReset();
|
||||||
this.reloadEvents();
|
this.reloadEvents();
|
||||||
|
@ -1643,7 +1641,6 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
||||||
onLoaded();
|
onLoaded();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise<void> => {
|
const prom = this.timelineWindow.load(eventId, INITIAL_SIZE).then(async (): Promise<void> => {
|
||||||
if (this.overlayTimelineWindow) {
|
if (this.overlayTimelineWindow) {
|
||||||
// TODO: use timestampToEvent to load the overlay timeline
|
// TODO: use timestampToEvent to load the overlay timeline
|
||||||
|
|
|
@ -329,12 +329,6 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowAllClick = async (): Promise<void> => {
|
private onShowAllClick = async (): Promise<void> => {
|
||||||
if (this.slidingSyncMode) {
|
|
||||||
const count = RoomListStore.instance.getCount(this.props.tagId);
|
|
||||||
await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, {
|
|
||||||
ranges: [[0, count]],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// read number of visible tiles before we mutate it
|
// read number of visible tiles before we mutate it
|
||||||
const numVisibleTiles = this.numVisibleTiles;
|
const numVisibleTiles = this.numVisibleTiles;
|
||||||
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
|
||||||
import { useLatestResult } from "./useLatestResult";
|
|
||||||
import { SlidingSyncManager } from "../SlidingSyncManager";
|
|
||||||
|
|
||||||
export interface SlidingSyncRoomSearchOpts {
|
|
||||||
limit: number;
|
|
||||||
query: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSlidingSyncRoomSearch = (): {
|
|
||||||
loading: boolean;
|
|
||||||
rooms: Room[];
|
|
||||||
search(opts: SlidingSyncRoomSearchOpts): Promise<boolean>;
|
|
||||||
} => {
|
|
||||||
const [rooms, setRooms] = useState<Room[]>([]);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms);
|
|
||||||
|
|
||||||
const search = useCallback(
|
|
||||||
async ({ limit = 100, query: term }: SlidingSyncRoomSearchOpts): Promise<boolean> => {
|
|
||||||
const opts = { limit, term };
|
|
||||||
updateQuery(opts);
|
|
||||||
|
|
||||||
if (!term?.length) {
|
|
||||||
setRooms([]);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, {
|
|
||||||
ranges: [[0, limit]],
|
|
||||||
filters: {
|
|
||||||
room_name_like: term,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const rooms: Room[] = [];
|
|
||||||
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync!.getListData(
|
|
||||||
SlidingSyncManager.ListSearch,
|
|
||||||
)!;
|
|
||||||
let i = 0;
|
|
||||||
while (roomIndexToRoomId[i]) {
|
|
||||||
const roomId = roomIndexToRoomId[i];
|
|
||||||
const room = MatrixClientPeg.safeGet().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],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading,
|
|
||||||
rooms,
|
|
||||||
search,
|
|
||||||
} as const;
|
|
||||||
};
|
|
|
@ -1465,7 +1465,7 @@
|
||||||
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
"render_reaction_images_description": "Sometimes referred to as \"custom emojis\".",
|
||||||
"report_to_moderators": "Report to moderators",
|
"report_to_moderators": "Report to moderators",
|
||||||
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
"report_to_moderators_description": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
||||||
"sliding_sync": "Sliding Sync mode",
|
"sliding_sync": "Simplified Sliding Sync mode",
|
||||||
"sliding_sync_description": "Under active development, cannot be disabled.",
|
"sliding_sync_description": "Under active development, cannot be disabled.",
|
||||||
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
"sliding_sync_disabled_notice": "Log out and back in to disable",
|
||||||
"sliding_sync_server_no_support": "Your server lacks support",
|
"sliding_sync_server_no_support": "Your server lacks support",
|
||||||
|
|
|
@ -377,11 +377,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
default: false,
|
default: false,
|
||||||
controller: new SlidingSyncController(),
|
controller: new SlidingSyncController(),
|
||||||
},
|
},
|
||||||
"feature_sliding_sync_proxy_url": {
|
|
||||||
// This is not a distinct feature, it is a legacy setting for feature_sliding_sync above
|
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
"feature_element_call_video_rooms": {
|
"feature_element_call_video_rooms": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.VoiceAndVideo,
|
labsGroup: LabGroup.VoiceAndVideo,
|
||||||
|
|
|
@ -27,7 +27,6 @@ import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||||
import { SpaceWatcher } from "./SpaceWatcher";
|
import { SpaceWatcher } from "./SpaceWatcher";
|
||||||
import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators";
|
import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators";
|
||||||
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
||||||
import { SlidingRoomListStoreClass } from "./SlidingRoomListStore";
|
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
import { SdkContextClass } from "../../contexts/SDKContext";
|
||||||
import { getChangedOverrideRoomMutePushRules } from "./utils/roomMute";
|
import { getChangedOverrideRoomMutePushRules } from "./utils/roomMute";
|
||||||
|
@ -640,17 +639,10 @@ export default class RoomListStore {
|
||||||
|
|
||||||
public static get instance(): Interface {
|
public static get instance(): Interface {
|
||||||
if (!RoomListStore.internalInstance) {
|
if (!RoomListStore.internalInstance) {
|
||||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
|
||||||
logger.info("using SlidingRoomListStoreClass");
|
|
||||||
const instance = new SlidingRoomListStoreClass(defaultDispatcher, SdkContextClass.instance);
|
|
||||||
instance.start();
|
|
||||||
RoomListStore.internalInstance = instance;
|
|
||||||
} else {
|
|
||||||
const instance = new RoomListStoreClass(defaultDispatcher);
|
const instance = new RoomListStoreClass(defaultDispatcher);
|
||||||
instance.start();
|
instance.start();
|
||||||
RoomListStore.internalInstance = instance;
|
RoomListStore.internalInstance = instance;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return this.internalInstance;
|
return this.internalInstance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,395 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
|
||||||
import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
|
|
||||||
import { Optional } from "matrix-events-sdk";
|
|
||||||
|
|
||||||
import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models";
|
|
||||||
import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
|
||||||
import { MatrixDispatcher } from "../../dispatcher/dispatcher";
|
|
||||||
import { IFilterCondition } from "./filters/IFilterCondition";
|
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
|
||||||
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
|
||||||
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces";
|
|
||||||
import { LISTS_LOADING_EVENT } from "./RoomListStore";
|
|
||||||
import { UPDATE_EVENT } from "../AsyncStore";
|
|
||||||
import { SdkContextClass } from "../../contexts/SDKContext";
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
// state is tracked in underlying classes
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SlidingSyncSortToFilter: Record<SortAlgorithm, string[]> = {
|
|
||||||
[SortAlgorithm.Alphabetic]: ["by_name", "by_recency"],
|
|
||||||
[SortAlgorithm.Recent]: ["by_notification_level", "by_recency"],
|
|
||||||
[SortAlgorithm.Manual]: ["by_recency"],
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterConditions: Record<TagID, MSC3575Filter> = {
|
|
||||||
[DefaultTagID.Invite]: {
|
|
||||||
is_invite: true,
|
|
||||||
},
|
|
||||||
[DefaultTagID.Favourite]: {
|
|
||||||
tags: ["m.favourite"],
|
|
||||||
},
|
|
||||||
[DefaultTagID.DM]: {
|
|
||||||
is_dm: true,
|
|
||||||
is_invite: false,
|
|
||||||
// If a DM has a Favourite & Low Prio tag then it'll be shown in those lists instead
|
|
||||||
not_tags: ["m.favourite", "m.lowpriority"],
|
|
||||||
},
|
|
||||||
[DefaultTagID.Untagged]: {
|
|
||||||
is_dm: false,
|
|
||||||
is_invite: false,
|
|
||||||
not_room_types: ["m.space"],
|
|
||||||
not_tags: ["m.favourite", "m.lowpriority"],
|
|
||||||
// spaces filter added dynamically
|
|
||||||
},
|
|
||||||
[DefaultTagID.LowPriority]: {
|
|
||||||
tags: ["m.lowpriority"],
|
|
||||||
// If a room has both Favourite & Low Prio tags then it'll be shown under Favourites
|
|
||||||
not_tags: ["m.favourite"],
|
|
||||||
},
|
|
||||||
// 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<IState> implements Interface {
|
|
||||||
private tagIdToSortAlgo: Record<TagID, SortAlgorithm> = {};
|
|
||||||
private tagMap: ITagMap = {};
|
|
||||||
private counts: Record<TagID, number> = {};
|
|
||||||
private stickyRoomId: Optional<string>;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
dis: MatrixDispatcher,
|
|
||||||
private readonly context: SdkContextClass,
|
|
||||||
) {
|
|
||||||
super(dis);
|
|
||||||
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise<void> {
|
|
||||||
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
|
|
||||||
this.tagIdToSortAlgo[tagId] = sort;
|
|
||||||
switch (sort) {
|
|
||||||
case SortAlgorithm.Alphabetic:
|
|
||||||
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
|
|
||||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case SortAlgorithm.Recent:
|
|
||||||
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
|
|
||||||
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): void {
|
|
||||||
// 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<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.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId);
|
|
||||||
if (!listData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const roomIndex in listData.roomIndexToRoomId) {
|
|
||||||
const roomId = listData.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): Promise<void> {
|
|
||||||
// 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<number, string>): 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 = this.context.roomViewStore.getRoomId();
|
|
||||||
let stickyRoomNewIndex = -1;
|
|
||||||
const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room): boolean => {
|
|
||||||
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<string>();
|
|
||||||
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: Room[] = [];
|
|
||||||
orderedRoomIds.forEach((roomId) => {
|
|
||||||
const room = this.matrixClient?.getRoom(roomId);
|
|
||||||
if (!room) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rooms.push(room);
|
|
||||||
});
|
|
||||||
tagMap[tagId] = rooms;
|
|
||||||
this.tagMap = tagMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record<number, string>): void {
|
|
||||||
this.counts[tagId] = joinCount;
|
|
||||||
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
|
||||||
// let the UI update
|
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRoomViewStoreUpdated(): void {
|
|
||||||
// we only care about this to know when the user has clicked on a room to set the stickiness value
|
|
||||||
if (this.context.roomViewStore.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 listData = this.context.slidingSyncManager.slidingSync?.getListData(tagId);
|
|
||||||
if (!listData) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
this.refreshOrderedLists(tagId, listData.roomIndexToRoomId);
|
|
||||||
hasUpdatedAnyList = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID.
|
|
||||||
this.stickyRoomId = this.context.roomViewStore.getRoomId();
|
|
||||||
|
|
||||||
if (hasUpdatedAnyList) {
|
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onReady(): Promise<any> {
|
|
||||||
logger.info("SlidingRoomListStore.onReady");
|
|
||||||
// permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation.
|
|
||||||
this.context.slidingSyncManager.slidingSync!.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this));
|
|
||||||
this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdated.bind(this));
|
|
||||||
this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this));
|
|
||||||
if (this.context.spaceStore.activeSpace) {
|
|
||||||
this.onSelectedSpaceUpdated(this.context.spaceStore.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);
|
|
||||||
this.context.slidingSyncManager
|
|
||||||
.ensureListRegistered(tagId, {
|
|
||||||
filters: filter,
|
|
||||||
sort: SlidingSyncSortToFilter[sort],
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.emit(LISTS_LOADING_EVENT, tagId, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean): void => {
|
|
||||||
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) {
|
|
||||||
// include subspaces in this list
|
|
||||||
this.context.spaceStore.traverseSpace(
|
|
||||||
activeSpace,
|
|
||||||
(roomId: string) => {
|
|
||||||
if (roomId === activeSpace) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!filters.spaces) {
|
|
||||||
filters.spaces = [];
|
|
||||||
}
|
|
||||||
filters.spaces.push(roomId); // add subspace
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
|
||||||
this.context.slidingSyncManager
|
|
||||||
.ensureListRegistered(tagId, {
|
|
||||||
filters: filters,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.emit(LISTS_LOADING_EVENT, tagId, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Intended for test usage
|
|
||||||
public async resetStore(): Promise<void> {
|
|
||||||
// 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 }): void {
|
|
||||||
// Test function
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onNotReady(): Promise<any> {
|
|
||||||
await this.resetStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload): Promise<void> {}
|
|
||||||
}
|
|
|
@ -69,6 +69,12 @@ const getLastTs = (r: Room, userId: string): number => {
|
||||||
if (!r?.timeline) {
|
if (!r?.timeline) {
|
||||||
return Number.MAX_SAFE_INTEGER;
|
return Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
|
// MSC4186: Simplified Sliding Sync sets this.
|
||||||
|
// If it's present, sort by it.
|
||||||
|
const bumpStamp = r.getBumpStamp();
|
||||||
|
if (bumpStamp) {
|
||||||
|
return bumpStamp;
|
||||||
|
}
|
||||||
|
|
||||||
// If the room hasn't been joined yet, it probably won't have a timeline to
|
// If the room hasn't been joined yet, it probably won't have a timeline to
|
||||||
// parse. We'll still fall back to the timeline if this fails, but chances
|
// parse. We'll still fall back to the timeline if this fails, but chances
|
||||||
|
|
|
@ -14,7 +14,6 @@ import fetchMockJest from "fetch-mock-jest";
|
||||||
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
|
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
|
||||||
import { stubClient } from "../test-utils";
|
import { stubClient } from "../test-utils";
|
||||||
import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController";
|
import SlidingSyncController from "../../src/settings/controllers/SlidingSyncController";
|
||||||
import SettingsStore from "../../src/settings/SettingsStore";
|
|
||||||
|
|
||||||
jest.mock("matrix-js-sdk/src/sliding-sync");
|
jest.mock("matrix-js-sdk/src/sliding-sync");
|
||||||
const MockSlidingSync = <jest.Mock<SlidingSync>>(<unknown>SlidingSync);
|
const MockSlidingSync = <jest.Mock<SlidingSync>>(<unknown>SlidingSync);
|
||||||
|
@ -30,7 +29,7 @@ describe("SlidingSyncManager", () => {
|
||||||
client = stubClient();
|
client = stubClient();
|
||||||
// by default the client has no rooms: stubClient magically makes rooms annoyingly.
|
// by default the client has no rooms: stubClient magically makes rooms annoyingly.
|
||||||
mocked(client.getRoom).mockReturnValue(null);
|
mocked(client.getRoom).mockReturnValue(null);
|
||||||
manager.configure(client, "invalid");
|
(manager as any).configure(client, "invalid");
|
||||||
manager.slidingSync = slidingSync;
|
manager.slidingSync = slidingSync;
|
||||||
fetchMockJest.reset();
|
fetchMockJest.reset();
|
||||||
fetchMockJest.get("https://proxy/client/server.json", {});
|
fetchMockJest.get("https://proxy/client/server.json", {});
|
||||||
|
@ -41,7 +40,6 @@ describe("SlidingSyncManager", () => {
|
||||||
const roomId = "!room:id";
|
const roomId = "!room:id";
|
||||||
const subs = new Set<string>();
|
const subs = new Set<string>();
|
||||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||||
mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep");
|
|
||||||
await manager.setRoomVisible(roomId, true);
|
await manager.setRoomVisible(roomId, true);
|
||||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||||
});
|
});
|
||||||
|
@ -67,7 +65,6 @@ describe("SlidingSyncManager", () => {
|
||||||
});
|
});
|
||||||
const subs = new Set<string>();
|
const subs = new Set<string>();
|
||||||
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
mocked(slidingSync.getRoomSubscriptions).mockReturnValue(subs);
|
||||||
mocked(slidingSync.modifyRoomSubscriptions).mockResolvedValue("yep");
|
|
||||||
await manager.setRoomVisible(roomId, true);
|
await manager.setRoomVisible(roomId, true);
|
||||||
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
expect(slidingSync.modifyRoomSubscriptions).toHaveBeenCalledWith(new Set<string>([roomId]));
|
||||||
// we aren't prescriptive about what the sub name is.
|
// we aren't prescriptive about what the sub name is.
|
||||||
|
@ -79,7 +76,6 @@ describe("SlidingSyncManager", () => {
|
||||||
it("creates a new list based on the key", async () => {
|
it("creates a new list based on the key", async () => {
|
||||||
const listKey = "key";
|
const listKey = "key";
|
||||||
mocked(slidingSync.getListParams).mockReturnValue(null);
|
mocked(slidingSync.getListParams).mockReturnValue(null);
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
await manager.ensureListRegistered(listKey, {
|
await manager.ensureListRegistered(listKey, {
|
||||||
sort: ["by_recency"],
|
sort: ["by_recency"],
|
||||||
});
|
});
|
||||||
|
@ -96,7 +92,6 @@ describe("SlidingSyncManager", () => {
|
||||||
mocked(slidingSync.getListParams).mockReturnValue({
|
mocked(slidingSync.getListParams).mockReturnValue({
|
||||||
ranges: [[0, 42]],
|
ranges: [[0, 42]],
|
||||||
});
|
});
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
await manager.ensureListRegistered(listKey, {
|
await manager.ensureListRegistered(listKey, {
|
||||||
sort: ["by_recency"],
|
sort: ["by_recency"],
|
||||||
});
|
});
|
||||||
|
@ -114,7 +109,6 @@ describe("SlidingSyncManager", () => {
|
||||||
mocked(slidingSync.getListParams).mockReturnValue({
|
mocked(slidingSync.getListParams).mockReturnValue({
|
||||||
ranges: [[0, 42]],
|
ranges: [[0, 42]],
|
||||||
});
|
});
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
await manager.ensureListRegistered(listKey, {
|
await manager.ensureListRegistered(listKey, {
|
||||||
ranges: [[0, 52]],
|
ranges: [[0, 52]],
|
||||||
});
|
});
|
||||||
|
@ -128,7 +122,6 @@ describe("SlidingSyncManager", () => {
|
||||||
ranges: [[0, 42]],
|
ranges: [[0, 42]],
|
||||||
sort: ["by_recency"],
|
sort: ["by_recency"],
|
||||||
});
|
});
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
await manager.ensureListRegistered(listKey, {
|
await manager.ensureListRegistered(listKey, {
|
||||||
ranges: [[0, 42]],
|
ranges: [[0, 42]],
|
||||||
sort: ["by_recency"],
|
sort: ["by_recency"],
|
||||||
|
@ -139,26 +132,25 @@ describe("SlidingSyncManager", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("startSpidering", () => {
|
describe("startSpidering", () => {
|
||||||
it("requests in batchSizes", async () => {
|
it("requests in expanding batchSizes", async () => {
|
||||||
const gapMs = 1;
|
const gapMs = 1;
|
||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
mocked(slidingSync.setListRanges).mockResolvedValue("yep");
|
|
||||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||||
return {
|
return {
|
||||||
joinedCount: 64,
|
joinedCount: 64,
|
||||||
roomIndexToRoomId: {},
|
roomIndexToRoomId: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await manager.startSpidering(batchSize, gapMs);
|
await (manager as any).startSpidering(batchSize, gapMs);
|
||||||
// we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69
|
// we expect calls for 10,19 -> 20,29 -> 30,39 -> 40,49 -> 50,59 -> 60,69
|
||||||
const wantWindows = [
|
const wantWindows = [
|
||||||
[10, 19],
|
[0, 10],
|
||||||
[20, 29],
|
[0, 20],
|
||||||
[30, 39],
|
[0, 30],
|
||||||
[40, 49],
|
[0, 40],
|
||||||
[50, 59],
|
[0, 50],
|
||||||
[60, 69],
|
[0, 60],
|
||||||
|
[0, 70],
|
||||||
];
|
];
|
||||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length);
|
expect(slidingSync.getListData).toHaveBeenCalledTimes(wantWindows.length);
|
||||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
||||||
|
@ -170,60 +162,30 @@ describe("SlidingSyncManager", () => {
|
||||||
SlidingSyncManager.ListSearch,
|
SlidingSyncManager.ListSearch,
|
||||||
// eslint-disable-next-line jest/no-conditional-expect
|
// eslint-disable-next-line jest/no-conditional-expect
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ranges: [[0, batchSize - 1], range],
|
ranges: [range],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [
|
expect(slidingSync.setListRanges).toHaveBeenCalledWith(SlidingSyncManager.ListSearch, [range]);
|
||||||
[0, batchSize - 1],
|
|
||||||
range,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("handles accounts with zero rooms", async () => {
|
it("handles accounts with zero rooms", async () => {
|
||||||
const gapMs = 1;
|
const gapMs = 1;
|
||||||
const batchSize = 10;
|
const batchSize = 10;
|
||||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
|
||||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||||
return {
|
return {
|
||||||
joinedCount: 0,
|
joinedCount: 0,
|
||||||
roomIndexToRoomId: {},
|
roomIndexToRoomId: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await manager.startSpidering(batchSize, gapMs);
|
await (manager as any).startSpidering(batchSize, gapMs);
|
||||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(1);
|
expect(slidingSync.getListData).toHaveBeenCalledTimes(1);
|
||||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
||||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
expect(slidingSync.setList).toHaveBeenCalledWith(
|
||||||
SlidingSyncManager.ListSearch,
|
SlidingSyncManager.ListSearch,
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ranges: [
|
ranges: [[0, batchSize]],
|
||||||
[0, batchSize - 1],
|
|
||||||
[batchSize, batchSize + batchSize - 1],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it("continues even when setList rejects", async () => {
|
|
||||||
const gapMs = 1;
|
|
||||||
const batchSize = 10;
|
|
||||||
mocked(slidingSync.setList).mockRejectedValue("narp");
|
|
||||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
|
||||||
return {
|
|
||||||
joinedCount: 0,
|
|
||||||
roomIndexToRoomId: {},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await manager.startSpidering(batchSize, gapMs);
|
|
||||||
expect(slidingSync.getListData).toHaveBeenCalledTimes(1);
|
|
||||||
expect(slidingSync.setList).toHaveBeenCalledTimes(1);
|
|
||||||
expect(slidingSync.setList).toHaveBeenCalledWith(
|
|
||||||
SlidingSyncManager.ListSearch,
|
|
||||||
expect.objectContaining({
|
|
||||||
ranges: [
|
|
||||||
[0, batchSize - 1],
|
|
||||||
[batchSize, batchSize + batchSize - 1],
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -231,91 +193,24 @@ describe("SlidingSyncManager", () => {
|
||||||
describe("checkSupport", () => {
|
describe("checkSupport", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
SlidingSyncController.serverSupportsSlidingSync = false;
|
SlidingSyncController.serverSupportsSlidingSync = false;
|
||||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
|
||||||
});
|
});
|
||||||
it("shorts out if the server has 'native' sliding sync support", async () => {
|
it("shorts out if the server has 'native' sliding sync support", async () => {
|
||||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true);
|
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(true);
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
||||||
await manager.checkSupport(client);
|
await manager.checkSupport(client);
|
||||||
expect(manager.getProxyFromWellKnown).not.toHaveBeenCalled(); // We return earlier
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
|
||||||
});
|
|
||||||
it("tries to find a sliding sync proxy url from the client well-known if there's no 'native' support", async () => {
|
|
||||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false);
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
|
||||||
await manager.checkSupport(client);
|
|
||||||
expect(manager.getProxyFromWellKnown).toHaveBeenCalled();
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
|
||||||
});
|
|
||||||
it("should query well-known on server_name not baseUrl", async () => {
|
|
||||||
fetchMockJest.get("https://matrix.org/.well-known/matrix/client", {
|
|
||||||
"m.homeserver": {
|
|
||||||
base_url: "https://matrix-client.matrix.org",
|
|
||||||
server: "matrix.org",
|
|
||||||
},
|
|
||||||
"org.matrix.msc3575.proxy": {
|
|
||||||
url: "https://proxy/",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
fetchMockJest.get("https://matrix-client.matrix.org/_matrix/client/versions", { versions: ["v1.4"] });
|
|
||||||
|
|
||||||
mocked(manager.getProxyFromWellKnown).mockRestore();
|
|
||||||
jest.spyOn(manager, "nativeSlidingSyncSupport").mockResolvedValue(false);
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
|
||||||
await manager.checkSupport(client);
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
|
||||||
expect(fetchMockJest).not.toHaveFetched("https://matrix-client.matrix.org/.well-known/matrix/client");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe("nativeSlidingSyncSupport", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
SlidingSyncController.serverSupportsSlidingSync = false;
|
|
||||||
});
|
|
||||||
it("should make an OPTIONS request to avoid unintended side effects", async () => {
|
|
||||||
// See https://github.com/element-hq/element-web/issues/27426
|
|
||||||
|
|
||||||
const unstableSpy = jest
|
|
||||||
.spyOn(client, "doesServerSupportUnstableFeature")
|
|
||||||
.mockImplementation(async (feature: string) => {
|
|
||||||
expect(feature).toBe("org.matrix.msc3575");
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const proxySpy = jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
|
||||||
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeFalsy();
|
|
||||||
await manager.checkSupport(client); // first thing it does is call nativeSlidingSyncSupport
|
|
||||||
expect(proxySpy).not.toHaveBeenCalled();
|
|
||||||
expect(unstableSpy).toHaveBeenCalled();
|
|
||||||
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
expect(SlidingSyncController.serverSupportsSlidingSync).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("setup", () => {
|
describe("setup", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(manager, "configure");
|
jest.spyOn(manager as any, "configure");
|
||||||
jest.spyOn(manager, "startSpidering");
|
jest.spyOn(manager as any, "startSpidering");
|
||||||
});
|
});
|
||||||
it("uses the baseUrl as a proxy if no proxy is set in the client well-known and the server has no native support", async () => {
|
it("uses the baseUrl", async () => {
|
||||||
await manager.setup(client);
|
await manager.setup(client);
|
||||||
expect(manager.configure).toHaveBeenCalled();
|
expect((manager as any).configure).toHaveBeenCalled();
|
||||||
expect(manager.configure).toHaveBeenCalledWith(client, client.baseUrl);
|
expect((manager as any).configure).toHaveBeenCalledWith(client, client.baseUrl);
|
||||||
expect(manager.startSpidering).toHaveBeenCalled();
|
expect((manager as any).startSpidering).toHaveBeenCalled();
|
||||||
});
|
|
||||||
it("uses the proxy declared in the client well-known", async () => {
|
|
||||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
|
||||||
await manager.setup(client);
|
|
||||||
expect(manager.configure).toHaveBeenCalled();
|
|
||||||
expect(manager.configure).toHaveBeenCalledWith(client, "https://proxy/");
|
|
||||||
expect(manager.startSpidering).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
it("uses the legacy `feature_sliding_sync_proxy_url` if it was set", async () => {
|
|
||||||
jest.spyOn(manager, "getProxyFromWellKnown").mockResolvedValue("https://proxy/");
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
|
|
||||||
if (name === "feature_sliding_sync_proxy_url") return "legacy-proxy";
|
|
||||||
});
|
|
||||||
await manager.setup(client);
|
|
||||||
expect(manager.configure).toHaveBeenCalled();
|
|
||||||
expect(manager.configure).toHaveBeenCalledWith(client, "legacy-proxy");
|
|
||||||
expect(manager.startSpidering).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { waitFor, renderHook, act } from "jest-matrix-react";
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import { SlidingSync } from "matrix-js-sdk/src/sliding-sync";
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import { useSlidingSyncRoomSearch } from "../../../src/hooks/useSlidingSyncRoomSearch";
|
|
||||||
import { MockEventEmitter, stubClient } from "../../test-utils";
|
|
||||||
import { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
|
||||||
|
|
||||||
describe("useSlidingSyncRoomSearch", () => {
|
|
||||||
afterAll(() => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should display rooms when searching", async () => {
|
|
||||||
const client = stubClient();
|
|
||||||
const roomA = new Room("!a:localhost", client, client.getUserId()!);
|
|
||||||
const roomB = new Room("!b:localhost", client, client.getUserId()!);
|
|
||||||
const slidingSync = mocked(
|
|
||||||
new MockEventEmitter({
|
|
||||||
getListData: jest.fn(),
|
|
||||||
}) as unknown as SlidingSync,
|
|
||||||
);
|
|
||||||
jest.spyOn(SlidingSyncManager.instance, "ensureListRegistered").mockResolvedValue({
|
|
||||||
ranges: [[0, 9]],
|
|
||||||
});
|
|
||||||
SlidingSyncManager.instance.slidingSync = slidingSync;
|
|
||||||
mocked(slidingSync.getListData).mockReturnValue({
|
|
||||||
joinedCount: 2,
|
|
||||||
roomIndexToRoomId: {
|
|
||||||
0: roomA.roomId,
|
|
||||||
1: roomB.roomId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
mocked(client.getRoom).mockImplementation((roomId) => {
|
|
||||||
switch (roomId) {
|
|
||||||
case roomA.roomId:
|
|
||||||
return roomA;
|
|
||||||
case roomB.roomId:
|
|
||||||
return roomB;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// first check that everything is empty
|
|
||||||
const { result } = renderHook(() => useSlidingSyncRoomSearch());
|
|
||||||
const query = {
|
|
||||||
limit: 10,
|
|
||||||
query: "foo",
|
|
||||||
};
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
expect(result.current.rooms).toEqual([]);
|
|
||||||
|
|
||||||
// run the query
|
|
||||||
act(() => {
|
|
||||||
result.current.search(query);
|
|
||||||
});
|
|
||||||
|
|
||||||
// wait for loading to finish
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
// now we expect there to be rooms
|
|
||||||
expect(result.current.rooms).toEqual([roomA, roomB]);
|
|
||||||
|
|
||||||
// run the query again
|
|
||||||
act(() => {
|
|
||||||
result.current.search(query);
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(result.current.loading).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,341 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import { SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import {
|
|
||||||
LISTS_UPDATE_EVENT,
|
|
||||||
SlidingRoomListStoreClass,
|
|
||||||
SlidingSyncSortToFilter,
|
|
||||||
} from "../../../../src/stores/room-list/SlidingRoomListStore";
|
|
||||||
import { SpaceStoreClass } from "../../../../src/stores/spaces/SpaceStore";
|
|
||||||
import { MockEventEmitter, stubClient, untilEmission } from "../../../test-utils";
|
|
||||||
import { TestSdkContext } from "../../TestSdkContext";
|
|
||||||
import { SlidingSyncManager } from "../../../../src/SlidingSyncManager";
|
|
||||||
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
|
|
||||||
import { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
|
|
||||||
import { SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
|
||||||
import { DefaultTagID, TagID } from "../../../../src/stores/room-list/models";
|
|
||||||
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
|
|
||||||
import { LISTS_LOADING_EVENT } from "../../../../src/stores/room-list/RoomListStore";
|
|
||||||
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
|
|
||||||
|
|
||||||
jest.mock("../../../../src/SlidingSyncManager");
|
|
||||||
const MockSlidingSyncManager = <jest.Mock<SlidingSyncManager>>(<unknown>SlidingSyncManager);
|
|
||||||
|
|
||||||
describe("SlidingRoomListStore", () => {
|
|
||||||
let store: SlidingRoomListStoreClass;
|
|
||||||
let context: TestSdkContext;
|
|
||||||
let dis: MatrixDispatcher;
|
|
||||||
let activeSpace: string;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
context = new TestSdkContext();
|
|
||||||
context.client = stubClient();
|
|
||||||
context._SpaceStore = new MockEventEmitter<SpaceStoreClass>({
|
|
||||||
traverseSpace: jest.fn(),
|
|
||||||
get activeSpace() {
|
|
||||||
return activeSpace;
|
|
||||||
},
|
|
||||||
}) as SpaceStoreClass;
|
|
||||||
context._SlidingSyncManager = new MockSlidingSyncManager();
|
|
||||||
context._SlidingSyncManager.slidingSync = mocked(
|
|
||||||
new MockEventEmitter({
|
|
||||||
getListData: jest.fn(),
|
|
||||||
}) as unknown as SlidingSync,
|
|
||||||
);
|
|
||||||
context._RoomViewStore = mocked(
|
|
||||||
new MockEventEmitter({
|
|
||||||
getRoomId: jest.fn(),
|
|
||||||
}) as unknown as RoomViewStore,
|
|
||||||
);
|
|
||||||
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
|
|
||||||
ranges: [[0, 10]],
|
|
||||||
});
|
|
||||||
|
|
||||||
dis = new MatrixDispatcher();
|
|
||||||
store = new SlidingRoomListStoreClass(dis, context);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("spaces", () => {
|
|
||||||
it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => {
|
|
||||||
await store.start(); // call onReady
|
|
||||||
const spaceRoomId = "!foo:bar";
|
|
||||||
|
|
||||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
|
||||||
return listName === DefaultTagID.Untagged && !isLoading;
|
|
||||||
});
|
|
||||||
|
|
||||||
// change the active space
|
|
||||||
activeSpace = spaceRoomId;
|
|
||||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
|
||||||
await p;
|
|
||||||
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
|
||||||
filters: expect.objectContaining({
|
|
||||||
spaces: [spaceRoomId],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gracefully handles subspaces in the home metaspace", async () => {
|
|
||||||
const subspace = "!sub:space";
|
|
||||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
|
||||||
(spaceId: string, fn: (roomId: string) => void) => {
|
|
||||||
fn(subspace);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
activeSpace = MetaSpace.Home;
|
|
||||||
await store.start(); // call onReady
|
|
||||||
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
|
||||||
filters: expect.objectContaining({
|
|
||||||
spaces: [subspace],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => {
|
|
||||||
// change the active space before we are ready
|
|
||||||
const spaceRoomId = "!foo2:bar";
|
|
||||||
activeSpace = spaceRoomId;
|
|
||||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
|
||||||
return listName === DefaultTagID.Untagged && !isLoading;
|
|
||||||
});
|
|
||||||
await store.start(); // call onReady
|
|
||||||
await p;
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
|
|
||||||
DefaultTagID.Untagged,
|
|
||||||
expect.objectContaining({
|
|
||||||
filters: expect.objectContaining({
|
|
||||||
spaces: [spaceRoomId],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => {
|
|
||||||
await store.start(); // call onReady
|
|
||||||
const spaceRoomId = "!foo:bar";
|
|
||||||
const subSpace1 = "!ss1:bar";
|
|
||||||
const subSpace2 = "!ss2:bar";
|
|
||||||
|
|
||||||
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
|
|
||||||
return listName === DefaultTagID.Untagged && !isLoading;
|
|
||||||
});
|
|
||||||
|
|
||||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
|
||||||
(spaceId: string, fn: (roomId: string) => void) => {
|
|
||||||
if (spaceId === spaceRoomId) {
|
|
||||||
fn(subSpace1);
|
|
||||||
fn(subSpace2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// change the active space
|
|
||||||
activeSpace = spaceRoomId;
|
|
||||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
|
||||||
await p;
|
|
||||||
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
|
||||||
filters: expect.objectContaining({
|
|
||||||
spaces: [spaceRoomId, subSpace1, subSpace2],
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("setTagSorting alters the 'sort' option in the list", async () => {
|
|
||||||
const tagId: TagID = "foo";
|
|
||||||
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
|
||||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
|
||||||
});
|
|
||||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
|
|
||||||
|
|
||||||
await store.setTagSorting(tagId, SortAlgorithm.Recent);
|
|
||||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
|
|
||||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
|
||||||
});
|
|
||||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("getTagsForRoom gets the tags for the room", async () => {
|
|
||||||
await store.start();
|
|
||||||
const roomA = "!a:localhost";
|
|
||||||
const roomB = "!b:localhost";
|
|
||||||
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
|
|
||||||
[DefaultTagID.Untagged]: {
|
|
||||||
joinedCount: 10,
|
|
||||||
roomIndexToRoomId: {
|
|
||||||
0: roomA,
|
|
||||||
1: roomB,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[DefaultTagID.Favourite]: {
|
|
||||||
joinedCount: 2,
|
|
||||||
roomIndexToRoomId: {
|
|
||||||
0: roomB,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
|
||||||
return keyToListData[key] || null;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()!))).toEqual([
|
|
||||||
DefaultTagID.Untagged,
|
|
||||||
]);
|
|
||||||
expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()!))).toEqual([
|
|
||||||
DefaultTagID.Favourite,
|
|
||||||
DefaultTagID.Untagged,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => {
|
|
||||||
await store.start();
|
|
||||||
const roomA = "!a:localhost";
|
|
||||||
const roomB = "!b:localhost";
|
|
||||||
const roomC = "!c:localhost";
|
|
||||||
const tagId = DefaultTagID.Favourite;
|
|
||||||
const joinCount = 10;
|
|
||||||
const roomIndexToRoomId = {
|
|
||||||
// mixed to ensure we sort
|
|
||||||
1: roomB,
|
|
||||||
2: roomC,
|
|
||||||
0: roomA,
|
|
||||||
};
|
|
||||||
const rooms = [
|
|
||||||
new Room(roomA, context.client!, context.client!.getUserId()!),
|
|
||||||
new Room(roomB, context.client!, context.client!.getUserId()!),
|
|
||||||
new Room(roomC, context.client!, context.client!.getUserId()!),
|
|
||||||
];
|
|
||||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
|
||||||
switch (roomId) {
|
|
||||||
case roomA:
|
|
||||||
return rooms[0];
|
|
||||||
case roomB:
|
|
||||||
return rooms[1];
|
|
||||||
case roomC:
|
|
||||||
return rooms[2];
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
|
||||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
|
||||||
await p;
|
|
||||||
expect(store.getCount(tagId)).toEqual(joinCount);
|
|
||||||
expect(store.orderedLists[tagId]).toEqual(rooms);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => {
|
|
||||||
await store.start();
|
|
||||||
// seed the store with 3 rooms
|
|
||||||
const roomIdA = "!a:localhost";
|
|
||||||
const roomIdB = "!b:localhost";
|
|
||||||
const roomIdC = "!c:localhost";
|
|
||||||
const tagId = DefaultTagID.Favourite;
|
|
||||||
const joinCount = 10;
|
|
||||||
const roomIndexToRoomId = {
|
|
||||||
// mixed to ensure we sort
|
|
||||||
1: roomIdB,
|
|
||||||
2: roomIdC,
|
|
||||||
0: roomIdA,
|
|
||||||
};
|
|
||||||
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
|
|
||||||
const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()!);
|
|
||||||
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
|
|
||||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
|
||||||
switch (roomId) {
|
|
||||||
case roomIdA:
|
|
||||||
return roomA;
|
|
||||||
case roomIdB:
|
|
||||||
return roomB;
|
|
||||||
case roomIdC:
|
|
||||||
return roomC;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
|
||||||
if (key !== tagId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
roomIndexToRoomId: roomIndexToRoomId,
|
|
||||||
joinedCount: joinCount,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
let p = untilEmission(store, LISTS_UPDATE_EVENT);
|
|
||||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
|
||||||
await p;
|
|
||||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
|
|
||||||
|
|
||||||
// make roomB sticky and inform the store
|
|
||||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB);
|
|
||||||
context.roomViewStore.emit(UPDATE_EVENT);
|
|
||||||
|
|
||||||
// bump room C to the top, room B should not move from i=1 despite the list update saying to
|
|
||||||
roomIndexToRoomId[0] = roomIdC;
|
|
||||||
roomIndexToRoomId[1] = roomIdA;
|
|
||||||
roomIndexToRoomId[2] = roomIdB;
|
|
||||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
|
||||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
|
||||||
await p;
|
|
||||||
|
|
||||||
// check that B didn't move and that A was put below B
|
|
||||||
expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]);
|
|
||||||
|
|
||||||
// make room C sticky: rooms should move as a result, without needing an additional list update
|
|
||||||
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC);
|
|
||||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
|
||||||
context.roomViewStore.emit(UPDATE_EVENT);
|
|
||||||
await p;
|
|
||||||
expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("gracefully handles unknown room IDs", async () => {
|
|
||||||
await store.start();
|
|
||||||
const roomIdA = "!a:localhost";
|
|
||||||
const roomIdB = "!b:localhost"; // does not exist
|
|
||||||
const roomIdC = "!c:localhost";
|
|
||||||
const roomIndexToRoomId = {
|
|
||||||
0: roomIdA,
|
|
||||||
1: roomIdB, // does not exist
|
|
||||||
2: roomIdC,
|
|
||||||
};
|
|
||||||
const tagId = DefaultTagID.Favourite;
|
|
||||||
const joinCount = 10;
|
|
||||||
// seed the store with 2 rooms
|
|
||||||
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
|
|
||||||
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
|
|
||||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
|
||||||
switch (roomId) {
|
|
||||||
case roomIdA:
|
|
||||||
return roomA;
|
|
||||||
case roomIdC:
|
|
||||||
return roomC;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
|
|
||||||
if (key !== tagId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
roomIndexToRoomId: roomIndexToRoomId,
|
|
||||||
joinedCount: joinCount,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
|
||||||
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
|
||||||
await p;
|
|
||||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomC]);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Add a link
Reference in a new issue