From ace05b97d0aff517f05eebb8d85315a6e9d22501 Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 1 Jul 2026 16:45:42 +0100 Subject: [PATCH] Combine the user's own display name/avatar into a single profile. This is in preparation for adding the user status fields into the profile. --- .../Sources/Application/AppCoordinator.swift | 2 +- ElementX/Sources/Mocks/ClientProxyMock.swift | 6 +- .../Mocks/Generated/GeneratedMocks.swift | 80 ++++++------------- .../Screens/HomeScreen/HomeScreenModels.swift | 4 +- .../HomeScreen/HomeScreenViewModel.swift | 14 +--- .../Screens/HomeScreen/View/HomeScreen.swift | 6 +- .../SettingsScreen/SettingsScreenModels.swift | 4 +- .../SettingsScreenViewModel.swift | 14 +--- .../SettingsScreen/View/SettingsScreen.swift | 10 +-- .../UserDetailsEditScreenModels.swift | 10 +-- .../UserDetailsEditScreenViewModel.swift | 28 ++----- .../View/UserDetailsEditScreen.swift | 6 +- .../SpacesScreen/SpacesScreenModels.swift | 4 +- .../SpacesScreen/SpacesScreenViewModel.swift | 11 +-- .../SpacesScreen/View/SpacesScreen.swift | 6 +- .../Sources/Services/Client/ClientProxy.swift | 50 +++++------- .../Services/Client/ClientProxyProtocol.swift | 8 +- 17 files changed, 88 insertions(+), 175 deletions(-) diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index 561a88d47..165c07ea7 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -715,7 +715,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg Task { let credentials = SoftLogoutScreenCredentials(userID: userSession.clientProxy.userID, homeserverName: userSession.clientProxy.homeserver, - userDisplayName: userSession.clientProxy.userDisplayNamePublisher.value ?? "", + userDisplayName: userSession.clientProxy.userProfilePublisher.value.displayName ?? "", deviceID: userSession.clientProxy.deviceID) let authenticationService = AuthenticationService(userSessionStore: userSessionStore, diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index a34a79401..5199e80b0 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -64,8 +64,7 @@ extension ClientProxyMock { verificationStatePublisher = .init(.unknown) homeserverReachabilityPublisher = .init(.reachable) - userAvatarURLPublisher = .init(nil) - userDisplayNamePublisher = .init("User display name") + userProfilePublisher = .init(UserProfile(userID: configuration.userID, displayName: "User display name")) ignoredUsersPublisher = .init([RoomMemberProxyMock].allMembers.map(\.userID)) @@ -85,9 +84,8 @@ extension ClientProxyMock { } joinRoomAliasReturnValue = .success(()) uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) + loadUserProfileReturnValue = .success(()) setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic)) - loadUserAvatarURLReturnValue = .success(()) setUserAvatarMediaReturnValue = .success(()) removeUserAvatarReturnValue = .success(()) isAliasAvailableReturnValue = .success(true) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 735011584..7cbd1f2b5 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2291,16 +2291,11 @@ nonisolated class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { } nonisolated(unsafe) var underlyingCanDeactivateAccount: Bool! nonisolated(unsafe) var userIDServerName: String? - var userDisplayNamePublisher: CurrentValuePublisher { - get { return underlyingUserDisplayNamePublisher } - set(value) { underlyingUserDisplayNamePublisher = value } + var userProfilePublisher: CurrentValuePublisher { + get { return underlyingUserProfilePublisher } + set(value) { underlyingUserProfilePublisher = value } } - nonisolated(unsafe) var underlyingUserDisplayNamePublisher: CurrentValuePublisher! - var userAvatarURLPublisher: CurrentValuePublisher { - get { return underlyingUserAvatarURLPublisher } - set(value) { underlyingUserAvatarURLPublisher = value } - } - nonisolated(unsafe) var underlyingUserAvatarURLPublisher: CurrentValuePublisher! + nonisolated(unsafe) var underlyingUserProfilePublisher: CurrentValuePublisher! var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get { return underlyingIgnoredUsersPublisher } set(value) { underlyingIgnoredUsersPublisher = value } @@ -3174,33 +3169,33 @@ nonisolated class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { return reportRoomForIdentifierReasonReturnValue } } - //MARK: - loadUserDisplayName + //MARK: - loadUserProfile - private let loadUserDisplayNameCallsCountLock = NSLock() - private nonisolated(unsafe) var loadUserDisplayNameUnderlyingCallsCount = 0 - var loadUserDisplayNameCallsCount: Int { - get { loadUserDisplayNameCallsCountLock.withLock { loadUserDisplayNameUnderlyingCallsCount } } - set { loadUserDisplayNameCallsCountLock.withLock { loadUserDisplayNameUnderlyingCallsCount = newValue } } + private let loadUserProfileCallsCountLock = NSLock() + private nonisolated(unsafe) var loadUserProfileUnderlyingCallsCount = 0 + var loadUserProfileCallsCount: Int { + get { loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount } } + set { loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount = newValue } } } - var loadUserDisplayNameCalled: Bool { - return loadUserDisplayNameCallsCount > 0 + var loadUserProfileCalled: Bool { + return loadUserProfileCallsCount > 0 } - private let loadUserDisplayNameReturnValueLock = NSLock() - private nonisolated(unsafe) var loadUserDisplayNameUnderlyingReturnValue: Result! - var loadUserDisplayNameReturnValue: Result! { - get { loadUserDisplayNameReturnValueLock.withLock { loadUserDisplayNameUnderlyingReturnValue } } - set { loadUserDisplayNameReturnValueLock.withLock { loadUserDisplayNameUnderlyingReturnValue = newValue } } + private let loadUserProfileReturnValueLock = NSLock() + private nonisolated(unsafe) var loadUserProfileUnderlyingReturnValue: Result! + var loadUserProfileReturnValue: Result! { + get { loadUserProfileReturnValueLock.withLock { loadUserProfileUnderlyingReturnValue } } + set { loadUserProfileReturnValueLock.withLock { loadUserProfileUnderlyingReturnValue = newValue } } } - nonisolated(unsafe) var loadUserDisplayNameClosure: (() async -> Result)? + nonisolated(unsafe) var loadUserProfileClosure: (() async -> Result)? @discardableResult - @concurrent func loadUserDisplayName() async -> Result { - loadUserDisplayNameCallsCountLock.withLock { loadUserDisplayNameUnderlyingCallsCount += 1 } - if let loadUserDisplayNameClosure = loadUserDisplayNameClosure { - return await loadUserDisplayNameClosure() + @concurrent func loadUserProfile() async -> Result { + loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount += 1 } + if let loadUserProfileClosure = loadUserProfileClosure { + return await loadUserProfileClosure() } else { - return loadUserDisplayNameReturnValue + return loadUserProfileReturnValue } } //MARK: - setUserDisplayName @@ -3245,35 +3240,6 @@ nonisolated class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { return setUserDisplayNameReturnValue } } - //MARK: - loadUserAvatarURL - - private let loadUserAvatarURLCallsCountLock = NSLock() - private nonisolated(unsafe) var loadUserAvatarURLUnderlyingCallsCount = 0 - var loadUserAvatarURLCallsCount: Int { - get { loadUserAvatarURLCallsCountLock.withLock { loadUserAvatarURLUnderlyingCallsCount } } - set { loadUserAvatarURLCallsCountLock.withLock { loadUserAvatarURLUnderlyingCallsCount = newValue } } - } - var loadUserAvatarURLCalled: Bool { - return loadUserAvatarURLCallsCount > 0 - } - - private let loadUserAvatarURLReturnValueLock = NSLock() - private nonisolated(unsafe) var loadUserAvatarURLUnderlyingReturnValue: Result! - var loadUserAvatarURLReturnValue: Result! { - get { loadUserAvatarURLReturnValueLock.withLock { loadUserAvatarURLUnderlyingReturnValue } } - set { loadUserAvatarURLReturnValueLock.withLock { loadUserAvatarURLUnderlyingReturnValue = newValue } } - } - nonisolated(unsafe) var loadUserAvatarURLClosure: (() async -> Result)? - - @discardableResult - @concurrent func loadUserAvatarURL() async -> Result { - loadUserAvatarURLCallsCountLock.withLock { loadUserAvatarURLUnderlyingCallsCount += 1 } - if let loadUserAvatarURLClosure = loadUserAvatarURLClosure { - return await loadUserAvatarURLClosure() - } else { - return loadUserAvatarURLReturnValue - } - } //MARK: - setUserAvatar private let setUserAvatarMediaCallsCountLock = NSLock() diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index a68022b9d..45d42386a 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -90,9 +90,7 @@ enum HomeScreenSecurityBannerMode: Equatable { } struct HomeScreenViewState: BindableState { - let userID: String - var userDisplayName: String? - var userAvatarURL: URL? + var userProfile: UserProfile var securityBannerMode = HomeScreenSecurityBannerMode.none var shouldShowNewSoundBanner = false diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 3072407d8..31dd16cb1 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -45,7 +45,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol roomSummaryProvider = userSession.clientProxy.roomSummaryProvider - super.init(initialViewState: .init(userID: userSession.clientProxy.userID, + super.init(initialViewState: .init(userProfile: userSession.clientProxy.userProfilePublisher.value, bindings: .init(filtersState: .init(appSettings: appSettings))), mediaProvider: userSession.mediaProvider) @@ -53,14 +53,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.isRoomListSearchEnabled = false } - userSession.clientProxy.userAvatarURLPublisher + userSession.clientProxy.userProfilePublisher .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userAvatarURL, on: self) - .store(in: &cancellables) - - userSession.clientProxy.userDisplayNamePublisher - .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userDisplayName, on: self) + .weakAssign(to: \.state.userProfile, on: self) .store(in: &cancellables) userSession.sessionSecurityStatePublisher @@ -353,8 +348,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // Delay user profile detail loading until after the initial room list loads if roomListMode == .rooms { Task { - await self.userSession.clientProxy.loadUserAvatarURL() - await self.userSession.clientProxy.loadUserDisplayName() + await self.userSession.clientProxy.loadUserProfile() } } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 135b2e184..77acb92d1 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -85,9 +85,9 @@ struct HomeScreen: View { Button { context.send(viewAction: .showSettings) } label: { - LoadableAvatarImage(url: context.viewState.userAvatarURL, - name: context.viewState.userDisplayName, - contentID: context.viewState.userID, + LoadableAvatarImage(url: context.viewState.userProfile.avatarURL, + name: context.viewState.userProfile.displayName, + contentID: context.viewState.userProfile.id, avatarSize: .user(on: .chats), mediaProvider: context.mediaProvider) .accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar) diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift index 12f2a09de..6af59dce8 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenModels.swift @@ -34,12 +34,10 @@ enum SettingsScreenSecuritySectionMode { struct SettingsScreenViewState: BindableState { var deviceID: String? - var userID: String + var userProfile: UserProfile var showLinkNewDeviceButton: Bool var accountProfileURL: URL? var showAccountDeactivation: Bool - var userAvatarURL: URL? - var userDisplayName: String? var showDeveloperOptions: Bool var securitySectionMode = SettingsScreenSecuritySectionMode.none diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift index 478a1b028..d45591d4c 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/SettingsScreenViewModel.swift @@ -24,7 +24,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo self.appSettings = appSettings super.init(initialViewState: .init(deviceID: userSession.clientProxy.deviceID, - userID: userSession.clientProxy.userID, + userProfile: userSession.clientProxy.userProfilePublisher.value, showLinkNewDeviceButton: appSettings.linkNewDeviceEnabled, showAccountDeactivation: userSession.clientProxy.canDeactivateAccount, showDeveloperOptions: appSettings.developerOptionsEnabled, @@ -41,14 +41,9 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .weakAssign(to: \.state.showLinkNewDeviceButton, on: self) .store(in: &cancellables) - userSession.clientProxy.userAvatarURLPublisher + userSession.clientProxy.userProfilePublisher .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userAvatarURL, on: self) - .store(in: &cancellables) - - userSession.clientProxy.userDisplayNamePublisher - .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userDisplayName, on: self) + .weakAssign(to: \.state.userProfile, on: self) .store(in: &cancellables) userSession.sessionSecurityStatePublisher @@ -86,8 +81,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo .store(in: &cancellables) Task { - await userSession.clientProxy.loadUserAvatarURL() - await userSession.clientProxy.loadUserDisplayName() + await userSession.clientProxy.loadUserProfile() await state.accountProfileURL = userSession.clientProxy.accountURL(action: .profile) } } diff --git a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift index 18df0f904..628af3a39 100644 --- a/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/SettingsScreen/View/SettingsScreen.swift @@ -51,18 +51,18 @@ struct SettingsScreen: View { context.send(viewAction: .userDetails) } label: { HStack(spacing: 12) { - LoadableAvatarImage(url: context.viewState.userAvatarURL, - name: context.viewState.userDisplayName, - contentID: context.viewState.userID, + LoadableAvatarImage(url: context.viewState.userProfile.avatarURL, + name: context.viewState.userProfile.displayName, + contentID: context.viewState.userProfile.id, avatarSize: .user(on: .settings), mediaProvider: context.mediaProvider) .accessibilityHidden(true) VStack(alignment: .leading, spacing: 2) { - Text(context.viewState.userDisplayName ?? "") + Text(context.viewState.userProfile.displayName ?? "") .font(.compound.headingMD) .foregroundColor(.compound.textPrimary) - Text(context.viewState.userID) + Text(context.viewState.userProfile.id) .font(.compound.bodySM) .foregroundColor(.compound.textSecondary) } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift index 66a569d06..1ba7a6137 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift @@ -16,26 +16,22 @@ enum UserDetailsEditScreenViewModelAction { } struct UserDetailsEditScreenViewState: BindableState { - let userID: String + var currentUserProfile: UserProfile var canEditAvatar = true var canEditDisplayName = true - var currentAvatarURL: URL? var selectedAvatarURL: URL? - - var currentDisplayName: String? - var localMedia: MediaInfo? var bindings: UserDetailsEditScreenViewStateBindings var nameDidChange: Bool { - bindings.name != currentDisplayName + bindings.name != currentUserProfile.displayName } var avatarDidChange: Bool { - localMedia != nil || selectedAvatarURL != currentAvatarURL + localMedia != nil || selectedAvatarURL != currentUserProfile.avatarURL } var canSave: Bool { diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift index c041bf3bf..fef50aea0 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenViewModel.swift @@ -28,36 +28,22 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe self.mediaUploadingPreprocessor = mediaUploadingPreprocessor self.userIndicatorController = userIndicatorController - super.init(initialViewState: UserDetailsEditScreenViewState(userID: clientProxy.userID, + super.init(initialViewState: UserDetailsEditScreenViewState(currentUserProfile: clientProxy.userProfilePublisher.value, bindings: .init()), mediaProvider: userSession.mediaProvider) - clientProxy.userAvatarURLPublisher + clientProxy.userProfilePublisher .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.currentAvatarURL, on: self) - .store(in: &cancellables) - - clientProxy.userAvatarURLPublisher - .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.selectedAvatarURL, on: self) - .store(in: &cancellables) - - clientProxy.userDisplayNamePublisher - .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.currentDisplayName, on: self) - .store(in: &cancellables) - - clientProxy.userDisplayNamePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] displayName in + .sink { [weak self] profile in guard let self else { return } - state.bindings.name = displayName ?? "" + state.currentUserProfile = profile + state.selectedAvatarURL = profile.avatarURL + state.bindings.name = profile.displayName ?? "" } .store(in: &cancellables) Task { - await self.clientProxy.loadUserAvatarURL() - await self.clientProxy.loadUserDisplayName() + await self.clientProxy.loadUserProfile() state.canEditAvatar = await clientProxy.capabilities.canChangeAvatar() state.canEditDisplayName = await clientProxy.capabilities.canChangeDisplayName() } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift index 6b8e6d1fb..5a01cb424 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift @@ -18,7 +18,7 @@ struct UserDetailsEditScreen: View { Section { avatar } footer: { - Text(context.viewState.userID) + Text(context.viewState.currentUserProfile.id) .frame(maxWidth: .infinity) .font(.compound.bodyLG) .foregroundColor(.compound.textPrimary) @@ -62,8 +62,8 @@ struct UserDetailsEditScreen: View { } label: { OverridableAvatarImage(overrideURL: context.viewState.localMedia?.thumbnailURL, url: context.viewState.selectedAvatarURL, - name: context.viewState.currentDisplayName, - contentID: context.viewState.userID, + name: context.viewState.currentUserProfile.displayName, + contentID: context.viewState.currentUserProfile.id, shape: .circle, avatarSize: .user(on: .editUserDetails), mediaProvider: context.mediaProvider) diff --git a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenModels.swift index c61fa734c..94515f49a 100644 --- a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenModels.swift @@ -15,9 +15,7 @@ enum SpacesScreenViewModelAction { } struct SpacesScreenViewState: BindableState { - let userID: String - var userDisplayName: String? - var userAvatarURL: URL? + var userProfile: UserProfile var topLevelSpaces: [SpaceServiceRoom] var selectedSpaceID: String? diff --git a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift index 63853465e..64212baa4 100644 --- a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift @@ -29,7 +29,7 @@ class SpacesScreenViewModel: SpacesScreenViewModelType, SpacesScreenViewModelPro self.appSettings = appSettings self.userIndicatorController = userIndicatorController - super.init(initialViewState: SpacesScreenViewState(userID: userSession.clientProxy.userID, + super.init(initialViewState: SpacesScreenViewState(userProfile: userSession.clientProxy.userProfilePublisher.value, topLevelSpaces: spaceServiceProxy.topLevelSpacesPublisher.value), mediaProvider: userSession.mediaProvider) @@ -42,14 +42,9 @@ class SpacesScreenViewModel: SpacesScreenViewModelType, SpacesScreenViewModelPro .weakAssign(to: \.state.selectedSpaceID, on: self) .store(in: &cancellables) - userSession.clientProxy.userAvatarURLPublisher + userSession.clientProxy.userProfilePublisher .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userAvatarURL, on: self) - .store(in: &cancellables) - - userSession.clientProxy.userDisplayNamePublisher - .receive(on: DispatchQueue.main) - .weakAssign(to: \.state.userDisplayName, on: self) + .weakAssign(to: \.state.userProfile, on: self) .store(in: &cancellables) } diff --git a/ElementX/Sources/Screens/Spaces/SpacesScreen/View/SpacesScreen.swift b/ElementX/Sources/Screens/Spaces/SpacesScreen/View/SpacesScreen.swift index ffdb88cec..f1aec6ee1 100644 --- a/ElementX/Sources/Screens/Spaces/SpacesScreen/View/SpacesScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpacesScreen/View/SpacesScreen.swift @@ -96,9 +96,9 @@ struct SpacesScreen: View { Button { context.send(viewAction: .showSettings) } label: { - LoadableAvatarImage(url: context.viewState.userAvatarURL, - name: context.viewState.userDisplayName, - contentID: context.viewState.userID, + LoadableAvatarImage(url: context.viewState.userProfile.avatarURL, + name: context.viewState.userProfile.displayName, + contentID: context.viewState.userProfile.id, avatarSize: .user(on: .spaces), mediaProvider: context.mediaProvider) .accessibilityIdentifier(A11yIdentifiers.homeScreen.userAvatar) diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 0e9853085..d7dc2d2cb 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -130,14 +130,9 @@ class ClientProxy: ClientProxyProtocol { } private var loadCachedAvatarURLTask: Task? - private let userAvatarURLSubject = CurrentValueSubject(nil) - var userAvatarURLPublisher: CurrentValuePublisher { - userAvatarURLSubject.asCurrentValuePublisher() - } - - private let userDisplayNameSubject = CurrentValueSubject(nil) - var userDisplayNamePublisher: CurrentValuePublisher { - userDisplayNameSubject.asCurrentValuePublisher() + private let userProfileSubject: CurrentValueSubject + var userProfilePublisher: CurrentValuePublisher { + userProfileSubject.asCurrentValuePublisher() } private let ignoredUsersSubject = CurrentValueSubject<[String]?, Never>(nil) @@ -213,6 +208,8 @@ class ClientProxy: ClientProxyProtocol { self.appSettings = appSettings self.analyticsService = analyticsService + userProfileSubject = .init(UserProfile(userID: (try? client.userId()) ?? "")) + if appSettings.automaticBackPaginationEnabled { // Must be called before creating the sync service, timelines etc. client.enableAutomaticBackpagination() @@ -677,13 +674,19 @@ class ClientProxy: ClientProxyProtocol { } } - func loadUserDisplayName() async -> Result { + func loadUserProfile() async -> Result { do { - let displayName = try await client.displayName() - userDisplayNameSubject.send(displayName) + async let displayName = client.displayName() + async let avatarURLString = client.avatarUrl() + + let profile = try await UserProfile(userID: userID, + displayName: displayName, + avatarURL: avatarURLString.flatMap(URL.init)) + loadCachedAvatarURLTask?.cancel() + userProfileSubject.send(profile) return .success(()) } catch { - MXLog.error("Failed loading user display name with error: \(error)") + MXLog.error("Failed loading user profile with error: \(error)") return .failure(.sdkError(error)) } } @@ -692,7 +695,7 @@ class ClientProxy: ClientProxyProtocol { do { try await client.setDisplayName(name: name) Task { - await self.loadUserDisplayName() + await self.loadUserProfile() } return .success(()) } catch { @@ -701,18 +704,6 @@ class ClientProxy: ClientProxyProtocol { } } - func loadUserAvatarURL() async -> Result { - do { - let urlString = try await client.avatarUrl() - loadCachedAvatarURLTask?.cancel() - userAvatarURLSubject.send(urlString.flatMap(URL.init)) - return .success(()) - } catch { - MXLog.error("Failed loading user avatar URL with error: \(error)") - return .failure(.sdkError(error)) - } - } - func setUserAvatar(media: MediaInfo) async -> Result { guard case let .image(imageURL, _, _) = media, let mimeType = media.mimeType else { MXLog.error("Failed uploading, invalid media: \(media)") @@ -723,7 +714,7 @@ class ClientProxy: ClientProxyProtocol { let data = try Data(contentsOf: imageURL) try await client.uploadAvatar(mimeType: mimeType, data: data) Task { - await self.loadUserAvatarURL() + await self.loadUserProfile() } return .success(()) } catch { @@ -736,7 +727,7 @@ class ClientProxy: ClientProxyProtocol { do { try await client.removeAvatar() Task { - await self.loadUserAvatarURL() + await self.loadUserProfile() } return .success(()) } catch { @@ -1172,7 +1163,10 @@ class ClientProxy: ClientProxyProtocol { do { let urlString = try await self.client.cachedAvatarUrl() guard !Task.isCancelled else { return } - self.userAvatarURLSubject.value = urlString.flatMap(URL.init) + let profile = self.userProfileSubject.value + self.userProfileSubject.value = UserProfile(userID: profile.id, + displayName: profile.displayName, + avatarURL: urlString.flatMap(URL.init)) } catch { MXLog.error("Failed to look for the avatar url in the cache: \(error)") } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 50c89589c..f48a5ad4f 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -120,9 +120,7 @@ protocol ClientProxyProtocol: AnyObject { var userIDServerName: String? { get } - var userDisplayNamePublisher: CurrentValuePublisher { get } - - var userAvatarURLPublisher: CurrentValuePublisher { get } + var userProfilePublisher: CurrentValuePublisher { get } /// We delay fetching this until after the first sync. Nil until then var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get } @@ -212,12 +210,10 @@ protocol ClientProxyProtocol: AnyObject { /// Will only work for rooms that are in our room list/local store func reportRoomForIdentifier(_ identifier: String, reason: String) async -> Result - @discardableResult func loadUserDisplayName() async -> Result + @discardableResult func loadUserProfile() async -> Result func setUserDisplayName(_ name: String) async -> Result - @discardableResult func loadUserAvatarURL() async -> Result - func setUserAvatar(media: MediaInfo) async -> Result func removeUserAvatar() async -> Result