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 */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.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 */; };
5D52925FEB1B780C65B0529F /* PinnedEventsTimelineScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA4F6D7000EDCD187E0989E7 /* PinnedEventsTimelineScreen.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>"; };
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>"; };
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>"; };
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>"; };
@ -3211,6 +3213,7 @@
07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */,
858DA81F2ACF484B7CAD6AE4 /* KnockedRoomProxy.swift */,
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */,
40A66E8BC8D9AE4A08EFB2DF /* RoomInfoProxy.swift */,
974AEAF3FE0C577A6C04AD6E /* RoomPermissions.swift */,
47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */,
2C0F49BD446849654C0D24E0 /* RoomMember */,
@ -6775,6 +6778,7 @@
42F1C8731166633E35A6D7E6 /* RoomEventStringBuilder.swift in Sources */,
D55AF9B5B55FEED04771A461 /* RoomFlowCoordinator.swift in Sources */,
9C63171267E22FEB288EC860 /* RoomHeaderView.swift in Sources */,
5C8804B4F25903516E2DAB81 /* RoomInfoProxy.swift in Sources */,
8A83D715940378B9BA9F739E /* RoomInviterLabel.swift in Sources */,
F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */,
4A9CEEE612D6D8B3DDBD28BA /* RoomListFilterView.swift in Sources */,

View File

@ -584,7 +584,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
timelineItemFactory: timelineItemFactory)
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)
@ -681,7 +681,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
await storeAndSubscribeToRoomProxy(roomProxy)
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 {
stateMachine.tryEvent(.dismissFlow, userInfo: EventUserInfo(animated: animated))
}

View File

@ -5824,67 +5824,21 @@ class ElementCallWidgetDriverMock: ElementCallWidgetDriverProtocol {
}
}
class InvitedRoomProxyMock: InvitedRoomProxyProtocol {
var inviterCallsCount = 0
var inviterCalled: Bool {
return inviterCallsCount > 0
var info: RoomInfoProxy {
get { return underlyingInfo }
set(value) { underlyingInfo = value }
}
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 underlyingInfo: RoomInfoProxy!
var id: String {
get { return underlyingId }
set(value) { underlyingId = value }
}
var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String {
get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value }
}
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
@ -6021,46 +5975,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingIsEncrypted = value }
}
var underlyingIsEncrypted: Bool!
var isFavouriteCallsCount = 0
var isFavouriteCalled: Bool {
return isFavouriteCallsCount > 0
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
get { return underlyingInfoPublisher }
set(value) { underlyingInfoPublisher = value }
}
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 underlyingInfoPublisher: CurrentValuePublisher<RoomInfoProxy, Never>!
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
get { return underlyingMembersPublisher }
set(value) { underlyingMembersPublisher = value }
@ -6076,11 +5995,6 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingIdentityStatusChangesPublisher = value }
}
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 {
get { return underlyingTimeline }
set(value) { underlyingTimeline = value }
@ -6108,45 +6022,11 @@ class JoinedRoomProxyMock: JoinedRoomProxyProtocol {
set(value) { underlyingId = value }
}
var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String {
get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value }
}
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
@ -9752,50 +9632,21 @@ class KeychainControllerMock: KeychainControllerProtocol {
}
}
class KnockedRoomProxyMock: KnockedRoomProxyProtocol {
var info: RoomInfoProxy {
get { return underlyingInfo }
set(value) { underlyingInfo = value }
}
var underlyingInfo: RoomInfoProxy!
var id: String {
get { return underlyingId }
set(value) { underlyingId = value }
}
var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String {
get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value }
}
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
@ -12934,45 +12785,11 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingId = value }
}
var underlyingId: String!
var canonicalAlias: String?
var ownUserID: String {
get { return underlyingOwnUserID }
set(value) { underlyingOwnUserID = value }
}
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 {

View File

@ -7,6 +7,7 @@
import Combine
import Foundation
import MatrixRustSDK
@MainActor
struct InvitedRoomProxyMockConfiguration {
@ -22,10 +23,55 @@ extension InvitedRoomProxyMock {
convenience init(_ configuration: InvitedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
name = configuration.name
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
info = RoomInfoProxy(roomInfo: .init(configuration))
}
}
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 Foundation
import MatrixRustSDK
enum RoomProxyMockError: Error {
case generic
@ -46,18 +47,7 @@ extension JoinedRoomProxyMock {
self.init()
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
hasOngoingCall = configuration.hasOngoingCall
canonicalAlias = configuration.canonicalAlias
underlyingPinnedEventIDs = configuration.pinnedEventIDs
let timeline = TimelineProxyMock()
timeline.sendMessageEventContentReturnValue = .success(())
@ -78,15 +68,12 @@ extension JoinedRoomProxyMock {
ownUserID = configuration.ownUserID
infoPublisher = CurrentValueSubject(.init(roomInfo: .init(configuration))).asCurrentValuePublisher()
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
typingMembersPublisher = 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 = { }
underlyingActionsPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) }
getMemberUserIDClosure = { [weak self] userID in
@ -102,7 +89,6 @@ extension JoinedRoomProxyMock {
flagAsUnreadReturnValue = .success(())
markAsReadReceiptTypeReturnValue = .success(())
underlyingIsFavourite = false
flagAsFavouriteReturnValue = .success(())
powerLevelsReturnValue = .success(.mock)
@ -154,3 +140,46 @@ extension JoinedRoomProxyMock {
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 Foundation
import MatrixRustSDK
@MainActor
struct KnockedRoomProxyMockConfiguration {
@ -21,9 +22,41 @@ extension KnockedRoomProxyMock {
convenience init(_ configuration: KnockedRoomProxyMockConfiguration) {
self.init()
id = configuration.id
name = configuration.name
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
info = RoomInfoProxy(roomInfo: .init(configuration))
}
}
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
}
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()

View File

@ -360,10 +360,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
return
}
if roomProxy.isPublic {
if roomProxy.infoPublisher.value.isPublic {
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .public)
} 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)
} else {
LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty)
@ -408,7 +408,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
switch await roomProxy.acceptInvitation() {
case .success:
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:
displayError()
}

View File

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

View File

@ -143,7 +143,8 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh
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.
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) {
case .success:

View File

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

View File

@ -63,14 +63,14 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.attributedStringBuilder = attributedStringBuilder
self.appSettings = appSettings
let topic = attributedStringBuilder.fromPlain(roomProxy.topic)
let topic = attributedStringBuilder.fromPlain(roomProxy.infoPublisher.value.topic)
super.init(initialViewState: .init(details: roomProxy.details,
isEncrypted: roomProxy.isEncrypted,
isDirect: roomProxy.isDirect,
isDirect: roomProxy.infoPublisher.value.isDirect,
topic: topic,
topicSummary: topic?.unattributedStringByReplacingNewlinesWithSpaces(),
joinedMembersCount: roomProxy.joinedMembersCount,
joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
notificationSettingsState: .loading,
bindings: .init()),
mediaProvider: mediaProvider)
@ -96,7 +96,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}
updateRoomInfo()
updateRoomInfo(roomProxy.infoPublisher.value)
Task { await updatePowerLevelPermissions() }
setupRoomSubscription()
@ -124,7 +124,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isEncryptedOneToOneRoom, state: .empty)
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:
Task { await leaveRoom() }
case .processTapIgnore:
@ -162,29 +164,25 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
// MARK: - Private
private func setupRoomSubscription() {
roomProxy.actionsPublisher
.filter { $0 == .roomInfoUpdate }
roomProxy.infoPublisher
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
self?.updateRoomInfo()
.sink { [weak self] roomInfo in
self?.updateRoomInfo(roomInfo)
Task { await self?.updatePowerLevelPermissions() }
}
.store(in: &cancellables)
}
private func updateRoomInfo() {
private func updateRoomInfo(_ roomInfo: RoomInfoProxy) {
state.details = roomProxy.details
let topic = attributedStringBuilder.fromPlain(roomProxy.topic)
let topic = attributedStringBuilder.fromPlain(roomInfo.topic)
state.topic = topic
state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
state.joinedMembersCount = roomProxy.joinedMembersCount
Task {
state.bindings.isFavourite = await roomProxy.isFavourite
}
state.joinedMembersCount = roomInfo.joinedMembersCount
state.bindings.isFavourite = roomInfo.isFavourite
}
private func fetchMembersIfNeeded() async {
@ -240,7 +238,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
do {
let notificationMode = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2)
isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
state.notificationSettingsState = .loaded(settings: notificationMode)
} catch {
state.notificationSettingsState = .error
@ -258,7 +256,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
do {
try await notificationSettingsProxy.unmuteRoom(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2)
isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
} catch {
state.bindings.alertInfo = AlertInfo(id: .alert,
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.
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.analytics = analytics
super.init(initialViewState: .init(joinedMembersCount: roomProxy.joinedMembersCount,
super.init(initialViewState: .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
bindings: .init(mode: initialMode)),
mediaProvider: mediaProvider)
@ -92,7 +92,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe
let members = members.sorted()
let roomMembersDetails = await buildMembersDetails(members: members)
self.members = members
self.state = .init(joinedMembersCount: roomProxy.joinedMembersCount,
self.state = .init(joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount,
joinedMembers: roomMembersDetails.joinedMembers,
invitedMembers: roomMembersDetails.invitedMembers,
bannedMembers: roomMembersDetails.bannedMembers,

View File

@ -26,11 +26,11 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie
let bindings = RoomNotificationSettingsScreenViewStateBindings()
self.notificationSettingsProxy = notificationSettingsProxy
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
super.init(initialViewState: RoomNotificationSettingsScreenViewState(bindings: bindings,
displayAsUserDefinedRoomSettings: displayAsUserDefinedRoomSettings,
navigationTitle: navigationTitle,
navigationTitle: navigationTitle ?? L10n.screenRoomDetailsNotificationTitle,
customSettingsSectionHeader: customSettingsSectionHeader))
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.
let settings = try await notificationSettingsProxy.getNotificationSettings(roomId: roomProxy.id,
isEncrypted: roomProxy.isEncrypted,
isOneToOne: roomProxy.activeMembersCount == 2)
isOneToOne: roomProxy.infoPublisher.value.activeMembersCount == 2)
guard !Task.isCancelled else { return }
state.notificationSettingsState = .loaded(settings: settings)
if !state.isRestoringDefaultSetting {

View File

@ -37,8 +37,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
updateMembers(roomProxy.membersPublisher.value)
// Automatically update the room permissions
roomProxy.actionsPublisher
.filter { $0 == .roomInfoUpdate }
roomProxy.infoPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
Task { await self?.updatePermissions() }
@ -93,7 +92,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM
showSavingIndicator()
// 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)]) {
case .success:

View File

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

View File

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

View File

@ -131,7 +131,7 @@ class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenVie
for roomSummary in filteredRoomsSummary {
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.
let isOneToOneRoom = roomProxy.activeMembersCount == 2
let isOneToOneRoom = roomProxy.infoPublisher.value.activeMembersCount == 2
// display only the rooms we're interested in
switch chatType {
case .oneToOneChat where isOneToOneRoom, .groupChat where !isOneToOneRoom:

View File

@ -82,6 +82,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
ownUserID: roomProxy.ownUserID,
isViewSourceEnabled: appSettings.viewSourceEnabled,
hideTimelineMedia: appSettings.hideTimelineMedia,
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
bindings: .init(reactionsCollapsed: [:]),
emojiProvider: emojiProvider),
mediaProvider: mediaProvider)
@ -91,10 +92,6 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
showFocusLoadingIndicator()
}
Task {
await updatePinnedEventIDs()
}
setupSubscriptions()
setupDirectRoomSubscriptionsIfNeeded()
@ -380,15 +377,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
}
.store(in: &cancellables)
let roomInfoSubscription = roomProxy
.actionsPublisher
.filter { $0 == .roomInfoUpdate }
let roomInfoSubscription = roomProxy.infoPublisher
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 {
return
}
await self?.updatePinnedEventIDs()
self?.state.pinnedEventIDs = roomInfo.pinnedEventIDs
await self?.updatePermissions()
}
}
@ -456,13 +451,9 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
.weakAssign(to: \.state.hideTimelineMedia, on: self)
.store(in: &cancellables)
}
private func updatePinnedEventIDs() async {
state.pinnedEventIDs = await roomProxy.pinnedEventIDs
}
private func setupDirectRoomSubscriptionsIfNeeded() {
guard roomProxy.isDirect else {
guard roomProxy.infoPublisher.value.isDirect else {
return
}
@ -471,7 +462,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
.map { [weak self] isFocused in
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
.first { $0 }
@ -742,7 +733,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
private let inviteLoadingIndicatorID = UUID().uuidString
private func inviteOtherDMUserBack() {
guard roomProxy.isUserAloneInDirectRoom else {
guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else {
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError)
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
var isUserAloneInDirectRoom: Bool {
isDirect && activeMembersCount == 1

View File

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

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
// for it to change to `false` otherwise wait for it to turn `true` before
// changing to `false`
let isCallOngoing = roomProxy.hasOngoingCall
let isCallOngoing = roomProxy.infoPublisher.value.hasRoomCall
roomProxy
.actionsPublisher
.compactMap { action -> (Bool, [String])? in
switch action {
case .roomInfoUpdate:
return (roomProxy.hasOngoingCall, roomProxy.activeRoomCallParticipants)
}
}
.infoPublisher
.compactMap { ($0.hasRoomCall, $0.activeRoomCallParticipants) }
.removeDuplicates { $0 == $1 }
.dropFirst(isCallOngoing ? 0 : 1)
.sink { [weak self] hasOngoingCall, activeRoomCallParticipants in

View File

@ -17,68 +17,15 @@ class InvitedRoomProxy: InvitedRoomProxyProtocol {
// multiple times over FFI
lazy var id: String = room.id()
var canonicalAlias: String? {
room.canonicalAlias()
}
var ownUserID: String { room.ownUserId() }
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 inviter: RoomMemberProxyProtocol? {
get async {
await (try? roomListItem.roomInfo().inviter).map(RoomMemberProxy.init)
}
}
let info: RoomInfoProxy
init(roomListItem: RoomListItemProtocol,
room: RoomProtocol) {
room: RoomProtocol) async throws {
self.roomListItem = roomListItem
self.room = room
info = try await RoomInfoProxy(roomInfo: room.roomInfo())
}
func acceptInvitation() async -> Result<Void, RoomProxyError> {

View File

@ -62,6 +62,11 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
private var identityStatusChangesObservationToken: TaskHandle?
private var subscribedForUpdates = false
private let infoSubject: CurrentValueSubject<RoomInfoProxy, Never>
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> {
infoSubject.asCurrentValuePublisher()
}
private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([])
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> {
@ -77,95 +82,17 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> {
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
// multiple times over FFI
lazy var id: String = room.id()
var canonicalAlias: String? {
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 ownUserID: String { room.ownUserId() }
var info: RoomInfoProxy { infoSubject.value }
var isEncrypted: Bool {
(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,
roomListItem: RoomListItemProtocol,
room: RoomProtocol) async throws {
@ -173,6 +100,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
self.roomListItem = roomListItem
self.room = room
infoSubject = try await .init(RoomInfoProxy(roomInfo: room.roomInfo()))
timeline = try await TimelineProxy(timeline: room.timeline(), kind: .live)
Task {
@ -210,9 +138,9 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol {
return
}
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] in
roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: RoomInfoUpdateListener { [weak self] roomInfo in
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 let onUpdateClosure: () -> Void
private let onUpdateClosure: (RoomInfo) -> Void
init(_ onUpdateClosure: @escaping () -> Void) {
init(_ onUpdateClosure: @escaping (RoomInfo) -> Void) {
self.onUpdateClosure = onUpdateClosure
}
func call(roomInfo: RoomInfo) {
onUpdateClosure()
onUpdateClosure(roomInfo)
}
}

View File

@ -17,62 +17,15 @@ class KnockedRoomProxy: KnockedRoomProxyProtocol {
// multiple times over FFI
lazy var id: String = room.id()
var canonicalAlias: String? {
room.canonicalAlias()
}
var ownUserID: String { room.ownUserId() }
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())
}
let info: RoomInfoProxy
init(roomListItem: RoomListItemProtocol,
room: RoomProtocol) {
room: RoomProtocol) async throws {
self.roomListItem = roomListItem
self.room = room
info = try await RoomInfoProxy(roomInfo: room.roomInfo())
}
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
protocol RoomProxyProtocol {
var id: String { get }
var canonicalAlias: 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
protocol InvitedRoomProxyProtocol: RoomProxyProtocol {
var inviter: RoomMemberProxyProtocol? { get async }
var info: RoomInfoProxy { get }
func rejectInvitation() async -> Result<Void, RoomProxyError>
func acceptInvitation() async -> Result<Void, RoomProxyError>
}
// sourcery: AutoMockable
protocol KnockedRoomProxyProtocol: RoomProxyProtocol {
var info: RoomInfoProxy { get }
func cancelKnock() async -> Result<Void, RoomProxyError>
}
@ -69,11 +51,8 @@ enum JoinedRoomProxyAction: Equatable {
// sourcery: AutoMockable
protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var isEncrypted: Bool { get }
var isFavourite: Bool { get async }
var pinnedEventIDs: Set<String> { get async }
var hasOngoingCall: Bool { get }
var activeRoomCallParticipants: [String] { get }
var infoPublisher: CurrentValuePublisher<RoomInfoProxy, Never> { get }
var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get }
@ -81,8 +60,6 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
var identityStatusChangesPublisher: CurrentValuePublisher<[IdentityStatusChange], Never> { get }
var actionsPublisher: AnyPublisher<JoinedRoomProxyAction, Never> { get }
var timeline: TimelineProxyProtocol { get }
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
@ -176,21 +153,15 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol {
extension JoinedRoomProxyProtocol {
var details: RoomDetails {
RoomDetails(id: id,
name: name,
avatar: avatar,
canonicalAlias: canonicalAlias,
name: infoPublisher.value.displayName,
avatar: infoPublisher.value.avatar,
canonicalAlias: infoPublisher.value.canonicalAlias,
isEncrypted: isEncrypted,
isPublic: isPublic)
}
// Avoids to duplicate the same logic around in the app
// Probably this should be done in rust.
var roomTitle: String {
name ?? "Unknown room 💥"
isPublic: infoPublisher.value.isPublic)
}
var isEncryptedOneToOneRoom: Bool {
isDirect && isEncrypted && activeMembersCount <= 2
infoPublisher.value.isDirect && isEncrypted && infoPublisher.value.activeMembersCount <= 2
}
func members() async -> [RoomMemberProxyProtocol]? {

View File

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

View File

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

View File

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