diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 7acda4b38..034e5f53d 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -350,6 +350,7 @@ 42A5A42ACF063EEE6B1980D2 /* ReportContentScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81B17B1F29448D1B9049B11C /* ReportContentScreenViewModel.swift */; }; 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4176C3E20C772DE8D182863C /* LegalInformationScreen.swift */; }; 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0A307A44F952CD73E63AE31 /* RoomEventStringBuilder.swift */; }; + 432EA37BDC97CEDBAB7B23A6 /* RoomInfoProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14517E5597594956FCE1950D /* RoomInfoProxyProtocol.swift */; }; 43F06DF42EC00B3CE2B020A4 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC3F82523D6F48B926D6AF68 /* AppSettings.swift */; }; 43F35A7E5703D64DB0519C59 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD469F7513574341181F7EAA /* ServerSelectionScreen.swift */; }; 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D505843AB66822EB91F0DF0 /* TimelineItemProxy.swift */; }; @@ -1486,6 +1487,7 @@ 13F354AD441E2FD83DED89AF /* SeparatorMediaEventsTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorMediaEventsTimelineView.swift; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 1434D5169F0EE319E226DA7F /* RoomMembershipDetailsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxyProtocol.swift; sourceTree = ""; }; + 14517E5597594956FCE1950D /* RoomInfoProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInfoProxyProtocol.swift; sourceTree = ""; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = ""; }; 1511B1DCECC0DC75EB267328 /* KnockRequestsListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestsListScreen.swift; sourceTree = ""; }; 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenCoordinator.swift; sourceTree = ""; }; @@ -3649,6 +3651,7 @@ C07851F4EA81AA3339806A7B /* KnockRequestProxyProtocol.swift */, B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */, 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */, + 14517E5597594956FCE1950D /* RoomInfoProxyProtocol.swift */, 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */, 4D635709C1D6D37C225AD40E /* RoomPowerLevelProxyProtocol.swift */, CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */, @@ -7561,6 +7564,7 @@ 8DF0EBD97753033C715D716E /* RoomFlowCoordinatorStateMachine.swift in Sources */, 9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */, 5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */, + 432EA37BDC97CEDBAB7B23A6 /* RoomInfoProxyProtocol.swift in Sources */, 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */, F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */, 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */, @@ -8744,7 +8748,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 25.06.20; + version = 25.06.23; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2edf465b6..b76841f89 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "68abbf0622825f008e2e9f70678b2433006e38ef", - "version" : "25.6.20" + "revision" : "f6220486952f912ec3e1224f49f9ab4d141799d0", + "version" : "25.6.23" } }, { diff --git a/ElementX/Sources/Mocks/BannedRoomProxyMock.swift b/ElementX/Sources/Mocks/BannedRoomProxyMock.swift index dcadf3fd2..3d27cb17e 100644 --- a/ElementX/Sources/Mocks/BannedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/BannedRoomProxyMock.swift @@ -19,47 +19,52 @@ struct BannedRoomProxyMockConfiguration { extension BannedRoomProxyMock { @MainActor - convenience init(_ configuration: KnockedRoomProxyMockConfiguration) { + convenience init(_ configuration: BannedRoomProxyMockConfiguration) { self.init() id = configuration.id - info = RoomInfoProxy(roomInfo: .init(configuration)) + info = RoomInfoProxyMock(configuration) } } -extension RoomInfo { - @MainActor init(_ configuration: BannedRoomProxyMockConfiguration) { - self.init(id: configuration.id, - encryptionState: .notEncrypted, - creator: nil, - displayName: configuration.name, - rawName: nil, - topic: nil, - avatarUrl: configuration.avatarURL?.absoluteString, - isDirect: false, - isPublic: false, - isSpace: false, - successorRoom: nil, - isFavourite: false, - canonicalAlias: nil, - alternativeAliases: [], - membership: .knocked, - inviter: nil, - heroes: [], - activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), - invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), - joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), - userPowerLevels: [:], - highlightCount: 0, - notificationCount: 0, - cachedUserDefinedNotificationMode: nil, - hasRoomCall: false, - activeRoomCallParticipants: [], - isMarkedUnread: false, - numUnreadMessages: 0, - numUnreadNotifications: 0, - numUnreadMentions: 0, - pinnedEventIds: [], - joinRule: .knock, - historyVisibility: .shared) +extension RoomInfoProxyMock { + @MainActor convenience init(_ configuration: BannedRoomProxyMockConfiguration) { + self.init() + + id = configuration.id + isEncrypted = false + creator = nil + displayName = configuration.name + rawName = nil + topic = nil + + avatarURL = configuration.avatarURL + + isDirect = false + isPublic = false + isSpace = false + successor = nil + isFavourite = false + canonicalAlias = nil + alternativeAliases = [] + membership = .knocked + inviter = nil + heroes = [] + activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count + invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count + joinedMembersCount = configuration.members.filter { $0.membership == .join }.count + highlightCount = 0 + notificationCount = 0 + cachedUserDefinedNotificationMode = nil + hasRoomCall = false + activeRoomCallParticipants = [] + isMarkedUnread = false + unreadMessagesCount = 0 + unreadNotificationsCount = 0 + unreadMentionsCount = 0 + pinnedEventIDs = [] + joinRule = .knock + historyVisibility = .shared + + powerLevels = RoomPowerLevelsProxyMock(configuration: .init()) } } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 14304db14..cab6beddf 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -6472,11 +6472,11 @@ class InvitedRoomProxyMock: InvitedRoomProxyProtocol, @unchecked Sendable { } } class JoinedRoomProxyMock: JoinedRoomProxyProtocol, @unchecked Sendable { - var infoPublisher: CurrentValuePublisher { + var infoPublisher: CurrentValuePublisher { get { return underlyingInfoPublisher } set(value) { underlyingInfoPublisher = value } } - var underlyingInfoPublisher: CurrentValuePublisher! + var underlyingInfoPublisher: CurrentValuePublisher! var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get { return underlyingMembersPublisher } set(value) { underlyingMembersPublisher = value } @@ -13406,6 +13406,127 @@ class RoomDirectorySearchProxyMock: RoomDirectorySearchProxyProtocol, @unchecked } } } +class RoomInfoProxyMock: RoomInfoProxyProtocol, @unchecked Sendable { + var id: String { + get { return underlyingId } + set(value) { underlyingId = value } + } + var underlyingId: String! + var creator: String? + var displayName: String? + var rawName: String? + var topic: String? + var avatarURL: URL? + var isEncrypted: Bool { + get { return underlyingIsEncrypted } + set(value) { underlyingIsEncrypted = value } + } + var underlyingIsEncrypted: Bool! + var isDirect: Bool { + get { return underlyingIsDirect } + set(value) { underlyingIsDirect = value } + } + var underlyingIsDirect: Bool! + var isPublic: Bool { + get { return underlyingIsPublic } + set(value) { underlyingIsPublic = value } + } + var underlyingIsPublic: Bool! + var isPrivate: Bool { + get { return underlyingIsPrivate } + set(value) { underlyingIsPrivate = value } + } + var underlyingIsPrivate: Bool! + var isSpace: Bool { + get { return underlyingIsSpace } + set(value) { underlyingIsSpace = value } + } + var underlyingIsSpace: Bool! + var isFavourite: Bool { + get { return underlyingIsFavourite } + set(value) { underlyingIsFavourite = value } + } + var underlyingIsFavourite: Bool! + var canonicalAlias: String? + var alternativeAliases: [String] = [] + var membership: Membership { + get { return underlyingMembership } + set(value) { underlyingMembership = value } + } + var underlyingMembership: Membership! + var inviter: RoomMemberProxyProtocol? + var activeMembersCount: Int { + get { return underlyingActiveMembersCount } + set(value) { underlyingActiveMembersCount = value } + } + var underlyingActiveMembersCount: Int! + var invitedMembersCount: Int { + get { return underlyingInvitedMembersCount } + set(value) { underlyingInvitedMembersCount = value } + } + var underlyingInvitedMembersCount: Int! + var joinedMembersCount: Int { + get { return underlyingJoinedMembersCount } + set(value) { underlyingJoinedMembersCount = value } + } + var underlyingJoinedMembersCount: Int! + var highlightCount: Int { + get { return underlyingHighlightCount } + set(value) { underlyingHighlightCount = value } + } + var underlyingHighlightCount: Int! + var notificationCount: Int { + get { return underlyingNotificationCount } + set(value) { underlyingNotificationCount = value } + } + var underlyingNotificationCount: Int! + var cachedUserDefinedNotificationMode: RoomNotificationMode? + var hasRoomCall: Bool { + get { return underlyingHasRoomCall } + set(value) { underlyingHasRoomCall = value } + } + var underlyingHasRoomCall: Bool! + var activeRoomCallParticipants: [String] = [] + var isMarkedUnread: Bool { + get { return underlyingIsMarkedUnread } + set(value) { underlyingIsMarkedUnread = value } + } + var underlyingIsMarkedUnread: Bool! + var unreadMessagesCount: UInt { + get { return underlyingUnreadMessagesCount } + set(value) { underlyingUnreadMessagesCount = value } + } + var underlyingUnreadMessagesCount: UInt! + var unreadNotificationsCount: UInt { + get { return underlyingUnreadNotificationsCount } + set(value) { underlyingUnreadNotificationsCount = value } + } + var underlyingUnreadNotificationsCount: UInt! + var unreadMentionsCount: UInt { + get { return underlyingUnreadMentionsCount } + set(value) { underlyingUnreadMentionsCount = value } + } + var underlyingUnreadMentionsCount: UInt! + var pinnedEventIDs: Set { + get { return underlyingPinnedEventIDs } + set(value) { underlyingPinnedEventIDs = value } + } + var underlyingPinnedEventIDs: Set! + var joinRule: JoinRule? + var historyVisibility: RoomHistoryVisibility { + get { return underlyingHistoryVisibility } + set(value) { underlyingHistoryVisibility = value } + } + var underlyingHistoryVisibility: RoomHistoryVisibility! + var powerLevels: RoomPowerLevelsProxyProtocol { + get { return underlyingPowerLevels } + set(value) { underlyingPowerLevels = value } + } + var underlyingPowerLevels: RoomPowerLevelsProxyProtocol! + var successor: SuccessorRoom? + var heroes: [RoomHero] = [] + +} class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Sendable { var userID: String { get { return underlyingUserID } @@ -13467,6 +13588,658 @@ class RoomPowerLevelsProxyMock: RoomPowerLevelsProxyProtocol, @unchecked Sendabl } var underlyingValues: RoomPowerLevelsValues! + //MARK: - canOwnUser + + var canOwnUserSendMessageUnderlyingCallsCount = 0 + var canOwnUserSendMessageCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserSendMessageUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserSendMessageUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserSendMessageUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserSendMessageUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserSendMessageCalled: Bool { + return canOwnUserSendMessageCallsCount > 0 + } + var canOwnUserSendMessageReceivedMessageType: MessageLikeEventType? + var canOwnUserSendMessageReceivedInvocations: [MessageLikeEventType] = [] + + var canOwnUserSendMessageUnderlyingReturnValue: Bool! + var canOwnUserSendMessageReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserSendMessageUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserSendMessageUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserSendMessageUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserSendMessageUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserSendMessageClosure: ((MessageLikeEventType) -> Bool)? + + func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool { + canOwnUserSendMessageCallsCount += 1 + canOwnUserSendMessageReceivedMessageType = messageType + DispatchQueue.main.async { + self.canOwnUserSendMessageReceivedInvocations.append(messageType) + } + if let canOwnUserSendMessageClosure = canOwnUserSendMessageClosure { + return canOwnUserSendMessageClosure(messageType) + } else { + return canOwnUserSendMessageReturnValue + } + } + //MARK: - canOwnUser + + var canOwnUserSendStateEventUnderlyingCallsCount = 0 + var canOwnUserSendStateEventCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserSendStateEventUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserSendStateEventUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserSendStateEventUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserSendStateEventUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserSendStateEventCalled: Bool { + return canOwnUserSendStateEventCallsCount > 0 + } + var canOwnUserSendStateEventReceivedEvent: StateEventType? + var canOwnUserSendStateEventReceivedInvocations: [StateEventType] = [] + + var canOwnUserSendStateEventUnderlyingReturnValue: Bool! + var canOwnUserSendStateEventReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserSendStateEventUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserSendStateEventUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserSendStateEventUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserSendStateEventUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserSendStateEventClosure: ((StateEventType) -> Bool)? + + func canOwnUser(sendStateEvent event: StateEventType) -> Bool { + canOwnUserSendStateEventCallsCount += 1 + canOwnUserSendStateEventReceivedEvent = event + DispatchQueue.main.async { + self.canOwnUserSendStateEventReceivedInvocations.append(event) + } + if let canOwnUserSendStateEventClosure = canOwnUserSendStateEventClosure { + return canOwnUserSendStateEventClosure(event) + } else { + return canOwnUserSendStateEventReturnValue + } + } + //MARK: - canOwnUserInvite + + var canOwnUserInviteUnderlyingCallsCount = 0 + var canOwnUserInviteCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserInviteUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserInviteUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserInviteUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserInviteUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserInviteCalled: Bool { + return canOwnUserInviteCallsCount > 0 + } + + var canOwnUserInviteUnderlyingReturnValue: Bool! + var canOwnUserInviteReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserInviteUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserInviteUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserInviteUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserInviteUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserInviteClosure: (() -> Bool)? + + func canOwnUserInvite() -> Bool { + canOwnUserInviteCallsCount += 1 + if let canOwnUserInviteClosure = canOwnUserInviteClosure { + return canOwnUserInviteClosure() + } else { + return canOwnUserInviteReturnValue + } + } + //MARK: - canOwnUserRedactOther + + var canOwnUserRedactOtherUnderlyingCallsCount = 0 + var canOwnUserRedactOtherCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserRedactOtherUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserRedactOtherUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserRedactOtherUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserRedactOtherUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserRedactOtherCalled: Bool { + return canOwnUserRedactOtherCallsCount > 0 + } + + var canOwnUserRedactOtherUnderlyingReturnValue: Bool! + var canOwnUserRedactOtherReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserRedactOtherUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserRedactOtherUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserRedactOtherUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserRedactOtherUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserRedactOtherClosure: (() -> Bool)? + + func canOwnUserRedactOther() -> Bool { + canOwnUserRedactOtherCallsCount += 1 + if let canOwnUserRedactOtherClosure = canOwnUserRedactOtherClosure { + return canOwnUserRedactOtherClosure() + } else { + return canOwnUserRedactOtherReturnValue + } + } + //MARK: - canOwnUserRedactOwn + + var canOwnUserRedactOwnUnderlyingCallsCount = 0 + var canOwnUserRedactOwnCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserRedactOwnUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserRedactOwnUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserRedactOwnUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserRedactOwnUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserRedactOwnCalled: Bool { + return canOwnUserRedactOwnCallsCount > 0 + } + + var canOwnUserRedactOwnUnderlyingReturnValue: Bool! + var canOwnUserRedactOwnReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserRedactOwnUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserRedactOwnUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserRedactOwnUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserRedactOwnUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserRedactOwnClosure: (() -> Bool)? + + func canOwnUserRedactOwn() -> Bool { + canOwnUserRedactOwnCallsCount += 1 + if let canOwnUserRedactOwnClosure = canOwnUserRedactOwnClosure { + return canOwnUserRedactOwnClosure() + } else { + return canOwnUserRedactOwnReturnValue + } + } + //MARK: - canOwnUserKick + + var canOwnUserKickUnderlyingCallsCount = 0 + var canOwnUserKickCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserKickUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserKickUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserKickUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserKickUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserKickCalled: Bool { + return canOwnUserKickCallsCount > 0 + } + + var canOwnUserKickUnderlyingReturnValue: Bool! + var canOwnUserKickReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserKickUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserKickUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserKickUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserKickUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserKickClosure: (() -> Bool)? + + func canOwnUserKick() -> Bool { + canOwnUserKickCallsCount += 1 + if let canOwnUserKickClosure = canOwnUserKickClosure { + return canOwnUserKickClosure() + } else { + return canOwnUserKickReturnValue + } + } + //MARK: - canOwnUserBan + + var canOwnUserBanUnderlyingCallsCount = 0 + var canOwnUserBanCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserBanUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserBanUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserBanUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserBanUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserBanCalled: Bool { + return canOwnUserBanCallsCount > 0 + } + + var canOwnUserBanUnderlyingReturnValue: Bool! + var canOwnUserBanReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserBanUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserBanUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserBanUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserBanUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserBanClosure: (() -> Bool)? + + func canOwnUserBan() -> Bool { + canOwnUserBanCallsCount += 1 + if let canOwnUserBanClosure = canOwnUserBanClosure { + return canOwnUserBanClosure() + } else { + return canOwnUserBanReturnValue + } + } + //MARK: - canOwnUserTriggerRoomNotification + + var canOwnUserTriggerRoomNotificationUnderlyingCallsCount = 0 + var canOwnUserTriggerRoomNotificationCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserTriggerRoomNotificationUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserTriggerRoomNotificationUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserTriggerRoomNotificationUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserTriggerRoomNotificationUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserTriggerRoomNotificationCalled: Bool { + return canOwnUserTriggerRoomNotificationCallsCount > 0 + } + + var canOwnUserTriggerRoomNotificationUnderlyingReturnValue: Bool! + var canOwnUserTriggerRoomNotificationReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserTriggerRoomNotificationUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserTriggerRoomNotificationUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserTriggerRoomNotificationUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserTriggerRoomNotificationUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserTriggerRoomNotificationClosure: (() -> Bool)? + + func canOwnUserTriggerRoomNotification() -> Bool { + canOwnUserTriggerRoomNotificationCallsCount += 1 + if let canOwnUserTriggerRoomNotificationClosure = canOwnUserTriggerRoomNotificationClosure { + return canOwnUserTriggerRoomNotificationClosure() + } else { + return canOwnUserTriggerRoomNotificationReturnValue + } + } + //MARK: - canOwnUserPinOrUnpin + + var canOwnUserPinOrUnpinUnderlyingCallsCount = 0 + var canOwnUserPinOrUnpinCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserPinOrUnpinUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserPinOrUnpinUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserPinOrUnpinUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserPinOrUnpinUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserPinOrUnpinCalled: Bool { + return canOwnUserPinOrUnpinCallsCount > 0 + } + + var canOwnUserPinOrUnpinUnderlyingReturnValue: Bool! + var canOwnUserPinOrUnpinReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserPinOrUnpinUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserPinOrUnpinUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserPinOrUnpinUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserPinOrUnpinUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserPinOrUnpinClosure: (() -> Bool)? + + func canOwnUserPinOrUnpin() -> Bool { + canOwnUserPinOrUnpinCallsCount += 1 + if let canOwnUserPinOrUnpinClosure = canOwnUserPinOrUnpinClosure { + return canOwnUserPinOrUnpinClosure() + } else { + return canOwnUserPinOrUnpinReturnValue + } + } + //MARK: - canOwnUserJoinCall + + var canOwnUserJoinCallUnderlyingCallsCount = 0 + var canOwnUserJoinCallCallsCount: Int { + get { + if Thread.isMainThread { + return canOwnUserJoinCallUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserJoinCallUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserJoinCallUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + canOwnUserJoinCallUnderlyingCallsCount = newValue + } + } + } + } + var canOwnUserJoinCallCalled: Bool { + return canOwnUserJoinCallCallsCount > 0 + } + + var canOwnUserJoinCallUnderlyingReturnValue: Bool! + var canOwnUserJoinCallReturnValue: Bool! { + get { + if Thread.isMainThread { + return canOwnUserJoinCallUnderlyingReturnValue + } else { + var returnValue: Bool? = nil + DispatchQueue.main.sync { + returnValue = canOwnUserJoinCallUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + canOwnUserJoinCallUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + canOwnUserJoinCallUnderlyingReturnValue = newValue + } + } + } + } + var canOwnUserJoinCallClosure: (() -> Bool)? + + func canOwnUserJoinCall() -> Bool { + canOwnUserJoinCallCallsCount += 1 + if let canOwnUserJoinCallClosure = canOwnUserJoinCallClosure { + return canOwnUserJoinCallClosure() + } else { + return canOwnUserJoinCallReturnValue + } + } //MARK: - canUser var canUserUserIDSendMessageUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift index e689701e6..8373fb21b 100644 --- a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -24,47 +24,52 @@ extension InvitedRoomProxyMock { self.init() id = configuration.id inviter = configuration.inviter - info = RoomInfoProxy(roomInfo: .init(configuration)) + info = RoomInfoProxyMock(configuration) rejectInvitationReturnValue = .success(()) } } -extension RoomInfo { - @MainActor init(_ configuration: InvitedRoomProxyMockConfiguration) { - self.init(id: configuration.id, - encryptionState: .notEncrypted, - creator: nil, - displayName: configuration.name, - rawName: nil, - topic: nil, - avatarUrl: configuration.avatarURL?.absoluteString, - isDirect: false, - isPublic: false, - isSpace: false, - successorRoom: nil, - isFavourite: false, - canonicalAlias: nil, - alternativeAliases: [], - membership: .knocked, - inviter: .init(configuration.inviter), - heroes: [], - activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), - invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), - joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), - userPowerLevels: [:], - highlightCount: 0, - notificationCount: 0, - cachedUserDefinedNotificationMode: nil, - hasRoomCall: false, - activeRoomCallParticipants: [], - isMarkedUnread: false, - numUnreadMessages: 0, - numUnreadNotifications: 0, - numUnreadMentions: 0, - pinnedEventIds: [], - joinRule: .invite, - historyVisibility: .shared) +extension RoomInfoProxyMock { + @MainActor convenience init(_ configuration: InvitedRoomProxyMockConfiguration) { + self.init() + + id = configuration.id + isEncrypted = false + creator = nil + displayName = configuration.name + rawName = nil + topic = nil + + avatarURL = configuration.avatarURL + + isDirect = false + isPublic = false + isSpace = false + successor = nil + isFavourite = false + canonicalAlias = nil + alternativeAliases = [] + membership = .knocked + inviter = .init(configuration.inviter) + heroes = [] + activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count + invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count + joinedMembersCount = configuration.members.filter { $0.membership == .join }.count + highlightCount = 0 + notificationCount = 0 + cachedUserDefinedNotificationMode = nil + hasRoomCall = false + activeRoomCallParticipants = [] + isMarkedUnread = false + unreadMessagesCount = 0 + unreadNotificationsCount = 0 + unreadMentionsCount = 0 + pinnedEventIDs = [] + joinRule = .invite + historyVisibility = .shared + + powerLevels = RoomPowerLevelsProxyMock(configuration: .init()) } } diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index 4427f0e72..98f45619d 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -62,7 +62,6 @@ extension JoinedRoomProxyMock { ownUserID = configuration.ownUserID - infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher() membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() knockRequestsStatePublisher = CurrentValueSubject(configuration.knockRequestsState).asCurrentValuePublisher() typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() @@ -134,53 +133,52 @@ extension JoinedRoomProxyMock { isVisibleInRoomDirectoryReturnValue = .success(configuration.isVisibleInPublicDirectory) predecessorRoom = configuration.predecessor + + let roomInfoProxyMock = RoomInfoProxyMock(configuration) + roomInfoProxyMock.powerLevels = powerLevelsProxyMock + + infoPublisher = CurrentValueSubject(roomInfoProxyMock).asCurrentValuePublisher() } } -extension RoomInfo { - @MainActor init(_ configuration: JoinedRoomProxyMockConfiguration) { - self.init(id: configuration.id, - encryptionState: configuration.isEncrypted ? .encrypted : .notEncrypted, - creator: nil, - displayName: configuration.name, - rawName: configuration.name, - topic: configuration.topic, - avatarUrl: configuration.avatarURL?.absoluteString, - isDirect: configuration.isDirect, - isPublic: configuration.isPublic, - isSpace: configuration.isSpace, - successorRoom: configuration.successor, - isFavourite: false, - canonicalAlias: configuration.canonicalAlias, - alternativeAliases: configuration.alternativeAliases, - membership: configuration.membership, - inviter: configuration.inviter.map { RoomMember(userId: $0.userID, - displayName: $0.displayName, - avatarUrl: $0.avatarURL?.absoluteString, - membership: $0.membership, - isNameAmbiguous: false, - powerLevel: Int64($0.powerLevel), - normalizedPowerLevel: Int64($0.powerLevel), - isIgnored: $0.isIgnored, - suggestedRoleForPowerLevel: $0.role, - membershipChangeReason: $0.membershipChangeReason) }, - heroes: configuration.heroes.map(RoomHero.init), - activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), - invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), - joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), - userPowerLevels: [:], - highlightCount: 0, - notificationCount: 0, - cachedUserDefinedNotificationMode: .allMessages, - hasRoomCall: configuration.hasOngoingCall, - activeRoomCallParticipants: [], - isMarkedUnread: false, - numUnreadMessages: 0, - numUnreadNotifications: 0, - numUnreadMentions: 0, - pinnedEventIds: Array(configuration.pinnedEventIDs), - joinRule: configuration.joinRule, - historyVisibility: .shared) +extension RoomInfoProxyMock { + @MainActor convenience init(_ configuration: JoinedRoomProxyMockConfiguration) { + self.init() + + id = configuration.id + isEncrypted = configuration.isEncrypted + creator = nil + displayName = configuration.name + rawName = configuration.name + topic = configuration.topic + avatarURL = configuration.avatarURL + isDirect = configuration.isDirect + isPublic = configuration.isPublic + isSpace = configuration.isSpace + successor = configuration.successor + isFavourite = false + canonicalAlias = configuration.canonicalAlias + alternativeAliases = configuration.alternativeAliases + membership = configuration.membership + inviter = configuration.inviter + heroes = configuration.heroes.map(RoomHero.init) + activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count + invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count + joinedMembersCount = configuration.members.filter { $0.membership == .join }.count + highlightCount = 0 + notificationCount = 0 + cachedUserDefinedNotificationMode = .allMessages + hasRoomCall = configuration.hasOngoingCall + activeRoomCallParticipants = [] + isMarkedUnread = false + unreadMessagesCount = 0 + unreadNotificationsCount = 0 + unreadMentionsCount = 0 + pinnedEventIDs = configuration.pinnedEventIDs + joinRule = configuration.joinRule + historyVisibility = .shared + + powerLevels = RoomPowerLevelsProxyMock(configuration: configuration.powerLevelsConfiguration) } } diff --git a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift index 969e9dc47..ed4244769 100644 --- a/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/KnockedRoomProxyMock.swift @@ -22,44 +22,49 @@ extension KnockedRoomProxyMock { convenience init(_ configuration: KnockedRoomProxyMockConfiguration) { self.init() id = configuration.id - info = RoomInfoProxy(roomInfo: .init(configuration)) + info = RoomInfoProxyMock(configuration) } } -extension RoomInfo { - @MainActor init(_ configuration: KnockedRoomProxyMockConfiguration) { - self.init(id: configuration.id, - encryptionState: .notEncrypted, - creator: nil, - displayName: configuration.name, - rawName: nil, - topic: nil, - avatarUrl: configuration.avatarURL?.absoluteString, - isDirect: false, - isPublic: false, - isSpace: false, - successorRoom: nil, - isFavourite: false, - canonicalAlias: nil, - alternativeAliases: [], - membership: .knocked, - inviter: nil, - heroes: [], - activeMembersCount: UInt64(configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count), - invitedMembersCount: UInt64(configuration.members.filter { $0.membership == .invite }.count), - joinedMembersCount: UInt64(configuration.members.filter { $0.membership == .join }.count), - userPowerLevels: [:], - highlightCount: 0, - notificationCount: 0, - cachedUserDefinedNotificationMode: nil, - hasRoomCall: false, - activeRoomCallParticipants: [], - isMarkedUnread: false, - numUnreadMessages: 0, - numUnreadNotifications: 0, - numUnreadMentions: 0, - pinnedEventIds: [], - joinRule: .knock, - historyVisibility: .shared) +extension RoomInfoProxyMock { + @MainActor convenience init(_ configuration: KnockedRoomProxyMockConfiguration) { + self.init() + + id = configuration.id + isEncrypted = false + creator = nil + displayName = configuration.name + rawName = nil + topic = nil + + avatarURL = configuration.avatarURL + + isDirect = false + isPublic = false + isSpace = false + successor = nil + isFavourite = false + canonicalAlias = nil + alternativeAliases = [] + membership = .knocked + inviter = nil + heroes = [] + activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count + invitedMembersCount = configuration.members.filter { $0.membership == .invite }.count + joinedMembersCount = configuration.members.filter { $0.membership == .join }.count + highlightCount = 0 + notificationCount = 0 + cachedUserDefinedNotificationMode = nil + hasRoomCall = false + activeRoomCallParticipants = [] + isMarkedUnread = false + unreadMessagesCount = 0 + unreadNotificationsCount = 0 + unreadMentionsCount = 0 + pinnedEventIDs = [] + joinRule = .knock + historyVisibility = .shared + + powerLevels = RoomPowerLevelsProxyMock(configuration: .init()) } } diff --git a/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift b/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift index f9a3cdd50..b0f5a2f64 100644 --- a/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift @@ -26,6 +26,17 @@ extension RoomPowerLevelsProxyMock { underlyingValues = RoomPowerLevelsValues.mock + canOwnUserSendMessageReturnValue = configuration.canUserSendMessage + canOwnUserSendStateEventReturnValue = configuration.canUserSendState + canOwnUserInviteReturnValue = configuration.canUserInvite + canOwnUserRedactOtherReturnValue = configuration.canUserRedactOther + canOwnUserRedactOwnReturnValue = configuration.canUserRedactOwn + canOwnUserKickReturnValue = configuration.canUserKick + canOwnUserBanReturnValue = configuration.canUserBan + canOwnUserTriggerRoomNotificationReturnValue = configuration.canUserTriggerRoomNotification + canOwnUserPinOrUnpinReturnValue = configuration.canUserPin + canOwnUserJoinCallReturnValue = configuration.canUserJoinCall + canUserUserIDSendMessageReturnValue = .success(configuration.canUserSendMessage) canUserUserIDSendStateEventReturnValue = .success(configuration.canUserSendState) canUserInviteUserIDReturnValue = .success(configuration.canUserInvite) diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift index 30fd1d4b1..59ba9819d 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift @@ -140,7 +140,7 @@ enum TimelineMediaPreviewItem: Equatable { case loading(Loading) /// Wraps a media file and title to be previewed with QuickLook. - class Media: NSObject, QLPreviewItem, Identifiable { + @Observable class Media: NSObject, QLPreviewItem, Identifiable { fileprivate(set) var timelineItem: EventBasedMessageTimelineItemProtocol var fileHandle: MediaFileHandleProxy? var downloadError: Error? diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift index 1cfe2d947..517915a69 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewDetailsView.swift @@ -180,27 +180,24 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie if case let .media(mediaItem) = viewModel.state.currentItem { TimelineMediaPreviewDetailsView(item: mediaItem, context: viewModel.context, sheetHeight: $sheetHeight) .previewDisplayName("Image") - .snapshotPreferences(expect: viewModel.context.$viewState.map { state in - state.currentItemActions?.secondaryActions.contains(.redact) ?? false - }) + .snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream()) } if case let .media(mediaItem) = loadingViewModel.state.currentItem { TimelineMediaPreviewDetailsView(item: mediaItem, context: loadingViewModel.context, sheetHeight: $sheetHeight) .previewDisplayName("Loading") - .snapshotPreferences(expect: loadingViewModel.context.$viewState.map { state in - state.currentItemActions?.secondaryActions.contains(.redact) ?? false - }) } if case let .media(mediaItem) = unknownTypeViewModel.state.currentItem { TimelineMediaPreviewDetailsView(item: mediaItem, context: unknownTypeViewModel.context, sheetHeight: $sheetHeight) .previewDisplayName("Unknown type") + .snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream()) } if case let .media(mediaItem) = presentedOnRoomViewModel.state.currentItem { TimelineMediaPreviewDetailsView(item: mediaItem, context: presentedOnRoomViewModel.context, sheetHeight: $sheetHeight) .previewDisplayName("Incoming on Room") + .snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream()) } } diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift index 5f4dcde90..a7561f67d 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift @@ -27,9 +27,6 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn super.init(initialViewState: KnockRequestsListScreenViewState(), mediaProvider: mediaProvider) updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) - Task { - await updatePermissions() - } setupSubscriptions() } @@ -189,7 +186,6 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn .receive(on: DispatchQueue.main) .sink { [weak self] roomInfo in self?.updateRoomInfo(roomInfo: roomInfo) - Task { await self?.updatePermissions() } } .store(in: &cancellables) @@ -214,20 +210,17 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn .store(in: &cancellables) } - private func updateRoomInfo(roomInfo: RoomInfoProxy) { + private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) { switch roomInfo.joinRule { case .knock, .knockRestricted: state.isKnockableRoom = true default: state.isKnockableRoom = false } - } - - private func updatePermissions() async { - let powerLevels = try? await roomProxy.powerLevels().get() - state.canAccept = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true - state.canDecline = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true - state.canBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true + + state.canAccept = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true + state.canDecline = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true } private static let loadingIndicatorIdentifier = "\(KnockRequestsListScreenViewModel.self)-Loading" diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift index 76cce137b..81d53d7dd 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenViewModel.swift @@ -39,12 +39,7 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe avatarURL: roomAvatar, bindings: .init(name: roomName ?? "", topic: roomTopic ?? "")), mediaProvider: mediaProvider) - Task { - let powerLevels = try? await roomProxy.powerLevels().get() - state.canEditAvatar = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == .some(true) - state.canEditName = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == .some(true) - state.canEditTopic = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == .some(true) - } + updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) } // MARK: - Public @@ -91,6 +86,12 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe // MARK: - Private + private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) { + state.canEditAvatar = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == .some(true) + state.canEditName = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == .some(true) + state.canEditTopic = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == .some(true) + } + private func saveRoomDetails() { Task { let userIndicatorID = UUID().uuidString diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 54da808d3..00a892d6c 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -97,7 +97,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } updateRoomInfo(roomProxy.infoPublisher.value) - Task { await updatePowerLevelPermissions() } setupRoomSubscription() Task { await fetchMembersIfNeeded() } @@ -183,7 +182,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] roomInfo in self?.updateRoomInfo(roomInfo) - Task { await self?.updatePowerLevelPermissions() } } .store(in: &cancellables) @@ -206,7 +204,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr .store(in: &cancellables) } - private func updateRoomInfo(_ roomInfo: RoomInfoProxy) { + private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) { state.isEncrypted = roomInfo.isEncrypted state.isDirect = roomInfo.isDirect state.bindings.isFavourite = roomInfo.isFavourite @@ -225,6 +223,18 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr default: state.isKnockableRoom = false } + + state.canEditRoomName = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true + state.canEditRoomTopic = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true + state.canEditRoomAvatar = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == true + state.canInviteUsers = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true + state.canKickUsers = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canBanUsers = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true + state.canJoinCall = (try? roomInfo.powerLevels.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true + + Task { + state.canEditRolesOrPermissions = await (try? roomProxy.suggestedRole(for: roomProxy.ownUserID).get()) == .administrator + } } private func fetchMembersIfNeeded() async { @@ -280,19 +290,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } } - private func updatePowerLevelPermissions() async { - state.canEditRolesOrPermissions = await (try? roomProxy.suggestedRole(for: roomProxy.ownUserID).get()) == .administrator - - let powerLevels = try? await roomProxy.powerLevels().get() - state.canEditRoomName = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomName).get()) == true - state.canEditRoomTopic = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomTopic).get()) == true - state.canEditRoomAvatar = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendStateEvent: .roomAvatar).get()) == true - state.canInviteUsers = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true - state.canKickUsers = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true - state.canBanUsers = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true - state.canJoinCall = (try? powerLevels?.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true - } - private func setupNotificationSettingsSubscription() { notificationSettingsProxy.callbacks .receive(on: DispatchQueue.main) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index f672dc0c4..d9eed682c 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -338,13 +338,13 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { static var previews: some View { RoomDetailsScreen(context: genericRoomViewModel.context) .snapshotPreferences(expect: genericRoomViewModel.context.$viewState.map { state in - state.shortcuts.contains(.invite) + state.canSeeSecurityAndPrivacy == true }) .previewDisplayName("Generic Room") RoomDetailsScreen(context: simpleRoomViewModel.context) .snapshotPreferences(expect: simpleRoomViewModel.context.$viewState.map { state in - state.shortcuts.contains(.invite) + state.canSeeSecurityAndPrivacy == true }) .previewDisplayName("Simple Room") diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index bedd562e3..6ade10fef 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -99,10 +99,10 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe bannedMembers: roomMembersDetails.bannedMembers, bindings: state.bindings) - let powerLevels = try? await roomProxy.powerLevels().get() - self.state.canInviteUsers = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true - self.state.canKickUsers = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true - self.state.canBanUsers = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true + let powerLevels = roomProxy.infoPublisher.value.powerLevels + self.state.canInviteUsers = (try? powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true + self.state.canKickUsers = (try? powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true + self.state.canBanUsers = (try? powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true hideLoadingIndicator(Self.updateStateLoadingIndicatorIdentifier) } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 8c31587dc..82c894a15 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -39,12 +39,12 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM // Automatically update the room permissions roomProxy.infoPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - Task { await self?.updatePermissions() } + .sink { [weak self] roomInfo in + self?.updateRoomInfo(roomInfo: roomInfo) } .store(in: &cancellables) - Task { await updatePermissions() } + updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) } // MARK: - Public @@ -113,13 +113,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM // MARK: - Permissions - private func updatePermissions() async { - switch await roomProxy.powerLevels() { - case .success(let powerLevels): - state.permissions = .init(powerLevels: powerLevels.values) - case .failure: - break - } + private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) { + state.permissions = .init(powerLevels: roomInfo.powerLevels.values) } private func editPermissions(group: RoomRolesAndPermissionsScreenPermissionsGroup) { diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index 3810305ca..c2531485f 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -47,9 +47,7 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { self?.suggestionTriggerSubject.value != nil ? .milliseconds(500) : .milliseconds(0) } - Task { - canMentionAllUsers = await (try? roomProxy.powerLevels().get().canUserTriggerRoomNotification(userID: roomProxy.ownUserID).get()) == true - } + canMentionAllUsers = (try? roomProxy.infoPublisher.value.powerLevels.canUserTriggerRoomNotification(userID: roomProxy.ownUserID).get()) == true } func processTextMessage(_ textMessage: String, selectedRange: NSRange) { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index ffe52ec20..2136d8d3f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -332,7 +332,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } - private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async { + private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) async { state.hasSuccessor = roomInfo.successor != nil let pinnedEventIDs = roomInfo.pinnedEventIDs @@ -348,12 +348,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.isKnockableRoom = false } - let powerLevels = try? await roomProxy.powerLevels().get() - state.canSendMessage = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true - state.canJoinCall = (try? powerLevels?.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true - state.canAcceptKnocks = (try? powerLevels?.canUserInvite(userID: roomProxy.ownUserID).get()) == true - state.canDeclineKnocks = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true - state.canBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true + state.canSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true + state.canJoinCall = (try? roomInfo.powerLevels.canUserJoinCall(userID: roomProxy.ownUserID).get()) == true + state.canAcceptKnocks = (try? roomInfo.powerLevels.canUserInvite(userID: roomProxy.ownUserID).get()) == true + state.canDeclineKnocks = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true } private func setupPinnedEventsTimelineItemProviderIfNeeded() { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift index 25ebaf94f..316442ca3 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenViewModel.swift @@ -29,14 +29,12 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi return } - await self?.handleRoomInfoUpdate(roomInfo) + self?.handleRoomInfoUpdate(roomInfo) } } .store(in: &cancellables) - Task { - await handleRoomInfoUpdate(roomProxy.infoPublisher.value) - } + handleRoomInfoUpdate(roomProxy.infoPublisher.value) } // MARK: - Public @@ -64,7 +62,7 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi // MARK: - Private - private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async { - state.canSendMessage = await (try? roomProxy.powerLevels().get().canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true + private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxyProtocol) { + state.canSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true } } diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index ed0db6106..dd753b1b8 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -119,9 +119,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { setupSubscriptions() setupDirectRoomSubscriptionsIfNeeded() - // Set initial values for redacting from the macOS context menu. - Task { await updatePermissions() } - + updateRoomInfo(roomInfo: roomProxy.infoPublisher.value) + state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in guard let self else { return nil @@ -403,14 +402,15 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } } - private func updatePermissions() async { - let powerLevels = try? await roomProxy.powerLevels().get() - state.canCurrentUserSendMessage = (try? powerLevels?.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true - state.canCurrentUserRedactOthers = (try? powerLevels?.canUserRedactOther(userID: roomProxy.ownUserID).get()) == true - state.canCurrentUserRedactSelf = (try? powerLevels?.canUserRedactOwn(userID: roomProxy.ownUserID).get()) == true - state.canCurrentUserPin = (try? powerLevels?.canUserPinOrUnpin(userID: roomProxy.ownUserID).get()) == true - state.canCurrentUserKick = (try? powerLevels?.canUserKick(userID: roomProxy.ownUserID).get()) == true - state.canCurrentUserBan = (try? powerLevels?.canUserBan(userID: roomProxy.ownUserID).get()) == true + private func updateRoomInfo(roomInfo: RoomInfoProxyProtocol) { + state.pinnedEventIDs = roomInfo.pinnedEventIDs + + state.canCurrentUserSendMessage = (try? roomInfo.powerLevels.canUser(userID: roomProxy.ownUserID, sendMessage: .roomMessage).get()) == true + state.canCurrentUserRedactOthers = (try? roomInfo.powerLevels.canUserRedactOther(userID: roomProxy.ownUserID).get()) == true + state.canCurrentUserRedactSelf = (try? roomInfo.powerLevels.canUserRedactOwn(userID: roomProxy.ownUserID).get()) == true + state.canCurrentUserPin = (try? roomInfo.powerLevels.canUserPinOrUnpin(userID: roomProxy.ownUserID).get()) == true + state.canCurrentUserKick = (try? roomInfo.powerLevels.canUserKick(userID: roomProxy.ownUserID).get()) == true + state.canCurrentUserBan = (try? roomInfo.powerLevels.canUserBan(userID: roomProxy.ownUserID).get()) == true } private func setupSubscriptions() { @@ -445,8 +445,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { guard !Task.isCancelled else { return } - self?.state.pinnedEventIDs = roomInfo.pinnedEventIDs - await self?.updatePermissions() + + self?.updateRoomInfo(roomInfo: roomInfo) } } .store(in: &cancellables) @@ -487,10 +487,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { case .displayMediaUploadPreviewScreen(let url): actionsSubject.send(.displayMediaUploadPreviewScreen(url: url)) case .showActionMenu(let actionMenuInfo): - Task { - await self.updatePermissions() - self.state.bindings.actionMenuInfo = actionMenuInfo - } + self.state.bindings.actionMenuInfo = actionMenuInfo case .showDebugInfo(let debugInfo): state.bindings.debugInfo = debugInfo case .viewInRoomTimeline(let eventID): @@ -996,13 +993,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } } -private extension RoomInfoProxy { - /// Checks if the other person left the room in a direct chat - var isUserAloneInDirectRoom: Bool { - isDirect && activeMembersCount == 1 - } -} - // MARK: - Mocks extension TimelineViewModel { diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index fb7058ab9..0e1fa3074 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -46,8 +46,8 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { let timeline: TimelineProxyProtocol - private let infoSubject: CurrentValueSubject - var infoPublisher: CurrentValuePublisher { + private let infoSubject: CurrentValueSubject + var infoPublisher: CurrentValuePublisher { infoSubject.asCurrentValuePublisher() } @@ -137,7 +137,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener { [weak self] roomInfo in MXLog.info("Received room info update") - self?.infoSubject.send(.init(roomInfo: roomInfo)) + self?.infoSubject.send(RoomInfoProxy(roomInfo: roomInfo)) }) } diff --git a/ElementX/Sources/Services/Room/RoomInfoProxy.swift b/ElementX/Sources/Services/Room/RoomInfoProxy.swift index d5e6bacb4..c4af9e950 100644 --- a/ElementX/Sources/Services/Room/RoomInfoProxy.swift +++ b/ElementX/Sources/Services/Room/RoomInfoProxy.swift @@ -8,20 +8,7 @@ import Foundation import MatrixRustSDK -protocol BaseRoomInfoProxyProtocol { - var id: String { get } - var displayName: String? { get } - var avatar: RoomAvatar { get } - var topic: String? { get } - var canonicalAlias: String? { get } - var avatarURL: URL? { get } - var activeMembersCount: Int { get } - var joinedMembersCount: Int { get } - var isDirect: Bool { get } - var isSpace: Bool { get } -} - -struct RoomInfoProxy: BaseRoomInfoProxyProtocol { +struct RoomInfoProxy: RoomInfoProxyProtocol { let roomInfo: RoomInfo var id: String { roomInfo.id } @@ -31,18 +18,6 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol { var topic: String? { roomInfo.topic } /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. var avatarURL: URL? { roomInfo.avatarUrl.flatMap(URL.init) } - /// The room's avatar info for use in a ``RoomAvatarImage``. - var avatar: RoomAvatar { - guard successor == nil else { - return .tombstoned - } - - if isDirect, avatarURL == nil, heroes.count == 1 { - return .heroes(heroes.map(UserProfileProxy.init)) - } - - return .room(id: id, name: displayName, avatarURL: avatarURL) - } // Here we're assuming unknown rooms are unencrypted. // Fortunately https://github.com/matrix-org/matrix-rust-sdk/pull/4778 makes that very much of an edge case and we @@ -54,28 +29,17 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol { var isDirect: Bool { roomInfo.isDirect } var isPublic: Bool { roomInfo.isPublic } - // A room might be non public but also not private given the fact that the join rule might be missing or unsupported. - var isPrivate: Bool { - switch roomInfo.joinRule { - case .invite, .knock, .restricted, .knockRestricted: - true - default: - false - } - } - var isSpace: Bool { roomInfo.isSpace } var successor: SuccessorRoom? { roomInfo.successorRoom } var isFavourite: Bool { roomInfo.isFavourite } var canonicalAlias: String? { roomInfo.canonicalAlias } var alternativeAliases: [String] { roomInfo.alternativeAliases } var membership: Membership { roomInfo.membership } - var inviter: RoomMemberProxy? { roomInfo.inviter.map(RoomMemberProxy.init) } + var inviter: RoomMemberProxyProtocol? { roomInfo.inviter.map(RoomMemberProxy.init) } var heroes: [RoomHero] { roomInfo.heroes } var activeMembersCount: Int { Int(roomInfo.activeMembersCount) } var invitedMembersCount: Int { Int(roomInfo.invitedMembersCount) } var joinedMembersCount: Int { Int(roomInfo.joinedMembersCount) } - var userPowerLevels: [String: Int] { roomInfo.userPowerLevels.mapValues(Int.init) } var highlightCount: Int { Int(roomInfo.highlightCount) } var notificationCount: Int { Int(roomInfo.notificationCount) } var cachedUserDefinedNotificationMode: RoomNotificationMode? { roomInfo.cachedUserDefinedNotificationMode } @@ -89,41 +53,14 @@ struct RoomInfoProxy: BaseRoomInfoProxyProtocol { var joinRule: JoinRule? { roomInfo.joinRule } var historyVisibility: RoomHistoryVisibility { roomInfo.historyVisibility } - /// Find the first alias that matches the given homeserver - /// - Parameters: - /// - serverName: the homserver in question - /// - useFallback: whether to return any alias if none match - func firstAliasMatching(serverName: String?, useFallback: Bool) -> String? { - guard let serverName else { return nil } - - // Check if the canonical alias matches the homeserver - if let canonicalAlias = roomInfo.canonicalAlias, - canonicalAlias.range(of: serverName) != nil { - return canonicalAlias - } - - // Otherwise check the alternative aliases and return the first one that matches - if let matchingAlternativeAlias = roomInfo.alternativeAliases.filter({ $0.range(of: serverName) != nil }).first { - return matchingAlternativeAlias - } - - guard useFallback else { - return nil - } - - // Or just return the canonical alias if any - if let canonicalAlias = roomInfo.canonicalAlias { - return canonicalAlias - } - - // And finally return whatever the first alternative alias is - return roomInfo.alternativeAliases.first - } + var powerLevels: RoomPowerLevelsProxyProtocol { RoomPowerLevelsProxy(roomInfo.powerLevels) } } struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { let roomPreviewInfo: RoomPreviewInfo + let successor: SuccessorRoom? = nil + var id: String { roomPreviewInfo.roomId } var displayName: String? { roomPreviewInfo.name } var heroes: [RoomHero] { roomPreviewInfo.heroes ?? [] } @@ -137,12 +74,4 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { var joinRule: JoinRule { roomPreviewInfo.joinRule } var membership: Membership? { roomPreviewInfo.membership } - - /// The room's avatar info for use in a ``RoomAvatarImage``. - var avatar: RoomAvatar { - if isDirect, avatarURL == nil, heroes.count == 1 { - return .heroes(heroes.map(UserProfileProxy.init)) - } - return .room(id: id, name: displayName, avatarURL: avatarURL) - } } diff --git a/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift new file mode 100644 index 000000000..fc6147f1a --- /dev/null +++ b/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift @@ -0,0 +1,130 @@ +// +// 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 + +protocol BaseRoomInfoProxyProtocol { + var id: String { get } + var displayName: String? { get } + var topic: String? { get } + var canonicalAlias: String? { get } + var avatarURL: URL? { get } + var activeMembersCount: Int { get } + var joinedMembersCount: Int { get } + var isDirect: Bool { get } + var isSpace: Bool { get } + + var successor: SuccessorRoom? { get } + var heroes: [RoomHero] { get } +} + +// sourcery: AutoMockable +protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol { + var id: String { get } + var creator: String? { get } + var displayName: String? { get } + var rawName: String? { get } + var topic: String? { get } + /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. + var avatarURL: URL? { get } + + var isEncrypted: Bool { get } + var isDirect: Bool { get } + var isPublic: Bool { get } + + var isPrivate: Bool { get } + + var isSpace: Bool { get } + + var isFavourite: Bool { get } + var canonicalAlias: String? { get } + var alternativeAliases: [String] { get } + var membership: Membership { get } + var inviter: RoomMemberProxyProtocol? { get } + + var activeMembersCount: Int { get } + var invitedMembersCount: Int { get } + var joinedMembersCount: Int { get } + var highlightCount: Int { get } + var notificationCount: Int { get } + var cachedUserDefinedNotificationMode: RoomNotificationMode? { get } + var hasRoomCall: Bool { get } + var activeRoomCallParticipants: [String] { get } + var isMarkedUnread: Bool { get } + var unreadMessagesCount: UInt { get } + var unreadNotificationsCount: UInt { get } + var unreadMentionsCount: UInt { get } + var pinnedEventIDs: Set { get } + var joinRule: JoinRule? { get } + var historyVisibility: RoomHistoryVisibility { get } + + var powerLevels: RoomPowerLevelsProxyProtocol { get } +} + +extension BaseRoomInfoProxyProtocol { + /// The room's avatar info for use in a ``RoomAvatarImage``. + var avatar: RoomAvatar { + guard successor == nil else { + return .tombstoned + } + + if isDirect, avatarURL == nil, heroes.count == 1 { + return .heroes(heroes.map(UserProfileProxy.init)) + } + + return .room(id: id, name: displayName, avatarURL: avatarURL) + } +} + +extension RoomInfoProxyProtocol { + /// A room might be non public but also not private given the fact that the join rule might be missing or unsupported. + var isPrivate: Bool { + switch joinRule { + case .invite, .knock, .restricted, .knockRestricted: + true + default: + false + } + } + + /// Checks if the other person left the room in a direct chat + var isUserAloneInDirectRoom: Bool { + isDirect && activeMembersCount == 1 + } + + /// Find the first alias that matches the given homeserver + /// - Parameters: + /// - serverName: the homserver in question + /// - useFallback: whether to return any alias if none match + func firstAliasMatching(serverName: String?, useFallback: Bool) -> String? { + guard let serverName else { return nil } + + // Check if the canonical alias matches the homeserver + if let canonicalAlias, + canonicalAlias.range(of: serverName) != nil { + return canonicalAlias + } + + // Otherwise check the alternative aliases and return the first one that matches + if let matchingAlternativeAlias = alternativeAliases.filter({ $0.range(of: serverName) != nil }).first { + return matchingAlternativeAlias + } + + guard useFallback else { + return nil + } + + // Or just return the canonical alias if any + if let canonicalAlias { + return canonicalAlias + } + + // And finally return whatever the first alternative alias is + return alternativeAliases.first + } +} diff --git a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift index 3c8d0e4a3..ac25cdafa 100644 --- a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift @@ -11,6 +11,17 @@ import MatrixRustSDK protocol RoomPowerLevelsProxyProtocol { var values: RoomPowerLevelsValues { get } + func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool + func canOwnUser(sendStateEvent event: StateEventType) -> Bool + func canOwnUserInvite() -> Bool + func canOwnUserRedactOther() -> Bool + func canOwnUserRedactOwn() -> Bool + func canOwnUserKick() -> Bool + func canOwnUserBan() -> Bool + func canOwnUserTriggerRoomNotification() -> Bool + func canOwnUserPinOrUnpin() -> Bool + func canOwnUserJoinCall() -> Bool + func canUser(userID: String, sendMessage messageType: MessageLikeEventType) -> Result func canUser(userID: String, sendStateEvent event: StateEventType) -> Result func canUserInvite(userID: String) -> Result diff --git a/ElementX/Sources/Services/Room/RoomPowerLevelsProxy.swift b/ElementX/Sources/Services/Room/RoomPowerLevelsProxy.swift index 887f59628..ae000502e 100644 --- a/ElementX/Sources/Services/Room/RoomPowerLevelsProxy.swift +++ b/ElementX/Sources/Services/Room/RoomPowerLevelsProxy.swift @@ -18,6 +18,46 @@ struct RoomPowerLevelsProxy: RoomPowerLevelsProxyProtocol { powerLevels.values() } + func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool { + powerLevels.canOwnUserSendMessage(message: messageType) + } + + func canOwnUser(sendStateEvent stateEventType: StateEventType) -> Bool { + powerLevels.canOwnUserSendState(stateEvent: stateEventType) + } + + func canOwnUserInvite() -> Bool { + powerLevels.canOwnUserInvite() + } + + func canOwnUserRedactOther() -> Bool { + powerLevels.canOwnUserRedactOther() + } + + func canOwnUserRedactOwn() -> Bool { + powerLevels.canOwnUserRedactOwn() + } + + func canOwnUserKick() -> Bool { + powerLevels.canOwnUserKick() + } + + func canOwnUserBan() -> Bool { + powerLevels.canOwnUserBan() + } + + func canOwnUserTriggerRoomNotification() -> Bool { + powerLevels.canOwnUserTriggerRoomNotification() + } + + func canOwnUserPinOrUnpin() -> Bool { + powerLevels.canOwnUserPinUnpin() + } + + func canOwnUserJoinCall() -> Bool { + powerLevels.canOwnUserSendState(stateEvent: .callMember) + } + func canUser(userID: String, sendMessage messageType: MessageLikeEventType) -> Result { do { return try .success(powerLevels.canUserSendMessage(userId: userID, message: messageType)) diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 2032083f9..dec47786a 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -64,7 +64,7 @@ enum KnockRequestsState { // sourcery: AutoMockable protocol JoinedRoomProxyProtocol: RoomProxyProtocol { - var infoPublisher: CurrentValuePublisher { get } + var infoPublisher: CurrentValuePublisher { get } var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } diff --git a/UnitTests/Sources/RoomDetailsViewModelTests.swift b/UnitTests/Sources/RoomDetailsViewModelTests.swift index 85025d1aa..4a9081e94 100644 --- a/UnitTests/Sources/RoomDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsViewModelTests.swift @@ -355,14 +355,24 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testCanEditAvatar() async { let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice] - roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init()) - roomProxyMock.powerLevelsReturnValue = .success(powerLevelsMock) + let configuration = JoinedRoomProxyMockConfiguration(name: "Test", + isDirect: false, + isPublic: false, + members: mockedMembers) - powerLevelsMock.canUserUserIDSendStateEventClosure = { _, event in + roomProxyMock = JoinedRoomProxyMock(configuration) + + let powerLevelsProxyMock = RoomPowerLevelsProxyMock(configuration: .init()) + powerLevelsProxyMock.canUserUserIDSendStateEventClosure = { _, event in .success(event == .roomAvatar) } + roomProxyMock.powerLevelsReturnValue = .success(powerLevelsProxyMock) + + let roomInfoProxyMock = RoomInfoProxyMock(configuration) + roomInfoProxyMock.powerLevels = powerLevelsProxyMock + roomProxyMock.infoPublisher = CurrentValueSubject(roomInfoProxyMock).asCurrentValuePublisher() + viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), mediaProvider: MediaProviderMock(configuration: .init()), @@ -383,14 +393,24 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testCanEditName() async { let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice] - roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init()) - roomProxyMock.powerLevelsReturnValue = .success(powerLevelsMock) + let configuration = JoinedRoomProxyMockConfiguration(name: "Test", + isDirect: false, + isPublic: false, + members: mockedMembers) - powerLevelsMock.canUserUserIDSendStateEventClosure = { _, event in + roomProxyMock = JoinedRoomProxyMock(configuration) + + let powerLevelsProxyMock = RoomPowerLevelsProxyMock(configuration: .init()) + powerLevelsProxyMock.canUserUserIDSendStateEventClosure = { _, event in .success(event == .roomName) } + roomProxyMock.powerLevelsReturnValue = .success(powerLevelsProxyMock) + + let roomInfoProxyMock = RoomInfoProxyMock(configuration) + roomInfoProxyMock.powerLevels = powerLevelsProxyMock + roomProxyMock.infoPublisher = CurrentValueSubject(roomInfoProxyMock).asCurrentValuePublisher() + viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), mediaProvider: MediaProviderMock(configuration: .init()), @@ -411,14 +431,24 @@ class RoomDetailsScreenViewModelTests: XCTestCase { func testCanEditTopic() async { let mockedMembers: [RoomMemberProxyMock] = [.mockMe, .mockBob, .mockAlice] - roomProxyMock = JoinedRoomProxyMock(.init(name: "Test", isDirect: false, isPublic: false, members: mockedMembers)) - let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init()) - roomProxyMock.powerLevelsReturnValue = .success(powerLevelsMock) + let configuration = JoinedRoomProxyMockConfiguration(name: "Test", + isDirect: false, + isPublic: false, + members: mockedMembers) - powerLevelsMock.canUserUserIDSendStateEventClosure = { _, event in + roomProxyMock = JoinedRoomProxyMock(configuration) + + let powerLevelsProxyMock = RoomPowerLevelsProxyMock(configuration: .init()) + powerLevelsProxyMock.canUserUserIDSendStateEventClosure = { _, event in .success(event == .roomTopic) } + roomProxyMock.powerLevelsReturnValue = .success(powerLevelsProxyMock) + + let roomInfoProxyMock = RoomInfoProxyMock(configuration) + roomInfoProxyMock.powerLevels = powerLevelsProxyMock + roomProxyMock.infoPublisher = CurrentValueSubject(roomInfoProxyMock).asCurrentValuePublisher() + viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, clientProxy: ClientProxyMock(.init()), mediaProvider: MediaProviderMock(configuration: .init()), diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index 584dea42a..7fa8c6dcd 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -274,7 +274,7 @@ class RoomFlowCoordinatorTests: XCTestCase { var configuration = JoinedRoomProxyMockConfiguration() let roomProxy = JoinedRoomProxyMock(configuration) - let roomInfoSubject = CurrentValueSubject(.init(roomInfo: .init(configuration))) + let roomInfoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) roomProxy.infoPublisher = roomInfoSubject.asCurrentValuePublisher() clientProxy.roomForIdentifierClosure = { _ in @@ -288,7 +288,7 @@ class RoomFlowCoordinatorTests: XCTestCase { } configuration.membership = .left - roomInfoSubject.send(.init(roomInfo: .init(configuration))) + roomInfoSubject.send(RoomInfoProxyMock(configuration)) try await fulfillment.fulfill() } diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index ae0804e5b..b91fb9cb7 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -26,7 +26,7 @@ class RoomScreenViewModelTests: XCTestCase { func testPinnedEventsBanner() async throws { var configuration = JoinedRoomProxyMockConfiguration() let timelineSubject = PassthroughSubject() - let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) let roomProxyMock = JoinedRoomProxyMock(configuration) // setup a way to inject the mock of the pinned events timeline roomProxyMock.pinnedEventsTimelineClosure = { @@ -63,7 +63,7 @@ class RoomScreenViewModelTests: XCTestCase { viewState.pinnedEventsBannerState.count == 2 } configuration.pinnedEventIDs = ["test1", "test2"] - infoSubject.send(.init(roomInfo: RoomInfo(configuration))) + infoSubject.send(RoomInfoProxyMock(configuration)) try await deferred.fulfill() XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) @@ -165,7 +165,7 @@ class RoomScreenViewModelTests: XCTestCase { func testRoomInfoUpdate() async throws { var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false) - let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) let roomProxyMock = JoinedRoomProxyMock(configuration) let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init()) @@ -206,7 +206,7 @@ class RoomScreenViewModelTests: XCTestCase { viewState.hasOngoingCall } - infoSubject.send(.init(roomInfo: RoomInfo(configuration))) + infoSubject.send(RoomInfoProxyMock(configuration)) try await deferred.fulfill() } diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 00bda843a..e963b471c 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -468,7 +468,7 @@ class TimelineViewModelTests: XCTestCase { var configuration = JoinedRoomProxyMockConfiguration(name: "", pinnedEventIDs: .init(["test1"])) let roomProxyMock = JoinedRoomProxyMock(configuration) - let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, @@ -489,7 +489,7 @@ class TimelineViewModelTests: XCTestCase { let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.pinnedEventIDs == ["test1", "test2"] } - infoSubject.send(.init(roomInfo: RoomInfo(configuration))) + infoSubject.send(RoomInfoProxyMock(configuration)) try await deferred.fulfill() } @@ -497,7 +497,7 @@ class TimelineViewModelTests: XCTestCase { let configuration = JoinedRoomProxyMockConfiguration(name: "", powerLevelsConfiguration: .init(canUserPin: true)) let roomProxyMock = JoinedRoomProxyMock(configuration) - let infoSubject = CurrentValueSubject(.init(roomInfo: RoomInfo(configuration))) + let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = TimelineViewModel(roomProxy: roomProxyMock, @@ -518,14 +518,17 @@ class TimelineViewModelTests: XCTestCase { } try await deferred.fulfill() - let powerLevelsMock = RoomPowerLevelsProxyMock(configuration: .init()) - powerLevelsMock.canUserPinOrUnpinUserIDReturnValue = .success(false) - roomProxyMock.powerLevelsReturnValue = .success(powerLevelsMock) + let powerLevelsProxyMock = RoomPowerLevelsProxyMock(configuration: .init()) + powerLevelsProxyMock.canUserPinOrUnpinUserIDReturnValue = .success(false) + roomProxyMock.powerLevelsReturnValue = .success(powerLevelsProxyMock) + + let roomInfoProxyMock = RoomInfoProxyMock(configuration) + roomInfoProxyMock.powerLevels = powerLevelsProxyMock deferred = deferFulfillment(viewModel.context.$viewState) { value in !value.canCurrentUserPin } - infoSubject.send(.init(roomInfo: RoomInfo(configuration))) + infoSubject.send(roomInfoProxyMock) try await deferred.fulfill() } diff --git a/project.yml b/project.yml index 4ab9af86e..dcff44461 100644 --- a/project.yml +++ b/project.yml @@ -65,7 +65,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 25.06.20 + exactVersion: 25.06.23 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios