Implement MSC3575: Sliding Sync (#8328)
* Add labs flag for sliding sync; add sliding_sync_proxy_url to config.json * Disable the labs toggle if sliding_sync_proxy_url is not set * Do validation checks on the sliding sync proxy URL before enabling it in Labs * Enable sliding sync and add SlidingSyncManager * Get room subscriptions working * Hijack renderSublists in sliding sync mode * Add support for sorting alphabetically/recency and room name filters * Filter out tombstoned rooms; start adding show more logic list ranges update but the UI doesn't * update the UI when the list is updated * bugfix: make sure the list sorts numerically * Get invites transitioning correctly * Force enable sliding sync and labs for now * Linting * Disable spotlight search * Initial cypress plugins for Sliding Sync Proxy * Use --rm when running Synapse in Docker for Cypress tests * Update src/MatrixClientPeg.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/settings/controllers/SlidingSyncController.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Travis Ralston <travisr@matrix.org> * WIP add room searching to spotlight search * Only read sliding sync results when there is a result, else use the local cache * Use feature_sliding_sync not slidingSync * Some review comments * More review comments * Use RoomViewStore to set room subscriptions * Comment why any * Update src/components/views/rooms/RoomSublist.tsx Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Fix cypress docker abstraction * Iterate sliding sync proxy support * Stash mostly functional test * Update sliding sync proxy image * i18n * Add support for spaces; use list ID -> index mappings - Mappings are more reusable and easier to understand than racing for index positions. - Register for all spaces immediately on startup. * When the active space is updated, update the list registration * Set spaces filter in the correct place * Skeleton placeholder whilst loading the space * Filter out spaces from the room list * Use the new txn_id promises * Ensure we actually resolve list registrations * Fix matrix-org/sliding-sync#30: don't show tombstoned search results * Remove unused imports * Add SYNCV3_SECRET to proxy to ensure it starts up; correct aliases for SS test * Add another basic sliding sync e2e test * Unbreak netlify * Add more logging for debugging duplicate rooms * If sliding sync is enabled, always use the rooms result even if it's empty * Drop-in copy of RoomListStore for sliding sync * Remove conditionals from RoomListStore - we have SlidingRoomListStore now * WIP SlidingRoomListStore * Add most sliding sync logic to SlidingRoomListStore Still lots of logic in RoomSublist. Broken things: - Join count is wrong completely. - No skeleton placeholder when switching spaces. * Migrate joined count to SS RLS * Reinstate the skeleton UI when the list is loading * linting * Add support for sticky rooms based on the currently active room * Add a bunch of passing SS E2E tests; some WIP * Unbreak build from git merge * Suppress unread indicators in sliding sync mode * Add regression test for https://github.com/matrix-org/sliding-sync/issues/28 * Add invite test flows; show the invite list The refactor to SS RLS removed the invite list entirely. * Remove show more click as it wasn't the bug * Linting and i18n * only enable SS by default on netlify * Jest fixes; merge conflict fixes; remove debug logging; use right sort enum values * Actually fix jest tests * Add support for favourites and low priority * Bump sliding sync version * Update sliding sync labs to be user configurable * delint * To disable SS or change proxy URL the user has to log out * Review comments * Linting * Apply suggestions from code review Co-authored-by: Travis Ralston <travisr@matrix.org> * Update src/stores/room-list/SlidingRoomListStore.ts Co-authored-by: Travis Ralston <travisr@matrix.org> * Review comments * Add issue link for TODO markers * Linting * Apply suggestions from code review Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * More review comments * More review comments * stricter types Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> Co-authored-by: Travis Ralston <travisr@matrix.org>
This commit is contained in:
parent
5bdae150fa
commit
a215027c6b
25 changed files with 1632 additions and 51 deletions
322
cypress/e2e/sliding-sync/sliding-sync.ts
Normal file
322
cypress/e2e/sliding-sync/sliding-sync.ts
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import _ from "lodash";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
|
import { ProxyInstance } from "../../plugins/sliding-sync";
|
||||||
|
|
||||||
|
describe("Sliding Sync", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.startSynapse("default").as("synapse").then(synapse => {
|
||||||
|
cy.startProxy(synapse).as("proxy");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.all([
|
||||||
|
cy.get<SynapseInstance>("@synapse"),
|
||||||
|
cy.get<ProxyInstance>("@proxy"),
|
||||||
|
]).then(([synapse, proxy]) => {
|
||||||
|
cy.enableLabsFeature("feature_sliding_sync");
|
||||||
|
|
||||||
|
cy.intercept("/config.json?cachebuster=*", req => {
|
||||||
|
return req.continue(res => {
|
||||||
|
res.send(200, {
|
||||||
|
...res.body,
|
||||||
|
setting_defaults: {
|
||||||
|
feature_sliding_sync_proxy_url: `http://localhost:${proxy.port}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.initTestUser(synapse, "Sloth").then(() => {
|
||||||
|
return cy.window({ log: false }).then(() => {
|
||||||
|
cy.createRoom({ name: "Test Room" }).as("roomId");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.get<SynapseInstance>("@synapse").then(cy.stopSynapse);
|
||||||
|
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
|
||||||
|
});
|
||||||
|
|
||||||
|
// assert order
|
||||||
|
const checkOrder = (wantOrder: string[]) => {
|
||||||
|
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomTile_title").should((elements) => {
|
||||||
|
expect(_.map(elements, (e) => {
|
||||||
|
return e.textContent;
|
||||||
|
}), "rooms are sorted").to.deep.equal(wantOrder);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const bumpRoom = (alias: string) => {
|
||||||
|
// Send a message into the given room, this should bump the room to the top
|
||||||
|
cy.get<string>(alias).then((roomId) => {
|
||||||
|
return cy.sendEvent(roomId, null, "m.room.message", {
|
||||||
|
body: "Hello world",
|
||||||
|
msgtype: "m.text",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const createAndJoinBob = () => {
|
||||||
|
// create a Bob user
|
||||||
|
cy.get<SynapseInstance>("@synapse").then((synapse) => {
|
||||||
|
return cy.getBot(synapse, {
|
||||||
|
displayName: "Bob",
|
||||||
|
}).as("bob");
|
||||||
|
});
|
||||||
|
|
||||||
|
// invite Bob to Test Room and accept then send a message.
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||||
|
return cy.inviteUser(roomId, bob.getUserId()).then(() => {
|
||||||
|
return bob.joinRoom(roomId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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"));
|
||||||
|
cy.createRoom({ name: "Pineapple" }).then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||||
|
cy.createRoom({ name: "Orange" }).then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||||
|
// check the rooms are in the right order
|
||||||
|
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
|
||||||
|
checkOrder([
|
||||||
|
"Orange", "Pineapple", "Apple", "Test Room",
|
||||||
|
]);
|
||||||
|
|
||||||
|
cy.contains(".mx_RoomSublist", "Rooms").find(".mx_RoomSublist_menuButton").click({ force: true });
|
||||||
|
cy.contains("A-Z").click();
|
||||||
|
cy.get('.mx_StyledRadioButton_checked').should("contain.text", "A-Z");
|
||||||
|
checkOrder([
|
||||||
|
"Apple", "Orange", "Pineapple", "Test Room",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should move rooms around as new events arrive", () => {
|
||||||
|
// create rooms and check room names are correct
|
||||||
|
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||||
|
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||||
|
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||||
|
|
||||||
|
// Select the Test Room
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||||
|
|
||||||
|
checkOrder([
|
||||||
|
"Orange", "Pineapple", "Apple", "Test Room",
|
||||||
|
]);
|
||||||
|
bumpRoom("@roomA");
|
||||||
|
checkOrder([
|
||||||
|
"Apple", "Orange", "Pineapple", "Test Room",
|
||||||
|
]);
|
||||||
|
bumpRoom("@roomO");
|
||||||
|
checkOrder([
|
||||||
|
"Orange", "Apple", "Pineapple", "Test Room",
|
||||||
|
]);
|
||||||
|
bumpRoom("@roomO");
|
||||||
|
checkOrder([
|
||||||
|
"Orange", "Apple", "Pineapple", "Test Room",
|
||||||
|
]);
|
||||||
|
bumpRoom("@roomP");
|
||||||
|
checkOrder([
|
||||||
|
"Pineapple", "Orange", "Apple", "Test Room",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not move the selected room: it should be sticky", () => {
|
||||||
|
// create rooms and check room names are correct
|
||||||
|
cy.createRoom({ name: "Apple" }).as("roomA").then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||||
|
cy.createRoom({ name: "Pineapple" }).as("roomP").then(() => cy.contains(".mx_RoomSublist", "Pineapple"));
|
||||||
|
cy.createRoom({ name: "Orange" }).as("roomO").then(() => cy.contains(".mx_RoomSublist", "Orange"));
|
||||||
|
|
||||||
|
// Given a list of Orange, Pineapple, Apple - if Pineapple is active and a message is sent in Apple, the list should
|
||||||
|
// turn into Apple, Pineapple, Orange - the index position of Pineapple never changes even though the list should technically
|
||||||
|
// be Apple, Orange Pineapple - only when you click on a different room do things reshuffle.
|
||||||
|
|
||||||
|
// Select the Pineapple room
|
||||||
|
cy.contains(".mx_RoomTile", "Pineapple").click();
|
||||||
|
checkOrder([
|
||||||
|
"Orange", "Pineapple", "Apple", "Test Room",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Move Apple
|
||||||
|
bumpRoom("@roomA");
|
||||||
|
checkOrder([
|
||||||
|
"Apple", "Pineapple", "Orange", "Test Room",
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Select the Test Room
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||||
|
|
||||||
|
// the rooms reshuffle to match reality
|
||||||
|
checkOrder([
|
||||||
|
"Apple", "Orange", "Pineapple", "Test Room",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the right unread notifications", () => {
|
||||||
|
createAndJoinBob();
|
||||||
|
|
||||||
|
// send a message in the test room: unread notif count shoould increment
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||||
|
return bob.sendTextMessage(roomId, "Hello World");
|
||||||
|
});
|
||||||
|
|
||||||
|
// check that there is an unread notification (grey) as 1
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "1");
|
||||||
|
cy.get(".mx_NotificationBadge").should("not.have.class", "mx_NotificationBadge_highlighted");
|
||||||
|
|
||||||
|
// send an @mention: highlight count (red) should be 2.
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||||
|
return bob.sendTextMessage(roomId, "Hello Sloth");
|
||||||
|
});
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").contains(".mx_NotificationBadge_count", "2");
|
||||||
|
cy.get(".mx_NotificationBadge").should("have.class", "mx_NotificationBadge_highlighted");
|
||||||
|
|
||||||
|
// click on the room, the notif counts should disappear
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").click();
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").should("not.have.class", "mx_NotificationBadge_count");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show unread indicators", () => { // TODO: for now. Later we should.
|
||||||
|
createAndJoinBob();
|
||||||
|
|
||||||
|
// disable notifs in this room (TODO: CS API call?)
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").find(".mx_RoomTile_notificationsButton").click({ force: true });
|
||||||
|
cy.contains("None").click();
|
||||||
|
|
||||||
|
// create a new room so we know when the message has been received as it'll re-shuffle the room list
|
||||||
|
cy.createRoom({
|
||||||
|
name: "Dummy",
|
||||||
|
});
|
||||||
|
checkOrder([
|
||||||
|
"Dummy", "Test Room",
|
||||||
|
]);
|
||||||
|
|
||||||
|
cy.all([cy.get<string>("@roomId"), cy.get<MatrixClient>("@bob")]).then(([roomId, bob]) => {
|
||||||
|
return bob.sendTextMessage(roomId, "Do you read me?");
|
||||||
|
});
|
||||||
|
// wait for this message to arrive, tell by the room list resorting
|
||||||
|
checkOrder([
|
||||||
|
"Test Room", "Dummy",
|
||||||
|
]);
|
||||||
|
|
||||||
|
cy.contains(".mx_RoomTile", "Test Room").get(".mx_NotificationBadge").should("not.exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update user settings promptly", () => {
|
||||||
|
cy.get(".mx_UserMenu_userAvatar").click();
|
||||||
|
cy.contains("All settings").click();
|
||||||
|
cy.contains("Preferences").click();
|
||||||
|
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
|
||||||
|
".mx_ToggleSwitch_on").should("not.exist");
|
||||||
|
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format").should("exist").find(
|
||||||
|
".mx_ToggleSwitch_ball").click();
|
||||||
|
cy.contains(".mx_SettingsFlag", "Show timestamps in 12 hour format", { timeout: 2000 }).should("exist").find(
|
||||||
|
".mx_ToggleSwitch_on", { timeout: 2000 },
|
||||||
|
).should("exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show and be able to accept/reject/rescind invites", () => {
|
||||||
|
createAndJoinBob();
|
||||||
|
|
||||||
|
let clientUserId;
|
||||||
|
cy.getClient().then((cli) => {
|
||||||
|
clientUserId = cli.getUserId();
|
||||||
|
});
|
||||||
|
|
||||||
|
// invite Sloth into 3 rooms:
|
||||||
|
// - roomJoin: will join this room
|
||||||
|
// - roomReject: will reject the invite
|
||||||
|
// - roomRescind: will make Bob rescind the invite
|
||||||
|
let roomJoin; let roomReject; let roomRescind; let bobClient;
|
||||||
|
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||||
|
bobClient = bob;
|
||||||
|
return Promise.all([
|
||||||
|
bob.createRoom({ name: "Join" }),
|
||||||
|
bob.createRoom({ name: "Reject" }),
|
||||||
|
bob.createRoom({ name: "Rescind" }),
|
||||||
|
]);
|
||||||
|
}).then(([join, reject, rescind]) => {
|
||||||
|
roomJoin = join.room_id;
|
||||||
|
roomReject = reject.room_id;
|
||||||
|
roomRescind = rescind.room_id;
|
||||||
|
return Promise.all([
|
||||||
|
bobClient.invite(roomJoin, clientUserId),
|
||||||
|
bobClient.invite(roomReject, clientUserId),
|
||||||
|
bobClient.invite(roomRescind, clientUserId),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for them all to be on the UI
|
||||||
|
cy.get(".mx_RoomTile").should('have.length', 4); // due to the Test Room in beforeEach
|
||||||
|
|
||||||
|
cy.contains(".mx_RoomTile", "Join").click();
|
||||||
|
cy.contains(".mx_AccessibleButton", "Accept").click();
|
||||||
|
|
||||||
|
checkOrder([
|
||||||
|
"Join", "Test Room",
|
||||||
|
]);
|
||||||
|
|
||||||
|
cy.contains(".mx_RoomTile", "Reject").click();
|
||||||
|
cy.get(".mx_RoomView").contains(".mx_AccessibleButton", "Reject").click();
|
||||||
|
|
||||||
|
// wait for the rejected room to disappear
|
||||||
|
cy.get(".mx_RoomTile").should('have.length', 3);
|
||||||
|
|
||||||
|
// check the lists are correct
|
||||||
|
checkOrder([
|
||||||
|
"Join", "Test Room",
|
||||||
|
]);
|
||||||
|
cy.contains(".mx_RoomSublist", "Invites").find(".mx_RoomTile_title").should((elements) => {
|
||||||
|
expect(_.map(elements, (e) => {
|
||||||
|
return e.textContent;
|
||||||
|
}), "rooms are sorted").to.deep.equal(["Rescind"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// now rescind the invite
|
||||||
|
cy.get<MatrixClient>("@bob").then((bob) => {
|
||||||
|
return bob.kick(roomRescind, clientUserId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait for the rescind to take effect and check the joined list once more
|
||||||
|
cy.get(".mx_RoomTile").should('have.length', 2);
|
||||||
|
checkOrder([
|
||||||
|
"Join", "Test Room",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
2
cypress/global.d.ts
vendored
2
cypress/global.d.ts
vendored
|
@ -28,6 +28,7 @@ import type {
|
||||||
RoomStateEvent,
|
RoomStateEvent,
|
||||||
Visibility,
|
Visibility,
|
||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
|
ICreateClientOpts,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
||||||
import type PerformanceMonitor from "../src/performance";
|
import type PerformanceMonitor from "../src/performance";
|
||||||
|
@ -55,6 +56,7 @@ declare global {
|
||||||
MemoryCryptoStore: typeof MemoryCryptoStore;
|
MemoryCryptoStore: typeof MemoryCryptoStore;
|
||||||
Visibility: typeof Visibility;
|
Visibility: typeof Visibility;
|
||||||
Preset: typeof Preset;
|
Preset: typeof Preset;
|
||||||
|
createClient(opts: ICreateClientOpts | string);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
|
import * as crypto from "crypto";
|
||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import * as fse from "fs-extra";
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
|
@ -25,28 +26,32 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
|
||||||
// A cypress plugin to run docker commands
|
// A cypress plugin to run docker commands
|
||||||
|
|
||||||
export function dockerRun(args: {
|
export function dockerRun(opts: {
|
||||||
image: string;
|
image: string;
|
||||||
containerName: string;
|
containerName: string;
|
||||||
params?: string[];
|
params?: string[];
|
||||||
|
cmd?: string;
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const userInfo = os.userInfo();
|
const userInfo = os.userInfo();
|
||||||
const params = args.params ?? [];
|
const params = opts.params ?? [];
|
||||||
|
|
||||||
if (userInfo.uid >= 0) {
|
if (params?.includes("-v") && userInfo.uid >= 0) {
|
||||||
// On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
|
// On *nix we run the docker container as our uid:gid otherwise cleaning it up its media_store can be difficult
|
||||||
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
|
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
const args = [
|
||||||
childProcess.execFile('docker', [
|
|
||||||
"run",
|
"run",
|
||||||
"--name", args.containerName,
|
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||||
"-d",
|
"-d",
|
||||||
...params,
|
...params,
|
||||||
args.image,
|
opts.image,
|
||||||
"run",
|
];
|
||||||
], (err, stdout) => {
|
|
||||||
|
if (opts.cmd) args.push(opts.cmd);
|
||||||
|
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
childProcess.execFile("docker", args, (err, stdout) => {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
resolve(stdout.trim());
|
resolve(stdout.trim());
|
||||||
});
|
});
|
||||||
|
@ -122,6 +127,21 @@ export function dockerRm(args: {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dockerIp(args: {
|
||||||
|
containerId: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
childProcess.execFile('docker', [
|
||||||
|
"inspect",
|
||||||
|
"-f", "{{ .NetworkSettings.IPAddress }}",
|
||||||
|
args.containerId,
|
||||||
|
], (err, stdout) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve(stdout.trim());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Cypress.PluginConfig}
|
* @type {Cypress.PluginConfig}
|
||||||
*/
|
*/
|
||||||
|
@ -132,5 +152,6 @@ export function docker(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
dockerLogs,
|
dockerLogs,
|
||||||
dockerStop,
|
dockerStop,
|
||||||
dockerRm,
|
dockerRm,
|
||||||
|
dockerIp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
import { performance } from "./performance";
|
import { performance } from "./performance";
|
||||||
import { synapseDocker } from "./synapsedocker";
|
import { synapseDocker } from "./synapsedocker";
|
||||||
|
import { slidingSyncProxyDocker } from "./sliding-sync";
|
||||||
import { webserver } from "./webserver";
|
import { webserver } from "./webserver";
|
||||||
import { docker } from "./docker";
|
import { docker } from "./docker";
|
||||||
import { log } from "./log";
|
import { log } from "./log";
|
||||||
|
@ -31,6 +32,7 @@ export default function(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
docker(on, config);
|
docker(on, config);
|
||||||
performance(on, config);
|
performance(on, config);
|
||||||
synapseDocker(on, config);
|
synapseDocker(on, config);
|
||||||
|
slidingSyncProxyDocker(on, config);
|
||||||
webserver(on, config);
|
webserver(on, config);
|
||||||
log(on, config);
|
log(on, config);
|
||||||
}
|
}
|
||||||
|
|
128
cypress/plugins/sliding-sync/index.ts
Normal file
128
cypress/plugins/sliding-sync/index.ts
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
|
||||||
|
import { getFreePort } from "../utils/port";
|
||||||
|
import { SynapseInstance } from "../synapsedocker";
|
||||||
|
|
||||||
|
// A cypress plugins to add command to start & stop https://github.com/matrix-org/sliding-sync
|
||||||
|
|
||||||
|
export interface ProxyInstance {
|
||||||
|
containerId: string;
|
||||||
|
postgresId: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instances = new Map<string, ProxyInstance>();
|
||||||
|
|
||||||
|
const PG_PASSWORD = "p4S5w0rD";
|
||||||
|
|
||||||
|
async function proxyStart(synapse: SynapseInstance): Promise<ProxyInstance> {
|
||||||
|
console.log(new Date(), "Starting sliding sync proxy...");
|
||||||
|
|
||||||
|
const postgresId = await dockerRun({
|
||||||
|
image: "postgres",
|
||||||
|
containerName: "react-sdk-cypress-sliding-sync-postgres",
|
||||||
|
params: [
|
||||||
|
"--rm",
|
||||||
|
"-e", `POSTGRES_PASSWORD=${PG_PASSWORD}`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const postgresIp = await dockerIp({ containerId: postgresId });
|
||||||
|
const synapseIp = await dockerIp({ containerId: synapse.synapseId });
|
||||||
|
console.log(new Date(), "postgres container up");
|
||||||
|
|
||||||
|
const waitTimeMillis = 30000;
|
||||||
|
const startTime = new Date().getTime();
|
||||||
|
let lastErr: Error;
|
||||||
|
while ((new Date().getTime() - startTime) < waitTimeMillis) {
|
||||||
|
try {
|
||||||
|
await dockerExec({
|
||||||
|
containerId: postgresId,
|
||||||
|
params: [
|
||||||
|
"pg_isready",
|
||||||
|
"-U", "postgres",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
lastErr = null;
|
||||||
|
break;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("pg_isready: failed");
|
||||||
|
lastErr = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lastErr) {
|
||||||
|
console.log("rethrowing");
|
||||||
|
throw lastErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
console.log(new Date(), "starting proxy container...");
|
||||||
|
const containerId = await dockerRun({
|
||||||
|
image: "ghcr.io/matrix-org/sliding-sync-proxy:v0.4.0",
|
||||||
|
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||||
|
params: [
|
||||||
|
"--rm",
|
||||||
|
"-p", `${port}:8008/tcp`,
|
||||||
|
"-e", "SYNCV3_SECRET=bwahahaha",
|
||||||
|
"-e", `SYNCV3_SERVER=http://${synapseIp}:8008`,
|
||||||
|
"-e", `SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
console.log(new Date(), "started!");
|
||||||
|
|
||||||
|
const instance: ProxyInstance = { containerId, postgresId, port };
|
||||||
|
instances.set(containerId, instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyStop(instance: ProxyInstance): Promise<void> {
|
||||||
|
await dockerStop({
|
||||||
|
containerId: instance.containerId,
|
||||||
|
});
|
||||||
|
await dockerStop({
|
||||||
|
containerId: instance.postgresId,
|
||||||
|
});
|
||||||
|
|
||||||
|
instances.delete(instance.containerId);
|
||||||
|
|
||||||
|
console.log(new Date(), "Stopped sliding sync proxy.");
|
||||||
|
// cypress deliberately fails if you return 'undefined', so
|
||||||
|
// return null to signal all is well, and we've handled the task.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
export function slidingSyncProxyDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
|
on("task", {
|
||||||
|
proxyStart,
|
||||||
|
proxyStop,
|
||||||
|
});
|
||||||
|
|
||||||
|
on("after:spec", async (spec) => {
|
||||||
|
for (const instance of instances.values()) {
|
||||||
|
console.warn(`Cleaning up proxy on port ${instance.port} after ${spec.name}`);
|
||||||
|
await proxyStop(instance);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -101,12 +101,13 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
|
|
||||||
const synapseId = await dockerRun({
|
const synapseId = await dockerRun({
|
||||||
image: "matrixdotorg/synapse:develop",
|
image: "matrixdotorg/synapse:develop",
|
||||||
containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`,
|
containerName: `react-sdk-cypress-synapse`,
|
||||||
params: [
|
params: [
|
||||||
"--rm",
|
"--rm",
|
||||||
"-v", `${synCfg.configDir}:/data`,
|
"-v", `${synCfg.configDir}:/data`,
|
||||||
"-p", `${synCfg.port}:8008/tcp`,
|
"-p", `${synCfg.port}:8008/tcp`,
|
||||||
],
|
],
|
||||||
|
cmd: "run",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||||
|
|
|
@ -36,4 +36,5 @@ import "./iframes";
|
||||||
import "./timeline";
|
import "./timeline";
|
||||||
import "./network";
|
import "./network";
|
||||||
import "./composer";
|
import "./composer";
|
||||||
|
import "./proxy";
|
||||||
import "./axe";
|
import "./axe";
|
||||||
|
|
58
cypress/support/proxy.ts
Normal file
58
cypress/support/proxy.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
import AUTWindow = Cypress.AUTWindow;
|
||||||
|
import { ProxyInstance } from '../plugins/sliding-sync';
|
||||||
|
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
/**
|
||||||
|
* Start a sliding sync proxy instance.
|
||||||
|
* @param synapse the synapse instance returned by startSynapse
|
||||||
|
*/
|
||||||
|
startProxy(synapse: SynapseInstance): Chainable<ProxyInstance>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
|
||||||
|
* for if Docker stopping races with the app's background sync loop.
|
||||||
|
* @param proxy the proxy instance returned by startProxy
|
||||||
|
*/
|
||||||
|
stopProxy(proxy: ProxyInstance): Chainable<AUTWindow>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startProxy(synapse: SynapseInstance): Chainable<ProxyInstance> {
|
||||||
|
return cy.task<ProxyInstance>("proxyStart", synapse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
|
||||||
|
if (!proxy) return;
|
||||||
|
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
||||||
|
return cy.window({ log: false }).then((win) => {
|
||||||
|
win.location.href = 'about:blank';
|
||||||
|
cy.task("proxyStop", proxy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("startProxy", startProxy);
|
||||||
|
Cypress.Commands.add("stopProxy", stopProxy);
|
|
@ -36,6 +36,7 @@ import * as StorageManager from './utils/StorageManager';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
import { SlidingSyncManager } from './SlidingSyncManager';
|
||||||
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
|
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
|
||||||
import { _t } from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
|
|
||||||
|
@ -237,6 +238,19 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
||||||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||||
opts.experimentalThreadSupport = SettingsStore.getValue("feature_thread");
|
opts.experimentalThreadSupport = SettingsStore.getValue("feature_thread");
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
|
const proxyUrl = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||||
|
if (proxyUrl) {
|
||||||
|
logger.log("Activating sliding sync using proxy at ", proxyUrl);
|
||||||
|
} else {
|
||||||
|
logger.log("Activating sliding sync");
|
||||||
|
}
|
||||||
|
opts.slidingSync = SlidingSyncManager.instance.configure(
|
||||||
|
this.matrixClient,
|
||||||
|
proxyUrl || this.matrixClient.baseUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Connect the matrix client to the dispatcher and setting handlers
|
// Connect the matrix client to the dispatcher and setting handlers
|
||||||
MatrixActionCreators.start(this.matrixClient);
|
MatrixActionCreators.start(this.matrixClient);
|
||||||
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
|
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
|
||||||
|
|
236
src/SlidingSyncManager.ts
Normal file
236
src/SlidingSyncManager.ts
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sliding Sync Architecture - MSC https://github.com/matrix-org/matrix-spec-proposals/pull/3575
|
||||||
|
*
|
||||||
|
* This is a holistic summary of the changes made to Element-Web / React SDK / JS SDK to enable sliding sync.
|
||||||
|
* This summary will hopefully signpost where developers need to look if they want to make changes to this code.
|
||||||
|
*
|
||||||
|
* At the lowest level, the JS SDK contains an HTTP API wrapper function in client.ts. This is used by
|
||||||
|
* a SlidingSync class in JS SDK, which contains code to handle list operations (INSERT/DELETE/SYNC/etc)
|
||||||
|
* and contains the main request API bodies, but has no code to control updating JS SDK structures: it just
|
||||||
|
* exposes an EventEmitter to listen for updates. When MatrixClient.startClient is called, callers need to
|
||||||
|
* provide a SlidingSync instance as this contains the main request API params (timeline limit, required state,
|
||||||
|
* how many lists, etc).
|
||||||
|
*
|
||||||
|
* The SlidingSyncSdk INTERNAL class in JS SDK attaches listeners to SlidingSync to update JS SDK Room objects,
|
||||||
|
* and it conveniently exposes an identical public API to SyncApi (to allow it to be a drop-in replacement).
|
||||||
|
*
|
||||||
|
* At the highest level, SlidingSyncManager contains mechanisms to tell UI lists which rooms to show,
|
||||||
|
* and contains the core request API params used in Element-Web. It does this by listening for events
|
||||||
|
* emitted by the SlidingSync class and by modifying the request API params on the SlidingSync class.
|
||||||
|
*
|
||||||
|
* (entry point) (updates JS SDK)
|
||||||
|
* SlidingSyncManager SlidingSyncSdk
|
||||||
|
* | |
|
||||||
|
* +------------------.------------------+
|
||||||
|
* listens | listens
|
||||||
|
* SlidingSync
|
||||||
|
* (sync loop,
|
||||||
|
* list ops)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||||
|
import { EventType } from 'matrix-js-sdk/src/@types/event';
|
||||||
|
import {
|
||||||
|
MSC3575Filter,
|
||||||
|
MSC3575List,
|
||||||
|
SlidingSync,
|
||||||
|
} from 'matrix-js-sdk/src/sliding-sync';
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { IDeferred, defer } from 'matrix-js-sdk/src/utils';
|
||||||
|
|
||||||
|
// how long to long poll for
|
||||||
|
const SLIDING_SYNC_TIMEOUT_MS = 20 * 1000;
|
||||||
|
|
||||||
|
// the things to fetch when a user clicks on a room
|
||||||
|
const DEFAULT_ROOM_SUBSCRIPTION_INFO = {
|
||||||
|
timeline_limit: 50,
|
||||||
|
required_state: [
|
||||||
|
["*", "*"], // all events
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PartialSlidingSyncRequest = {
|
||||||
|
filters?: MSC3575Filter;
|
||||||
|
sort?: string[];
|
||||||
|
ranges?: [startIndex: number, endIndex: number][];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class manages the entirety of sliding sync at a high UI/UX level. It controls the placement
|
||||||
|
* of placeholders in lists, controls updating sliding window ranges, and controls which events
|
||||||
|
* are pulled down when. The intention behind this manager is be the single place to look for sliding
|
||||||
|
* sync options and code.
|
||||||
|
*/
|
||||||
|
export class SlidingSyncManager {
|
||||||
|
public static readonly ListSpaces = "space_list";
|
||||||
|
public static readonly ListSearch = "search_list";
|
||||||
|
private static readonly internalInstance = new SlidingSyncManager();
|
||||||
|
|
||||||
|
public slidingSync: SlidingSync;
|
||||||
|
private client: MatrixClient;
|
||||||
|
private listIdToIndex: Record<string, number>;
|
||||||
|
|
||||||
|
private configureDefer: IDeferred<void>;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
this.listIdToIndex = {};
|
||||||
|
this.configureDefer = defer<void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static get instance(): SlidingSyncManager {
|
||||||
|
return SlidingSyncManager.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||||
|
this.client = client;
|
||||||
|
this.listIdToIndex = {};
|
||||||
|
this.slidingSync = new SlidingSync(
|
||||||
|
proxyUrl, [], DEFAULT_ROOM_SUBSCRIPTION_INFO, client, SLIDING_SYNC_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
// set the space list
|
||||||
|
this.slidingSync.setList(this.getOrAllocateListIndex(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, "*"], // all space children
|
||||||
|
[EventType.SpaceParent, "*"], // all space parents
|
||||||
|
[EventType.RoomMember, this.client.getUserId()!], // lets the client calculate that we are in fact in the room
|
||||||
|
],
|
||||||
|
filters: {
|
||||||
|
room_types: ["m.space"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.configureDefer.resolve();
|
||||||
|
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 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);
|
||||||
|
await this.configureDefer.promise;
|
||||||
|
let list = this.slidingSync.getList(listIndex);
|
||||||
|
if (!list) {
|
||||||
|
list = {
|
||||||
|
ranges: [[0, 20]],
|
||||||
|
sort: [
|
||||||
|
"by_highlight_count", "by_notification_count", "by_recency",
|
||||||
|
],
|
||||||
|
timeline_limit: 1, // most recent message display: though this seems to only be needed for favourites?
|
||||||
|
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.RoomMember, this.client.getUserId()], // lets the client calculate that we are in fact in the room
|
||||||
|
],
|
||||||
|
};
|
||||||
|
list = Object.assign(list, updateArgs);
|
||||||
|
} else {
|
||||||
|
const updatedList = Object.assign({}, list, updateArgs);
|
||||||
|
// cannot use objectHasDiff as we need to do deep diff checking
|
||||||
|
if (JSON.stringify(list) === JSON.stringify(updatedList)) {
|
||||||
|
logger.debug("list matches, not sending, update => ", updateArgs);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
list = updatedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
} else {
|
||||||
|
await this.slidingSync.setList(listIndex, list);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug("ensureListRegistered: update failed txn_id=", err);
|
||||||
|
}
|
||||||
|
return this.slidingSync.getList(listIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
|
||||||
|
await this.configureDefer.promise;
|
||||||
|
const subscriptions = this.slidingSync.getRoomSubscriptions();
|
||||||
|
if (visible) {
|
||||||
|
subscriptions.add(roomId);
|
||||||
|
} else {
|
||||||
|
subscriptions.delete(roomId);
|
||||||
|
}
|
||||||
|
logger.log("SlidingSync setRoomVisible:", roomId, visible);
|
||||||
|
const p = this.slidingSync.modifyRoomSubscriptions(subscriptions);
|
||||||
|
if (this.client.getRoom(roomId)) {
|
||||||
|
return roomId; // we have data already for this room, show immediately e.g it's in a list
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// wait until the next sync before returning as RoomView may need to know the current state
|
||||||
|
await p;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn("SlidingSync setRoomVisible:", roomId, visible, "failed to confirm transaction");
|
||||||
|
}
|
||||||
|
return roomId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,6 +55,12 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
|
// TODO: https://github.com/vector-im/element-web/issues/23207
|
||||||
|
// Sliding Sync doesn't support unread indicator dots (yet...)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
// get the most recent read receipt sent by our account.
|
// get the most recent read receipt sent by our account.
|
||||||
|
|
130
src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
Normal file
130
src/components/views/dialogs/SlidingSyncOptionsDialog.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { MatrixClient } from 'matrix-js-sdk/src/matrix';
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import TextInputDialog from "./TextInputDialog";
|
||||||
|
import withValidation from "../elements/Validation";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
|
||||||
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the server natively supports sliding sync.
|
||||||
|
* @param cli The MatrixClient of the logged in user.
|
||||||
|
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||||
|
*/
|
||||||
|
async function syncHealthCheck(cli: MatrixClient): Promise<void> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||||
|
const url = cli.http.getUrl("/sync", {}, "/_matrix/client/unstable/org.matrix.msc3575");
|
||||||
|
const res = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
clearTimeout(id);
|
||||||
|
if (res.status != 200) {
|
||||||
|
throw new Error(`syncHealthCheck: server returned HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
logger.info("server natively support sliding sync OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check that the proxy url is in fact a sliding sync proxy endpoint and it is up.
|
||||||
|
* @param endpoint The proxy endpoint url
|
||||||
|
* @param hsUrl The homeserver url of the logged in user.
|
||||||
|
* @throws if the proxy server is unreachable or not configured to the given homeserver
|
||||||
|
*/
|
||||||
|
async function proxyHealthCheck(endpoint: string, hsUrl?: string): Promise<void> {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const id = setTimeout(() => controller.abort(), 10 * 1000); // 10s
|
||||||
|
const res = await fetch(endpoint + "/client/server.json", {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
clearTimeout(id);
|
||||||
|
if (res.status != 200) {
|
||||||
|
throw new Error(`proxyHealthCheck: proxy server returned HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const body = await res.json();
|
||||||
|
if (body.server !== hsUrl) {
|
||||||
|
throw new Error(`proxyHealthCheck: client using ${hsUrl} but server is as ${body.server}`);
|
||||||
|
}
|
||||||
|
logger.info("sliding sync proxy is OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlidingSyncOptionsDialog: React.FC<IDialogProps> = ({ onFinished }) => {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const currentProxy = SettingsStore.getValue("feature_sliding_sync_proxy_url");
|
||||||
|
const hasNativeSupport = useAsyncMemo(() => syncHealthCheck(cli).then(() => true, () => false), [], null);
|
||||||
|
|
||||||
|
let nativeSupport: string;
|
||||||
|
if (hasNativeSupport === null) {
|
||||||
|
nativeSupport = _t("Checking...");
|
||||||
|
} else {
|
||||||
|
nativeSupport = hasNativeSupport
|
||||||
|
? _t("Your server has native support")
|
||||||
|
: _t("Your server lacks native support");
|
||||||
|
}
|
||||||
|
|
||||||
|
const validProxy = withValidation<undefined, { error?: Error }>({
|
||||||
|
async deriveData({ value }): Promise<{ error?: Error }> {
|
||||||
|
try {
|
||||||
|
await proxyHealthCheck(value, MatrixClientPeg.get().baseUrl);
|
||||||
|
return {};
|
||||||
|
} catch (error) {
|
||||||
|
return { error };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test: async ({ value }) => !!value || hasNativeSupport,
|
||||||
|
invalid: () => _t("Your server lacks native support, you must specify a proxy"),
|
||||||
|
}, {
|
||||||
|
key: "working",
|
||||||
|
final: true,
|
||||||
|
test: async (_, { error }) => !error,
|
||||||
|
valid: () => _t("Looks good"),
|
||||||
|
invalid: ({ error }) => error?.message,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return <TextInputDialog
|
||||||
|
title={_t("Sliding Sync configuration")}
|
||||||
|
description={<div>
|
||||||
|
<div><b>{ _t("To disable you will need to log out and back in, use with caution!") }</b></div>
|
||||||
|
{ nativeSupport }
|
||||||
|
</div>}
|
||||||
|
placeholder={hasNativeSupport ? _t('Proxy URL (optional)') : _t('Proxy URL')}
|
||||||
|
value={currentProxy}
|
||||||
|
button={_t("Enable")}
|
||||||
|
validator={validProxy}
|
||||||
|
onFinished={(enable: boolean, proxyUrl: string) => {
|
||||||
|
if (enable) {
|
||||||
|
SettingsStore.setValue("feature_sliding_sync_proxy_url", null, SettingLevel.DEVICE, proxyUrl);
|
||||||
|
onFinished(true);
|
||||||
|
} else {
|
||||||
|
onFinished(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>;
|
||||||
|
};
|
|
@ -32,7 +32,7 @@ interface IProps extends IDialogProps {
|
||||||
busyMessage?: string; // pass _td string
|
busyMessage?: string; // pass _td string
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
hasCancel?: boolean;
|
hasCancel?: boolean;
|
||||||
validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
|
validator?: (fieldState: IFieldState) => Promise<IValidationResult>; // result of withValidation
|
||||||
fixedWidth?: boolean;
|
fixedWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,6 +92,7 @@ import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
||||||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||||
import { TooltipOption } from "./TooltipOption";
|
import { TooltipOption } from "./TooltipOption";
|
||||||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||||
|
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
||||||
|
|
||||||
const MAX_RECENT_SEARCHES = 10;
|
const MAX_RECENT_SEARCHES = 10;
|
||||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||||
|
@ -242,7 +243,7 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
||||||
}, [numResults, queryLength, viaSpotlight]);
|
}, [numResults, queryLength, viaSpotlight]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findVisibleRooms = (cli: MatrixClient) => {
|
const findVisibleRooms = (cli: MatrixClient): Room[] => {
|
||||||
return cli.getVisibleRooms().filter(room => {
|
return cli.getVisibleRooms().filter(room => {
|
||||||
// Do not show local rooms
|
// Do not show local rooms
|
||||||
if (isLocalRoom(room)) return false;
|
if (isLocalRoom(room)) return false;
|
||||||
|
@ -340,19 +341,36 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
searchProfileInfo,
|
searchProfileInfo,
|
||||||
searchParams,
|
searchParams,
|
||||||
);
|
);
|
||||||
|
const isSlidingSyncEnabled = SettingsStore.getValue("feature_sliding_sync");
|
||||||
|
let {
|
||||||
|
loading: slidingSyncRoomSearchLoading,
|
||||||
|
rooms: slidingSyncRooms,
|
||||||
|
search: searchRoomsServerside,
|
||||||
|
} = useSlidingSyncRoomSearch();
|
||||||
|
useDebouncedCallback(isSlidingSyncEnabled, searchRoomsServerside, searchParams);
|
||||||
|
if (!isSlidingSyncEnabled) {
|
||||||
|
slidingSyncRoomSearchLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
const possibleResults = useMemo<Result[]>(
|
const possibleResults = useMemo<Result[]>(
|
||||||
() => {
|
() => {
|
||||||
const roomResults = findVisibleRooms(cli).map(toRoomResult);
|
const userResults: IMemberResult[] = [];
|
||||||
|
let roomResults: IRoomResult[];
|
||||||
|
let alreadyAddedUserIds: Set<string>;
|
||||||
|
if (isSlidingSyncEnabled) {
|
||||||
|
// use the rooms sliding sync returned as the server has already worked it out for us
|
||||||
|
roomResults = slidingSyncRooms.map(toRoomResult);
|
||||||
|
} else {
|
||||||
|
roomResults = findVisibleRooms(cli).map(toRoomResult);
|
||||||
// If we already have a DM with the user we're looking for, we will
|
// If we already have a DM with the user we're looking for, we will
|
||||||
// show that DM instead of the user themselves
|
// show that DM instead of the user themselves
|
||||||
const alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
||||||
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
||||||
if (!userId) return userIds;
|
if (!userId) return userIds;
|
||||||
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
||||||
userIds.add(userId);
|
userIds.add(userId);
|
||||||
return userIds;
|
return userIds;
|
||||||
}, new Set<string>());
|
}, new Set<string>());
|
||||||
const userResults = [];
|
|
||||||
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
||||||
// Make sure we don't have any user more than once
|
// Make sure we don't have any user more than once
|
||||||
if (alreadyAddedUserIds.has(user.userId)) continue;
|
if (alreadyAddedUserIds.has(user.userId)) continue;
|
||||||
|
@ -360,6 +378,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
|
|
||||||
userResults.push(toMemberResult(user));
|
userResults.push(toMemberResult(user));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...SpaceStore.instance.enabledMetaSpaces.map(spaceKey => ({
|
...SpaceStore.instance.enabledMetaSpaces.map(spaceKey => ({
|
||||||
|
@ -382,7 +401,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
...publicRooms.map(toPublicRoomResult),
|
...publicRooms.map(toPublicRoomResult),
|
||||||
].filter(result => filter === null || result.filter.includes(filter));
|
].filter(result => filter === null || result.filter.includes(filter));
|
||||||
},
|
},
|
||||||
[cli, users, profile, publicRooms, filter],
|
[cli, users, profile, publicRooms, slidingSyncRooms, isSlidingSyncEnabled, filter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const results = useMemo<Record<Section, Result[]>>(() => {
|
const results = useMemo<Record<Section, Result[]>>(() => {
|
||||||
|
@ -401,10 +420,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
|
|
||||||
possibleResults.forEach(entry => {
|
possibleResults.forEach(entry => {
|
||||||
if (isRoomResult(entry)) {
|
if (isRoomResult(entry)) {
|
||||||
|
// sliding sync gives the correct rooms in the list so we don't need to filter
|
||||||
|
if (!isSlidingSyncEnabled) {
|
||||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||||
!entry.query?.some(q => q.includes(lcQuery))
|
!entry.query?.some(q => q.includes(lcQuery))
|
||||||
) return; // bail, does not match query
|
) return; // bail, does not match query
|
||||||
|
}
|
||||||
} else if (isMemberResult(entry)) {
|
} else if (isMemberResult(entry)) {
|
||||||
if (!entry.query?.some(q => q.includes(lcQuery))) return; // bail, does not match query
|
if (!entry.query?.some(q => q.includes(lcQuery))) return; // bail, does not match query
|
||||||
} else if (isPublicRoomResult(entry)) {
|
} else if (isPublicRoomResult(entry)) {
|
||||||
|
@ -455,7 +477,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}, [trimmedQuery, filter, cli, possibleResults, memberComparator]);
|
}, [trimmedQuery, filter, cli, possibleResults, isSlidingSyncEnabled, memberComparator]);
|
||||||
|
|
||||||
const numResults = sum(Object.values(results).map(it => it.length));
|
const numResults = sum(Object.values(results).map(it => it.length));
|
||||||
useWebSearchMetrics(numResults, query.length, true);
|
useWebSearchMetrics(numResults, query.length, true);
|
||||||
|
@ -1208,7 +1230,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
aria-label={_t("Search")}
|
aria-label={_t("Search")}
|
||||||
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
|
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
|
||||||
/>
|
/>
|
||||||
{ (publicRoomsLoading || peopleLoading || profileLoading) && (
|
{ (publicRoomsLoading || peopleLoading || profileLoading || slidingSyncRoomSearchLoading) && (
|
||||||
<Spinner w={24} h={24} />
|
<Spinner w={24} h={24} />
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Dispatcher } from "flux";
|
import { Dispatcher } from "flux";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
|
||||||
import { Enable, Resizable } from "re-resizable";
|
import { Enable, Resizable } from "re-resizable";
|
||||||
import { Direction } from "re-resizable/lib/resizer";
|
import { Direction } from "re-resizable/lib/resizer";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
@ -39,7 +39,7 @@ import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorith
|
||||||
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
||||||
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
import RoomListStore, { LISTS_UPDATE_EVENT, LISTS_LOADING_EVENT } from "../../../stores/room-list/RoomListStore";
|
||||||
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
||||||
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
|
@ -52,6 +52,8 @@ import ContextMenu, {
|
||||||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ExtraTile from "./ExtraTile";
|
import ExtraTile from "./ExtraTile";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { SlidingSyncManager } from "../../../SlidingSyncManager";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
import RoomTile from "./RoomTile";
|
import RoomTile from "./RoomTile";
|
||||||
|
|
||||||
|
@ -98,6 +100,7 @@ interface IState {
|
||||||
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
||||||
height: number;
|
height: number;
|
||||||
rooms: Room[];
|
rooms: Room[];
|
||||||
|
roomsLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomSublist extends React.Component<IProps, IState> {
|
export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
|
@ -109,8 +112,12 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private heightAtStart: number;
|
private heightAtStart: number;
|
||||||
private notificationState: ListNotificationState;
|
private notificationState: ListNotificationState;
|
||||||
|
|
||||||
|
private slidingSyncMode: boolean;
|
||||||
|
|
||||||
constructor(props: IProps) {
|
constructor(props: IProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
// when this setting is toggled it restarts the app so it's safe to not watch this.
|
||||||
|
this.slidingSyncMode = SettingsStore.getValue("feature_sliding_sync");
|
||||||
|
|
||||||
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
||||||
this.heightAtStart = 0;
|
this.heightAtStart = 0;
|
||||||
|
@ -121,6 +128,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
isExpanded: !this.layout.isCollapsed,
|
isExpanded: !this.layout.isCollapsed,
|
||||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||||
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
||||||
|
roomsLoading: false,
|
||||||
};
|
};
|
||||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||||
this.state = Object.assign(this.state, { height: this.calculateInitialHeight() });
|
this.state = Object.assign(this.state, { height: this.calculateInitialHeight() });
|
||||||
|
@ -167,6 +175,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get numVisibleTiles(): number {
|
private get numVisibleTiles(): number {
|
||||||
|
if (this.slidingSyncMode) {
|
||||||
|
return this.state.rooms.length;
|
||||||
|
}
|
||||||
const nVisible = Math.ceil(this.layout.visibleTiles);
|
const nVisible = Math.ceil(this.layout.visibleTiles);
|
||||||
return Math.min(nVisible, this.numTiles);
|
return Math.min(nVisible, this.numTiles);
|
||||||
}
|
}
|
||||||
|
@ -239,6 +250,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
RoomListStore.instance.on(LISTS_LOADING_EVENT, this.onListsLoading);
|
||||||
|
|
||||||
// Using the passive option to not block the main thread
|
// Using the passive option to not block the main thread
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
||||||
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
||||||
|
@ -247,9 +260,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
defaultDispatcher.unregister(this.dispatcherRef);
|
defaultDispatcher.unregister(this.dispatcherRef);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||||
|
RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading);
|
||||||
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onListsLoading = (tagId: TagID, isLoading: boolean) => {
|
||||||
|
if (this.props.tagId !== tagId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
roomsLoading: isLoading,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onListsUpdated = () => {
|
private onListsUpdated = () => {
|
||||||
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
const stateUpdates: IState & any = {}; // &any is to avoid a cast on the initializer
|
||||||
|
|
||||||
|
@ -315,7 +338,16 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
this.setState({ isResizing: false, height: newHeight });
|
this.setState({ isResizing: false, height: newHeight });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShowAllClick = () => {
|
private onShowAllClick = async () => {
|
||||||
|
if (this.slidingSyncMode) {
|
||||||
|
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||||
|
const count = RoomListStore.instance.getCount(this.props.tagId);
|
||||||
|
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
|
||||||
|
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);
|
||||||
|
@ -531,8 +563,17 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.contextMenuPosition) {
|
if (this.state.contextMenuPosition) {
|
||||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
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_highlight_count" ||
|
||||||
|
slidingList.sort[0] === "by_notification_count"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Invites don't get some nonsense options, so only add them if we have to.
|
// Invites don't get some nonsense options, so only add them if we have to.
|
||||||
let otherSections = null;
|
let otherSections = null;
|
||||||
|
@ -717,7 +758,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
let content = null;
|
let content = null;
|
||||||
if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
if (this.state.roomsLoading) {
|
||||||
|
content = <div className="mx_RoomSublist_skeletonUI" />;
|
||||||
|
} else if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
||||||
content = <div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
content = <div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
||||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||||
{ visibleTiles }
|
{ visibleTiles }
|
||||||
|
@ -739,12 +782,18 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
// floats above the resize handle, if we have one present. If the user has all
|
// floats above the resize handle, if we have one present. If the user has all
|
||||||
// tiles visible, it becomes 'show less'.
|
// tiles visible, it becomes 'show less'.
|
||||||
let showNButton = null;
|
let showNButton = null;
|
||||||
|
const hasMoreSlidingSync = (
|
||||||
|
this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
|
||||||
|
);
|
||||||
|
|
||||||
if (maxTilesPx > this.state.height) {
|
if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
|
||||||
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
||||||
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
|
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
|
||||||
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
|
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
|
||||||
const numMissing = this.numTiles - amountFullyShown;
|
let numMissing = this.numTiles - amountFullyShown;
|
||||||
|
if (this.slidingSyncMode) {
|
||||||
|
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
|
||||||
|
}
|
||||||
const label = _t("Show %(count)s more", { count: numMissing });
|
const label = _t("Show %(count)s more", { count: numMissing });
|
||||||
let showMoreText = (
|
let showMoreText = (
|
||||||
<span className='mx_RoomSublist_showNButtonText'>
|
<span className='mx_RoomSublist_showNButtonText'>
|
||||||
|
|
86
src/hooks/useSlidingSyncRoomSearch.ts
Normal file
86
src/hooks/useSlidingSyncRoomSearch.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
|
import { useLatestResult } from "./useLatestResult";
|
||||||
|
import { SlidingSyncManager } from "../SlidingSyncManager";
|
||||||
|
|
||||||
|
export interface SlidingSyncRoomSearchOpts {
|
||||||
|
limit: number;
|
||||||
|
query?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSlidingSyncRoomSearch = () => {
|
||||||
|
const [rooms, setRooms] = useState<Room[]>([]);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
|
||||||
|
|
||||||
|
const [updateQuery, updateResult] = useLatestResult<{ term: string, limit?: number }, Room[]>(setRooms);
|
||||||
|
|
||||||
|
const search = useCallback(async ({
|
||||||
|
limit = 100,
|
||||||
|
query: term,
|
||||||
|
}: SlidingSyncRoomSearchOpts): Promise<boolean> => {
|
||||||
|
const opts = { limit, term };
|
||||||
|
updateQuery(opts);
|
||||||
|
|
||||||
|
if (!term?.length) {
|
||||||
|
setRooms([]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await SlidingSyncManager.instance.ensureListRegistered(listIndex, {
|
||||||
|
ranges: [[0, limit]],
|
||||||
|
filters: {
|
||||||
|
room_name_like: term,
|
||||||
|
is_tombstoned: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rooms = [];
|
||||||
|
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex);
|
||||||
|
let i = 0;
|
||||||
|
while (roomIndexToRoomId[i]) {
|
||||||
|
const roomId = roomIndexToRoomId[i];
|
||||||
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
|
if (room) {
|
||||||
|
rooms.push(room);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
updateResult(opts, rooms);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not fetch sliding sync rooms for params", { limit, term }, e);
|
||||||
|
updateResult(opts, []);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
// TODO: delete the list?
|
||||||
|
}
|
||||||
|
}, [updateQuery, updateResult, listIndex]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
rooms,
|
||||||
|
search,
|
||||||
|
} as const;
|
||||||
|
};
|
|
@ -907,6 +907,7 @@
|
||||||
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
"Right panel stays open (defaults to room member list)": "Right panel stays open (defaults to room member list)",
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
"Send read receipts": "Send read receipts",
|
"Send read receipts": "Send read receipts",
|
||||||
|
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
|
||||||
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
|
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
|
||||||
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
|
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
|
||||||
"Use new session manager (under active development)": "Use new session manager (under active development)",
|
"Use new session manager (under active development)": "Use new session manager (under active development)",
|
||||||
|
@ -2856,6 +2857,14 @@
|
||||||
"Link to selected message": "Link to selected message",
|
"Link to selected message": "Link to selected message",
|
||||||
"Link to room": "Link to room",
|
"Link to room": "Link to room",
|
||||||
"Command Help": "Command Help",
|
"Command Help": "Command Help",
|
||||||
|
"Checking...": "Checking...",
|
||||||
|
"Your server has native support": "Your server has native support",
|
||||||
|
"Your server lacks native support": "Your server lacks native support",
|
||||||
|
"Your server lacks native support, you must specify a proxy": "Your server lacks native support, you must specify a proxy",
|
||||||
|
"Sliding Sync configuration": "Sliding Sync configuration",
|
||||||
|
"To disable you will need to log out and back in, use with caution!": "To disable you will need to log out and back in, use with caution!",
|
||||||
|
"Proxy URL (optional)": "Proxy URL (optional)",
|
||||||
|
"Proxy URL": "Proxy URL",
|
||||||
"Sections to show": "Sections to show",
|
"Sections to show": "Sections to show",
|
||||||
"This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.",
|
"This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.",
|
||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
|
|
|
@ -41,6 +41,7 @@ import IncompatibleController from "./controllers/IncompatibleController";
|
||||||
import { ImageSize } from "./enums/ImageSize";
|
import { ImageSize } from "./enums/ImageSize";
|
||||||
import { MetaSpace } from "../stores/spaces";
|
import { MetaSpace } from "../stores/spaces";
|
||||||
import SdkConfig from "../SdkConfig";
|
import SdkConfig from "../SdkConfig";
|
||||||
|
import SlidingSyncController from './controllers/SlidingSyncController';
|
||||||
import ThreadBetaController from './controllers/ThreadBetaController';
|
import ThreadBetaController from './controllers/ThreadBetaController';
|
||||||
import { FontWatcher } from "./watchers/FontWatcher";
|
import { FontWatcher } from "./watchers/FontWatcher";
|
||||||
|
|
||||||
|
@ -406,6 +407,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
displayName: _td("Send read receipts"),
|
displayName: _td("Send read receipts"),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
"feature_sliding_sync": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Developer,
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
|
displayName: _td('Sliding Sync mode (under active development, cannot be disabled)'),
|
||||||
|
default: false,
|
||||||
|
controller: new SlidingSyncController(),
|
||||||
|
},
|
||||||
|
"feature_sliding_sync_proxy_url": {
|
||||||
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
"feature_location_share_live": {
|
"feature_location_share_live": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Messaging,
|
labsGroup: LabGroup.Messaging,
|
||||||
|
|
39
src/settings/controllers/SlidingSyncController.ts
Normal file
39
src/settings/controllers/SlidingSyncController.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SettingController from "./SettingController";
|
||||||
|
import PlatformPeg from "../../PlatformPeg";
|
||||||
|
import { SettingLevel } from "../SettingLevel";
|
||||||
|
import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
|
||||||
|
import Modal from "../../Modal";
|
||||||
|
import SettingsStore from "../SettingsStore";
|
||||||
|
|
||||||
|
export default class SlidingSyncController extends SettingController {
|
||||||
|
public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
|
||||||
|
const { finished } = Modal.createDialog(SlidingSyncOptionsDialog);
|
||||||
|
const [value] = await finished;
|
||||||
|
return newValue === value; // abort the operation if we're already in the state the user chose via modal
|
||||||
|
}
|
||||||
|
|
||||||
|
public async onChange(): Promise<void> {
|
||||||
|
PlatformPeg.get().reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get settingDisabled(): boolean {
|
||||||
|
// Cannot be disabled once enabled, user has been warned and must log out and back in.
|
||||||
|
return SettingsStore.getValue("feature_sliding_sync");
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,8 @@ import { JoinRoomErrorPayload } from "../dispatcher/payloads/JoinRoomErrorPayloa
|
||||||
import { ViewRoomErrorPayload } from "../dispatcher/payloads/ViewRoomErrorPayload";
|
import { ViewRoomErrorPayload } from "../dispatcher/payloads/ViewRoomErrorPayload";
|
||||||
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||||
import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload";
|
import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { SlidingSyncManager } from "../SlidingSyncManager";
|
||||||
import { awaitRoomDownSync } from "../utils/RoomUpgrade";
|
import { awaitRoomDownSync } from "../utils/RoomUpgrade";
|
||||||
|
|
||||||
const NUM_JOIN_RETRY = 5;
|
const NUM_JOIN_RETRY = 5;
|
||||||
|
@ -278,6 +280,32 @@ export class RoomViewStore extends Store<ActionPayload> {
|
||||||
activeSpace,
|
activeSpace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
|
||||||
|
if (this.state.roomId) {
|
||||||
|
// unsubscribe from this room, but don't await it as we don't care when this gets done.
|
||||||
|
SlidingSyncManager.instance.setRoomVisible(this.state.roomId, false);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
roomId: payload.room_id,
|
||||||
|
initialEventId: null,
|
||||||
|
initialEventPixelOffset: null,
|
||||||
|
isInitialEventHighlighted: null,
|
||||||
|
initialEventScrollIntoView: true,
|
||||||
|
roomAlias: null,
|
||||||
|
roomLoading: true,
|
||||||
|
roomLoadError: null,
|
||||||
|
viaServers: payload.via_servers,
|
||||||
|
wasContextSwitch: payload.context_switch,
|
||||||
|
});
|
||||||
|
// set this room as the room subscription. We need to await for it as this will fetch
|
||||||
|
// all room state for this room, which is required before we get the state below.
|
||||||
|
await SlidingSyncManager.instance.setRoomVisible(payload.room_id, true);
|
||||||
|
// Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now
|
||||||
|
dis.dispatch({
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
roomId: payload.room_id,
|
roomId: payload.room_id,
|
||||||
|
|
|
@ -23,6 +23,9 @@ import { IFilterCondition } from "./filters/IFilterCondition";
|
||||||
export enum RoomListStoreEvent {
|
export enum RoomListStoreEvent {
|
||||||
// The event/channel which is called when the room lists have been changed.
|
// The event/channel which is called when the room lists have been changed.
|
||||||
ListsUpdate = "lists_update",
|
ListsUpdate = "lists_update",
|
||||||
|
// The event which is called when the room list is loading.
|
||||||
|
// Called with the (tagId, bool) which is true when the list is loading, else false.
|
||||||
|
ListsLoading = "lists_loading",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoomListStore extends EventEmitter {
|
export interface RoomListStore extends EventEmitter {
|
||||||
|
@ -33,6 +36,15 @@ export interface RoomListStore extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
get orderedLists(): ITagMap;
|
get orderedLists(): ITagMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total number of rooms in this list. Prefer this method to
|
||||||
|
* RoomListStore.orderedLists[tagId].length because the client may not
|
||||||
|
* be aware of all the rooms in this list (e.g in Sliding Sync).
|
||||||
|
* @param tagId the tag to get the room count for.
|
||||||
|
* @returns the number of rooms in this list, or 0 if the list is unknown.
|
||||||
|
*/
|
||||||
|
getCount(tagId: TagID): number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the sort algorithm for the specified tag.
|
* Set the sort algorithm for the specified tag.
|
||||||
* @param tagId the tag to set the algorithm for
|
* @param tagId the tag to set the algorithm for
|
||||||
|
|
|
@ -38,12 +38,14 @@ 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";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
// state is tracked in underlying classes
|
// state is tracked in underlying classes
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
|
export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
|
||||||
|
export const LISTS_LOADING_EVENT = RoomListStoreEvent.ListsLoading; // unused; used by SlidingRoomListStore
|
||||||
|
|
||||||
export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements Interface {
|
export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements Interface {
|
||||||
/**
|
/**
|
||||||
|
@ -585,6 +587,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
||||||
return algorithmTags;
|
return algorithmTags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCount(tagId: TagID): number {
|
||||||
|
// The room list store knows about all the rooms, so just return the length.
|
||||||
|
return this.orderedLists[tagId].length || 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually update a room with a given cause. This should only be used if the
|
* 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
|
* room list store would otherwise be incapable of doing the update itself. Note
|
||||||
|
@ -602,10 +609,17 @@ export default class RoomListStore {
|
||||||
private static internalInstance: Interface;
|
private static internalInstance: Interface;
|
||||||
|
|
||||||
public static get instance(): Interface {
|
public static get instance(): Interface {
|
||||||
if (!this.internalInstance) {
|
if (!RoomListStore.internalInstance) {
|
||||||
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
|
logger.info("using SlidingRoomListStoreClass");
|
||||||
|
const instance = new SlidingRoomListStoreClass();
|
||||||
|
instance.start();
|
||||||
|
RoomListStore.internalInstance = instance;
|
||||||
|
} else {
|
||||||
const instance = new RoomListStoreClass();
|
const instance = new RoomListStoreClass();
|
||||||
instance.start();
|
instance.start();
|
||||||
this.internalInstance = instance;
|
RoomListStore.internalInstance = instance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.internalInstance;
|
return this.internalInstance;
|
||||||
|
|
386
src/stores/room-list/SlidingRoomListStore.ts
Normal file
386
src/stores/room-list/SlidingRoomListStore.ts
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MSC3575Filter, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
|
||||||
|
|
||||||
|
import { RoomUpdateCause, TagID, OrderedDefaultTagIDs, DefaultTagID } from "./models";
|
||||||
|
import { ITagMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||||
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
|
import { IFilterCondition } from "./filters/IFilterCondition";
|
||||||
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
|
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
||||||
|
import { SlidingSyncManager } from "../../SlidingSyncManager";
|
||||||
|
import SpaceStore from "../spaces/SpaceStore";
|
||||||
|
import { MetaSpace, SpaceKey, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||||
|
import { LISTS_LOADING_EVENT } from "./RoomListStore";
|
||||||
|
import { RoomViewStore } from "../RoomViewStore";
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// state is tracked in underlying classes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlidingSyncSortToFilter: Record<SortAlgorithm, string[]> = {
|
||||||
|
[SortAlgorithm.Alphabetic]: ["by_name", "by_recency"],
|
||||||
|
[SortAlgorithm.Recent]: ["by_highlight_count", "by_notification_count", "by_recency"],
|
||||||
|
[SortAlgorithm.Manual]: ["by_recency"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterConditions: Record<TagID, MSC3575Filter> = {
|
||||||
|
[DefaultTagID.Invite]: {
|
||||||
|
is_invite: true,
|
||||||
|
},
|
||||||
|
[DefaultTagID.Favourite]: {
|
||||||
|
tags: ["m.favourite"],
|
||||||
|
is_tombstoned: false,
|
||||||
|
},
|
||||||
|
// TODO https://github.com/vector-im/element-web/issues/23207
|
||||||
|
// DefaultTagID.SavedItems,
|
||||||
|
[DefaultTagID.DM]: {
|
||||||
|
is_dm: true,
|
||||||
|
is_invite: false,
|
||||||
|
is_tombstoned: false,
|
||||||
|
},
|
||||||
|
[DefaultTagID.Untagged]: {
|
||||||
|
is_dm: false,
|
||||||
|
is_invite: false,
|
||||||
|
is_tombstoned: false,
|
||||||
|
not_room_types: ["m.space"],
|
||||||
|
not_tags: ["m.favourite", "m.lowpriority"],
|
||||||
|
// spaces filter added dynamically
|
||||||
|
},
|
||||||
|
[DefaultTagID.LowPriority]: {
|
||||||
|
tags: ["m.lowpriority"],
|
||||||
|
is_tombstoned: false,
|
||||||
|
},
|
||||||
|
// TODO https://github.com/vector-im/element-web/issues/23207
|
||||||
|
// DefaultTagID.ServerNotice,
|
||||||
|
// DefaultTagID.Suggested,
|
||||||
|
// DefaultTagID.Archived,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
|
||||||
|
|
||||||
|
export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> implements Interface {
|
||||||
|
private tagIdToSortAlgo: Record<TagID, SortAlgorithm> = {};
|
||||||
|
private tagMap: ITagMap = {};
|
||||||
|
private counts: Record<TagID, number> = {};
|
||||||
|
private stickyRoomId: string | null;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super(defaultDispatcher);
|
||||||
|
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
|
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
|
||||||
|
this.tagIdToSortAlgo[tagId] = sort;
|
||||||
|
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
||||||
|
switch (sort) {
|
||||||
|
case SortAlgorithm.Alphabetic:
|
||||||
|
await SlidingSyncManager.instance.ensureListRegistered(
|
||||||
|
slidingSyncIndex, {
|
||||||
|
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SortAlgorithm.Recent:
|
||||||
|
await SlidingSyncManager.instance.ensureListRegistered(
|
||||||
|
slidingSyncIndex, {
|
||||||
|
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SortAlgorithm.Manual:
|
||||||
|
logger.error("cannot enable manual sort in sliding sync mode");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.error("unknown sort mode: ", sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTagSorting(tagId: TagID): SortAlgorithm {
|
||||||
|
let algo = this.tagIdToSortAlgo[tagId];
|
||||||
|
if (!algo) {
|
||||||
|
logger.warn("SlidingRoomListStore.getTagSorting: no sort algorithm for tag ", tagId);
|
||||||
|
algo = SortAlgorithm.Recent; // why not, we have to do something..
|
||||||
|
}
|
||||||
|
return algo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCount(tagId: TagID): number {
|
||||||
|
return this.counts[tagId] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setListOrder(tagId: TagID, order: ListAlgorithm) {
|
||||||
|
// TODO: https://github.com/vector-im/element-web/issues/23207
|
||||||
|
}
|
||||||
|
|
||||||
|
public getListOrder(tagId: TagID): ListAlgorithm {
|
||||||
|
// TODO: handle unread msgs first? https://github.com/vector-im/element-web/issues/23207
|
||||||
|
return ListAlgorithm.Natural;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a filter condition to the room list store. Filters may be applied async,
|
||||||
|
* and thus might not cause an update to the store immediately.
|
||||||
|
* @param {IFilterCondition} filter The filter condition to add.
|
||||||
|
*/
|
||||||
|
public async addFilter(filter: IFilterCondition): Promise<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 index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
||||||
|
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
|
||||||
|
for (const roomIndex in roomIndexToRoomId) {
|
||||||
|
const roomId = roomIndexToRoomId[roomIndex];
|
||||||
|
if (roomId === room.roomId) {
|
||||||
|
tags.push(tagId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually update a room with a given cause. This should only be used if the
|
||||||
|
* room list store would otherwise be incapable of doing the update itself. Note
|
||||||
|
* that this may race with the room list's regular operation.
|
||||||
|
* @param {Room} room The room to update.
|
||||||
|
* @param {RoomUpdateCause} cause The cause to update for.
|
||||||
|
*/
|
||||||
|
public async manualRoomUpdate(room: Room, cause: RoomUpdateCause) {
|
||||||
|
// TODO: this is only used when you forget a room, not that important for now.
|
||||||
|
}
|
||||||
|
|
||||||
|
public get orderedLists(): ITagMap {
|
||||||
|
return this.tagMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshOrderedLists(tagId: string, roomIndexToRoomId: Record<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 = RoomViewStore.instance.getRoomId();
|
||||||
|
let stickyRoomNewIndex = -1;
|
||||||
|
const stickyRoomOldIndex = (tagMap[tagId] || []).findIndex((room) => {
|
||||||
|
return room.roomId === this.stickyRoomId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// order from low to high
|
||||||
|
const orderedRoomIndexes = Object.keys(roomIndexToRoomId).map((numStr) => {
|
||||||
|
return Number(numStr);
|
||||||
|
}).sort((a, b) => {
|
||||||
|
return a-b;
|
||||||
|
});
|
||||||
|
const seenRoomIds = new Set<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 = orderedRoomIds.map((roomId) => {
|
||||||
|
return this.matrixClient.getRoom(roomId);
|
||||||
|
});
|
||||||
|
tagMap[tagId] = rooms;
|
||||||
|
this.tagMap = tagMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSlidingSyncListUpdate(listIndex: number, joinCount: number, roomIndexToRoomId: Record<number, string>) {
|
||||||
|
const tagId = SlidingSyncManager.instance.listIdForIndex(listIndex);
|
||||||
|
this.counts[tagId]= joinCount;
|
||||||
|
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
||||||
|
// let the UI update
|
||||||
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRoomViewStoreUpdated() {
|
||||||
|
// we only care about this to know when the user has clicked on a room to set the stickiness value
|
||||||
|
if (RoomViewStore.instance.getRoomId() === this.stickyRoomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasUpdatedAnyList = false;
|
||||||
|
|
||||||
|
// every list with the OLD sticky room ID needs to be resorted because it now needs to take
|
||||||
|
// its proper place as it is no longer sticky. The newly sticky room can remain the same though,
|
||||||
|
// as we only actually care about its sticky status when we get list updates.
|
||||||
|
const oldStickyRoom = this.stickyRoomId;
|
||||||
|
// it's not safe to check the data in slidingSync as it is tracking the server's view of the
|
||||||
|
// room list. There's an edge case whereby the sticky room has gone outside the window and so
|
||||||
|
// would not be present in the roomIndexToRoomId map anymore, and hence clicking away from it
|
||||||
|
// will make it disappear eventually. We need to check orderedLists as that is the actual
|
||||||
|
// sorted renderable list of rooms which sticky rooms apply to.
|
||||||
|
for (const tagId in this.orderedLists) {
|
||||||
|
const list = this.orderedLists[tagId];
|
||||||
|
const room = list.find((room) => {
|
||||||
|
return room.roomId === oldStickyRoom;
|
||||||
|
});
|
||||||
|
if (room) {
|
||||||
|
// resort it based on the slidingSync view of the list. This may cause this old sticky
|
||||||
|
// room to cease to exist.
|
||||||
|
const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
||||||
|
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(index);
|
||||||
|
this.refreshOrderedLists(tagId, roomIndexToRoomId);
|
||||||
|
hasUpdatedAnyList = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// in the event we didn't call refreshOrderedLists, it helps to still remember the sticky room ID.
|
||||||
|
this.stickyRoomId = RoomViewStore.instance.getRoomId();
|
||||||
|
|
||||||
|
if (hasUpdatedAnyList) {
|
||||||
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onReady(): Promise<any> {
|
||||||
|
logger.info("SlidingRoomListStore.onReady");
|
||||||
|
// permanent listeners: never get destroyed. Could be an issue if we want to test this in isolation.
|
||||||
|
SlidingSyncManager.instance.slidingSync.on(SlidingSyncEvent.List, this.onSlidingSyncListUpdate.bind(this));
|
||||||
|
RoomViewStore.instance.addListener(this.onRoomViewStoreUpdated.bind(this));
|
||||||
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated.bind(this));
|
||||||
|
if (SpaceStore.instance.activeSpace) {
|
||||||
|
this.onSelectedSpaceUpdated(SpaceStore.instance.activeSpace, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliding sync has an initial response for spaces. Now request all the lists.
|
||||||
|
// We do the spaces list _first_ to avoid potential flickering on DefaultTagID.Untagged list
|
||||||
|
// which would be caused by initially having no `spaces` filter set, and then suddenly setting one.
|
||||||
|
OrderedDefaultTagIDs.forEach((tagId) => {
|
||||||
|
const filter = filterConditions[tagId];
|
||||||
|
if (!filter) {
|
||||||
|
logger.info("SlidingRoomListStore.onReady unsupported list ", tagId);
|
||||||
|
return; // we do not support this list yet.
|
||||||
|
}
|
||||||
|
const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config
|
||||||
|
this.tagIdToSortAlgo[tagId] = sort;
|
||||||
|
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
||||||
|
const index = SlidingSyncManager.instance.getOrAllocateListIndex(tagId);
|
||||||
|
SlidingSyncManager.instance.ensureListRegistered(index, {
|
||||||
|
filters: filter,
|
||||||
|
sort: SlidingSyncSortToFilter[sort],
|
||||||
|
}).then(() => {
|
||||||
|
this.emit(LISTS_LOADING_EVENT, tagId, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSelectedSpaceUpdated = (activeSpace: SpaceKey, allRoomsInHome: boolean) => {
|
||||||
|
logger.info("SlidingRoomListStore.onSelectedSpaceUpdated", activeSpace);
|
||||||
|
// update the untagged filter
|
||||||
|
const tagId = DefaultTagID.Untagged;
|
||||||
|
const filters = filterConditions[tagId];
|
||||||
|
const oldSpace = filters.spaces?.[0];
|
||||||
|
filters.spaces = (activeSpace && activeSpace != MetaSpace.Home) ? [activeSpace] : undefined;
|
||||||
|
if (oldSpace !== activeSpace) {
|
||||||
|
this.emit(LISTS_LOADING_EVENT, tagId, true);
|
||||||
|
SlidingSyncManager.instance.ensureListRegistered(
|
||||||
|
SlidingSyncManager.instance.getOrAllocateListIndex(tagId),
|
||||||
|
{
|
||||||
|
filters: filters,
|
||||||
|
},
|
||||||
|
).then(() => {
|
||||||
|
this.emit(LISTS_LOADING_EVENT, tagId, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Intended for test usage
|
||||||
|
public async resetStore() {
|
||||||
|
// Test function
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerates the room whole room list, discarding any previous results.
|
||||||
|
*
|
||||||
|
* Note: This is only exposed externally for the tests. Do not call this from within
|
||||||
|
* the app.
|
||||||
|
* @param trigger Set to false to prevent a list update from being sent. Should only
|
||||||
|
* be used if the calling code will manually trigger the update.
|
||||||
|
*/
|
||||||
|
public regenerateAllLists({ trigger = true }) {
|
||||||
|
// Test function
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onNotReady(): Promise<any> {
|
||||||
|
await this.resetStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async onDispatchAsync(payload: ActionPayload) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RoomListStoreClass } from "./RoomListStore";
|
import { RoomListStore as Interface } from "./Interface";
|
||||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||||
import SpaceStore from "../spaces/SpaceStore";
|
import SpaceStore from "../spaces/SpaceStore";
|
||||||
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||||
|
@ -28,7 +28,7 @@ export class SpaceWatcher {
|
||||||
private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
|
private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
|
||||||
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
|
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
|
||||||
|
|
||||||
constructor(private store: RoomListStoreClass) {
|
constructor(private store: Interface) {
|
||||||
if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
|
if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
|
||||||
this.updateFilter();
|
this.updateFilter();
|
||||||
store.addFilter(this.filter);
|
store.addFilter(this.filter);
|
||||||
|
|
|
@ -41,6 +41,7 @@ describe('RoomViewStore', function() {
|
||||||
joinRoom: jest.fn(),
|
joinRoom: jest.fn(),
|
||||||
getRoom: jest.fn(),
|
getRoom: jest.fn(),
|
||||||
getRoomIdForAlias: jest.fn(),
|
getRoomIdForAlias: jest.fn(),
|
||||||
|
isGuest: jest.fn(),
|
||||||
});
|
});
|
||||||
const room = new Room('!room:server', mockClient, userId);
|
const room = new Room('!room:server', mockClient, userId);
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ describe('RoomViewStore', function() {
|
||||||
mockClient.credentials = { userId: "@test:example.com" };
|
mockClient.credentials = { userId: "@test:example.com" };
|
||||||
mockClient.joinRoom.mockResolvedValue(room);
|
mockClient.joinRoom.mockResolvedValue(room);
|
||||||
mockClient.getRoom.mockReturnValue(room);
|
mockClient.getRoom.mockReturnValue(room);
|
||||||
|
mockClient.isGuest.mockReturnValue(false);
|
||||||
|
|
||||||
// Reset the state of the store
|
// Reset the state of the store
|
||||||
RoomViewStore.instance.reset();
|
RoomViewStore.instance.reset();
|
||||||
|
|
Loading…
Reference in a new issue