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,
|
||||
Visibility,
|
||||
RoomMemberEvent,
|
||||
ICreateClientOpts,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import type { MatrixDispatcher } from "../src/dispatcher/dispatcher";
|
||||
import type PerformanceMonitor from "../src/performance";
|
||||
|
@ -55,6 +56,7 @@ declare global {
|
|||
MemoryCryptoStore: typeof MemoryCryptoStore;
|
||||
Visibility: typeof Visibility;
|
||||
Preset: typeof Preset;
|
||||
createClient(opts: ICreateClientOpts | string);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
/// <reference types="cypress" />
|
||||
|
||||
import * as os from "os";
|
||||
import * as crypto from "crypto";
|
||||
import * as childProcess from "child_process";
|
||||
import * as fse from "fs-extra";
|
||||
|
||||
|
@ -25,28 +26,32 @@ import PluginConfigOptions = Cypress.PluginConfigOptions;
|
|||
|
||||
// A cypress plugin to run docker commands
|
||||
|
||||
export function dockerRun(args: {
|
||||
export function dockerRun(opts: {
|
||||
image: string;
|
||||
containerName: string;
|
||||
params?: string[];
|
||||
cmd?: string;
|
||||
}): Promise<string> {
|
||||
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
|
||||
params.push("-u", `${userInfo.uid}:${userInfo.gid}`);
|
||||
}
|
||||
|
||||
const args = [
|
||||
"run",
|
||||
"--name", `${opts.containerName}-${crypto.randomBytes(4).toString("hex")}`,
|
||||
"-d",
|
||||
...params,
|
||||
opts.image,
|
||||
];
|
||||
|
||||
if (opts.cmd) args.push(opts.cmd);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
childProcess.execFile('docker', [
|
||||
"run",
|
||||
"--name", args.containerName,
|
||||
"-d",
|
||||
...params,
|
||||
args.image,
|
||||
"run",
|
||||
], (err, stdout) => {
|
||||
childProcess.execFile("docker", args, (err, stdout) => {
|
||||
if (err) reject(err);
|
||||
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}
|
||||
*/
|
||||
|
@ -132,5 +152,6 @@ export function docker(on: PluginEvents, config: PluginConfigOptions) {
|
|||
dockerLogs,
|
||||
dockerStop,
|
||||
dockerRm,
|
||||
dockerIp,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import PluginEvents = Cypress.PluginEvents;
|
|||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||
import { performance } from "./performance";
|
||||
import { synapseDocker } from "./synapsedocker";
|
||||
import { slidingSyncProxyDocker } from "./sliding-sync";
|
||||
import { webserver } from "./webserver";
|
||||
import { docker } from "./docker";
|
||||
import { log } from "./log";
|
||||
|
@ -31,6 +32,7 @@ export default function(on: PluginEvents, config: PluginConfigOptions) {
|
|||
docker(on, config);
|
||||
performance(on, config);
|
||||
synapseDocker(on, config);
|
||||
slidingSyncProxyDocker(on, config);
|
||||
webserver(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({
|
||||
image: "matrixdotorg/synapse:develop",
|
||||
containerName: `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`,
|
||||
containerName: `react-sdk-cypress-synapse`,
|
||||
params: [
|
||||
"--rm",
|
||||
"-v", `${synCfg.configDir}:/data`,
|
||||
"-p", `${synCfg.port}:8008/tcp`,
|
||||
],
|
||||
cmd: "run",
|
||||
});
|
||||
|
||||
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||
|
|
|
@ -36,4 +36,5 @@ import "./iframes";
|
|||
import "./timeline";
|
||||
import "./network";
|
||||
import "./composer";
|
||||
import "./proxy";
|
||||
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 { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import { SlidingSyncManager } from './SlidingSyncManager';
|
||||
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
|
||||
import { _t } from "./languageHandler";
|
||||
|
||||
|
@ -237,6 +238,19 @@ class MatrixClientPegClass implements IMatrixClientPeg {
|
|||
opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours
|
||||
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
|
||||
MatrixActionCreators.start(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 {
|
||||
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();
|
||||
|
||||
// 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
|
||||
focus?: boolean;
|
||||
hasCancel?: boolean;
|
||||
validator?: (fieldState: IFieldState) => IValidationResult; // result of withValidation
|
||||
validator?: (fieldState: IFieldState) => Promise<IValidationResult>; // result of withValidation
|
||||
fixedWidth?: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
|||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||
import { TooltipOption } from "./TooltipOption";
|
||||
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||
import { useSlidingSyncRoomSearch } from "../../../../hooks/useSlidingSyncRoomSearch";
|
||||
|
||||
const MAX_RECENT_SEARCHES = 10;
|
||||
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]);
|
||||
};
|
||||
|
||||
const findVisibleRooms = (cli: MatrixClient) => {
|
||||
const findVisibleRooms = (cli: MatrixClient): Room[] => {
|
||||
return cli.getVisibleRooms().filter(room => {
|
||||
// Do not show local rooms
|
||||
if (isLocalRoom(room)) return false;
|
||||
|
@ -340,25 +341,43 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
searchProfileInfo,
|
||||
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 roomResults = findVisibleRooms(cli).map(toRoomResult);
|
||||
// If we already have a DM with the user we're looking for, we will
|
||||
// show that DM instead of the user themselves
|
||||
const alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
||||
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
||||
if (!userId) return userIds;
|
||||
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
||||
userIds.add(userId);
|
||||
return userIds;
|
||||
}, new Set<string>());
|
||||
const userResults = [];
|
||||
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
||||
// Make sure we don't have any user more than once
|
||||
if (alreadyAddedUserIds.has(user.userId)) continue;
|
||||
alreadyAddedUserIds.add(user.userId);
|
||||
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
|
||||
// show that DM instead of the user themselves
|
||||
alreadyAddedUserIds = roomResults.reduce((userIds, result) => {
|
||||
const userId = DMRoomMap.shared().getUserIdForRoomId(result.room.roomId);
|
||||
if (!userId) return userIds;
|
||||
if (result.room.getJoinedMemberCount() > 2) return userIds;
|
||||
userIds.add(userId);
|
||||
return userIds;
|
||||
}, new Set<string>());
|
||||
for (const user of [...findVisibleRoomMembers(cli), ...users]) {
|
||||
// Make sure we don't have any user more than once
|
||||
if (alreadyAddedUserIds.has(user.userId)) continue;
|
||||
alreadyAddedUserIds.add(user.userId);
|
||||
|
||||
userResults.push(toMemberResult(user));
|
||||
userResults.push(toMemberResult(user));
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
|
@ -382,7 +401,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
...publicRooms.map(toPublicRoomResult),
|
||||
].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[]>>(() => {
|
||||
|
@ -401,10 +420,13 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
|
||||
possibleResults.forEach(entry => {
|
||||
if (isRoomResult(entry)) {
|
||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||
!entry.query?.some(q => q.includes(lcQuery))
|
||||
) return; // bail, does not match query
|
||||
// sliding sync gives the correct rooms in the list so we don't need to filter
|
||||
if (!isSlidingSyncEnabled) {
|
||||
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||
!entry.query?.some(q => q.includes(lcQuery))
|
||||
) return; // bail, does not match query
|
||||
}
|
||||
} else if (isMemberResult(entry)) {
|
||||
if (!entry.query?.some(q => q.includes(lcQuery))) return; // bail, does not match query
|
||||
} else if (isPublicRoomResult(entry)) {
|
||||
|
@ -455,7 +477,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
}
|
||||
|
||||
return results;
|
||||
}, [trimmedQuery, filter, cli, possibleResults, memberComparator]);
|
||||
}, [trimmedQuery, filter, cli, possibleResults, isSlidingSyncEnabled, memberComparator]);
|
||||
|
||||
const numResults = sum(Object.values(results).map(it => it.length));
|
||||
useWebSearchMetrics(numResults, query.length, true);
|
||||
|
@ -1208,7 +1230,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
|||
aria-label={_t("Search")}
|
||||
aria-describedby="mx_SpotlightDialog_keyboardPrompt"
|
||||
/>
|
||||
{ (publicRoomsLoading || peopleLoading || profileLoading) && (
|
||||
{ (publicRoomsLoading || peopleLoading || profileLoading || slidingSyncRoomSearchLoading) && (
|
||||
<Spinner w={24} h={24} />
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import classNames from 'classnames';
|
||||
import { Dispatcher } from "flux";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { Enable, Resizable } from "re-resizable";
|
||||
import { Direction } from "re-resizable/lib/resizer";
|
||||
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 { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||
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 { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||
|
@ -52,6 +52,8 @@ import ContextMenu, {
|
|||
import AccessibleButton from "../../views/elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import ExtraTile from "./ExtraTile";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SlidingSyncManager } from "../../../SlidingSyncManager";
|
||||
import NotificationBadge from "./NotificationBadge";
|
||||
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
|
||||
height: number;
|
||||
rooms: Room[];
|
||||
roomsLoading: boolean;
|
||||
}
|
||||
|
||||
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 notificationState: ListNotificationState;
|
||||
|
||||
private slidingSyncMode: boolean;
|
||||
|
||||
constructor(props: IProps) {
|
||||
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.heightAtStart = 0;
|
||||
|
@ -121,6 +128,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
isExpanded: !this.layout.isCollapsed,
|
||||
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
||||
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
||||
roomsLoading: false,
|
||||
};
|
||||
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
||||
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 {
|
||||
if (this.slidingSyncMode) {
|
||||
return this.state.rooms.length;
|
||||
}
|
||||
const nVisible = Math.ceil(this.layout.visibleTiles);
|
||||
return Math.min(nVisible, this.numTiles);
|
||||
}
|
||||
|
@ -239,6 +250,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentDidMount() {
|
||||
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
||||
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
|
||||
// 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 });
|
||||
|
@ -247,9 +260,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
public componentWillUnmount() {
|
||||
defaultDispatcher.unregister(this.dispatcherRef);
|
||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
||||
RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading);
|
||||
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 = () => {
|
||||
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 });
|
||||
};
|
||||
|
||||
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
|
||||
const numVisibleTiles = this.numVisibleTiles;
|
||||
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;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
||||
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
||||
if (this.slidingSyncMode) {
|
||||
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
|
||||
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
|
||||
isAlphabetical = slidingList.sort[0] === "by_name";
|
||||
isUnreadFirst = (
|
||||
slidingList.sort[0] === "by_highlight_count" ||
|
||||
slidingList.sort[0] === "by_notification_count"
|
||||
);
|
||||
}
|
||||
|
||||
// Invites don't get some nonsense options, so only add them if we have to.
|
||||
let otherSections = null;
|
||||
|
@ -717,7 +758,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
|||
});
|
||||
|
||||
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">
|
||||
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
||||
{ 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
|
||||
// tiles visible, it becomes 'show less'.
|
||||
let showNButton = null;
|
||||
const hasMoreSlidingSync = (
|
||||
this.slidingSyncMode && (RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length)
|
||||
);
|
||||
|
||||
if (maxTilesPx > this.state.height) {
|
||||
if ((maxTilesPx > this.state.height) || hasMoreSlidingSync) {
|
||||
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
||||
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
|
||||
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
|
||||
const numMissing = this.numTiles - amountFullyShown;
|
||||
let numMissing = this.numTiles - amountFullyShown;
|
||||
if (this.slidingSyncMode) {
|
||||
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
|
||||
}
|
||||
const label = _t("Show %(count)s more", { count: numMissing });
|
||||
let showMoreText = (
|
||||
<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)",
|
||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||
"Send read receipts": "Send read receipts",
|
||||
"Sliding Sync mode (under active development, cannot be disabled)": "Sliding Sync mode (under active development, cannot be disabled)",
|
||||
"Live Location Sharing (temporary implementation: locations persist in room history)": "Live Location Sharing (temporary implementation: locations persist in room history)",
|
||||
"Favourite Messages (under active development)": "Favourite Messages (under active development)",
|
||||
"Use new session manager (under active development)": "Use new session manager (under active development)",
|
||||
|
@ -2856,6 +2857,14 @@
|
|||
"Link to selected message": "Link to selected message",
|
||||
"Link to room": "Link to room",
|
||||
"Command Help": "Command Help",
|
||||
"Checking...": "Checking...",
|
||||
"Your server has native support": "Your server has native support",
|
||||
"Your server lacks native support": "Your server lacks native support",
|
||||
"Your server lacks native support, you must specify a proxy": "Your server lacks native support, you must specify a proxy",
|
||||
"Sliding Sync configuration": "Sliding Sync configuration",
|
||||
"To disable you will need to log out and back in, use with caution!": "To disable you will need to log out and back in, use with caution!",
|
||||
"Proxy URL (optional)": "Proxy URL (optional)",
|
||||
"Proxy URL": "Proxy URL",
|
||||
"Sections to show": "Sections to show",
|
||||
"This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.": "This groups your chats with members of this space. Turning this off will hide those chats from your view of %(spaceName)s.",
|
||||
"Space settings": "Space settings",
|
||||
|
|
|
@ -41,6 +41,7 @@ import IncompatibleController from "./controllers/IncompatibleController";
|
|||
import { ImageSize } from "./enums/ImageSize";
|
||||
import { MetaSpace } from "../stores/spaces";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import SlidingSyncController from './controllers/SlidingSyncController';
|
||||
import ThreadBetaController from './controllers/ThreadBetaController';
|
||||
import { FontWatcher } from "./watchers/FontWatcher";
|
||||
|
||||
|
@ -406,6 +407,18 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
|||
displayName: _td("Send read receipts"),
|
||||
default: true,
|
||||
},
|
||||
"feature_sliding_sync": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Developer,
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
displayName: _td('Sliding Sync mode (under active development, cannot be disabled)'),
|
||||
default: false,
|
||||
controller: new SlidingSyncController(),
|
||||
},
|
||||
"feature_sliding_sync_proxy_url": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||
default: "",
|
||||
},
|
||||
"feature_location_share_live": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Messaging,
|
||||
|
|
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 ErrorDialog from "../components/views/dialogs/ErrorDialog";
|
||||
import { ActiveRoomChangedPayload } from "../dispatcher/payloads/ActiveRoomChangedPayload";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import { SlidingSyncManager } from "../SlidingSyncManager";
|
||||
import { awaitRoomDownSync } from "../utils/RoomUpgrade";
|
||||
|
||||
const NUM_JOIN_RETRY = 5;
|
||||
|
@ -278,6 +280,32 @@ export class RoomViewStore extends Store<ActionPayload> {
|
|||
activeSpace,
|
||||
});
|
||||
}
|
||||
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
|
||||
if (this.state.roomId) {
|
||||
// unsubscribe from this room, but don't await it as we don't care when this gets done.
|
||||
SlidingSyncManager.instance.setRoomVisible(this.state.roomId, false);
|
||||
}
|
||||
this.setState({
|
||||
roomId: payload.room_id,
|
||||
initialEventId: null,
|
||||
initialEventPixelOffset: null,
|
||||
isInitialEventHighlighted: null,
|
||||
initialEventScrollIntoView: true,
|
||||
roomAlias: null,
|
||||
roomLoading: true,
|
||||
roomLoadError: null,
|
||||
viaServers: payload.via_servers,
|
||||
wasContextSwitch: payload.context_switch,
|
||||
});
|
||||
// set this room as the room subscription. We need to await for it as this will fetch
|
||||
// all room state for this room, which is required before we get the state below.
|
||||
await SlidingSyncManager.instance.setRoomVisible(payload.room_id, true);
|
||||
// Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now
|
||||
dis.dispatch({
|
||||
...payload,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = {
|
||||
roomId: payload.room_id,
|
||||
|
|
|
@ -23,6 +23,9 @@ import { IFilterCondition } from "./filters/IFilterCondition";
|
|||
export enum RoomListStoreEvent {
|
||||
// The event/channel which is called when the room lists have been changed.
|
||||
ListsUpdate = "lists_update",
|
||||
// The event which is called when the room list is loading.
|
||||
// Called with the (tagId, bool) which is true when the list is loading, else false.
|
||||
ListsLoading = "lists_loading",
|
||||
}
|
||||
|
||||
export interface RoomListStore extends EventEmitter {
|
||||
|
@ -33,6 +36,15 @@ export interface RoomListStore extends EventEmitter {
|
|||
*/
|
||||
get orderedLists(): ITagMap;
|
||||
|
||||
/**
|
||||
* Return the total number of rooms in this list. Prefer this method to
|
||||
* RoomListStore.orderedLists[tagId].length because the client may not
|
||||
* be aware of all the rooms in this list (e.g in Sliding Sync).
|
||||
* @param tagId the tag to get the room count for.
|
||||
* @returns the number of rooms in this list, or 0 if the list is unknown.
|
||||
*/
|
||||
getCount(tagId: TagID): number;
|
||||
|
||||
/**
|
||||
* Set the sort algorithm for the specified tag.
|
||||
* @param tagId the tag to set the algorithm for
|
||||
|
|
|
@ -38,12 +38,14 @@ import { VisibilityProvider } from "./filters/VisibilityProvider";
|
|||
import { SpaceWatcher } from "./SpaceWatcher";
|
||||
import { IRoomTimelineActionPayload } from "../../actions/MatrixActionCreators";
|
||||
import { RoomListStore as Interface, RoomListStoreEvent } from "./Interface";
|
||||
import { SlidingRoomListStoreClass } from "./SlidingRoomListStore";
|
||||
|
||||
interface IState {
|
||||
// state is tracked in underlying classes
|
||||
}
|
||||
|
||||
export const LISTS_UPDATE_EVENT = RoomListStoreEvent.ListsUpdate;
|
||||
export const LISTS_LOADING_EVENT = RoomListStoreEvent.ListsLoading; // unused; used by SlidingRoomListStore
|
||||
|
||||
export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements Interface {
|
||||
/**
|
||||
|
@ -585,6 +587,11 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> implements
|
|||
return algorithmTags;
|
||||
}
|
||||
|
||||
public getCount(tagId: TagID): number {
|
||||
// The room list store knows about all the rooms, so just return the length.
|
||||
return this.orderedLists[tagId].length || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually update a room with a given cause. This should only be used if the
|
||||
* room list store would otherwise be incapable of doing the update itself. Note
|
||||
|
@ -602,10 +609,17 @@ export default class RoomListStore {
|
|||
private static internalInstance: Interface;
|
||||
|
||||
public static get instance(): Interface {
|
||||
if (!this.internalInstance) {
|
||||
const instance = new RoomListStoreClass();
|
||||
instance.start();
|
||||
this.internalInstance = instance;
|
||||
if (!RoomListStore.internalInstance) {
|
||||
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||
logger.info("using SlidingRoomListStoreClass");
|
||||
const instance = new SlidingRoomListStoreClass();
|
||||
instance.start();
|
||||
RoomListStore.internalInstance = instance;
|
||||
} else {
|
||||
const instance = new RoomListStoreClass();
|
||||
instance.start();
|
||||
RoomListStore.internalInstance = instance;
|
||||
}
|
||||
}
|
||||
|
||||
return this.internalInstance;
|
||||
|
|
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.
|
||||
*/
|
||||
|
||||
import { RoomListStoreClass } from "./RoomListStore";
|
||||
import { RoomListStore as Interface } from "./Interface";
|
||||
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
|
||||
import SpaceStore from "../spaces/SpaceStore";
|
||||
import { MetaSpace, SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||
|
@ -28,7 +28,7 @@ export class SpaceWatcher {
|
|||
private activeSpace: SpaceKey = SpaceStore.instance.activeSpace;
|
||||
private allRoomsInHome: boolean = SpaceStore.instance.allRoomsInHome;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
constructor(private store: Interface) {
|
||||
if (SpaceWatcher.needsFilter(this.activeSpace, this.allRoomsInHome)) {
|
||||
this.updateFilter();
|
||||
store.addFilter(this.filter);
|
||||
|
|
|
@ -41,6 +41,7 @@ describe('RoomViewStore', function() {
|
|||
joinRoom: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getRoomIdForAlias: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
});
|
||||
const room = new Room('!room:server', mockClient, userId);
|
||||
|
||||
|
@ -49,6 +50,7 @@ describe('RoomViewStore', function() {
|
|||
mockClient.credentials = { userId: "@test:example.com" };
|
||||
mockClient.joinRoom.mockResolvedValue(room);
|
||||
mockClient.getRoom.mockReturnValue(room);
|
||||
mockClient.isGuest.mockReturnValue(false);
|
||||
|
||||
// Reset the state of the store
|
||||
RoomViewStore.instance.reset();
|
||||
|
|
Loading…
Reference in a new issue