Replace individual RoomProxy properties with a stored RoomInfo. (#3445)

* Store RoomInfo updates in JoinedRoomProxy and read from them directly.

* Remove all RoomProxy properties that were reading from the RoomInfo.
This commit is contained in:
Doug 2024-10-28 12:29:31 +00:00 committed by GitHub
parent 7c28d9709f
commit 7c75498b4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 353 additions and 580 deletions

View File

@ -412,6 +412,7 @@
5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; }; 5BC6C4ADFE7F2A795ECDE130 /* SecureBackupKeyBackupScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; }; 5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; }; 5C164551F7D26E24F09083D3 /* StaticLocationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C616D90B1E2F033CAA325439 /* StaticLocationScreenViewModelProtocol.swift */; };
5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */; };
5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; }; 5D27B6537591471A42C89027 /* EmoteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */; };
5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; }; 5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.swift */; };
5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; }; 5D53AE9342A4C06B704247ED /* MediaLoaderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A02406480C351B8C6E0682C /* MediaLoaderProtocol.swift */; };
@ -1495,6 +1496,7 @@
40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = "<group>"; }; 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageMediaManager.swift; sourceTree = "<group>"; };
40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = "<group>"; }; 40316EFFEAC7B206EE9A55AE /* SecureBackupScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupScreenViewModelTests.swift; sourceTree = "<group>"; };
406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; }; 406C90AF8C3E98DF5D4E5430 /* ElementCallServiceConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceConstants.swift; sourceTree = "<group>"; };
40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomInfoProxy.swift; sourceTree = "<group>"; };
40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; }; 40B21E611DADDEF00307E7AC /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = "<group>"; }; 4100DDE6BF3C566AB66B80CC /* MentionSuggestionItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionSuggestionItemView.swift; sourceTree = "<group>"; };
4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = "<group>"; }; 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenFooterView.swift; sourceTree = "<group>"; };
@ -3211,6 +3213,7 @@
07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */, 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */,
858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */, 858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */,
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */, B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */,
40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */,
974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */, 974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */,
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */, 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */,
2C0F49BD446849654C0D24E0 /* RoomMember */, 2C0F49BD446849654C0D24E0 /* RoomMember */,
@ -6775,6 +6778,7 @@
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */, 42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */,
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */, D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */,
9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */, 9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */,
5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */,
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */, 8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */,
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */, F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */,
4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */, 4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */,

View File

@ -584,7 +584,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineItemFactory: timelineItemFactory) timelineItemFactory: timelineItemFactory)
self.timelineController = timelineController self.timelineController = timelineController
analytics.trackViewRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace) analytics.trackViewRoom(isDM: roomProxy.infoPublisher.value.isDirect, isSpace: roomProxy.infoPublisher.value.isSpace)
let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy) let completionSuggestionService = CompletionSuggestionService(roomProxy: roomProxy)
@ -681,7 +681,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
await storeAndSubscribeToRoomProxy(roomProxy) await storeAndSubscribeToRoomProxy(roomProxy)
stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.presentRoom(focussedEvent: nil), userInfo: EventUserInfo(animated: animated))
analytics.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount)) analytics.trackJoinedRoom(isDM: roomProxy.infoPublisher.value.isDirect,
isSpace: roomProxy.infoPublisher.value.isSpace,
activeMemberCount: UInt(roomProxy.infoPublisher.value.activeMembersCount))
} else { } else {
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated)) stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
} }

View File

@ -5824,67 +5824,21 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol {
} }
} }
class InvitedRoomProxyMock: InvitedRoomProxyProtocol { class InvitedRoomProxyMock: InvitedRoomProxyProtocol {
var inviterCallsCount = 0 var info: RoomInfoProxy {
var inviterCalled: Bool { get { return underlyingInfo }
return inviterCallsCount > 0 set(value) { underlyingInfo = value }
} }
var underlyingInfo: RoomInfoProxy!
var inviter: RoomMemberProxyProtocol? {
get async {
inviterCallsCount += 1
if let inviterClosure = inviterClosure {
return await inviterClosure()
} else {
return underlyingInviter
}
}
}
var underlyingInviter: RoomMemberProxyProtocol?
var inviterClosure: (() async -> RoomMemberProxyProtocol?)?
var id: String { var id: String {
get { return underlyingId } get { return underlyingId }
set(value) { underlyingId = value } set(value) { underlyingId = value }
} }
var underlyingId: String! var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String { var ownUserID: String {
get { return underlyingOwnUserID } get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value } set(value) { underlyingOwnUserID = value }
} }
var underlyingOwnUserID: String! var underlyingOwnUserID: String!
var name: String?
var topic: String?
var avatar: RoomAvatar {
get { return underlyingAvatar }
set(value) { underlyingAvatar = value }
}
var underlyingAvatar: RoomAvatar!
var avatarURL: URL?
var isPublic: Bool {
get { return underlyingIsPublic }
set(value) { underlyingIsPublic = value }
}
var underlyingIsPublic: Bool!
var isDirect: Bool {
get { return underlyingIsDirect }
set(value) { underlyingIsDirect = value }
}
var underlyingIsDirect: Bool!
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var activeMembersCount: Int {
get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
//MARK: - rejectInvitation //MARK: - rejectInvitation
@ -6021,46 +5975,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingIsEncrypted = value } set(value) { underlyingIsEncrypted = value }
} }
var underlyingIsEncrypted: Bool! var underlyingIsEncrypted: Bool!
var isFavouriteCallsCount = 0 var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
var isFavouriteCalled: Bool { get { return underlyingInfoPublisher }
return isFavouriteCallsCount > 0 set(value) { underlyingInfoPublisher = value }
} }
var underlyingInfoPublisher: CurrentValuePublisher<RoomInfoProxy, Never>!
var isFavourite: Bool {
get async {
isFavouriteCallsCount += 1
if let isFavouriteClosure = isFavouriteClosure {
return await isFavouriteClosure()
} else {
return underlyingIsFavourite
}
}
}
var underlyingIsFavourite: Bool!
var isFavouriteClosure: (() async -> Bool)?
var pinnedEventIDsCallsCount = 0
var pinnedEventIDsCalled: Bool {
return pinnedEventIDsCallsCount > 0
}
var pinnedEventIDs: Set<String> {
get async {
pinnedEventIDsCallsCount += 1
if let pinnedEventIDsClosure = pinnedEventIDsClosure {
return await pinnedEventIDsClosure()
} else {
return underlyingPinnedEventIDs
}
}
}
var underlyingPinnedEventIDs: Set<String>!
var pinnedEventIDsClosure: (() async -> Set<String>)?
var hasOngoingCall: Bool {
get { return underlyingHasOngoingCall }
set(value) { underlyingHasOngoingCall = value }
}
var underlyingHasOngoingCall: Bool!
var activeRoomCallParticipants: [String] = []
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
get { return underlyingMembersPublisher } get { return underlyingMembersPublisher }
set(value) { underlyingMembersPublisher = value } set(value) { underlyingMembersPublisher = value }
@ -6076,11 +5995,6 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingIdentityStatusChangesPublisher = value } set(value) { underlyingIdentityStatusChangesPublisher = value }
} }
var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>! var underlyingIdentityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never>!
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> {
get { return underlyingActionsPublisher }
set(value) { underlyingActionsPublisher = value }
}
var underlyingActionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never>!
var timeline: TimelineProxyProtocol { var timeline: TimelineProxyProtocol {
get { return underlyingTimeline } get { return underlyingTimeline }
set(value) { underlyingTimeline = value } set(value) { underlyingTimeline = value }
@ -6108,45 +6022,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingId = value } set(value) { underlyingId = value }
} }
var underlyingId: String! var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String { var ownUserID: String {
get { return underlyingOwnUserID } get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value } set(value) { underlyingOwnUserID = value }
} }
var underlyingOwnUserID: String! var underlyingOwnUserID: String!
var name: String?
var topic: String?
var avatar: RoomAvatar {
get { return underlyingAvatar }
set(value) { underlyingAvatar = value }
}
var underlyingAvatar: RoomAvatar!
var avatarURL: URL?
var isPublic: Bool {
get { return underlyingIsPublic }
set(value) { underlyingIsPublic = value }
}
var underlyingIsPublic: Bool!
var isDirect: Bool {
get { return underlyingIsDirect }
set(value) { underlyingIsDirect = value }
}
var underlyingIsDirect: Bool!
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var activeMembersCount: Int {
get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
//MARK: - subscribeForUpdates //MARK: - subscribeForUpdates
@ -9752,50 +9632,21 @@ class KeychainControllerMock: KeychainControllerProtocol {
} }
} }
class KnockedRoomProxyMock: KnockedRoomProxyProtocol { class KnockedRoomProxyMock: KnockedRoomProxyProtocol {
var info: RoomInfoProxy {
get { return underlyingInfo }
set(value) { underlyingInfo = value }
}
var underlyingInfo: RoomInfoProxy!
var id: String { var id: String {
get { return underlyingId } get { return underlyingId }
set(value) { underlyingId = value } set(value) { underlyingId = value }
} }
var underlyingId: String! var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String { var ownUserID: String {
get { return underlyingOwnUserID } get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value } set(value) { underlyingOwnUserID = value }
} }
var underlyingOwnUserID: String! var underlyingOwnUserID: String!
var name: String?
var topic: String?
var avatar: RoomAvatar {
get { return underlyingAvatar }
set(value) { underlyingAvatar = value }
}
var underlyingAvatar: RoomAvatar!
var avatarURL: URL?
var isPublic: Bool {
get { return underlyingIsPublic }
set(value) { underlyingIsPublic = value }
}
var underlyingIsPublic: Bool!
var isDirect: Bool {
get { return underlyingIsDirect }
set(value) { underlyingIsDirect = value }
}
var underlyingIsDirect: Bool!
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var activeMembersCount: Int {
get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
//MARK: - cancelKnock //MARK: - cancelKnock
@ -12934,45 +12785,11 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingId = value } set(value) { underlyingId = value }
} }
var underlyingId: String! var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String { var ownUserID: String {
get { return underlyingOwnUserID } get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value } set(value) { underlyingOwnUserID = value }
} }
var underlyingOwnUserID: String! var underlyingOwnUserID: String!
var name: String?
var topic: String?
var avatar: RoomAvatar {
get { return underlyingAvatar }
set(value) { underlyingAvatar = value }
}
var underlyingAvatar: RoomAvatar!
var avatarURL: URL?
var isPublic: Bool {
get { return underlyingIsPublic }
set(value) { underlyingIsPublic = value }
}
var underlyingIsPublic: Bool!
var isDirect: Bool {
get { return underlyingIsDirect }
set(value) { underlyingIsDirect = value }
}
var underlyingIsDirect: Bool!
var isSpace: Bool {
get { return underlyingIsSpace }
set(value) { underlyingIsSpace = value }
}
var underlyingIsSpace: Bool!
var joinedMembersCount: Int {
get { return underlyingJoinedMembersCount }
set(value) { underlyingJoinedMembersCount = value }
}
var underlyingJoinedMembersCount: Int!
var activeMembersCount: Int {
get { return underlyingActiveMembersCount }
set(value) { underlyingActiveMembersCount = value }
}
var underlyingActiveMembersCount: Int!
} }
class RoomSummaryProviderMock: RoomSummaryProviderProtocol { class RoomSummaryProviderMock: RoomSummaryProviderProtocol {

View File

@ -7,6 +7,7 @@
import Combine import Combine
import Foundation import Foundation
import MatrixRustSDK
@MainActor @MainActor
struct InvitedRoomProxyMockConfiguration { struct InvitedRoomProxyMockConfiguration {
@ -22,10 +23,55 @@ extension InvitedRoomProxyMock {
convenience init(_ configuration: InvitedRoomProxyMockConfiguration) { convenience init(_ configuration: InvitedRoomProxyMockConfiguration) {
self.init() self.init()
id = configuration.id id = configuration.id
name = configuration.name info = RoomInfoProxy(roomInfo: .init(configuration))
avatarURL = configuration.avatarURL }
avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic. }
underlyingInviter = configuration.inviter
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count extension RoomInfo {
@MainActor init(_ configuration: InvitedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
creator: nil,
displayName: configuration.name,
rawName: nil,
topic: nil,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: false,
isPublic: false,
isSpace: false,
isTombstoned: false,
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: [])
}
}
private extension RoomMember {
init(_ proxy: RoomMemberProxyProtocol) {
self.init(userId: proxy.userID,
displayName: proxy.displayName,
avatarUrl: proxy.avatarURL?.absoluteString,
membership: proxy.membership,
isNameAmbiguous: proxy.disambiguatedDisplayName != proxy.displayName,
powerLevel: Int64(proxy.powerLevel),
normalizedPowerLevel: Int64(proxy.powerLevel),
isIgnored: proxy.isIgnored,
suggestedRoleForPowerLevel: proxy.role)
} }
} }

View File

@ -7,6 +7,7 @@
import Combine import Combine
import Foundation import Foundation
import MatrixRustSDK
enum RoomProxyMockError: Error { enum RoomProxyMockError: Error {
case generic case generic
@ -46,18 +47,7 @@ extension JoinedRoomProxyMock {
self.init() self.init()
id = configuration.id id = configuration.id
name = configuration.name
topic = configuration.topic
avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic.
avatarURL = configuration.avatarURL
isDirect = configuration.isDirect
isSpace = configuration.isSpace
isPublic = configuration.isPublic
isEncrypted = configuration.isEncrypted isEncrypted = configuration.isEncrypted
hasOngoingCall = configuration.hasOngoingCall
canonicalAlias = configuration.canonicalAlias
underlyingPinnedEventIDs = configuration.pinnedEventIDs
let timeline = TimelineProxyMock() let timeline = TimelineProxyMock()
timeline.sendMessageEventContentReturnValue = .success(()) timeline.sendMessageEventContentReturnValue = .success(())
@ -78,15 +68,12 @@ extension JoinedRoomProxyMock {
ownUserID = configuration.ownUserID ownUserID = configuration.ownUserID
infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher()
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher() identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
joinedMembersCount = configuration.members.filter { $0.membership == .join }.count
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
updateMembersClosure = { } updateMembersClosure = { }
underlyingActionsPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
setNameClosure = { _ in .success(()) } setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) } setTopicClosure = { _ in .success(()) }
getMemberUserIDClosure = { [weak self] userID in getMemberUserIDClosure = { [weak self] userID in
@ -102,7 +89,6 @@ extension JoinedRoomProxyMock {
flagAsUnreadReturnValue = .success(()) flagAsUnreadReturnValue = .success(())
markAsReadReceiptTypeReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(())
underlyingIsFavourite = false
flagAsFavouriteReturnValue = .success(()) flagAsFavouriteReturnValue = .success(())
powerLevelsReturnValue = .success(.mock) powerLevelsReturnValue = .success(.mock)
@ -154,3 +140,46 @@ extension JoinedRoomProxyMock {
clearDraftReturnValue = .success(()) clearDraftReturnValue = .success(())
} }
} }
extension RoomInfo {
@MainActor init(_ configuration: JoinedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
creator: nil,
displayName: configuration.name,
rawName: configuration.name,
topic: configuration.topic,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: configuration.isDirect,
isPublic: configuration.isPublic,
isSpace: configuration.isSpace,
isTombstoned: false,
isFavourite: false,
canonicalAlias: configuration.canonicalAlias,
alternativeAliases: [],
membership: .joined,
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) },
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: .allMessages,
hasRoomCall: configuration.hasOngoingCall,
activeRoomCallParticipants: [],
isMarkedUnread: false,
numUnreadMessages: 0,
numUnreadNotifications: 0,
numUnreadMentions: 0,
pinnedEventIds: Array(configuration.pinnedEventIDs))
}
}

View File

@ -7,6 +7,7 @@
import Combine import Combine
import Foundation import Foundation
import MatrixRustSDK
@MainActor @MainActor
struct KnockedRoomProxyMockConfiguration { struct KnockedRoomProxyMockConfiguration {
@ -21,9 +22,41 @@ extension KnockedRoomProxyMock {
convenience init(_ configuration: KnockedRoomProxyMockConfiguration) { convenience init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init() self.init()
id = configuration.id id = configuration.id
name = configuration.name info = RoomInfoProxy(roomInfo: .init(configuration))
avatarURL = configuration.avatarURL }
avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic. }
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
extension RoomInfo {
@MainActor init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init(id: configuration.id,
creator: nil,
displayName: configuration.name,
rawName: nil,
topic: nil,
avatarUrl: configuration.avatarURL?.absoluteString,
isDirect: false,
isPublic: false,
isSpace: false,
isTombstoned: false,
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: [])
} }
} }

View File

@ -170,7 +170,8 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol
return return
} }
await elementCallService.setupCallSession(roomID: roomProxy.id, roomDisplayName: roomProxy.roomTitle) await elementCallService.setupCallSession(roomID: roomProxy.id,
roomDisplayName: roomProxy.infoPublisher.value.displayName ?? roomProxy.id)
_ = await roomProxy.sendCallNotificationIfNeeded() _ = await roomProxy.sendCallNotificationIfNeeded()

View File

@ -360,10 +360,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return return
} }
if roomProxy.isPublic { if roomProxy.infoPublisher.value.isPublic {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .public) state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .public)
} else { } else {
state.bindings.leaveRoomAlertItem = if roomProxy.joinedMembersCount > 1 { state.bindings.leaveRoomAlertItem = if roomProxy.infoPublisher.value.joinedMembersCount > 1 {
LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .private) LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .private)
} else { } else {
LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty) LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty)
@ -408,7 +408,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
switch await roomProxy.acceptInvitation() { switch await roomProxy.acceptInvitation() {
case .success: case .success:
actionsSubject.send(.presentRoom(roomIdentifier: roomID)) actionsSubject.send(.presentRoom(roomIdentifier: roomID))
analyticsService.trackJoinedRoom(isDM: roomProxy.isDirect, isSpace: roomProxy.isSpace, activeMemberCount: UInt(roomProxy.activeMembersCount)) analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect,
isSpace: roomProxy.info.isSpace,
activeMemberCount: UInt(roomProxy.info.activeMembersCount))
case .failure: case .failure:
displayError() displayError()
} }

View File

@ -74,7 +74,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
defer { defer {
hideLoadingIndicator() hideLoadingIndicator()
Task { await updateRoomDetails() } updateRoomDetails()
} }
await updateRoom() await updateRoom()
@ -82,7 +82,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) { switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) {
case .success(let roomPreviewDetails): case .success(let roomPreviewDetails):
self.roomPreviewDetails = roomPreviewDetails self.roomPreviewDetails = roomPreviewDetails
await updateRoomDetails() updateRoomDetails()
case .failure(.roomPreviewIsPrivate): case .failure(.roomPreviewIsPrivate):
break // Handled by the mode, we don't need an error indicator. break // Handled by the mode, we don't need an error indicator.
case .failure: case .failure:
@ -97,32 +97,32 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
// take priority over the preview one. // take priority over the preview one.
if let room = await clientProxy.roomForIdentifier(roomID) { if let room = await clientProxy.roomForIdentifier(roomID) {
self.room = room self.room = room
await updateRoomDetails() updateRoomDetails()
} }
} }
private func updateRoomDetails() async { private func updateRoomDetails() {
var roomProxy: RoomProxyProtocol? var roomInfo: RoomInfoProxy?
var inviter: RoomInviterDetails? var inviter: RoomInviterDetails?
switch room { switch room {
case .joined(let joinedRoomProxy): case .joined(let joinedRoomProxy):
roomProxy = joinedRoomProxy roomInfo = joinedRoomProxy.infoPublisher.value
case .invited(let invitedRoomProxy): case .invited(let invitedRoomProxy):
inviter = await invitedRoomProxy.inviter.flatMap(RoomInviterDetails.init) inviter = invitedRoomProxy.info.inviter.flatMap(RoomInviterDetails.init)
roomProxy = invitedRoomProxy roomInfo = invitedRoomProxy.info
case .knocked(let knockedRoomProxy): case .knocked(let knockedRoomProxy):
roomProxy = knockedRoomProxy roomInfo = knockedRoomProxy.info
default: default:
break break
} }
let name = roomProxy?.name ?? roomPreviewDetails?.name let name = roomInfo?.displayName ?? roomPreviewDetails?.name
state.roomDetails = JoinRoomScreenRoomDetails(name: name, state.roomDetails = JoinRoomScreenRoomDetails(name: name,
topic: roomProxy?.topic ?? roomPreviewDetails?.topic, topic: roomInfo?.topic ?? roomPreviewDetails?.topic,
canonicalAlias: roomProxy?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias, canonicalAlias: roomInfo?.canonicalAlias ?? roomPreviewDetails?.canonicalAlias,
avatar: roomProxy?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL), avatar: roomInfo?.avatar ?? .room(id: roomID, name: name ?? "", avatarURL: roomPreviewDetails?.avatarURL),
memberCount: UInt(roomProxy?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)), memberCount: UInt(roomInfo?.activeMembersCount ?? Int(roomPreviewDetails?.memberCount ?? 0)),
inviter: inviter) inviter: inviter)
updateMode() updateMode()

View File

@ -143,7 +143,8 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
let demotingUpdates = state.membersToDemote.map { ($0.id, Int64(0)) } let demotingUpdates = state.membersToDemote.map { ($0.id, Int64(0)) }
// A task we can await until the room's info gets modified with the new power levels. // A task we can await until the room's info gets modified with the new power levels.
let infoTask = Task { await roomProxy.actionsPublisher.values.first { $0 == .roomInfoUpdate } } // Note: Ignore the first value as the publisher is backed by a current value subject.
let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } }
switch await roomProxy.updatePowerLevelsForUsers(promotingUpdates + demotingUpdates) { switch await roomProxy.updatePowerLevelsForUsers(promotingUpdates + demotingUpdates) {
case .success: case .success:

View File

@ -28,9 +28,9 @@ class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDe
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
let roomAvatar = roomProxy.avatarURL let roomAvatar = roomProxy.infoPublisher.value.avatarURL
let roomName = roomProxy.name let roomName = roomProxy.infoPublisher.value.displayName
let roomTopic = roomProxy.topic let roomTopic = roomProxy.infoPublisher.value.topic
super.init(initialViewState: RoomDetailsEditScreenViewState(roomID: roomProxy.id, super.init(initialViewState: RoomDetailsEditScreenViewState(roomID: roomProxy.id,
initialAvatarURL: roomAvatar, initialAvatarURL: roomAvatar,

View File

@ -63,14 +63,14 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.attributedStringBuilder = attributedStringBuilder self.attributedStringBuilder = attributedStringBuilder
self.appSettings = appSettings self.appSettings = appSettings
let topic = attributedStringBuilder.fromPlain(roomProxy.topic) let topic = attributedStringBuilder.fromPlain(roomProxy.infoPublisher.value.topic)
super.init(initialViewState: .init(details: roomProxy.details, super.init(initialViewState: .init(details: roomProxy.details,
isEncrypted: roomProxy.isEncrypted, isEncrypted: roomProxy.isEncrypted,
isDirect: roomProxy.isDirect, isDirect: roomProxy.infoPublisher.value.isDirect,
topic: topic, topic: topic,
topicSummary: topic?.unattributedStringByReplacingNewlinesWithSpaces(), topicSummary: topic?.unattributedStringByReplacingNewlinesWithSpaces(),
joinedMembersCount: roomProxy.joinedMembersCount, joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
notificationSettingsState: .loading, notificationSettingsState: .loading,
bindings: .init()), bindings: .init()),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
@ -96,7 +96,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
} }
} }
updateRoomInfo() updateRoomInfo(roomProxy.infoPublisher.value)
Task { await updatePowerLevelPermissions() } Task { await updatePowerLevelPermissions() }
setupRoomSubscription() setupRoomSubscription()
@ -124,7 +124,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty) state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty)
return return
} }
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: roomProxy.isPublic ? .public : .private) state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id,
isDM: roomProxy.isEncryptedOneToOneRoom,
state: roomProxy.infoPublisher.value.isPublic ? .public : .private)
case .confirmLeave: case .confirmLeave:
Task { await leaveRoom() } Task { await leaveRoom() }
case .processTapIgnore: case .processTapIgnore:
@ -164,27 +166,23 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Private // MARK: - Private
private func setupRoomSubscription() { private func setupRoomSubscription() {
roomProxy.actionsPublisher roomProxy.infoPublisher
.filter { $0 == .roomInfoUpdate }
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true) .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in .sink { [weak self] roomInfo in
self?.updateRoomInfo() self?.updateRoomInfo(roomInfo)
Task { await self?.updatePowerLevelPermissions() } Task { await self?.updatePowerLevelPermissions() }
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
private func updateRoomInfo() { private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
state.details = roomProxy.details state.details = roomProxy.details
let topic = attributedStringBuilder.fromPlain(roomProxy.topic) let topic = attributedStringBuilder.fromPlain(roomInfo.topic)
state.topic = topic state.topic = topic
state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces() state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
state.joinedMembersCount = roomProxy.joinedMembersCount state.joinedMembersCount = roomInfo.joinedMembersCount
state.bindings.isFavourite = roomInfo.isFavourite
Task {
state.bindings.isFavourite = await roomProxy.isFavourite
}
} }
private func fetchMembersIfNeeded() async { private func fetchMembersIfNeeded() async {
@ -240,7 +238,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
do { do {
let notificationMode = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id, let notificationMode = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted, isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2) isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
state.notificationSettingsState = .loaded(settings: notificationMode) state.notificationSettingsState = .loaded(settings: notificationMode)
} catch { } catch {
state.notificationSettingsState = .error state.notificationSettingsState = .error
@ -258,7 +256,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
do { do {
try await notificationSettingsProxy.unmuteRoom(roomId: roomProxy.id, try await notificationSettingsProxy.unmuteRoom(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted, isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2) isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
} catch { } catch {
state.bindings.alertInfo = AlertInfo(id: .alert, state.bindings.alertInfo = AlertInfo(id: .alert,
title: L10n.commonError, title: L10n.commonError,
@ -352,7 +350,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// We don't actually know the mime type here, assume it's an image. // We don't actually know the mime type here, assume it's an image.
if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) { if case let .success(file) = await mediaProvider.loadFileFromSource(.init(url: url, mimeType: "image/jpeg")) {
state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.roomTitle) state.bindings.mediaPreviewItem = MediaPreviewItem(file: file, title: roomProxy.infoPublisher.value.displayName)
} }
} }
} }

View File

@ -32,7 +32,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
self.userIndicatorController = userIndicatorController self.userIndicatorController = userIndicatorController
self.analytics = analytics self.analytics = analytics
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount, super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
bindings: .init(mode: initialMode)), bindings: .init(mode: initialMode)),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
@ -92,7 +92,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
let members = members.sorted() let members = members.sorted()
let roomMembersDetails = await buildMembersDetails(members: members) let roomMembersDetails = await buildMembersDetails(members: members)
self.members = members self.members = members
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount, self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers, joinedMembers: roomMembersDetails.joinedMembers,
invitedMembers: roomMembersDetails.invitedMembers, invitedMembers: roomMembersDetails.invitedMembers,
bannedMembers: roomMembersDetails.bannedMembers, bannedMembers: roomMembersDetails.bannedMembers,

View File

@ -26,11 +26,11 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
let bindings = RoomNotificationSettingsScreenViewStateBindings() let bindings = RoomNotificationSettingsScreenViewStateBindings()
self.notificationSettingsProxy = notificationSettingsProxy self.notificationSettingsProxy = notificationSettingsProxy
self.roomProxy = roomProxy self.roomProxy = roomProxy
let navigationTitle = displayAsUserDefinedRoomSettings ? roomProxy.roomTitle : L10n.screenRoomDetailsNotificationTitle let navigationTitle = displayAsUserDefinedRoomSettings ? roomProxy.infoPublisher.value.displayName : L10n.screenRoomDetailsNotificationTitle
let customSettingsSectionHeader = displayAsUserDefinedRoomSettings ? L10n.screenRoomNotificationSettingsRoomCustomSettingsTitle : L10n.screenRoomNotificationSettingsCustomSettingsTitle let customSettingsSectionHeader = displayAsUserDefinedRoomSettings ? L10n.screenRoomNotificationSettingsRoomCustomSettingsTitle : L10n.screenRoomNotificationSettingsCustomSettingsTitle
super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings, super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings,
displayAsUserDefinedRoomSettings: displayAsUserDefinedRoomSettings, displayAsUserDefinedRoomSettings: displayAsUserDefinedRoomSettings,
navigationTitle: navigationTitle, navigationTitle: navigationTitle ?? L10n.screenRoomDetailsNotificationTitle,
customSettingsSectionHeader: customSettingsSectionHeader)) customSettingsSectionHeader: customSettingsSectionHeader))
setupNotificationSettingsSubscription() setupNotificationSettingsSubscription()
@ -80,7 +80,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
// `isOneToOne` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. // `isOneToOne` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members.
let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id, let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted, isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2) isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
guard !Task.isCancelled else { return } guard !Task.isCancelled else { return }
state.notificationSettingsState = .loaded(settings: settings) state.notificationSettingsState = .loaded(settings: settings)
if !state.isRestoringDefaultSetting { if !state.isRestoringDefaultSetting {

View File

@ -37,8 +37,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
updateMembers(roomProxy.membersPublisher.value) updateMembers(roomProxy.membersPublisher.value)
// Automatically update the room permissions // Automatically update the room permissions
roomProxy.actionsPublisher roomProxy.infoPublisher
.filter { $0 == .roomInfoUpdate }
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] _ in .sink { [weak self] _ in
Task { await self?.updatePermissions() } Task { await self?.updatePermissions() }
@ -93,7 +92,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
showSavingIndicator() showSavingIndicator()
// A task we can await until the room's info gets modified with the new power levels. // A task we can await until the room's info gets modified with the new power levels.
let infoTask = Task { await roomProxy.actionsPublisher.values.first { $0 == .roomInfoUpdate } } // Note: Ignore the first value as the publisher is backed by a current value subject.
let infoTask = Task { await roomProxy.infoPublisher.dropFirst().values.first { _ in true } }
switch await roomProxy.updatePowerLevelsForUsers([(userID: roomProxy.ownUserID, powerLevel: role.rustPowerLevel)]) { switch await roomProxy.updatePowerLevelsForUsers([(userID: roomProxy.ownUserID, powerLevel: role.rustPowerLevel)]) {
case .success: case .success:

View File

@ -51,7 +51,7 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol {
membersSuggestion membersSuggestion
.insert(SuggestionItem.allUsers(item: .init(id: PillConstants.atRoom, .insert(SuggestionItem.allUsers(item: .init(id: PillConstants.atRoom,
displayName: PillConstants.everyone, displayName: PillConstants.everyone,
avatarURL: self.roomProxy.avatarURL, avatarURL: self.roomProxy.infoPublisher.value.avatarURL,
range: suggestionTrigger.range)), at: 0) range: suggestionTrigger.range)), at: 0)
} }

View File

@ -68,14 +68,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
self.initialSelectedPinnedEventID = initialSelectedPinnedEventID self.initialSelectedPinnedEventID = initialSelectedPinnedEventID
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
super.init(initialViewState: .init(roomTitle: roomProxy.roomTitle, super.init(initialViewState: .init(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id,
roomAvatar: roomProxy.avatar, roomAvatar: roomProxy.infoPublisher.value.avatar,
hasOngoingCall: roomProxy.hasOngoingCall, hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall,
bindings: .init()), bindings: .init()),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
Task { Task {
await handleRoomInfoUpdate() await handleRoomInfoUpdate(roomProxy.infoPublisher.value)
} }
setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher) setupSubscriptions(ongoingCallRoomIDPublisher: ongoingCallRoomIDPublisher)
@ -118,26 +118,25 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) { private func setupSubscriptions(ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never>) {
let roomInfoSubscription = roomProxy let roomInfoSubscription = roomProxy
.actionsPublisher .infoPublisher
.filter { $0 == .roomInfoUpdate }
roomInfoSubscription roomInfoSubscription
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in .sink { [weak self] roomInfo in
guard let self else { return } guard let self else { return }
state.roomTitle = roomProxy.roomTitle state.roomTitle = roomInfo.displayName ?? roomProxy.id
state.roomAvatar = roomProxy.avatar state.roomAvatar = roomInfo.avatar
state.hasOngoingCall = roomProxy.hasOngoingCall state.hasOngoingCall = roomInfo.hasRoomCall
} }
.store(in: &cancellables) .store(in: &cancellables)
Task { [weak self] in Task { [weak self] in
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values {
guard !Task.isCancelled else { guard !Task.isCancelled else {
return return
} }
await self?.handleRoomInfoUpdate() await self?.handleRoomInfoUpdate(roomInfo)
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -230,8 +229,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
} }
} }
private func handleRoomInfoUpdate() async { private func handleRoomInfoUpdate(_ roomInfo: RoomInfoProxy) async {
let pinnedEventIDs = await roomProxy.pinnedEventIDs let pinnedEventIDs = roomInfo.pinnedEventIDs
// Only update the loading state of the banner // Only update the loading state of the banner
if state.pinnedEventsBannerState.isLoading { if state.pinnedEventsBannerState.isLoading {
state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count) state.pinnedEventsBannerState = .loading(numbersOfEvents: pinnedEventIDs.count)

View File

@ -131,7 +131,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie
for roomSummary in filteredRoomsSummary { for roomSummary in filteredRoomsSummary {
guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomSummary.id) else { continue } guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomSummary.id) else { continue }
// `isOneToOneRoom` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members. // `isOneToOneRoom` here is not the same as `isDirect` on the room. From the point of view of the push rule, a one-to-one room is a room with exactly two active members.
let isOneToOneRoom = roomProxy.activeMembersCount == 2 let isOneToOneRoom = roomProxy.infoPublisher.value.activeMembersCount == 2
// display only the rooms we're interested in // display only the rooms we're interested in
switch chatType { switch chatType {
case .oneToOneChat where isOneToOneRoom, .groupChat where !isOneToOneRoom: case .oneToOneChat where isOneToOneRoom, .groupChat where !isOneToOneRoom:

View File

@ -82,6 +82,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
ownUserID: roomProxy.ownUserID, ownUserID: roomProxy.ownUserID,
isViewSourceEnabled: appSettings.viewSourceEnabled, isViewSourceEnabled: appSettings.viewSourceEnabled,
hideTimelineMedia: appSettings.hideTimelineMedia, hideTimelineMedia: appSettings.hideTimelineMedia,
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
bindings: .init(reactionsCollapsed: [:]), bindings: .init(reactionsCollapsed: [:]),
emojiProvider: emojiProvider), emojiProvider: emojiProvider),
mediaProvider: mediaProvider) mediaProvider: mediaProvider)
@ -91,10 +92,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
showFocusLoadingIndicator() showFocusLoadingIndicator()
} }
Task {
await updatePinnedEventIDs()
}
setupSubscriptions() setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded() setupDirectRoomSubscriptionsIfNeeded()
@ -380,15 +377,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
.store(in: &cancellables) .store(in: &cancellables)
let roomInfoSubscription = roomProxy let roomInfoSubscription = roomProxy.infoPublisher
.actionsPublisher
.filter { $0 == .roomInfoUpdate }
Task { [weak self] in Task { [weak self] in
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values { for await roomInfo in roomInfoSubscription.receive(on: DispatchQueue.main).values {
guard !Task.isCancelled else { guard !Task.isCancelled else {
return return
} }
await self?.updatePinnedEventIDs() self?.state.pinnedEventIDs = roomInfo.pinnedEventIDs
await self?.updatePermissions() await self?.updatePermissions()
} }
} }
@ -457,12 +452,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
.store(in: &cancellables) .store(in: &cancellables)
} }
private func updatePinnedEventIDs() async {
state.pinnedEventIDs = await roomProxy.pinnedEventIDs
}
private func setupDirectRoomSubscriptionsIfNeeded() { private func setupDirectRoomSubscriptionsIfNeeded() {
guard roomProxy.isDirect else { guard roomProxy.infoPublisher.value.isDirect else {
return return
} }
@ -471,7 +462,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
.map { [weak self] isFocused in .map { [weak self] isFocused in
guard let self else { return false } guard let self else { return false }
return isFocused && self.roomProxy.isUserAloneInDirectRoom return isFocused && self.roomProxy.infoPublisher.value.isUserAloneInDirectRoom
} }
// We want to show the alert just once, so we are taking the first "true" emitted // We want to show the alert just once, so we are taking the first "true" emitted
.first { $0 } .first { $0 }
@ -742,7 +733,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private let inviteLoadingIndicatorID = UUID().uuidString private let inviteLoadingIndicatorID = UUID().uuidString
private func inviteOtherDMUserBack() { private func inviteOtherDMUserBack() {
guard roomProxy.isUserAloneInDirectRoom else { guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError) userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
return return
} }
@ -848,7 +839,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
} }
} }
private extension RoomProxyProtocol { private extension RoomInfoProxy {
/// Checks if the other person left the room in a direct chat /// Checks if the other person left the room in a direct chat
var isUserAloneInDirectRoom: Bool { var isUserAloneInDirectRoom: Bool {
isDirect && activeMembersCount == 1 isDirect && activeMembersCount == 1

View File

@ -696,7 +696,7 @@ class ClientProxy: ClientProxyProtocol {
for roomID in roomIdentifiers { for roomID in roomIdentifiers {
guard case let .joined(roomProxy) = await roomForIdentifier(roomID), guard case let .joined(roomProxy) = await roomForIdentifier(roomID),
roomProxy.isDirect, roomProxy.infoPublisher.value.isDirect,
let members = await roomProxy.members() else { let members = await roomProxy.members() else {
continue continue
} }
@ -869,14 +869,14 @@ class ClientProxy: ClientProxyProtocol {
switch roomListItem.membership() { switch roomListItem.membership() {
case .invited: case .invited:
return try .invited(InvitedRoomProxy(roomListItem: roomListItem, return try await .invited(InvitedRoomProxy(roomListItem: roomListItem,
room: roomListItem.invitedRoom())) room: roomListItem.invitedRoom()))
case .knocked: case .knocked:
if appSettings.knockingEnabled { if appSettings.knockingEnabled {
return try .knocked(KnockedRoomProxy(roomListItem: roomListItem, return try await .knocked(KnockedRoomProxy(roomListItem: roomListItem,
room: roomListItem.invitedRoom())) room: roomListItem.invitedRoom()))
} else { } else {
return try .invited(InvitedRoomProxy(roomListItem: roomListItem, return try await .invited(InvitedRoomProxy(roomListItem: roomListItem,
room: roomListItem.invitedRoom())) room: roomListItem.invitedRoom()))
} }
case .joined: case .joined:

View File

@ -291,16 +291,11 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe
// it from what we have. If the call is running before subscribing then wait // it from what we have. If the call is running before subscribing then wait
// for it to change to `false` otherwise wait for it to turn `true` before // for it to change to `false` otherwise wait for it to turn `true` before
// changing to `false` // changing to `false`
let isCallOngoing = roomProxy.hasOngoingCall let isCallOngoing = roomProxy.infoPublisher.value.hasRoomCall
roomProxy roomProxy
.actionsPublisher .infoPublisher
.compactMap { action -> (Bool, [String])? in .compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) }
switch action {
case .roomInfoUpdate:
return (roomProxy.hasOngoingCall, roomProxy.activeRoomCallParticipants)
}
}
.removeDuplicates { $0 == $1 } .removeDuplicates { $0 == $1 }
.dropFirst(isCallOngoing ? 0 : 1) .dropFirst(isCallOngoing ? 0 : 1)
.sink { [weak self] hasOngoingCall, activeRoomCallParticipants in .sink { [weak self] hasOngoingCall, activeRoomCallParticipants in

View File

@ -17,68 +17,15 @@ class InvitedRoomProxy: InvitedRoomProxyProtocol {
// multiple times over FFI // multiple times over FFI
lazy var id: String = room.id() lazy var id: String = room.id()
var canonicalAlias: String? { var ownUserID: String { room.ownUserId() }
room.canonicalAlias()
}
var ownUserID: String { let info: RoomInfoProxy
room.ownUserId()
}
var name: String? {
roomListItem.displayName()
}
var topic: String? {
room.topic()
}
var avatarURL: URL? {
roomListItem.avatarUrl().flatMap(URL.init(string:))
}
var avatar: RoomAvatar {
if isDirect, avatarURL == nil {
let heroes = room.heroes()
if heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
}
return .room(id: id, name: name, avatarURL: avatarURL)
}
var isDirect: Bool {
room.isDirect()
}
var isPublic: Bool {
room.isPublic()
}
var isSpace: Bool {
room.isSpace()
}
var joinedMembersCount: Int {
Int(room.joinedMembersCount())
}
var activeMembersCount: Int {
Int(room.activeMembersCount())
}
var inviter: RoomMemberProxyProtocol? {
get async {
await (try? roomListItem.roomInfo().inviter).map(RoomMemberProxy.init)
}
}
init(roomListItem: RoomListItemProtocol, init(roomListItem: RoomListItemProtocol,
room: RoomProtocol) { room: RoomProtocol) async throws {
self.roomListItem = roomListItem self.roomListItem = roomListItem
self.room = room self.room = room
info = try await RoomInfoProxy(roomInfo: room.roomInfo())
} }
func acceptInvitation() async -> Result<Void, RoomProxyError> { func acceptInvitation() async -> Result<Void, RoomProxyError> {

View File

@ -63,6 +63,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private var subscribedForUpdates = false private var subscribedForUpdates = false
private let infoSubject: CurrentValueSubject<RoomInfoProxy, Never>
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
infoSubject.asCurrentValuePublisher()
}
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
membersSubject.asCurrentValuePublisher() membersSubject.asCurrentValuePublisher()
@ -78,94 +83,16 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
identityStatusChangesSubject.asCurrentValuePublisher() identityStatusChangesSubject.asCurrentValuePublisher()
} }
private let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
// A room identifier is constant and lazy stops it from being fetched // A room identifier is constant and lazy stops it from being fetched
// multiple times over FFI // multiple times over FFI
lazy var id: String = room.id() lazy var id: String = room.id()
var ownUserID: String { room.ownUserId() }
var canonicalAlias: String? { var info: RoomInfoProxy { infoSubject.value }
room.canonicalAlias()
}
var ownUserID: String {
room.ownUserId()
}
var name: String? {
roomListItem.displayName()
}
var topic: String? {
room.topic()
}
var avatarURL: URL? {
roomListItem.avatarUrl().flatMap(URL.init(string:))
}
var avatar: RoomAvatar {
if isDirect, avatarURL == nil {
let heroes = room.heroes()
if heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
}
return .room(id: id, name: name, avatarURL: avatarURL)
}
var isDirect: Bool {
room.isDirect()
}
var isPublic: Bool {
room.isPublic()
}
var isSpace: Bool {
room.isSpace()
}
var joinedMembersCount: Int {
Int(room.joinedMembersCount())
}
var activeMembersCount: Int {
Int(room.activeMembersCount())
}
var isEncrypted: Bool { var isEncrypted: Bool {
(try? room.isEncrypted()) ?? false (try? room.isEncrypted()) ?? false
} }
var isFavourite: Bool {
get async {
await (try? room.roomInfo().isFavourite) ?? false
}
}
var pinnedEventIDs: Set<String> {
get async {
guard let pinnedEventIDs = try? await room.roomInfo().pinnedEventIds else {
return []
}
return .init(pinnedEventIDs)
}
}
var hasOngoingCall: Bool {
room.hasActiveRoomCall()
}
var activeRoomCallParticipants: [String] {
room.activeRoomCallParticipants()
}
init(roomListService: RoomListServiceProtocol, init(roomListService: RoomListServiceProtocol,
roomListItem: RoomListItemProtocol, roomListItem: RoomListItemProtocol,
room: RoomProtocol) async throws { room: RoomProtocol) async throws {
@ -173,6 +100,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
self.roomListItem = roomListItem self.roomListItem = roomListItem
self.room = room self.room = room
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live) timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live)
Task { Task {
@ -210,9 +138,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
return return
} }
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] roomInfo in
MXLog.info("Received room info update") MXLog.info("Received room info update")
self?.actionsSubject.send(.roomInfoUpdate) self?.infoSubject.send(.init(roomInfo: roomInfo))
}) })
} }
@ -730,14 +658,14 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
} }
private final class RoomInfoUpdateListener: RoomInfoListener { private final class RoomInfoUpdateListener: RoomInfoListener {
private let onUpdateClosure: () -> Void private let onUpdateClosure: (RoomInfo) -> Void
init(_ onUpdateClosure: @escaping () -> Void) { init(_ onUpdateClosure: @escaping (RoomInfo) -> Void) {
self.onUpdateClosure = onUpdateClosure self.onUpdateClosure = onUpdateClosure
} }
func call(roomInfo: RoomInfo) { func call(roomInfo: RoomInfo) {
onUpdateClosure() onUpdateClosure(roomInfo)
} }
} }

View File

@ -17,62 +17,15 @@ class KnockedRoomProxy: KnockedRoomProxyProtocol {
// multiple times over FFI // multiple times over FFI
lazy var id: String = room.id() lazy var id: String = room.id()
var canonicalAlias: String? { var ownUserID: String { room.ownUserId() }
room.canonicalAlias()
}
var ownUserID: String { let info: RoomInfoProxy
room.ownUserId()
}
var name: String? {
roomListItem.displayName()
}
var topic: String? {
room.topic()
}
var avatarURL: URL? {
roomListItem.avatarUrl().flatMap(URL.init(string:))
}
var avatar: RoomAvatar {
if isDirect, avatarURL == nil {
let heroes = room.heroes()
if heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
}
return .room(id: id, name: name, avatarURL: avatarURL)
}
var isDirect: Bool {
room.isDirect()
}
var isPublic: Bool {
room.isPublic()
}
var isSpace: Bool {
room.isSpace()
}
var joinedMembersCount: Int {
Int(room.joinedMembersCount())
}
var activeMembersCount: Int {
Int(room.activeMembersCount())
}
init(roomListItem: RoomListItemProtocol, init(roomListItem: RoomListItemProtocol,
room: RoomProtocol) { room: RoomProtocol) async throws {
self.roomListItem = roomListItem self.roomListItem = roomListItem
self.room = room self.room = room
info = try await RoomInfoProxy(roomInfo: room.roomInfo())
} }
func cancelKnock() async -> Result<Void, RoomProxyError> { func cancelKnock() async -> Result<Void, RoomProxyError> {

View File

@ -0,0 +1,56 @@
//
// Copyright 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Foundation
import MatrixRustSDK
struct RoomInfoProxy {
let roomInfo: RoomInfo
var id: String { roomInfo.id }
var creator: String? { roomInfo.creator }
var displayName: String? { roomInfo.displayName }
var rawName: String? { roomInfo.rawName }
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 {
if isDirect, avatarURL == nil {
if heroes.count == 1 {
return .heroes(heroes.map(UserProfileProxy.init))
}
}
return .room(id: id, name: displayName, avatarURL: avatarURL)
}
var isDirect: Bool { roomInfo.isDirect }
var isPublic: Bool { roomInfo.isPublic }
var isSpace: Bool { roomInfo.isSpace }
var isTombstoned: Bool { roomInfo.isTombstoned }
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 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 }
var hasRoomCall: Bool { roomInfo.hasRoomCall }
var activeRoomCallParticipants: [String] { roomInfo.activeRoomCallParticipants }
var isMarkedUnread: Bool { roomInfo.isMarkedUnread }
var unreadMessagesCount: UInt { UInt(roomInfo.numUnreadMessages) }
var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) }
var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) }
var pinnedEventIDs: Set<String> { Set(roomInfo.pinnedEventIds) }
}

View File

@ -28,37 +28,19 @@ enum RoomProxyType {
// sourcery: AutoMockable // sourcery: AutoMockable
protocol RoomProxyProtocol { protocol RoomProxyProtocol {
var id: String { get } var id: String { get }
var canonicalAlias: String? { get }
var ownUserID: String { get } var ownUserID: String { get }
var name: String? { get }
var topic: String? { get }
/// The room's avatar info for use in a ``RoomAvatarImage``.
var avatar: RoomAvatar { get }
/// The room's avatar URL. Use this for editing and favour ``avatar`` for display.
var avatarURL: URL? { get }
var isPublic: Bool { get }
var isDirect: Bool { get }
var isSpace: Bool { get }
var joinedMembersCount: Int { get }
var activeMembersCount: Int { get }
} }
// sourcery: AutoMockable // sourcery: AutoMockable
protocol InvitedRoomProxyProtocol: RoomProxyProtocol { protocol InvitedRoomProxyProtocol: RoomProxyProtocol {
var inviter: RoomMemberProxyProtocol? { get async } var info: RoomInfoProxy { get }
func rejectInvitation() async -> Result<Void, RoomProxyError> func rejectInvitation() async -> Result<Void, RoomProxyError>
func acceptInvitation() async -> Result<Void, RoomProxyError> func acceptInvitation() async -> Result<Void, RoomProxyError>
} }
// sourcery: AutoMockable // sourcery: AutoMockable
protocol KnockedRoomProxyProtocol: RoomProxyProtocol { protocol KnockedRoomProxyProtocol: RoomProxyProtocol {
var info: RoomInfoProxy { get }
func cancelKnock() async -> Result<Void, RoomProxyError> func cancelKnock() async -> Result<Void, RoomProxyError>
} }
@ -69,11 +51,8 @@ enum JoinedRoomProxyAction: Equatable {
// sourcery: AutoMockable // sourcery: AutoMockable
protocol JoinedRoomProxyProtocol: RoomProxyProtocol { protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var isEncrypted: Bool { get } var isEncrypted: Bool { get }
var isFavourite: Bool { get async }
var pinnedEventIDs: Set<String> { get async }
var hasOngoingCall: Bool { get } var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> { get }
var activeRoomCallParticipants: [String] { get }
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get }
@ -81,8 +60,6 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get } var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> { get }
var timeline: TimelineProxyProtocol { get } var timeline: TimelineProxyProtocol { get }
var pinnedEventsTimeline: TimelineProxyProtocol? { get async } var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
@ -176,21 +153,15 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
extension JoinedRoomProxyProtocol { extension JoinedRoomProxyProtocol {
var details: RoomDetails { var details: RoomDetails {
RoomDetails(id: id, RoomDetails(id: id,
name: name, name: infoPublisher.value.displayName,
avatar: avatar, avatar: infoPublisher.value.avatar,
canonicalAlias: canonicalAlias, canonicalAlias: infoPublisher.value.canonicalAlias,
isEncrypted: isEncrypted, isEncrypted: isEncrypted,
isPublic: isPublic) isPublic: infoPublisher.value.isPublic)
}
// Avoids to duplicate the same logic around in the app
// Probably this should be done in rust.
var roomTitle: String {
name ?? "Unknown room 💥"
} }
var isEncryptedOneToOneRoom: Bool { var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount <= 2 infoPublisher.value.isDirect && isEncrypted && infoPublisher.value.activeMembersCount <= 2
} }
func members() async -> [RoomMemberProxyProtocol]? { func members() async -> [RoomMemberProxyProtocol]? {

View File

@ -326,7 +326,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
switch paginationState.backward { switch paginationState.backward {
case .timelineEndReached: case .timelineEndReached:
if timelineKind != .pinned, !roomProxy.isEncryptedOneToOneRoom { if timelineKind != .pinned, !roomProxy.isEncryptedOneToOneRoom {
let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.name) let timelineStart = TimelineStartRoomTimelineItem(name: roomProxy.infoPublisher.value.displayName)
newTimelineItems.insert(timelineStart, at: 0) newTimelineItems.insert(timelineStart, at: 0)
} }
case .paginating: case .paginating:

View File

@ -24,15 +24,16 @@ class RoomScreenViewModelTests: XCTestCase {
} }
func testPinnedEventsBanner() async throws { func testPinnedEventsBanner() async throws {
var configuration = JoinedRoomProxyMockConfiguration()
let timelineSubject = PassthroughSubject<TimelineProxyProtocol, Never>() let timelineSubject = PassthroughSubject<TimelineProxyProtocol, Never>()
let updateSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() let infoSubject = CurrentValueSubject<RoomInfoProxy, Never>(.init(roomInfo: RoomInfo(configuration)))
let roomProxyMock = JoinedRoomProxyMock(.init()) let roomProxyMock = JoinedRoomProxyMock(configuration)
// setup a way to inject the mock of the pinned events timeline // setup a way to inject the mock of the pinned events timeline
roomProxyMock.pinnedEventsTimelineClosure = { roomProxyMock.pinnedEventsTimelineClosure = {
await timelineSubject.values.first() await timelineSubject.values.first()
} }
// setup the room proxy actions publisher // setup the room proxy actions publisher
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
@ -56,8 +57,8 @@ class RoomScreenViewModelTests: XCTestCase {
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
viewState.pinnedEventsBannerState.count == 2 viewState.pinnedEventsBannerState.count == 2
} }
roomProxyMock.underlyingPinnedEventIDs = ["test1", "test2"] configuration.pinnedEventIDs = ["test1", "test2"]
updateSubject.send(.roomInfoUpdate) infoSubject.send(.init(roomInfo: RoomInfo(configuration)))
try await deferred.fulfill() try await deferred.fulfill()
XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading) XCTAssertTrue(viewModel.context.viewState.pinnedEventsBannerState.isLoading)
XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner) XCTAssertTrue(viewModel.context.viewState.shouldShowPinnedEventsBanner)
@ -157,11 +158,12 @@ class RoomScreenViewModelTests: XCTestCase {
} }
func testRoomInfoUpdate() async throws { func testRoomInfoUpdate() async throws {
let updateSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() var configuration = JoinedRoomProxyMockConfiguration(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)
let roomProxyMock = JoinedRoomProxyMock(.init(id: "TestID", name: "StartingName", avatarURL: nil, hasOngoingCall: false)) let infoSubject = CurrentValueSubject<RoomInfoProxy, Never>(.init(roomInfo: RoomInfo(configuration)))
let roomProxyMock = JoinedRoomProxyMock(configuration)
// setup the room proxy actions publisher // setup the room proxy actions publisher
roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false) roomProxyMock.canUserJoinCallUserIDReturnValue = .success(false)
roomProxyMock.underlyingActionsPublisher = updateSubject.eraseToAnyPublisher() roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(), let viewModel = RoomScreenViewModel(clientProxy: ClientProxyMock(),
roomProxy: roomProxyMock, roomProxy: roomProxyMock,
initialSelectedPinnedEventID: nil, initialSelectedPinnedEventID: nil,
@ -181,9 +183,9 @@ class RoomScreenViewModelTests: XCTestCase {
} }
try await deferred.fulfill() try await deferred.fulfill()
roomProxyMock.name = "NewName" configuration.name = "NewName"
roomProxyMock.avatar = .room(id: "TestID", name: "NewName", avatarURL: .documentsDirectory) configuration.avatarURL = .documentsDirectory
roomProxyMock.hasOngoingCall = true configuration.hasOngoingCall = true
roomProxyMock.canUserJoinCallUserIDReturnValue = .success(true) roomProxyMock.canUserJoinCallUserIDReturnValue = .success(true)
deferred = deferFulfillment(viewModel.context.$viewState) { viewState in deferred = deferFulfillment(viewModel.context.$viewState) { viewState in
@ -193,7 +195,7 @@ class RoomScreenViewModelTests: XCTestCase {
viewState.hasOngoingCall viewState.hasOngoingCall
} }
updateSubject.send(.roomInfoUpdate) infoSubject.send(.init(roomInfo: RoomInfo(configuration)))
try await deferred.fulfill() try await deferred.fulfill()
} }

View File

@ -349,10 +349,11 @@ class TimelineViewModelTests: XCTestCase {
// MARK: - Pins // MARK: - Pins
func testPinnedEvents() async throws { func testPinnedEvents() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(name: "", var configuration = JoinedRoomProxyMockConfiguration(name: "",
pinnedEventIDs: .init(["test1"]))) pinnedEventIDs: .init(["test1"]))
let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() let roomProxyMock = JoinedRoomProxyMock(configuration)
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() let infoSubject = CurrentValueSubject<RoomInfoProxy, Never>(.init(roomInfo: RoomInfo(configuration)))
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
@ -364,24 +365,21 @@ class TimelineViewModelTests: XCTestCase {
appSettings: ServiceLocator.shared.settings, appSettings: ServiceLocator.shared.settings,
analyticsService: ServiceLocator.shared.analytics, analyticsService: ServiceLocator.shared.analytics,
emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings)) emojiProvider: EmojiProvider(appSettings: ServiceLocator.shared.settings))
XCTAssertEqual(configuration.pinnedEventIDs, viewModel.context.viewState.pinnedEventIDs)
var deferred = deferFulfillment(viewModel.context.$viewState) { value in configuration.pinnedEventIDs = ["test1", "test2"]
value.pinnedEventIDs == ["test1"] let deferred = deferFulfillment(viewModel.context.$viewState) { value in
}
try await deferred.fulfill()
roomProxyMock.underlyingPinnedEventIDs = ["test1", "test2"]
deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.pinnedEventIDs == ["test1", "test2"] value.pinnedEventIDs == ["test1", "test2"]
} }
actionsSubject.send(.roomInfoUpdate) infoSubject.send(.init(roomInfo: RoomInfo(configuration)))
try await deferred.fulfill() try await deferred.fulfill()
} }
func testCanUserPinEvents() async throws { func testCanUserPinEvents() async throws {
let roomProxyMock = JoinedRoomProxyMock(.init(name: "", canUserPin: true)) let configuration = JoinedRoomProxyMockConfiguration(name: "", canUserPin: true)
let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() let roomProxyMock = JoinedRoomProxyMock(configuration)
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() let infoSubject = CurrentValueSubject<RoomInfoProxy, Never>(.init(roomInfo: RoomInfo(configuration)))
roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
@ -403,7 +401,7 @@ class TimelineViewModelTests: XCTestCase {
deferred = deferFulfillment(viewModel.context.$viewState) { value in deferred = deferFulfillment(viewModel.context.$viewState) { value in
!value.canCurrentUserPin !value.canCurrentUserPin
} }
actionsSubject.send(.roomInfoUpdate) infoSubject.send(.init(roomInfo: RoomInfo(configuration)))
try await deferred.fulfill() try await deferred.fulfill()
} }