diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index 8425a7269c..2217af9387 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -30,7 +30,6 @@ import RoomAliasField from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; @@ -66,7 +65,7 @@ export default class CreateRoomDialog extends React.Component { constructor(props) { super(props); - this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + this.supportsRestricted = !!this.props.parentSpace; let joinRule = JoinRule.Invite; if (this.props.defaultPublic) { diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index a44d16dd40..c371f64f33 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -26,7 +26,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { BetaPill } from "../beta/BetaCard"; import Field from "../elements/Field"; import RoomAliasField from "../elements/RoomAliasField"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import { createSpace, SpaceCreateForm } from "../spaces/SpaceCreateMenu"; import { SubspaceSelector } from "./AddExistingToSpaceDialog"; import JoinRuleDropdown from "../elements/JoinRuleDropdown"; @@ -48,14 +47,10 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick const [avatar, setAvatar] = useState(null); const [topic, setTopic] = useState(""); - const supportsRestricted = !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; - const spaceJoinRule = space.getJoinRule(); - let defaultJoinRule = JoinRule.Invite; + let defaultJoinRule = JoinRule.Restricted; if (spaceJoinRule === JoinRule.Public) { defaultJoinRule = JoinRule.Public; - } else if (supportsRestricted) { - defaultJoinRule = JoinRule.Restricted; } const [joinRule, setJoinRule] = useState(defaultJoinRule); @@ -150,7 +145,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick label={_t("Space visibility")} labelInvite={_t("Private space (invite only)")} labelPublic={_t("Public space")} - labelRestricted={supportsRestricted ? _t("Visible to space members") : undefined} + labelRestricted={_t("Visible to space members")} width={478} value={joinRule} onChange={setJoinRule} diff --git a/src/components/views/settings/JoinRuleSettings.tsx b/src/components/views/settings/JoinRuleSettings.tsx index c7423d1b24..49295f0e83 100644 --- a/src/components/views/settings/JoinRuleSettings.tsx +++ b/src/components/views/settings/JoinRuleSettings.tsx @@ -35,6 +35,7 @@ import dis from "../../../dispatcher/dispatcher"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; import { Action } from "../../../dispatcher/actions"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions"; interface IProps { room: Room; @@ -48,11 +49,9 @@ interface IProps { const JoinRuleSettings = ({ room, promptUpgrade, aliasWarning, onError, beforeChange, closeSettingsFn }: IProps) => { const cli = room.client; - const restrictedRoomCapabilities = SpaceStore.instance.restrictedJoinRuleSupport; - const roomSupportsRestricted = Array.isArray(restrictedRoomCapabilities?.support) - && restrictedRoomCapabilities.support.includes(room.getVersion()); + const roomSupportsRestricted = doesRoomVersionSupport(room.getVersion(), PreferredRoomVersions.RestrictedRooms); const preferredRestrictionVersion = !roomSupportsRestricted && promptUpgrade - ? restrictedRoomCapabilities?.preferred + ? PreferredRoomVersions.RestrictedRooms : undefined; const disabled = !room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, cli); diff --git a/src/createRoom.ts b/src/createRoom.ts index 3e258d28cb..3f85be9f00 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -45,6 +45,7 @@ import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload"; import { findDMForUser } from "./utils/direct-messages"; import { privateShouldBeEncrypted } from "./utils/rooms"; import { waitForMember } from "./utils/membership"; +import { PreferredRoomVersions } from "./utils/PreferredRoomVersions"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -191,20 +192,18 @@ export default async function createRoom(opts: IOpts): Promise { } if (opts.joinRule === JoinRule.Restricted) { - if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) { - createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred; + createOpts.room_version = PreferredRoomVersions.RestrictedRooms; - createOpts.initial_state.push({ - type: EventType.RoomJoinRules, - content: { - "join_rule": JoinRule.Restricted, - "allow": [{ - "type": RestrictedAllowType.RoomMembership, - "room_id": opts.parentSpace.roomId, - }], - }, - }); - } + createOpts.initial_state.push({ + type: EventType.RoomJoinRules, + content: { + "join_rule": JoinRule.Restricted, + "allow": [{ + "type": RestrictedAllowType.RoomMembership, + "room_id": opts.parentSpace.roomId, + }], + }, + }); } } diff --git a/src/stores/spaces/SpaceStore.ts b/src/stores/spaces/SpaceStore.ts index ec4556f48c..4fd785602b 100644 --- a/src/stores/spaces/SpaceStore.ts +++ b/src/stores/spaces/SpaceStore.ts @@ -18,7 +18,7 @@ import { ListIteratee, Many, sortBy } from "lodash"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { Room, RoomEvent } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { ClientEvent, IRoomCapability } from "matrix-js-sdk/src/client"; +import { ClientEvent } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; @@ -132,7 +132,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); - private _restrictedJoinRuleSupport?: IRoomCapability; // The following properties are set by onReady as they live in account_data private _allRoomsInHome = false; private _enabledMetaSpaces: MetaSpace[] = []; @@ -210,10 +209,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } } - public get restrictedJoinRuleSupport(): IRoomCapability { - return this._restrictedJoinRuleSupport; - } - /** * Sets the active space, updates room list filters, * optionally switches the user's room back to where they were when they last viewed that space. @@ -1066,11 +1061,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on(RoomStateEvent.Members, this.onRoomStateMembers); this.matrixClient.on(ClientEvent.AccountData, this.onAccountData); - this.matrixClient.getCapabilities().then(capabilities => { - this._restrictedJoinRuleSupport = capabilities - ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]; - }); - const oldMetaSpaces = this._enabledMetaSpaces; const enabledMetaSpaces = SettingsStore.getValue("Spaces.enabledMetaSpaces"); this._enabledMetaSpaces = metaSpaceOrder.filter(k => enabledMetaSpaces[k]); diff --git a/src/utils/PreferredRoomVersions.ts b/src/utils/PreferredRoomVersions.ts new file mode 100644 index 0000000000..2dc269da6c --- /dev/null +++ b/src/utils/PreferredRoomVersions.ts @@ -0,0 +1,55 @@ +/* +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. +*/ + +/** + * The preferred room versions for various features within the app. The + * room versions here are selected based on the client's support for the + * possible room versions in combination with server support in the + * ecosystem. + * + * Loosely follows https://spec.matrix.org/latest/rooms/#feature-matrix + */ +export class PreferredRoomVersions { + /** + * The room version to use when creating "restricted" rooms. + */ + public static readonly RestrictedRooms = "9"; + + private constructor() { + // readonly, static, class + } +} + +/** + * Determines if a room version supports the given feature using heuristics + * for how Matrix works. + * @param roomVer The room version to check support within. + * @param featureVer The room version of the feature. Should be from PreferredRoomVersions. + * @see PreferredRoomVersions + */ +export function doesRoomVersionSupport(roomVer: string, featureVer: string): boolean { + // Assumption: all unstable room versions don't support the feature. Calling code can check for unstable + // room versions explicitly if it wants to. The spec reserves [0-9] and `.` for its room versions. + if (!roomVer.match(/[\d.]+/)) { + return false; + } + + // Dev note: While the spec says room versions are not linear, we can make reasonable assumptions + // until the room versions prove themselves to be non-linear in the spec. We should see this coming + // from a mile away and can course-correct this function if needed. + return Number(roomVer) >= Number(featureVer); +} + diff --git a/test/PreferredRoomVersions-test.ts b/test/PreferredRoomVersions-test.ts new file mode 100644 index 0000000000..caaf3fe5ae --- /dev/null +++ b/test/PreferredRoomVersions-test.ts @@ -0,0 +1,46 @@ +/* +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 { doesRoomVersionSupport, PreferredRoomVersions } from "../src/utils/PreferredRoomVersions"; + +describe("doesRoomVersionSupport", () => { + it("should detect unstable as unsupported", () => { + expect(doesRoomVersionSupport("org.example.unstable", "1")).toBe(false); + expect(doesRoomVersionSupport("1.2-beta", "1")).toBe(false); + }); + + it("should detect support properly", () => { + expect(doesRoomVersionSupport("1", "2")).toBe(false); // older + expect(doesRoomVersionSupport("2", "2")).toBe(true); // exact + expect(doesRoomVersionSupport("3", "2")).toBe(true); // newer + }); + + it("should handle decimal versions", () => { + expect(doesRoomVersionSupport("1.1", "2.2")).toBe(false); // older + expect(doesRoomVersionSupport("2.1", "2.2")).toBe(false); // exact-ish + expect(doesRoomVersionSupport("2.2", "2.2")).toBe(true); // exact + expect(doesRoomVersionSupport("2.3", "2.2")).toBe(true); // exact-ish + expect(doesRoomVersionSupport("3.1", "2.2")).toBe(true); // newer + }); + + it("should detect restricted rooms in v9 and v10", () => { + // Dev note: we consider it a feature that v8 rooms have to upgrade considering the bug in v8. + // https://spec.matrix.org/v1.3/rooms/v8/#redactions + expect(doesRoomVersionSupport("8", PreferredRoomVersions.RestrictedRooms)).toBe(false); + expect(doesRoomVersionSupport("9", PreferredRoomVersions.RestrictedRooms)).toBe(true); + expect(doesRoomVersionSupport("10", PreferredRoomVersions.RestrictedRooms)).toBe(true); + }); +});