Use our own JoinRule type which doesn't include the reserved .private case. (#5056)

* Stop using Rust's JoinRule.private in mocks.

It isn't used anywhere else (see MSC4413 for more context).

* Use our own JoinRule (and AllowRule) types.

* Update ElementX/Sources/Services/Room/JoinRule.swift

Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>

---------

Co-authored-by: Stefan Ceriu <stefan.ceriu@gmail.com>
This commit is contained in:
Doug
2026-02-06 13:54:53 +00:00
committed by GitHub
parent 61763d9893
commit bff328554a
18 changed files with 119 additions and 41 deletions
@@ -80,7 +80,7 @@ extension RoomPreviewProxyMock {
roomType: configuration.roomType,
isHistoryWorldReadable: nil,
membership: configuration.membership,
joinRule: configuration.joinRule,
joinRule: configuration.joinRule.rustValue,
isDirect: configuration.isDirect,
heroes: nil))
@@ -56,7 +56,7 @@ extension [LeaveSpaceRoom] {
name: "Sound",
isSpace: false,
memberCount: 20,
joinRule: .private),
joinRule: .invite),
isLastOwner: false,
areCreatorsPrivileged: false),
LeaveSpaceRoom(spaceRoom: SpaceRoom(id: "3",
@@ -70,7 +70,7 @@ extension [LeaveSpaceRoom] {
name: "The Theatre",
isSpace: true,
memberCount: 100,
joinRule: .private,
joinRule: .invite,
childrenCount: 20),
isLastOwner: false,
areCreatorsPrivileged: false),
@@ -78,7 +78,7 @@ extension [LeaveSpaceRoom] {
name: "Bookings",
isSpace: false,
memberCount: 200,
joinRule: .private,
joinRule: .invite,
childrenCount: 0),
isLastOwner: true,
areCreatorsPrivileged: false),
@@ -160,7 +160,7 @@ private extension SpaceRoom {
avatarUrl: avatarURL?.absoluteString,
roomType: isSpace ? .space : .room,
numJoinedMembers: memberCount,
joinRule: joinRule,
joinRule: joinRule?.rustValue,
worldReadable: true,
guestCanJoin: false,
isDirect: isDirect,
@@ -379,7 +379,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
static let selectedSpaceViewModel = makeViewModel(selectionMode: .preSelected(SpaceServiceRoom.mock(name: "Awesome Space",
isSpace: true,
joinRule: .private)))
joinRule: .invite)))
static let selectedSpaceWithListViewModel = {
let viewModel = makeViewModel()
@@ -391,7 +391,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
let viewModel = makeViewModel(isKnockingEnabled: true,
selectionMode: .preSelected(SpaceServiceRoom.mock(name: "Awesome Space",
isSpace: true,
joinRule: .private)))
joinRule: .invite)))
viewModel.context.selectedAccessType = .askToJoinWithSpaceMembers
return viewModel
}()
@@ -235,7 +235,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
state.mode = .banned(sender: nil, reason: nil)
default:
switch spaceServiceRoom.joinRule {
case .private, .invite:
case .invite:
state.mode = .inviteRequired
case .knock, .knockRestricted:
state.mode = appSettings.knockingEnabled ? .knockable : .joinable
@@ -258,7 +258,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
reason: membershipDetails?.ownRoomMember.membershipChangeReason)
default:
switch roomPreview.info.joinRule {
case .private, .invite:
case .invite:
state.mode = .inviteRequired
case .knock, .knockRestricted:
state.mode = appSettings.knockingEnabled ? .knockable : .joinable
@@ -350,9 +350,9 @@ private extension SecurityAndPrivacyRoomAccessType {
case .anyone:
.public
case .spaceMembers(let spaceIDs):
.restricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
.restricted(rules: spaceIDs.map { .roomMembership(roomID: $0) })
case .askToJoinWithSpaceMembers(let spaceIDs):
.knockRestricted(rules: spaceIDs.map { .roomMembership(roomId: $0) })
.knockRestricted(rules: spaceIDs.map { .roomMembership(roomID: $0) })
}
}
}
@@ -269,7 +269,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
canonicalAlias: "#room:matrix.org",
members: .allMembersAsCreator,
joinRule: .restricted(rules: [.roomMembership(roomId: space.id)]),
joinRule: .restricted(rules: [.roomMembership(roomID: space.id)]),
isVisibleInPublicDirectory: true)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
spaceServiceConfiguration: .init(joinedParentSpaces: [space]))),
@@ -286,7 +286,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
canonicalAlias: "#room:matrix.org",
members: .allMembersAsCreator,
joinRule: .restricted(rules: spaces.map { .roomMembership(roomId: $0.id) }),
joinRule: .restricted(rules: spaces.map { .roomMembership(roomID: $0.id) }),
isVisibleInPublicDirectory: true)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
spaceServiceConfiguration: .init(joinedParentSpaces: spaces))),
@@ -319,7 +319,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
canonicalAlias: "#room:matrix.org",
members: .allMembersAsCreator,
joinRule: .knockRestricted(rules: [.roomMembership(roomId: space.id)]),
joinRule: .knockRestricted(rules: [.roomMembership(roomID: space.id)]),
isVisibleInPublicDirectory: true)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
spaceServiceConfiguration: .init(joinedParentSpaces: [space]))),
@@ -337,7 +337,7 @@ struct SecurityAndPrivacyScreen_Previews: PreviewProvider, TestablePreview {
return SecurityAndPrivacyScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(isEncrypted: false,
canonicalAlias: "#room:matrix.org",
members: .allMembersAsCreator,
joinRule: .knockRestricted(rules: spaces.map { .roomMembership(roomId: $0.id) }),
joinRule: .knockRestricted(rules: spaces.map { .roomMembership(roomID: $0.id) }),
isVisibleInPublicDirectory: true)),
clientProxy: ClientProxyMock(.init(userIDServerName: "matrix.org",
spaceServiceConfiguration: .init(joinedParentSpaces: spaces))),
@@ -141,7 +141,7 @@ struct SpaceHeaderView_Previews: PreviewProvider, TestablePreview {
"Interdum mauris ultrices tincidunt proin morbi erat aenean risus nibh.",
"Diam amet sit fermentum vulputate faucibus."].joined(separator: " "),
canonicalAlias: "#subspace:matrix.org",
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))
joinRule: .knockRestricted(rules: [.roomMembership(roomID: "")]))
]
}
}
@@ -156,7 +156,7 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
joinedMembersCount: 76,
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))
joinRule: .knockRestricted(rules: [.roomMembership(roomID: "")]))
static func makeViewModel(mode: LeaveSpaceHandleProxy.Mode) -> LeaveSpaceViewModel {
let rooms: [LeaveSpaceRoom] = switch mode {
@@ -220,7 +220,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose],
topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.",
canonicalAlias: "#engineering-team:element.io",
joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))
joinRule: .knockRestricted(rules: [.roomMembership(roomID: "")]))
let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceServiceRoom: spaceServiceRoom,
initialSpaceRooms: isNewSpace ? [] : .mockSpaceList))
@@ -559,7 +559,7 @@ class ClientProxy: ClientProxyProtocol {
invite: userIDs,
avatar: avatarURL?.absoluteString,
powerLevelContentOverride: powerLevelContentOverride,
joinRuleOverride: accessType.joinRuleOverride,
joinRuleOverride: accessType.joinRuleOverride?.rustValue,
historyVisibilityOverride: accessType.historyVisibilityOverride,
// This is an FFI naming mistake, what is required is the `aliasLocalPart` not the whole alias
canonicalAlias: aliasLocalPart,
@@ -1432,9 +1432,9 @@ private extension CreateRoomAccessType {
case .askToJoin:
.knock
case .spaceMembers(let spaceID):
.restricted(rules: [.roomMembership(roomId: spaceID)])
.restricted(rules: [.roomMembership(roomID: spaceID)])
case .askToJoinWithSpaceMembers(let spaceID):
.knockRestricted(rules: [.roomMembership(roomId: spaceID)])
.knockRestricted(rules: [.roomMembership(roomID: spaceID)])
case .private, .public:
nil
}
@@ -0,0 +1,74 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import MatrixRustSDK
/// The rule used for users wishing to join a room.
public enum JoinRule: Equatable, Hashable {
/// Anyone can join the room without any prior action.
case `public`
/// A user who wishes to join the room must first receive an invite.
case invite
/// Users can join the room if they are invited, or they can request an invite to the room.
///
/// They can be allowed (invited) or denied (kicked/banned) access.
case knock
/// Users can join the room if they are invited, or if they meet any of the conditions described
/// in a set of ``AllowRule``s.
case restricted(rules: [AllowRule])
/// Users can join the room if they are invited, or if they meet any of the conditions described
/// in a set of ``AllowRule``s, or they can request an invite to the room.
case knockRestricted(rules: [AllowRule])
/// A custom join rule, up for interpretation by the consumer.
case custom(rawValue: String)
init(rustValue: MatrixRustSDK.JoinRule) {
self = switch rustValue {
case .public: .public
case .invite: .invite
case .knock: .knock
case .private: .invite // Assume .private is .invite – private shouldn't be used (see MSC4413).
case .restricted(let rules): .restricted(rules: rules.map(AllowRule.init))
case .knockRestricted(let rules): .knockRestricted(rules: rules.map(AllowRule.init))
case .custom(let rawValue): .custom(rawValue: rawValue)
}
}
var rustValue: MatrixRustSDK.JoinRule {
switch self {
case .public: .public
case .invite: .invite
case .knock: .knock
case .restricted(rules: let rules): .restricted(rules: rules.map(\.rustValue))
case .knockRestricted(rules: let rules): .knockRestricted(rules: rules.map(\.rustValue))
case .custom(let rawValue): .custom(repr: rawValue)
}
}
}
/// An allow rule which defines a condition that allows joining a room.
public enum AllowRule: Equatable, Hashable {
/// Only a member of the `room_id` Room can join the one this rule is used in.
case roomMembership(roomID: String)
/// A custom allow rule implementation, containing its JSON representation as a `String`.
case custom(json: String)
init(rustValue: MatrixRustSDK.AllowRule) {
self = switch rustValue {
case .roomMembership(let roomID): .roomMembership(roomID: roomID)
case .custom(let json): .custom(json: json)
}
}
var rustValue: MatrixRustSDK.AllowRule {
switch self {
case .roomMembership(let roomID): .roomMembership(roomId: roomID)
case .custom(let json): .custom(json: json)
}
}
}
@@ -479,7 +479,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
func updateJoinRule(_ rule: JoinRule) async -> Result<Void, RoomProxyError> {
do {
try await room.updateJoinRules(newRule: rule)
try await room.updateJoinRules(newRule: rule.rustValue)
return .success(())
} catch {
MXLog.error("Failed updating join rule with error: \(error)")
@@ -135,7 +135,7 @@ struct RoomInfoProxy: RoomInfoProxyProtocol {
}
var joinRule: JoinRule? {
roomInfo.joinRule
roomInfo.joinRule.map(JoinRule.init)
}
var historyVisibility: RoomHistoryVisibility {
@@ -193,7 +193,7 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol {
}
var joinRule: JoinRule? {
roomPreviewInfo.joinRule
roomPreviewInfo.joinRule.map(JoinRule.init)
}
var membership: Membership? {
@@ -90,7 +90,7 @@ extension RoomInfoProxyProtocol {
}
return switch joinRule {
case .invite, .knock, .restricted, .knockRestricted, .private:
case .invite, .knock, .restricted, .knockRestricted:
true
case .public:
false
@@ -53,7 +53,7 @@ struct SpaceServiceRoom {
.public
case .restricted, .knockRestricted:
.restricted
case .invite, .knock, .private, .custom:
case .invite, .knock, .custom:
.private
case .none:
.none
@@ -77,7 +77,7 @@ extension SpaceServiceRoom {
topic = spaceRoom.topic
canonicalAlias = spaceRoom.canonicalAlias
joinRule = spaceRoom.joinRule
joinRule = spaceRoom.joinRule.map(JoinRule.init)
worldReadable = spaceRoom.worldReadable
guestCanJoin = spaceRoom.guestCanJoin
state = spaceRoom.state
@@ -130,10 +130,10 @@ extension SpaceServiceRoom {
case .restricted:
joinRule = .restricted(rules: [])
case .inviteRequired:
joinRule = .private
joinRule = .invite
case .invited:
state = .invited
joinRule = .private
joinRule = .invite
case .knockable:
joinRule = .knock
case .knocked:
@@ -218,7 +218,7 @@ extension [SpaceServiceRoom] {
childrenCount: 1,
joinedMembersCount: 500,
canonicalAlias: "#the-foundation:matrix.org",
joinRule: .private,
joinRule: .invite,
state: .joined),
SpaceServiceRoom.mock(id: "space2",
name: "The Second Foundation",