diff --git a/spec/unit/filter-component.spec.js b/spec/unit/filter-component.spec.ts similarity index 89% rename from spec/unit/filter-component.spec.js rename to spec/unit/filter-component.spec.ts index d5d9fa287..636a413f6 100644 --- a/spec/unit/filter-component.spec.js +++ b/spec/unit/filter-component.spec.ts @@ -1,4 +1,8 @@ -import { RelationType, UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "../../src"; +import { + RelationType, + UNSTABLE_FILTER_RELATED_BY_REL_TYPES, + UNSTABLE_FILTER_RELATED_BY_SENDERS, +} from "../../src"; import { FilterComponent } from "../../src/filter-component"; import { mkEvent } from '../test-utils'; @@ -35,7 +39,7 @@ describe("Filter Component", function() { it("should filter out events by relation participation", function() { const currentUserId = '@me:server.org'; const filter = new FilterComponent({ - [UNSTABLE_FILTER_RELATION_SENDERS.name]: currentUserId, + [UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId], }, currentUserId); const threadRootNotParticipated = mkEvent({ @@ -60,7 +64,7 @@ describe("Filter Component", function() { it("should keep events by relation participation", function() { const currentUserId = '@me:server.org'; const filter = new FilterComponent({ - [UNSTABLE_FILTER_RELATION_SENDERS.name]: currentUserId, + [UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: [currentUserId], }, currentUserId); const threadRootParticipated = mkEvent({ @@ -84,7 +88,7 @@ describe("Filter Component", function() { it("should filter out events by relation type", function() { const filter = new FilterComponent({ - [UNSTABLE_FILTER_RELATION_TYPES.name]: RelationType.Thread, + [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread], }); const referenceRelationEvent = mkEvent({ @@ -104,7 +108,7 @@ describe("Filter Component", function() { it("should keep events by relation type", function() { const filter = new FilterComponent({ - [UNSTABLE_FILTER_RELATION_TYPES.name]: RelationType.Thread, + [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: [RelationType.Thread], }); const threadRootEvent = mkEvent({ diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index a094c191e..2d3aaa5e5 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -1,3 +1,24 @@ +/* +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. +*/ + +/** + * This is an internal module. See {@link MatrixClient} for the public class. + * @module client + */ + import * as utils from "../test-utils"; import { DuplicateStrategy, EventStatus, MatrixEvent, PendingEventOrdering, RoomEvent } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; @@ -5,6 +26,7 @@ import { Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { RelationType, UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; +import { Thread } from "../../src/models/thread"; describe("Room", function() { const roomId = "!foo:bar"; @@ -1845,6 +1867,54 @@ describe("Room", function() { expect(() => room.createThread(rootEvent, [])).not.toThrow(); }); + + it("should not add events before server supports is known", function() { + Thread.hasServerSideSupport = undefined; + + const rootEvent = new MatrixEvent({ + event_id: "$666", + room_id: roomId, + content: {}, + unsigned: { + "age": 1, + "m.relations": { + [RelationType.Thread]: { + latest_event: null, + count: 1, + current_user_participated: false, + }, + }, + }, + }); + + let age = 1; + function mkEvt(id): MatrixEvent { + return new MatrixEvent({ + event_id: id, + room_id: roomId, + content: { + "m.relates_to": { + "rel_type": RelationType.Thread, + "event_id": "$666", + }, + }, + unsigned: { + "age": age++, + }, + }); + } + + const thread = room.createThread(rootEvent, []); + expect(thread.length).toBe(0); + + thread.addEvent(mkEvt("$1")); + expect(thread.length).toBe(0); + + Thread.hasServerSideSupport = true; + + thread.addEvent(mkEvt("$2")); + expect(thread.length).toBeGreaterThan(0); + }); }); }); }); diff --git a/src/filter-component.ts b/src/filter-component.ts index a69b9f454..7d310203d 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -15,7 +15,10 @@ limitations under the License. */ import { RelationType } from "./@types/event"; -import { UNSTABLE_FILTER_RELATION_SENDERS, UNSTABLE_FILTER_RELATION_TYPES } from "./filter"; +import { + UNSTABLE_FILTER_RELATED_BY_REL_TYPES, + UNSTABLE_FILTER_RELATED_BY_SENDERS, +} from "./filter"; import { MatrixEvent } from "./models/event"; /** @@ -48,7 +51,8 @@ export interface IFilterComponent { not_senders?: string[]; contains_url?: boolean; limit?: number; - [UNSTABLE_FILTER_RELATION_TYPES.name]?: Array; + [UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[]; + [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array; } /* eslint-enable camelcase */ @@ -106,8 +110,8 @@ export class FilterComponent { senders: this.filterJson.senders || null, not_senders: this.filterJson.not_senders || [], contains_url: this.filterJson.contains_url || null, - [UNSTABLE_FILTER_RELATION_SENDERS.name]: UNSTABLE_FILTER_RELATION_SENDERS.findIn(this.filterJson), - [UNSTABLE_FILTER_RELATION_TYPES.name]: UNSTABLE_FILTER_RELATION_TYPES.findIn(this.filterJson), + [UNSTABLE_FILTER_RELATED_BY_SENDERS.name]: UNSTABLE_FILTER_RELATED_BY_SENDERS.findIn(this.filterJson), + [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]: UNSTABLE_FILTER_RELATED_BY_REL_TYPES.findIn(this.filterJson), }; } @@ -161,14 +165,14 @@ export class FilterComponent { return false; } - const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATION_TYPES.name]; + const relationTypesFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]; if (relationTypesFilter !== undefined) { if (!this.arrayMatchesFilter(relationTypesFilter, relationTypes)) { return false; } } - const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATION_SENDERS.name]; + const relationSendersFilter = this.filterJson[UNSTABLE_FILTER_RELATED_BY_SENDERS.name]; if (relationSendersFilter !== undefined) { if (!this.arrayMatchesFilter(relationSendersFilter, relationSenders)) { return false; diff --git a/src/filter.ts b/src/filter.ts index 888d82a61..7ceaaba57 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -26,13 +26,13 @@ import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; import { UnstableValue } from "./NamespacedValue"; -export const UNSTABLE_FILTER_RELATION_SENDERS = new UnstableValue( - "relation_senders", +export const UNSTABLE_FILTER_RELATED_BY_SENDERS = new UnstableValue( + "related_by_senders", "io.element.relation_senders", ); -export const UNSTABLE_FILTER_RELATION_TYPES = new UnstableValue( - "relation_types", +export const UNSTABLE_FILTER_RELATED_BY_REL_TYPES = new UnstableValue( + "related_by_rel_types", "io.element.relation_types", ); @@ -66,8 +66,8 @@ export interface IRoomEventFilter extends IFilterComponent { lazy_load_members?: boolean; include_redundant_members?: boolean; types?: Array; - [UNSTABLE_FILTER_RELATION_TYPES.name]?: Array; - [UNSTABLE_FILTER_RELATION_SENDERS.name]?: string[]; + [UNSTABLE_FILTER_RELATED_BY_REL_TYPES.name]?: Array; + [UNSTABLE_FILTER_RELATED_BY_SENDERS.name]?: string[]; } interface IStateFilter extends IRoomEventFilter {} diff --git a/src/models/thread.ts b/src/models/thread.ts index 4c63bdf4a..94f12a3eb 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -52,6 +52,9 @@ interface IThreadOpts { * @experimental */ export class Thread extends TypedEventEmitter { + public static hasServerSideSupport: boolean; + private static serverSupportPromise: Promise | null; + /** * A reference to all the events ID at the bottom of the threads */ @@ -91,6 +94,15 @@ export class Thread extends TypedEventEmitter { RoomEvent.TimelineReset, ]); + if (Thread.hasServerSideSupport === undefined) { + Thread.serverSupportPromise = this.client.doesServerSupportUnstableFeature("org.matrix.msc3440"); + Thread.serverSupportPromise.then((serverSupportsThread) => { + Thread.hasServerSideSupport = serverSupportsThread; + }).catch(() => { + Thread.serverSupportPromise = null; + }); + } + // If we weren't able to find the root event, it's probably missing // and we define the thread ID from one of the thread relation if (!rootEvent) { @@ -107,11 +119,6 @@ export class Thread extends TypedEventEmitter { this.room.on(RoomEvent.Timeline, this.onEcho); } - public get hasServerSideSupport(): boolean { - return this.client.cachedCapabilities - ?.capabilities?.[RelationType.Thread]?.enabled; - } - private onEcho = (event: MatrixEvent) => { if (this.timelineSet.eventIdToTimeline(event.getId())) { this.emit(ThreadEvent.Update, this); @@ -152,8 +159,12 @@ export class Thread extends TypedEventEmitter { * to the start (and not the end) of the timeline. */ public async addEvent(event: MatrixEvent, toStartOfTimeline = false): Promise { + if (Thread.hasServerSideSupport === undefined) { + await Thread.serverSupportPromise; + } + // Add all incoming events to the thread's timeline set when there's no server support - if (!this.hasServerSideSupport) { + if (!Thread.hasServerSideSupport) { // all the relevant membership info to hydrate events with a sender // is held in the main room timeline // We want to fetch the room state from there and pass it down to this thread @@ -165,7 +176,7 @@ export class Thread extends TypedEventEmitter { await this.client.decryptEventIfNeeded(event, {}); } - if (this.hasServerSideSupport && this.initialEventsFetched) { + if (Thread.hasServerSideSupport && this.initialEventsFetched) { if (event.localTimestamp > this.lastReply().localTimestamp) { this.addEventToTimeline(event, false); } @@ -178,7 +189,7 @@ export class Thread extends TypedEventEmitter { const isThreadReply = event.getRelation()?.rel_type === RelationType.Thread; // If no thread support exists we want to count all thread relation // added as a reply. We can't rely on the bundled relationships count - if (!this.hasServerSideSupport && isThreadReply) { + if (!Thread.hasServerSideSupport && isThreadReply) { this.replyCount++; } @@ -191,7 +202,7 @@ export class Thread extends TypedEventEmitter { // This counting only works when server side support is enabled // as we started the counting from the value returned in the // bundled relationship - if (this.hasServerSideSupport) { + if (Thread.hasServerSideSupport) { this.replyCount++; } @@ -203,10 +214,17 @@ export class Thread extends TypedEventEmitter { } private initialiseThread(rootEvent: MatrixEvent | undefined): void { + if (Thread.hasServerSideSupport === undefined) { + Thread.serverSupportPromise.then(() => { + this.initialiseThread(rootEvent); + }); + return; + } + const bundledRelationship = rootEvent ?.getServerAggregatedRelation(RelationType.Thread); - if (this.hasServerSideSupport && bundledRelationship) { + if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; this._currentUserParticipated = bundledRelationship.current_user_participated; @@ -221,6 +239,9 @@ export class Thread extends TypedEventEmitter { } public async fetchInitialEvents(): Promise { + if (Thread.hasServerSideSupport === undefined) { + await Thread.serverSupportPromise; + } try { await this.fetchEvents(); this.initialEventsFetched = true; @@ -296,6 +317,10 @@ export class Thread extends TypedEventEmitter { nextBatch?: string; prevBatch?: string; }> { + if (Thread.serverSupportPromise) { + await Thread.serverSupportPromise; + } + let { originalEvent, events,