mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-15 20:54:59 +08:00
Merge pull request #9934 from matrix-org/kegan/lists-as-keys
refactor: sliding sync: convert to lists-as-keys rather than indexes
This commit is contained in:
commit
51b4555106
@ -30,8 +30,8 @@ export default defineConfig({
|
||||
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
|
||||
},
|
||||
env: {
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||
SLIDING_SYNC_PROXY_TAG: "v0.6.0",
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
|
||||
SLIDING_SYNC_PROXY_TAG: "v0.99.0-rc1",
|
||||
HOMESERVER: "synapse",
|
||||
},
|
||||
retries: {
|
||||
|
@ -21,8 +21,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { Interception } from "cypress/types/net-stubbing";
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { ProxyInstance } from "../../plugins/sliding-sync";
|
||||
|
||||
describe("Sliding Sync", () => {
|
||||
@ -102,21 +100,6 @@ describe("Sliding Sync", () => {
|
||||
});
|
||||
};
|
||||
|
||||
// sanity check everything works
|
||||
it("should correctly render expected messages", () => {
|
||||
cy.get<string>("@roomId").then((roomId) => cy.visit("/#/room/" + roomId));
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
);
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
});
|
||||
|
||||
it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
|
||||
// create rooms and check room names are correct
|
||||
cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
|
@ -23,7 +23,7 @@ import { getFreePort } from "../utils/port";
|
||||
import { HomeserverInstance } from "../utils/homeserver";
|
||||
|
||||
// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
|
||||
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
|
||||
|
||||
export interface ProxyInstance {
|
||||
containerId: string;
|
||||
@ -72,7 +72,7 @@ async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Pr
|
||||
const port = await getFreePort();
|
||||
console.log(new Date(), "starting proxy container...", dockerTag);
|
||||
const containerId = await dockerRun({
|
||||
image: "ghcr.io/matrix-org/sliding-sync-proxy:" + dockerTag,
|
||||
image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag,
|
||||
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||
params: [
|
||||
"--rm",
|
||||
|
@ -119,12 +119,10 @@ export class SlidingSyncManager {
|
||||
|
||||
public slidingSync: SlidingSync;
|
||||
private client: MatrixClient;
|
||||
private listIdToIndex: Record<string, number>;
|
||||
|
||||
private configureDefer: IDeferred<void>;
|
||||
|
||||
public constructor() {
|
||||
this.listIdToIndex = {};
|
||||
this.configureDefer = defer<void>();
|
||||
}
|
||||
|
||||
@ -134,13 +132,18 @@ export class SlidingSyncManager {
|
||||
|
||||
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||
this.client = client;
|
||||
this.listIdToIndex = {};
|
||||
// by default use the encrypted subscription as that gets everything, which is a safer
|
||||
// default than potentially missing member events.
|
||||
this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
|
||||
this.slidingSync = new SlidingSync(
|
||||
proxyUrl,
|
||||
new Map(),
|
||||
ENCRYPTED_SUBSCRIPTION,
|
||||
client,
|
||||
SLIDING_SYNC_TIMEOUT_MS,
|
||||
);
|
||||
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
||||
// set the space list
|
||||
this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), {
|
||||
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
|
||||
ranges: [[0, 20]],
|
||||
sort: ["by_name"],
|
||||
slow_get_all_rooms: true,
|
||||
@ -173,47 +176,16 @@ export class SlidingSyncManager {
|
||||
return this.slidingSync;
|
||||
}
|
||||
|
||||
public listIdForIndex(index: number): string | null {
|
||||
for (const listId in this.listIdToIndex) {
|
||||
if (this.listIdToIndex[listId] === index) {
|
||||
return listId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces
|
||||
* @param listId A string which represents the list.
|
||||
* @returns The index to use when registering lists or listening for callbacks.
|
||||
*/
|
||||
public getOrAllocateListIndex(listId: string): number {
|
||||
let index = this.listIdToIndex[listId];
|
||||
if (index === undefined) {
|
||||
// assign next highest index
|
||||
index = -1;
|
||||
for (const id in this.listIdToIndex) {
|
||||
const listIndex = this.listIdToIndex[id];
|
||||
if (listIndex > index) {
|
||||
index = listIndex;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
this.listIdToIndex[listId] = index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that this list is registered.
|
||||
* @param listIndex The list index to register
|
||||
* @param listKey The list key to register
|
||||
* @param updateArgs The fields to update on the list.
|
||||
* @returns The complete list request params
|
||||
*/
|
||||
public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
|
||||
logger.debug("ensureListRegistered:::", listIndex, updateArgs);
|
||||
public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
|
||||
logger.debug("ensureListRegistered:::", listKey, updateArgs);
|
||||
await this.configureDefer.promise;
|
||||
let list = this.slidingSync.getList(listIndex);
|
||||
let list = this.slidingSync.getListParams(listKey);
|
||||
if (!list) {
|
||||
list = {
|
||||
ranges: [[0, 20]],
|
||||
@ -252,14 +224,14 @@ export class SlidingSyncManager {
|
||||
try {
|
||||
// if we only have range changes then call a different function so we don't nuke the list from before
|
||||
if (updateArgs.ranges && Object.keys(updateArgs).length === 1) {
|
||||
await this.slidingSync.setListRanges(listIndex, updateArgs.ranges);
|
||||
await this.slidingSync.setListRanges(listKey, updateArgs.ranges);
|
||||
} else {
|
||||
await this.slidingSync.setList(listIndex, list);
|
||||
await this.slidingSync.setList(listKey, list);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.debug("ensureListRegistered: update failed txn_id=", err);
|
||||
}
|
||||
return this.slidingSync.getList(listIndex);
|
||||
return this.slidingSync.getListParams(listKey)!;
|
||||
}
|
||||
|
||||
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
|
||||
@ -304,7 +276,6 @@ export class SlidingSyncManager {
|
||||
*/
|
||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
|
||||
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
|
||||
const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
|
||||
let startIndex = batchSize;
|
||||
let hasMore = true;
|
||||
let firstTime = true;
|
||||
@ -316,7 +287,7 @@ export class SlidingSyncManager {
|
||||
[startIndex, endIndex],
|
||||
];
|
||||
if (firstTime) {
|
||||
await this.slidingSync.setList(listIndex, {
|
||||
await this.slidingSync.setList(SlidingSyncManager.ListSearch, {
|
||||
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
|
||||
// any changes to the list whilst spidering are caught.
|
||||
ranges: ranges,
|
||||
@ -342,15 +313,17 @@ export class SlidingSyncManager {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await this.slidingSync.setListRanges(listIndex, ranges);
|
||||
await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges);
|
||||
}
|
||||
// gradually request more over time
|
||||
await sleep(gapBetweenRequestsMs);
|
||||
} catch (err) {
|
||||
// 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);
|
||||
}
|
||||
hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount;
|
||||
const listData = this.slidingSync.getListData(SlidingSyncManager.ListSearch)!;
|
||||
hasMore = endIndex + 1 < listData.joinedCount;
|
||||
startIndex += batchSize;
|
||||
firstTime = false;
|
||||
}
|
||||
|
@ -340,9 +340,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
|
||||
private onShowAllClick = async (): Promise<void> => {
|
||||
if (this.slidingSyncMode) {
|
||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const count = RoomListStore.instance.getCount(this.props.tagId);
|
||||
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
|
||||
await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, {
|
||||
ranges: [[0, count]],
|
||||
});
|
||||
}
|
||||
@ -566,10 +565,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
if (this.slidingSyncMode) {
|
||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
||||
isAlphabetical = slidingList.sort[0] === "by_name";
|
||||
isUnreadFirst = slidingList.sort[0] === "by_notification_level";
|
||||
const slidingList = SlidingSyncManager.instance.slidingSync.getListParams(this.props.tagId);
|
||||
isAlphabetical = (slidingList?.sort || [])[0] === "by_name";
|
||||
isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level";
|
||||
}
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
|
@ -34,7 +34,6 @@ export const useSlidingSyncRoomSearch = (): {
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
|
||||
|
||||
const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms);
|
||||
|
||||
@ -50,14 +49,16 @@ export const useSlidingSyncRoomSearch = (): {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await SlidingSyncManager.instance.ensureListRegistered(listIndex, {
|
||||
await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, {
|
||||
ranges: [[0, limit]],
|
||||
filters: {
|
||||
room_name_like: term,
|
||||
},
|
||||
});
|
||||
const rooms = [];
|
||||
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex);
|
||||
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(
|
||||
SlidingSyncManager.ListSearch,
|
||||
)!;
|
||||
let i = 0;
|
||||
while (roomIndexToRoomId[i]) {
|
||||
const roomId = roomIndexToRoomId[i];
|
||||
@ -78,7 +79,7 @@ export const useSlidingSyncRoomSearch = (): {
|
||||
// TODO: delete the list?
|
||||
}
|
||||
},
|
||||
[updateQuery, updateResult, listIndex],
|
||||
[updateQuery, updateResult],
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -84,20 +84,20 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
public constructor(dis: MatrixDispatcher, private readonly context: SdkContextClass) {
|
||||
super(dis);
|
||||
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
||||
this.stickyRoomId = null;
|
||||
}
|
||||
|
||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise<void> {
|
||||
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
|
||||
this.tagIdToSortAlgo[tagId] = sort;
|
||||
const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
switch (sort) {
|
||||
case SortAlgorithm.Alphabetic:
|
||||
await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, {
|
||||
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||
});
|
||||
break;
|
||||
case SortAlgorithm.Recent:
|
||||
await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, {
|
||||
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||
});
|
||||
break;
|
||||
@ -164,8 +164,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
// 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 = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
|
||||
const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId);
|
||||
if (!listData) {
|
||||
continue;
|
||||
}
|
||||
@ -251,19 +250,19 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
}
|
||||
|
||||
// now set the rooms
|
||||
const rooms = orderedRoomIds.map((roomId) => {
|
||||
return this.matrixClient.getRoom(roomId);
|
||||
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(
|
||||
listIndex: number,
|
||||
joinCount: number,
|
||||
roomIndexToRoomId: Record<number, string>,
|
||||
): void {
|
||||
const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex);
|
||||
private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record<number, string>): void {
|
||||
this.counts[tagId] = joinCount;
|
||||
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
||||
// let the UI update
|
||||
@ -295,8 +294,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
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 = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
|
||||
const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId);
|
||||
if (!listData) {
|
||||
continue;
|
||||
}
|
||||
@ -334,9 +332,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
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 = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
this.context.slidingSyncManager
|
||||
.ensureListRegistered(index, {
|
||||
.ensureListRegistered(tagId, {
|
||||
filters: filter,
|
||||
sort: SlidingSyncSortToFilter[sort],
|
||||
})
|
||||
@ -361,15 +358,17 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
|
||||
if (roomId === activeSpace) {
|
||||
return;
|
||||
}
|
||||
if (!filters.spaces) {
|
||||
filters.spaces = [];
|
||||
}
|
||||
filters.spaces.push(roomId); // add subspace
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
||||
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
this.context.slidingSyncManager
|
||||
.ensureListRegistered(index, {
|
||||
.ensureListRegistered(tagId, {
|
||||
filters: filters,
|
||||
})
|
||||
.then(() => {
|
||||
|
@ -78,13 +78,76 @@ describe("SlidingSyncManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ensureListRegistered", () => {
|
||||
it("creates a new list based on the key", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue(null);
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).toBeCalledWith(
|
||||
listKey,
|
||||
expect.objectContaining({
|
||||
sort: ["by_recency"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates an existing list based on the key", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).toBeCalledWith(
|
||||
listKey,
|
||||
expect.objectContaining({
|
||||
sort: ["by_recency"],
|
||||
ranges: [[0, 42]],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates ranges on an existing list based on the key if there's no other changes", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 52]],
|
||||
});
|
||||
expect(slidingSync.setList).not.toBeCalled();
|
||||
expect(slidingSync.setListRanges).toBeCalledWith(listKey, [[0, 52]]);
|
||||
});
|
||||
|
||||
it("no-ops for idential changes", async () => {
|
||||
const listKey = "key";
|
||||
mocked(slidingSync.getListParams).mockReturnValue({
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
await manager.ensureListRegistered(listKey, {
|
||||
ranges: [[0, 42]],
|
||||
sort: ["by_recency"],
|
||||
});
|
||||
expect(slidingSync.setList).not.toBeCalled();
|
||||
expect(slidingSync.setListRanges).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("startSpidering", () => {
|
||||
it("requests in batchSizes", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
mocked(slidingSync.setListRanges).mockResolvedValue("yep");
|
||||
mocked(slidingSync.getListData).mockImplementation((i) => {
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 64,
|
||||
roomIndexToRoomId: {},
|
||||
@ -106,24 +169,24 @@ describe("SlidingSyncManager", () => {
|
||||
wantWindows.forEach((range, i) => {
|
||||
if (i === 0) {
|
||||
expect(slidingSync.setList).toBeCalledWith(
|
||||
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
|
||||
SlidingSyncManager.ListSearch,
|
||||
expect.objectContaining({
|
||||
ranges: [[0, batchSize - 1], range],
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
expect(slidingSync.setListRanges).toBeCalledWith(
|
||||
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
|
||||
[[0, batchSize - 1], range],
|
||||
);
|
||||
expect(slidingSync.setListRanges).toBeCalledWith(SlidingSyncManager.ListSearch, [
|
||||
[0, batchSize - 1],
|
||||
range,
|
||||
]);
|
||||
});
|
||||
});
|
||||
it("handles accounts with zero rooms", async () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockResolvedValue("yep");
|
||||
mocked(slidingSync.getListData).mockImplementation((i) => {
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 0,
|
||||
roomIndexToRoomId: {},
|
||||
@ -133,7 +196,7 @@ describe("SlidingSyncManager", () => {
|
||||
expect(slidingSync.getListData).toBeCalledTimes(1);
|
||||
expect(slidingSync.setList).toBeCalledTimes(1);
|
||||
expect(slidingSync.setList).toBeCalledWith(
|
||||
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
|
||||
SlidingSyncManager.ListSearch,
|
||||
expect.objectContaining({
|
||||
ranges: [
|
||||
[0, batchSize - 1],
|
||||
@ -146,7 +209,7 @@ describe("SlidingSyncManager", () => {
|
||||
const gapMs = 1;
|
||||
const batchSize = 10;
|
||||
mocked(slidingSync.setList).mockRejectedValue("narp");
|
||||
mocked(slidingSync.getListData).mockImplementation((i) => {
|
||||
mocked(slidingSync.getListData).mockImplementation((key) => {
|
||||
return {
|
||||
joinedCount: 0,
|
||||
roomIndexToRoomId: {},
|
||||
@ -156,7 +219,7 @@ describe("SlidingSyncManager", () => {
|
||||
expect(slidingSync.getListData).toBeCalledTimes(1);
|
||||
expect(slidingSync.setList).toBeCalledTimes(1);
|
||||
expect(slidingSync.setList).toBeCalledWith(
|
||||
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
|
||||
SlidingSyncManager.ListSearch,
|
||||
expect.objectContaining({
|
||||
ranges: [
|
||||
[0, batchSize - 1],
|
||||
|
111
test/hooks/useSlidingSyncRoomSearch-test.tsx
Normal file
111
test/hooks/useSlidingSyncRoomSearch-test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2023 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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { sleep } from "matrix-js-sdk/src/utils";
|
||||
import React from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { SlidingSync } from "matrix-js-sdk/src/sliding-sync";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { SlidingSyncRoomSearchOpts, useSlidingSyncRoomSearch } from "../../src/hooks/useSlidingSyncRoomSearch";
|
||||
import { MockEventEmitter, stubClient } from "../test-utils";
|
||||
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
|
||||
|
||||
type RoomSearchHook = {
|
||||
loading: boolean;
|
||||
rooms: Room[];
|
||||
search(opts: SlidingSyncRoomSearchOpts): Promise<boolean>;
|
||||
};
|
||||
|
||||
// hooks must be inside a React component else you get:
|
||||
// "Invalid hook call. Hooks can only be called inside of the body of a function component."
|
||||
function RoomSearchComponent(props: { onClick: (h: RoomSearchHook) => void }) {
|
||||
const roomSearch = useSlidingSyncRoomSearch();
|
||||
return <div onClick={() => props.onClick(roomSearch)} />;
|
||||
}
|
||||
|
||||
describe("useSlidingSyncRoomSearch", () => {
|
||||
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 and then do the search
|
||||
let executeHook = (roomSearch: RoomSearchHook) => {
|
||||
expect(roomSearch.loading).toBe(false);
|
||||
expect(roomSearch.rooms).toEqual([]);
|
||||
roomSearch.search({
|
||||
limit: 10,
|
||||
query: "foo",
|
||||
});
|
||||
};
|
||||
const wrapper = mount(
|
||||
<RoomSearchComponent
|
||||
onClick={(roomSearch: RoomSearchHook) => {
|
||||
executeHook(roomSearch);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
// run the query
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
// now we expect there to be rooms
|
||||
executeHook = (roomSearch) => {
|
||||
expect(roomSearch.loading).toBe(false);
|
||||
expect(roomSearch.rooms).toEqual([roomA, roomB]);
|
||||
};
|
||||
|
||||
// run the query
|
||||
await act(async () => {
|
||||
await sleep(1);
|
||||
wrapper.simulate("click");
|
||||
return act(() => sleep(1));
|
||||
});
|
||||
});
|
||||
});
|
@ -30,7 +30,7 @@ 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 { UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces";
|
||||
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";
|
||||
|
||||
@ -42,7 +42,6 @@ describe("SlidingRoomListStore", () => {
|
||||
let context: TestSdkContext;
|
||||
let dis: MatrixDispatcher;
|
||||
let activeSpace: string;
|
||||
let tagIdToIndex = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
context = new TestSdkContext();
|
||||
@ -64,27 +63,6 @@ describe("SlidingRoomListStore", () => {
|
||||
getRoomId: jest.fn(),
|
||||
}) as unknown as RoomViewStore,
|
||||
);
|
||||
|
||||
// mock implementations to allow the store to map tag IDs to sliding sync list indexes and vice versa
|
||||
let index = 0;
|
||||
tagIdToIndex = {};
|
||||
mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockImplementation((listId: string): number => {
|
||||
if (tagIdToIndex[listId] != null) {
|
||||
return tagIdToIndex[listId];
|
||||
}
|
||||
tagIdToIndex[listId] = index;
|
||||
index++;
|
||||
return index;
|
||||
});
|
||||
mocked(context.slidingSyncManager.listIdForIndex).mockImplementation((i) => {
|
||||
for (const tagId in tagIdToIndex) {
|
||||
const j = tagIdToIndex[tagId];
|
||||
if (i === j) {
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
|
||||
ranges: [[0, 10]],
|
||||
});
|
||||
@ -104,17 +82,31 @@ describe("SlidingRoomListStore", () => {
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
|
||||
tagIdToIndex[DefaultTagID.Untagged],
|
||||
{
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId],
|
||||
}),
|
||||
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 () => {
|
||||
@ -126,8 +118,8 @@ describe("SlidingRoomListStore", () => {
|
||||
});
|
||||
await store.start(); // call onReady
|
||||
await p;
|
||||
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
|
||||
tagIdToIndex[DefaultTagID.Untagged],
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
|
||||
DefaultTagID.Untagged,
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId],
|
||||
@ -146,7 +138,7 @@ describe("SlidingRoomListStore", () => {
|
||||
return listName === DefaultTagID.Untagged && !isLoading;
|
||||
});
|
||||
|
||||
mocked(context._SpaceStore.traverseSpace).mockImplementation(
|
||||
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
|
||||
(spaceId: string, fn: (roomId: string) => void) => {
|
||||
if (spaceId === spaceRoomId) {
|
||||
fn(subSpace1);
|
||||
@ -157,31 +149,27 @@ describe("SlidingRoomListStore", () => {
|
||||
|
||||
// change the active space
|
||||
activeSpace = spaceRoomId;
|
||||
context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
|
||||
await p;
|
||||
|
||||
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
|
||||
tagIdToIndex[DefaultTagID.Untagged],
|
||||
{
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId, subSpace1, subSpace2],
|
||||
}),
|
||||
},
|
||||
);
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
|
||||
filters: expect.objectContaining({
|
||||
spaces: [spaceRoomId, subSpace1, subSpace2],
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("setTagSorting alters the 'sort' option in the list", async () => {
|
||||
mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockReturnValue(0);
|
||||
const tagId: TagID = "foo";
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
|
||||
expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, {
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
|
||||
|
||||
await store.setTagSorting(tagId, SortAlgorithm.Recent);
|
||||
expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, {
|
||||
expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, {
|
||||
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||
});
|
||||
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
|
||||
@ -189,33 +177,31 @@ describe("SlidingRoomListStore", () => {
|
||||
|
||||
it("getTagsForRoom gets the tags for the room", async () => {
|
||||
await store.start();
|
||||
const untaggedIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Untagged);
|
||||
const favIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Favourite);
|
||||
const roomA = "!a:localhost";
|
||||
const roomB = "!b:localhost";
|
||||
const indexToListData = {
|
||||
[untaggedIndex]: {
|
||||
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
|
||||
[DefaultTagID.Untagged]: {
|
||||
joinedCount: 10,
|
||||
roomIndexToRoomId: {
|
||||
0: roomA,
|
||||
1: roomB,
|
||||
},
|
||||
},
|
||||
[favIndex]: {
|
||||
[DefaultTagID.Favourite]: {
|
||||
joinedCount: 2,
|
||||
roomIndexToRoomId: {
|
||||
0: roomB,
|
||||
},
|
||||
},
|
||||
};
|
||||
mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => {
|
||||
return indexToListData[i] || null;
|
||||
mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => {
|
||||
return keyToListData[key] || null;
|
||||
});
|
||||
|
||||
expect(store.getTagsForRoom(new Room(roomA, context.client, context.client.getUserId()))).toEqual([
|
||||
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([
|
||||
expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()))).toEqual([
|
||||
DefaultTagID.Favourite,
|
||||
DefaultTagID.Untagged,
|
||||
]);
|
||||
@ -227,7 +213,6 @@ describe("SlidingRoomListStore", () => {
|
||||
const roomB = "!b:localhost";
|
||||
const roomC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
@ -236,11 +221,11 @@ describe("SlidingRoomListStore", () => {
|
||||
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()),
|
||||
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) => {
|
||||
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
|
||||
switch (roomId) {
|
||||
case roomA:
|
||||
return rooms[0];
|
||||
@ -252,7 +237,7 @@ describe("SlidingRoomListStore", () => {
|
||||
return null;
|
||||
});
|
||||
const p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.getCount(tagId)).toEqual(joinCount);
|
||||
expect(store.orderedLists[tagId]).toEqual(rooms);
|
||||
@ -265,7 +250,6 @@ describe("SlidingRoomListStore", () => {
|
||||
const roomIdB = "!b:localhost";
|
||||
const roomIdC = "!c:localhost";
|
||||
const tagId = DefaultTagID.Favourite;
|
||||
const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId);
|
||||
const joinCount = 10;
|
||||
const roomIndexToRoomId = {
|
||||
// mixed to ensure we sort
|
||||
@ -273,10 +257,10 @@ describe("SlidingRoomListStore", () => {
|
||||
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) => {
|
||||
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;
|
||||
@ -287,8 +271,8 @@ describe("SlidingRoomListStore", () => {
|
||||
}
|
||||
return null;
|
||||
});
|
||||
mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => {
|
||||
if (i !== listIndex) {
|
||||
mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => {
|
||||
if (key !== tagId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@ -297,7 +281,7 @@ describe("SlidingRoomListStore", () => {
|
||||
};
|
||||
});
|
||||
let p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
|
||||
|
||||
@ -310,7 +294,7 @@ describe("SlidingRoomListStore", () => {
|
||||
roomIndexToRoomId[1] = roomIdA;
|
||||
roomIndexToRoomId[2] = roomIdB;
|
||||
p = untilEmission(store, LISTS_UPDATE_EVENT);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
|
||||
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
|
||||
await p;
|
||||
|
||||
// check that B didn't move and that A was put below B
|
||||
@ -323,4 +307,43 @@ describe("SlidingRoomListStore", () => {
|
||||
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…
Reference in New Issue
Block a user