Skip to content

Commit

Permalink
Refactor the JoinRoom screen to take advantage of newer APIs and supp…
Browse files Browse the repository at this point in the history
…ort more joinRule/membership combinations (i.e. invite required, restricted, banned) (#3685)

- expose the full RoomPreview and RoomMembershipDetails through their own proxies
- implement standard mocks for all the different combinations
- converge on a single room info provider
- rebuild all the previews
- prioritise the preview data over the room one.
  • Loading branch information
stefanceriu authored Jan 20, 2025
1 parent f20847f commit 8577f53
Show file tree
Hide file tree
Showing 48 changed files with 499 additions and 243 deletions.
36 changes: 36 additions & 0 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

44 changes: 39 additions & 5 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3187,13 +3187,13 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
var roomPreviewForIdentifierViaReceivedArguments: (identifier: String, via: [String])?
var roomPreviewForIdentifierViaReceivedInvocations: [(identifier: String, via: [String])] = []

var roomPreviewForIdentifierViaUnderlyingReturnValue: Result<RoomPreviewDetails, ClientProxyError>!
var roomPreviewForIdentifierViaReturnValue: Result<RoomPreviewDetails, ClientProxyError>! {
var roomPreviewForIdentifierViaUnderlyingReturnValue: Result<RoomPreviewProxyProtocol, ClientProxyError>!
var roomPreviewForIdentifierViaReturnValue: Result<RoomPreviewProxyProtocol, ClientProxyError>! {
get {
if Thread.isMainThread {
return roomPreviewForIdentifierViaUnderlyingReturnValue
} else {
var returnValue: Result<RoomPreviewDetails, ClientProxyError>? = nil
var returnValue: Result<RoomPreviewProxyProtocol, ClientProxyError>? = nil
DispatchQueue.main.sync {
returnValue = roomPreviewForIdentifierViaUnderlyingReturnValue
}
Expand All @@ -3211,9 +3211,9 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
}
}
}
var roomPreviewForIdentifierViaClosure: ((String, [String]) async -> Result<RoomPreviewDetails, ClientProxyError>)?
var roomPreviewForIdentifierViaClosure: ((String, [String]) async -> Result<RoomPreviewProxyProtocol, ClientProxyError>)?

func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewDetails, ClientProxyError> {
func roomPreviewForIdentifier(_ identifier: String, via: [String]) async -> Result<RoomPreviewProxyProtocol, ClientProxyError> {
roomPreviewForIdentifierViaCallsCount += 1
roomPreviewForIdentifierViaReceivedArguments = (identifier: identifier, via: via)
DispatchQueue.main.async {
Expand Down Expand Up @@ -13416,6 +13416,15 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Sendable {
}
var underlyingRole: RoomMemberRole!

}
class RoomMembershipDetailsProxyMock: RoomMembershipDetailsProxyProtocol, @unchecked Sendable {
var ownRoomMember: RoomMemberProxyProtocol {
get { return underlyingOwnRoomMember }
set(value) { underlyingOwnRoomMember = value }
}
var underlyingOwnRoomMember: RoomMemberProxyProtocol!
var senderRoomMember: RoomMemberProxyProtocol?

}
class RoomNotificationSettingsProxyMock: RoomNotificationSettingsProxyProtocol, @unchecked Sendable {
var mode: RoomNotificationModeProxy {
Expand All @@ -13429,6 +13438,31 @@ class RoomNotificationSettingsProxyMock: RoomNotificationSettingsProxyProtocol,
}
var underlyingIsDefault: Bool!

}
class RoomPreviewProxyMock: RoomPreviewProxyProtocol, @unchecked Sendable {
var info: RoomPreviewInfoProxy {
get { return underlyingInfo }
set(value) { underlyingInfo = value }
}
var underlyingInfo: RoomPreviewInfoProxy!
var ownMembershipDetailsCallsCount = 0
var ownMembershipDetailsCalled: Bool {
return ownMembershipDetailsCallsCount > 0
}

var ownMembershipDetails: RoomMembershipDetailsProxyProtocol? {
get async {
ownMembershipDetailsCallsCount += 1
if let ownMembershipDetailsClosure = ownMembershipDetailsClosure {
return await ownMembershipDetailsClosure()
} else {
return underlyingOwnMembershipDetails
}
}
}
var underlyingOwnMembershipDetails: RoomMembershipDetailsProxyProtocol?
var ownMembershipDetailsClosure: (() async -> RoomMembershipDetailsProxyProtocol?)?

}
class RoomProxyMock: RoomProxyProtocol, @unchecked Sendable {
var id: String {
Expand Down
90 changes: 90 additions & 0 deletions ElementX/Sources/Mocks/RoomPreviewProxyMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// Copyright 2025 New Vector 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

extension RoomPreviewProxyMock {
struct Configuration {
var roomID = "1"
var canonicalAlias = "#3🌞problem:matrix.org"
var name = "The Three-Body Problem - 三体"
var topic = "“Science and technology were the only keys to opening the door to the future, and people approached science with the faith and sincerity of elementary school students.”"
var avatarURL = URL.mockMXCAvatar.absoluteString
var numJoinedMembers = UInt64(100)
var numActiveMembers = UInt64(100)
var roomType = RoomType.room
var membership: Membership?
var joinRule: JoinRule
}

static var joinable: RoomPreviewProxyMock {
.init(.init(membership: nil, joinRule: .public))
}

static var restricted: RoomPreviewProxyMock {
.init(.init(membership: nil, joinRule: .restricted(rules: [])))
}

static var inviteRequired: RoomPreviewProxyMock {
.init(.init(membership: nil, joinRule: .invite))
}

static func invited(roomID: String? = nil) -> RoomPreviewProxyMock {
if let roomID {
return .init(.init(roomID: roomID, membership: .invited, joinRule: .invite))
}

return .init(.init(membership: .invited, joinRule: .invite))
}

static var knockable: RoomPreviewProxyMock {
.init(.init(membership: nil, joinRule: .knock))
}

static var knockableRestricted: RoomPreviewProxyMock {
.init(.init(membership: nil, joinRule: .knockRestricted(rules: [])))
}

static var knocked: RoomPreviewProxyMock {
.init(.init(membership: .knocked, joinRule: .knock))
}

static var banned: RoomPreviewProxyMock {
.init(.init(membership: .banned, joinRule: .public))
}

convenience init(_ configuration: RoomPreviewProxyMock.Configuration) {
self.init()
underlyingInfo = .init(roomPreviewInfo: .init(roomId: configuration.roomID,
canonicalAlias: configuration.canonicalAlias,
name: configuration.name,
topic: configuration.topic,
avatarUrl: configuration.avatarURL,
numJoinedMembers: configuration.numJoinedMembers,
numActiveMembers: configuration.numActiveMembers,
roomType: configuration.roomType,
isHistoryWorldReadable: nil,
membership: configuration.membership,
joinRule: configuration.joinRule,
isDirect: nil,
heroes: nil))

let roomMembershipDetails = RoomMembershipDetailsProxyMock()

let mockMember = RoomMemberProxyMock()
mockMember.userID = "@bob:matrix.org"
mockMember.displayName = "Billy Bob"
mockMember.avatarURL = .mockMXCUserAvatar
mockMember.membershipChangeReason = "Ain't nobody need no reason"

roomMembershipDetails.senderRoomMember = mockMember
roomMembershipDetails.ownRoomMember = mockMember

underlyingOwnMembershipDetails = roomMembershipDetails
}
}
24 changes: 15 additions & 9 deletions ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ enum JoinRoomScreenViewModelAction {
case dismiss
}

enum JoinRoomScreenInteractionMode {
enum JoinRoomScreenMode: Equatable {
case loading
case unknown
case joinable
case restricted
case inviteRequired
case invited
case join
case knock
case knockable
case knocked
case banned(sender: String?, reason: String?)
}

struct JoinRoomScreenRoomDetails {
Expand All @@ -31,12 +34,11 @@ struct JoinRoomScreenRoomDetails {
}

struct JoinRoomScreenViewState: BindableState {
// Maybe use room summary details or similar here??
let roomID: String

var roomDetails: JoinRoomScreenRoomDetails?

var mode: JoinRoomScreenInteractionMode = .loading
var mode: JoinRoomScreenMode = .loading

var bindings = JoinRoomScreenViewStateBindings()

Expand All @@ -46,10 +48,14 @@ struct JoinRoomScreenViewState: BindableState {

var subtitle: String? {
switch mode {
case .loading: nil
case .unknown: L10n.screenJoinRoomSubtitleNoPreview
case .invited, .join, .knock: roomDetails?.canonicalAlias
case .knocked: nil
case .loading:
nil
case .unknown:
L10n.screenJoinRoomSubtitleNoPreview
case .knocked:
nil
default:
roomDetails?.canonicalAlias
}
}

Expand Down
100 changes: 59 additions & 41 deletions ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
private let clientProxy: ClientProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol

private var roomPreviewDetails: RoomPreviewDetails?
private var roomPreview: RoomPreviewProxyProtocol?
private var room: RoomProxyType?

private let actionsSubject: PassthroughSubject<JoinRoomScreenViewModelAction, Never> = .init()
Expand Down Expand Up @@ -72,22 +72,21 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
private func loadRoomDetails() async {
showLoadingIndicator()

defer {
hideLoadingIndicator()
updateRoomDetails()
}

await updateRoom()

switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) {
case .success(let roomPreviewDetails):
self.roomPreviewDetails = roomPreviewDetails
updateRoomDetails()
case .success(let roomPreview):
self.roomPreview = roomPreview
await updateRoomDetails()
case .failure(.roomPreviewIsPrivate):
break // Handled by the mode, we don't need an error indicator.
case .failure:
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
}

hideLoadingIndicator()

await updateRoomDetails()
}

private func updateRoom() async {
Expand All @@ -97,58 +96,77 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
// take priority over the preview one.
if let room = await clientProxy.roomForIdentifier(roomID) {
self.room = room
updateRoomDetails()
await updateRoomDetails()
}
}

private func updateRoomDetails() {
var roomPreviewInfo: BaseRoomInfoProxyProtocol?
private func updateRoomDetails() async {
var roomInfo: BaseRoomInfoProxyProtocol?
var inviter: RoomInviterDetails?

switch room {
case .joined(let joinedRoomProxy):
roomPreviewInfo = joinedRoomProxy.infoPublisher.value
roomInfo = joinedRoomProxy.infoPublisher.value
case .invited(let invitedRoomProxy):
inviter = invitedRoomProxy.inviter.map(RoomInviterDetails.init)
roomPreviewInfo = invitedRoomProxy.info
roomInfo = invitedRoomProxy.info
case .knocked(let knockedRoomProxy):
roomPreviewInfo = knockedRoomProxy.info
roomInfo = knockedRoomProxy.info
default:
break
}
let name = roomPreviewInfo?.displayName ?? roomPreviewDetails?.name
state.roomDetails = JoinRoomScreenRoomDetails(name: name,
topic: roomPreviewInfo?.topic ?? roomPreviewDetails?.topic,
canonicalAlias: roomPreviewInfo?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias,
avatar: roomPreviewInfo?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL),
memberCount: UInt(roomPreviewInfo?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)),

let info = roomPreview?.info ?? roomInfo
state.roomDetails = JoinRoomScreenRoomDetails(name: info?.displayName,
topic: info?.topic,
canonicalAlias: info?.canonicalAlias,
avatar: info?.avatar ?? .room(id: roomID, name: info?.displayName ?? "", avatarURL: nil),
memberCount: UInt(info?.activeMembersCount ?? 0),
inviter: inviter)

updateMode()
await updateMode()
}

private func updateMode() {
if case .knocked = room {
state.mode = .knocked
private func updateMode() async {
if roomPreview == nil, room == nil {
state.mode = .unknown
return
}

// Check invites first to show Accept/Decline buttons on public rooms.
if case .invited = room {
state.mode = .invited
return
}

if roomPreviewDetails?.isInvited ?? false {
state.mode = .invited
return
}

if roomPreviewDetails?.canKnock ?? false, appSettings.knockingEnabled {
state.mode = .knock
} else {
state.mode = .join
if let roomPreview {
let membershipDetails = await roomPreview.ownMembershipDetails

switch roomPreview.info.membership {
case .invited:
state.mode = .invited
case .knocked:
state.mode = .knocked
case .banned:
state.mode = .banned(sender: membershipDetails?.senderRoomMember?.displayName ?? membershipDetails?.senderRoomMember?.userID,
reason: membershipDetails?.ownRoomMember.membershipChangeReason)
default:
switch roomPreview.info.joinRule {
case .private, .invite:
state.mode = .inviteRequired
case .knock, .knockRestricted:
state.mode = appSettings.knockingEnabled ? .knockable : .joinable
case .restricted:
state.mode = .restricted
default:
state.mode = .joinable
}
}
} else if let room {
switch room {
case .invited:
state.mode = .invited
case .knocked:
state.mode = .knocked
case .banned:
state.mode = .banned(sender: nil, reason: nil)
default:
state.mode = .joinable
}
}
}

Expand Down
Loading

0 comments on commit 8577f53

Please sign in to comment.