diff --git a/spec/unit/feature.spec.ts b/spec/unit/feature.spec.ts new file mode 100644 index 00000000000..97420947d56 --- /dev/null +++ b/spec/unit/feature.spec.ts @@ -0,0 +1,62 @@ +/* +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 { buildFeatureSupportMap, Feature, ServerSupport } from "../../src/feature"; + +describe("Feature detection", () => { + it("checks the matrix version", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.3"], + unstable_features: {}, + }); + + expect(support.get(Feature.Thread)).toBe(ServerSupport.Stable); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported); + }); + + it("checks the matrix msc number", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.2"], + unstable_features: { + "org.matrix.msc3771": true, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unstable); + }); + + it("requires two MSCs to pass", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.2"], + unstable_features: { + "org.matrix.msc3771": false, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Unsupported); + }); + + it("requires two MSCs OR matrix versions to pass", async () => { + const support = await buildFeatureSupportMap({ + versions: ["v1.4"], + unstable_features: { + "org.matrix.msc3771": false, + "org.matrix.msc3773": true, + }, + }); + expect(support.get(Feature.ThreadUnreadNotifications)).toBe(ServerSupport.Stable); + }); +}); diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index 925915729c9..e246ec1a2b2 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -1,3 +1,4 @@ +import { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { Filter, IFilterDefinition } from "../../src/filter"; describe("Filter", function() { @@ -50,7 +51,7 @@ describe("Filter", function() { expect(filter.getDefinition()).toEqual({ room: { timeline: { - unread_thread_notifications: true, + [UNREAD_THREAD_NOTIFICATIONS.name]: true, }, }, }); diff --git a/src/@types/sync.ts b/src/@types/sync.ts index f25bbf2e497..036c542bafa 100644 --- a/src/@types/sync.ts +++ b/src/@types/sync.ts @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { UnstableValue } from "matrix-events-sdk/lib/NamespacedValue"; +import { ServerControlledNamespacedValue } from "../NamespacedValue"; /** * https://github.com/matrix-org/matrix-doc/pull/3773 * * @experimental */ -export const UNREAD_THREAD_NOTIFICATIONS = new UnstableValue( +export const UNREAD_THREAD_NOTIFICATIONS = new ServerControlledNamespacedValue( "unread_thread_notifications", "org.matrix.msc3773.unread_thread_notifications"); diff --git a/src/client.ts b/src/client.ts index 80296c472ab..2858cab7d1f 100644 --- a/src/client.ts +++ b/src/client.ts @@ -204,6 +204,8 @@ import { MAIN_ROOM_TIMELINE } from "./models/read-receipt"; import { IgnoredInvites } from "./models/invites-ignorer"; import { UIARequest, UIAResponse } from "./@types/uia"; import { LocalNotificationSettings } from "./@types/local_notifications"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; +import { buildFeatureSupportMap, Feature, ServerSupport } from "./feature"; export type Store = IStore; @@ -528,7 +530,7 @@ export interface ITurnServer { credential: string; } -interface IServerVersions { +export interface IServerVersions { versions: string[]; unstable_features: Record; } @@ -967,6 +969,8 @@ export class MatrixClient extends TypedEventEmitter; protected canResetTimelineCallback: ResetTimelineCallback; + public canSupport = new Map(); + // The pushprocessor caches useful things, so keep one and re-use it protected pushProcessor = new PushProcessor(this); @@ -1197,6 +1201,12 @@ export class MatrixClient extends TypedEventEmitter = { + [Feature.Thread]: { + unstablePrefixes: ["org.matrix.msc3440"], + matrixVersion: "v1.3", + }, + [Feature.ThreadUnreadNotifications]: { + unstablePrefixes: ["org.matrix.msc3771", "org.matrix.msc3773"], + matrixVersion: "v1.4", + }, +}; + +export async function buildFeatureSupportMap(versions: IServerVersions): Promise> { + const supportMap = new Map(); + for (const [feature, supportCondition] of Object.entries(featureSupportResolver)) { + const supportMatrixVersion = versions.versions?.includes(supportCondition.matrixVersion || "") ?? false; + const supportUnstablePrefixes = supportCondition.unstablePrefixes?.every(unstablePrefix => { + return versions.unstable_features?.[unstablePrefix] === true; + }) ?? false; + if (supportMatrixVersion) { + supportMap.set(feature as Feature, ServerSupport.Stable); + } else if (supportUnstablePrefixes) { + supportMap.set(feature as Feature, ServerSupport.Unstable); + } else { + supportMap.set(feature as Feature, ServerSupport.Unsupported); + } + } + return supportMap; +} diff --git a/src/filter-component.ts b/src/filter-component.ts index 8cfbea667f3..5e38238c698 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -73,7 +73,7 @@ export interface IFilterComponent { * @param {Object} filterJson the definition of this filter JSON, e.g. { 'contains_url': true } */ export class FilterComponent { - constructor(private filterJson: IFilterComponent, public readonly userId?: string) {} + constructor(private filterJson: IFilterComponent, public readonly userId?: string | undefined | null) {} /** * Checks with the filter component matches the given event diff --git a/src/filter.ts b/src/filter.ts index 0cf2d1c99e5..14565c26f1a 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -22,6 +22,7 @@ import { EventType, RelationType, } from "./@types/event"; +import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; import { FilterComponent, IFilterComponent } from "./filter-component"; import { MatrixEvent } from "./models/event"; @@ -99,7 +100,7 @@ export class Filter { * @param {Object} jsonObj * @return {Filter} */ - public static fromJson(userId: string, filterId: string, jsonObj: IFilterDefinition): Filter { + public static fromJson(userId: string | undefined | null, filterId: string, jsonObj: IFilterDefinition): Filter { const filter = new Filter(userId, filterId); filter.setDefinition(jsonObj); return filter; @@ -109,7 +110,7 @@ export class Filter { private roomFilter: FilterComponent; private roomTimelineFilter: FilterComponent; - constructor(public readonly userId: string, public filterId?: string) {} + constructor(public readonly userId: string | undefined | null, public filterId?: string) {} /** * Get the ID of this filter on your homeserver (if known) @@ -227,7 +228,16 @@ export class Filter { * @param {boolean} enabled */ public setUnreadThreadNotifications(enabled: boolean): void { - setProp(this.definition, "room.timeline.unread_thread_notifications", !!enabled); + this.definition = { + ...this.definition, + room: { + ...this.definition?.room, + timeline: { + ...this.definition?.room?.timeline, + [UNREAD_THREAD_NOTIFICATIONS.name]: !!enabled, + }, + }, + }; } setLazyLoadMembers(enabled: boolean): void { diff --git a/src/store/memory.ts b/src/store/memory.ts index 0ed43a5b5ac..b44f24ca462 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -227,7 +227,7 @@ export class MemoryStore implements IStore { * @param {Filter} filter */ public storeFilter(filter: Filter): void { - if (!filter) { + if (!filter?.userId) { return; } if (!this.filters[filter.userId]) { diff --git a/src/sync.ts b/src/sync.ts index 0026831d591..cee5e7f09f4 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -59,6 +59,7 @@ import { BeaconEvent } from "./models/beacon"; import { IEventsResponse } from "./@types/requests"; import { IAbortablePromise } from "./@types/partials"; import { UNREAD_THREAD_NOTIFICATIONS } from "./@types/sync"; +import { Feature, ServerSupport } from "./feature"; const DEBUG = true; @@ -562,7 +563,11 @@ export class SyncApi { }; private buildDefaultFilter = () => { - return new Filter(this.client.credentials.userId); + const filter = new Filter(this.client.credentials.userId); + if (this.client.canSupport.get(Feature.ThreadUnreadNotifications) !== ServerSupport.Unsupported) { + filter.setUnreadThreadNotifications(true); + } + return filter; }; private checkLazyLoadStatus = async () => { @@ -706,10 +711,6 @@ export class SyncApi { const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); initialFilter.setTimelineLimit(this.opts.initialSyncLimit); - const supportsThreadNotifications = - await this.client.doesServerSupportUnstableFeature("org.matrix.msc3773") - || await this.client.isVersionSupported("v1.4"); - initialFilter.setUnreadThreadNotifications(supportsThreadNotifications); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); }