mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Update the SDK and use RoomHero.status and subscribeToOwnProfile. (#5905)
* Update the SDK and hook up the status from Room Heroes. Also start requesting profiles via sss when the feature flag is enabled. * Add support for subscribing to your own profile.
This commit is contained in:
@@ -88,7 +88,7 @@ extension ClientProxyMock {
|
||||
}
|
||||
joinRoomAliasReturnValue = .success(())
|
||||
uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
loadUserProfileReturnValue = .success(())
|
||||
loadUserProfileIfNeededReturnValue = .success(())
|
||||
setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
setUserAvatarMediaReturnValue = .success(())
|
||||
removeUserAvatarReturnValue = .success(())
|
||||
|
||||
@@ -3094,33 +3094,33 @@ nonisolated class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable {
|
||||
return reportRoomForIdentifierReasonReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - loadUserProfile
|
||||
//MARK: - loadUserProfileIfNeeded
|
||||
|
||||
private let loadUserProfileCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var loadUserProfileUnderlyingCallsCount = 0
|
||||
var loadUserProfileCallsCount: Int {
|
||||
get { loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount } }
|
||||
set { loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount = newValue } }
|
||||
private let loadUserProfileIfNeededCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var loadUserProfileIfNeededUnderlyingCallsCount = 0
|
||||
var loadUserProfileIfNeededCallsCount: Int {
|
||||
get { loadUserProfileIfNeededCallsCountLock.withLock { loadUserProfileIfNeededUnderlyingCallsCount } }
|
||||
set { loadUserProfileIfNeededCallsCountLock.withLock { loadUserProfileIfNeededUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var loadUserProfileCalled: Bool {
|
||||
return loadUserProfileCallsCount > 0
|
||||
var loadUserProfileIfNeededCalled: Bool {
|
||||
return loadUserProfileIfNeededCallsCount > 0
|
||||
}
|
||||
|
||||
private let loadUserProfileReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var loadUserProfileUnderlyingReturnValue: Result<Void, ClientProxyError>!
|
||||
var loadUserProfileReturnValue: Result<Void, ClientProxyError>! {
|
||||
get { loadUserProfileReturnValueLock.withLock { loadUserProfileUnderlyingReturnValue } }
|
||||
set { loadUserProfileReturnValueLock.withLock { loadUserProfileUnderlyingReturnValue = newValue } }
|
||||
private let loadUserProfileIfNeededReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var loadUserProfileIfNeededUnderlyingReturnValue: Result<Void, ClientProxyError>!
|
||||
var loadUserProfileIfNeededReturnValue: Result<Void, ClientProxyError>! {
|
||||
get { loadUserProfileIfNeededReturnValueLock.withLock { loadUserProfileIfNeededUnderlyingReturnValue } }
|
||||
set { loadUserProfileIfNeededReturnValueLock.withLock { loadUserProfileIfNeededUnderlyingReturnValue = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var loadUserProfileClosure: (() async -> Result<Void, ClientProxyError>)?
|
||||
nonisolated(unsafe) var loadUserProfileIfNeededClosure: (() async -> Result<Void, ClientProxyError>)?
|
||||
|
||||
@discardableResult
|
||||
@concurrent func loadUserProfile() async -> Result<Void, ClientProxyError> {
|
||||
loadUserProfileCallsCountLock.withLock { loadUserProfileUnderlyingCallsCount += 1 }
|
||||
if let loadUserProfileClosure = loadUserProfileClosure {
|
||||
return await loadUserProfileClosure()
|
||||
@concurrent func loadUserProfileIfNeeded() async -> Result<Void, ClientProxyError> {
|
||||
loadUserProfileIfNeededCallsCountLock.withLock { loadUserProfileIfNeededUnderlyingCallsCount += 1 }
|
||||
if let loadUserProfileIfNeededClosure = loadUserProfileIfNeededClosure {
|
||||
return await loadUserProfileIfNeededClosure()
|
||||
} else {
|
||||
return loadUserProfileReturnValue
|
||||
return loadUserProfileIfNeededReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - setUserDisplayName
|
||||
|
||||
@@ -176,7 +176,9 @@ private extension RoomHero {
|
||||
init(from memberProxy: RoomMemberProxyMock) {
|
||||
self.init(userId: memberProxy.userID,
|
||||
displayName: memberProxy.displayName,
|
||||
avatarUrl: memberProxy.avatarURL?.absoluteString)
|
||||
avatarUrl: memberProxy.avatarURL?.absoluteString,
|
||||
status: memberProxy.status.raw?.rustValue,
|
||||
call: memberProxy.status.call?.rustValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ nonisolated extension SDKListener: VerificationStateListener where T == Verifica
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension SDKListener: ProfileListener where T == MatrixRustSDK.UserProfile {
|
||||
func onUpdate(profile: MatrixRustSDK.UserProfile) {
|
||||
onUpdateClosure(profile)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated extension SDKListener: IgnoredUsersListener where T == [String] {
|
||||
func call(ignoredUserIds: [String]) {
|
||||
onUpdateClosure(ignoredUserIds)
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
|
||||
struct RoomHeaderView: View {
|
||||
struct DMRecipientDetails {
|
||||
var status: UserStatus?
|
||||
var statusEmoji: Character?
|
||||
var verification: UserIdentityVerificationState?
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ struct RoomHeaderView: View {
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
.accessibilityIdentifier(A11yIdentifiers.roomScreen.name)
|
||||
|
||||
if let statusEmoji = dmRecipientDetails.status?.displayed?.emoji {
|
||||
if let statusEmoji = dmRecipientDetails.statusEmoji {
|
||||
Text(String(statusEmoji))
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textPrimary)
|
||||
@@ -179,7 +179,8 @@ struct RoomHeaderView_Previews: PreviewProvider, TestablePreview {
|
||||
roomAvatar: .room(id: "1",
|
||||
name: roomName,
|
||||
avatarURL: avatarURL),
|
||||
dmRecipientDetails: .init(status: userStatus, verification: verificationState),
|
||||
dmRecipientDetails: .init(statusEmoji: userStatus?.displayed?.emoji,
|
||||
verification: verificationState),
|
||||
roomHistorySharingState: historySharingState,
|
||||
|
||||
mediaProvider: MediaProviderMock(.init())) { }
|
||||
|
||||
@@ -348,7 +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.loadUserProfile()
|
||||
await self.userSession.clientProxy.loadUserProfileIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
|
||||
state.roomTitle = roomInfo.displayName ?? roomProxy.id
|
||||
state.roomAvatar = roomInfo.avatar
|
||||
state.dmRecipientDetails.statusEmoji = roomInfo.statusEmoji
|
||||
state.hasOngoingCall = roomInfo.hasRoomCall
|
||||
state.activeRoomCallIntent = roomInfo.activeRoomCallIntent
|
||||
state.hasSuccessor = roomInfo.successor != nil
|
||||
|
||||
@@ -86,7 +86,7 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
await userSession.clientProxy.loadUserProfile()
|
||||
await userSession.clientProxy.loadUserProfileIfNeeded()
|
||||
await state.accountProfileURL = userSession.clientProxy.accountURL(action: .profile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ struct SettingsScreen: View {
|
||||
SettingsScreenUserStatusPickerView { action in
|
||||
context.send(viewAction: .userStatus(action))
|
||||
}
|
||||
.presentationDetents([.medium]) // Stop using a List and calculate the exact height?
|
||||
.presentationBackground(.compound.bgCanvasDefault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDe
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
await self.clientProxy.loadUserProfile()
|
||||
await self.clientProxy.loadUserProfileIfNeeded()
|
||||
state.canEditAvatar = await clientProxy.capabilities.canChangeAvatar()
|
||||
state.canEditDisplayName = await clientProxy.capabilities.canChangeDisplayName()
|
||||
}
|
||||
|
||||
@@ -103,6 +103,8 @@ class ThreadTimelineScreenViewModel: ThreadTimelineScreenViewModelType, ThreadTi
|
||||
private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) {
|
||||
state.roomTitle = roomInfo.displayName ?? roomProxy.id
|
||||
state.roomAvatar = roomInfo.avatar
|
||||
state.dmRecipientDetails.statusEmoji = roomInfo.statusEmoji
|
||||
|
||||
if let powerLevels = roomInfo.powerLevels {
|
||||
state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ class ClientProxy: ClientProxyProtocol {
|
||||
// periphery: ignore - only for retain
|
||||
private var syncServiceStateUpdateTaskHandle: TaskHandle?
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var userProfileListenerTaskHandle: TaskHandle?
|
||||
|
||||
// periphery:ignore - required for instance retention in the rust codebase
|
||||
private var ignoredUsersListenerTaskHandle: TaskHandle?
|
||||
|
||||
@@ -278,9 +281,13 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
try await client.setUtdDelegate(utdDelegate: ClientDecryptionErrorDelegate(actionsSubject: actionsSubject))
|
||||
|
||||
loadUserAvatarURLFromCache()
|
||||
let canSubscribeToUserProfile = appSettings.userStatusEnabled // TODO: @pixlwave to add a /versions check too.
|
||||
|
||||
await setupSubscriptions()
|
||||
if !canSubscribeToUserProfile {
|
||||
loadUserAvatarURLFromCache()
|
||||
}
|
||||
|
||||
await setupSubscriptions(canSubscribeToUserProfile: canSubscribeToUserProfile)
|
||||
|
||||
Task {
|
||||
do {
|
||||
@@ -686,14 +693,18 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func loadUserProfile() async -> Result<Void, ClientProxyError> {
|
||||
func loadUserProfileIfNeeded() async -> Result<Void, ClientProxyError> {
|
||||
// There's no need to load the profile if we're subscribed to it via /sync
|
||||
guard userProfileListenerTaskHandle == nil else { return .success(()) }
|
||||
|
||||
do {
|
||||
async let displayName = client.displayName()
|
||||
async let avatarURLString = client.avatarUrl()
|
||||
|
||||
let profile = try await UserProfile(userID: userID,
|
||||
displayName: displayName,
|
||||
avatarURL: avatarURLString.flatMap(URL.init))
|
||||
avatarURL: avatarURLString.flatMap(URL.init),
|
||||
status: userProfileSubject.value.status)
|
||||
loadCachedAvatarURLTask?.cancel()
|
||||
userProfileSubject.send(profile)
|
||||
return .success(())
|
||||
@@ -706,7 +717,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
try await client.setDisplayName(name: name)
|
||||
Task { await self.loadUserProfile() }
|
||||
Task { await self.loadUserProfileIfNeeded() }
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed setting user display name with error: \(error)")
|
||||
@@ -723,7 +734,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
do {
|
||||
let data = try Data(contentsOf: imageURL)
|
||||
try await client.uploadAvatar(mimeType: mimeType, data: data)
|
||||
Task { await self.loadUserProfile() }
|
||||
Task { await self.loadUserProfileIfNeeded() }
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed setting user avatar with error: \(error)")
|
||||
@@ -734,7 +745,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func removeUserAvatar() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
try await client.removeAvatar()
|
||||
Task { await self.loadUserProfile() }
|
||||
Task { await self.loadUserProfileIfNeeded() }
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed removing user avatar with error: \(error)")
|
||||
@@ -746,7 +757,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func setUserStatus(_ status: UserStatus.Raw) async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
try await client.setUserStatus(status: status.rustValue)
|
||||
Task { await self.loadUserProfile() }
|
||||
// No need to refresh the profile, we only support user status with the profiles /sync extension.
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed setting user status with error: \(error)")
|
||||
@@ -758,7 +769,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
func removeUserStatus() async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
try await client.clearUserStatus()
|
||||
Task { await self.loadUserProfile() }
|
||||
Task { await self.loadUserProfileIfNeeded() }
|
||||
return .success(())
|
||||
} catch {
|
||||
MXLog.error("Failed removing user status with error: \(error)")
|
||||
@@ -1017,7 +1028,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupSubscriptions() async {
|
||||
private func setupSubscriptions(canSubscribeToUserProfile: Bool) async {
|
||||
networkMonitor.reachabilityPublisher
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
@@ -1030,6 +1041,12 @@ class ClientProxy: ClientProxyProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
if canSubscribeToUserProfile {
|
||||
userProfileListenerTaskHandle = try? client.subscribeToOwnProfile(listener: SDKListener.onMainActor { [weak self] profile in
|
||||
self?.userProfileSubject.send(.init(rustUserProfile: profile))
|
||||
})
|
||||
}
|
||||
|
||||
ignoredUsersListenerTaskHandle = client.subscribeToIgnoredUsers(listener: SDKListener.onMainActor { [weak self] ignoredUsers in
|
||||
self?.ignoredUsersSubject.send(ignoredUsers)
|
||||
})
|
||||
@@ -1201,7 +1218,8 @@ class ClientProxy: ClientProxyProtocol {
|
||||
let profile = self.userProfileSubject.value
|
||||
self.userProfileSubject.value = UserProfile(userID: profile.id,
|
||||
displayName: profile.displayName,
|
||||
avatarURL: urlString.flatMap(URL.init))
|
||||
avatarURL: urlString.flatMap(URL.init),
|
||||
status: profile.status)
|
||||
} catch {
|
||||
MXLog.error("Failed to look for the avatar url in the cache: \(error)")
|
||||
}
|
||||
@@ -1481,11 +1499,14 @@ private struct ClientProxyServices {
|
||||
init(client: ClientProtocol,
|
||||
notificationSettings: NotificationSettingsProxyProtocol,
|
||||
appSettings: AppSettings) async throws {
|
||||
let syncService = try await client
|
||||
var syncServiceBuilder = client
|
||||
.syncService()
|
||||
.withOfflineMode()
|
||||
.withSharePos(enable: true)
|
||||
.finish()
|
||||
if appSettings.userStatusEnabled {
|
||||
syncServiceBuilder = syncServiceBuilder.withProfilesExtension()
|
||||
}
|
||||
let syncService = try await syncServiceBuilder.finish()
|
||||
|
||||
let roomListService = syncService.roomListService()
|
||||
|
||||
|
||||
@@ -212,7 +212,11 @@ protocol ClientProxyProtocol: AnyObject {
|
||||
/// Will only work for rooms that are in our room list/local store
|
||||
func reportRoomForIdentifier(_ identifier: String, reason: String) async -> Result<Void, ClientProxyError>
|
||||
|
||||
@discardableResult func loadUserProfile() async -> Result<Void, ClientProxyError>
|
||||
/// Loads the user's own profile when the server doesn't support MSC4262 and both returns the profile
|
||||
/// as well as publishing it via ``userProfilePublisher``.
|
||||
///
|
||||
/// When the server does support the MSC, then the client automatically publishes profile and keeps it up to date.
|
||||
@discardableResult func loadUserProfileIfNeeded() async -> Result<Void, ClientProxyError>
|
||||
func setUserDisplayName(_ name: String) async -> Result<Void, ClientProxyError>
|
||||
func setUserAvatar(media: MediaInfo) async -> Result<Void, ClientProxyError>
|
||||
func removeUserAvatar() async -> Result<Void, ClientProxyError>
|
||||
|
||||
@@ -94,6 +94,11 @@ extension BaseRoomInfoProxyProtocol {
|
||||
|
||||
return .room(id: id, name: displayName, avatarURL: avatarURL)
|
||||
}
|
||||
|
||||
var statusEmoji: Character? {
|
||||
guard case let .heroes(heroes) = avatar else { return nil }
|
||||
return heroes.first?.status.displayed?.emoji
|
||||
}
|
||||
}
|
||||
|
||||
extension RoomInfoProxyProtocol {
|
||||
|
||||
@@ -186,7 +186,7 @@ nonisolated extension RoomSummary {
|
||||
}
|
||||
|
||||
var statusEmoji: Character? {
|
||||
guard isDirect, case let .heroes(heroes) = avatar, heroes.count == 1 else { return nil }
|
||||
guard case let .heroes(heroes) = avatar else { return nil }
|
||||
return heroes.first?.status.displayed?.emoji
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ nonisolated struct UserProfile: Hashable, Identifiable {
|
||||
id = rustRoomHero.userId
|
||||
displayName = rustRoomHero.displayName
|
||||
avatarURL = rustRoomHero.avatarUrl.flatMap(URL.init(string:))
|
||||
status = .init() // Requires https://github.com/matrix-org/matrix-rust-sdk/pull/6733
|
||||
status = .init(rustStatus: rustRoomHero.status, rustCall: rustRoomHero.call)
|
||||
}
|
||||
|
||||
init(member: RoomMemberProxyProtocol) {
|
||||
|
||||
Reference in New Issue
Block a user