Cached avatar support (from Rust) (#633)

* loading a cached avatar at first, and then actually doing the request so that the avatar can still be displayed without connection

* improved the code

* fix + changelog

* policy on how to fetch the avatar url added

* code improvements

* addressed al PR comments

* improved the error handling

* improved the async task code

* fixed a typo

* fixed another typo
This commit is contained in:
Mauro
2023-03-08 18:50:06 +01:00
committed by GitHub
parent c67c4e362e
commit fb27efee10
10 changed files with 62 additions and 31 deletions
+2 -1
View File
@@ -49,7 +49,8 @@ let allowList = ["stefanceriu",
"manuroe", "manuroe",
"gileluard", "gileluard",
"phlniji", "phlniji",
"aringenbach"] "aringenbach",
"Velin92"]
let requiresSignOff = !allowList.contains(where: { let requiresSignOff = !allowList.contains(where: {
$0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame $0.caseInsensitiveCompare(danger.github.pullRequest.user.login) == .orderedSame
@@ -95,8 +95,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-rust-components-swift", "location" : "https://github.com/matrix-org/matrix-rust-components-swift",
"state" : { "state" : {
"revision" : "32645162ea39badc586d610d2ada6bbc52f398ba", "revision" : "8d7ace5140d0f024b9a03939cdff29c837dc5645",
"version" : "1.0.43-alpha" "version" : "1.0.45-alpha"
} }
}, },
{ {
@@ -30,7 +30,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
var callback: ((HomeScreenViewModelAction) -> Void)? var callback: ((HomeScreenViewModelAction) -> Void)?
// swiftlint:disable:next function_body_length cyclomatic_complexity // swiftlint:disable:next function_body_length
init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) { init(userSession: UserSessionProtocol, attributedStringBuilder: AttributedStringBuilderProtocol) {
self.userSession = userSession self.userSession = userSession
self.attributedStringBuilder = attributedStringBuilder self.attributedStringBuilder = attributedStringBuilder
@@ -55,6 +55,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
} }
.store(in: &cancellables) .store(in: &cancellables)
userSession.clientProxy.avatarURLPublisher
.weakAssign(to: \.state.userAvatarURL, on: self)
.store(in: &cancellables)
guard let visibleRoomsSummaryProvider, let allRoomsSummaryProvider else { guard let visibleRoomsSummaryProvider, let allRoomsSummaryProvider else {
MXLog.error("Room summary provider unavailable") MXLog.error("Room summary provider unavailable")
return return
@@ -91,9 +95,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
// Delay user profile detail loading until after the initial room list loads // Delay user profile detail loading until after the initial room list loads
if roomListMode == .rooms { if roomListMode == .rooms {
Task { Task {
if case let .success(url) = await userSession.clientProxy.loadUserAvatarURL() { await userSession.clientProxy.loadUserAvatarURL()
self.state.userAvatarURL = url
}
} }
Task { Task {
@@ -44,10 +44,12 @@ class SettingsScreenViewModel: SettingsScreenViewModelType, SettingsScreenViewMo
.weakAssign(to: \.state.bindings.timelineStyle, on: self) .weakAssign(to: \.state.bindings.timelineStyle, on: self)
.store(in: &cancellables) .store(in: &cancellables)
userSession.clientProxy.avatarURLPublisher
.weakAssign(to: \.state.userAvatarURL, on: self)
.store(in: &cancellables)
Task { Task {
if case let .success(userAvatarURL) = await userSession.clientProxy.loadUserAvatarURL() { await userSession.clientProxy.loadUserAvatarURL()
state.userAvatarURL = userAvatarURL
}
} }
Task { Task {
@@ -59,6 +59,15 @@ class ClientProxy: ClientProxyProtocol {
var allRoomsSlidingSyncView: SlidingSyncList? var allRoomsSlidingSyncView: SlidingSyncList?
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? var allRoomsSummaryProvider: RoomSummaryProviderProtocol?
private var loadCachedAvatarURLTask: Task<Void, Never>?
private let avatarURLSubject = CurrentValueSubject<URL?, Never>(nil)
var avatarURLPublisher: AnyPublisher<URL?, Never> {
avatarURLSubject
.removeDuplicates()
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var visibleRoomsViewProxyStateObservationToken: AnyCancellable? private var visibleRoomsViewProxyStateObservationToken: AnyCancellable?
@@ -82,6 +91,8 @@ class ClientProxy: ClientProxyProtocol {
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self)) client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
configureSlidingSync() configureSlidingSync()
loadUserAvatarURLFromCache()
} }
var userID: String { var userID: String {
@@ -147,6 +158,7 @@ class ClientProxy: ClientProxyProtocol {
func loadUserDisplayName() async -> Result<String, ClientProxyError> { func loadUserDisplayName() async -> Result<String, ClientProxyError> {
await Task.dispatch(on: clientQueue) { await Task.dispatch(on: clientQueue) {
do { do {
self.loadCachedAvatarURLTask?.cancel()
let displayName = try self.client.displayName() let displayName = try self.client.displayName()
return .success(displayName) return .success(displayName)
} catch { } catch {
@@ -155,19 +167,14 @@ class ClientProxy: ClientProxyProtocol {
} }
} }
func loadUserAvatarURL() async -> Result<URL, ClientProxyError> { func loadUserAvatarURL() async {
await Task.dispatch(on: clientQueue) { await Task.dispatch(on: clientQueue) {
do { do {
let urlString = try self.client.avatarUrl() let urlString = try self.client.avatarUrl()
self.avatarURLSubject.value = urlString.flatMap(URL.init)
guard let url = URL(string: urlString) else {
MXLog.error("Invalid avatar URL string: \(String(describing: urlString))")
return .failure(.failedRetrievingAvatarURL)
}
return .success(url)
} catch { } catch {
return .failure(.failedRetrievingAvatarURL) MXLog.error("Failed fetching the user avatar url: \(error)")
return
} }
} }
} }
@@ -233,6 +240,21 @@ class ClientProxy: ClientProxyProtocol {
// MARK: Private // MARK: Private
private func loadUserAvatarURLFromCache() {
loadCachedAvatarURLTask = Task {
let urlString = await Task.dispatch(on: clientQueue) {
do {
return try self.client.cachedAvatarUrl()
} catch {
MXLog.error("Failed to look for the avatar url in the cache: \(error)")
return nil
}
}
guard !Task.isCancelled else { return }
self.avatarURLSubject.value = urlString.flatMap(URL.init)
}
}
private func restartSync() { private func restartSync() {
MXLog.info("Restarting sync") MXLog.info("Restarting sync")
stopSync() stopSync()
@@ -24,7 +24,6 @@ enum ClientProxyCallback {
} }
enum ClientProxyError: Error { enum ClientProxyError: Error {
case failedRetrievingAvatarURL
case failedRetrievingDisplayName case failedRetrievingDisplayName
case failedRetrievingAccountData case failedRetrievingAccountData
case failedSettingAccountData case failedSettingAccountData
@@ -72,6 +71,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
var homeserver: String { get } var homeserver: String { get }
var avatarURLPublisher: AnyPublisher<URL?, Never> { get }
var restorationToken: RestorationToken? { get } var restorationToken: RestorationToken? { get }
var visibleRoomsSummaryProvider: RoomSummaryProviderProtocol? { get } var visibleRoomsSummaryProvider: RoomSummaryProviderProtocol? { get }
@@ -86,7 +87,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {
func loadUserDisplayName() async -> Result<String, ClientProxyError> func loadUserDisplayName() async -> Result<String, ClientProxyError>
func loadUserAvatarURL() async -> Result<URL, ClientProxyError> func loadUserAvatarURL() async
func accountDataEvent<Content: Decodable>(type: String) async -> Result<Content?, ClientProxyError> func accountDataEvent<Content: Decodable>(type: String) async -> Result<Content?, ClientProxyError>
@@ -30,11 +30,15 @@ class MockClientProxy: ClientProxyProtocol {
var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider() var allRoomsSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()
var avatarURLPublisher: AnyPublisher<URL?, Never> { Empty().eraseToAnyPublisher() }
internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) { internal init(userID: String, roomSummaryProvider: RoomSummaryProviderProtocol? = MockRoomSummaryProvider()) {
self.userID = userID self.userID = userID
visibleRoomsSummaryProvider = roomSummaryProvider visibleRoomsSummaryProvider = roomSummaryProvider
} }
func loadUserAvatarURL() async { }
func startSync() { } func startSync() { }
func stopSync() { } func stopSync() { }
@@ -56,10 +60,6 @@ class MockClientProxy: ClientProxyProtocol {
.success("User display name") .success("User display name")
} }
func loadUserAvatarURL() async -> Result<URL, ClientProxyError> {
.failure(.failedRetrievingAvatarURL)
}
func accountDataEvent<Content>(type: String) async -> Result<Content?, ClientProxyError> where Content: Decodable { func accountDataEvent<Content>(type: String) async -> Result<Content?, ClientProxyError> where Content: Decodable {
.failure(.failedRetrievingAccountData) .failure(.failedRetrievingAccountData)
} }
@@ -83,7 +83,9 @@ private class MockMediaLoadingClient: ClientProtocol {
func setDisplayName(name: String) throws { } func setDisplayName(name: String) throws { }
func avatarUrl() throws -> String { fatalError() } func avatarUrl() throws -> String? { fatalError() }
func cachedAvatarUrl() throws -> String? { fatalError() }
func deviceId() throws -> String { fatalError() } func deviceId() throws -> String { fatalError() }
+1
View File
@@ -0,0 +1 @@
Avatar url is now cached on the rust side.
+1 -1
View File
@@ -40,7 +40,7 @@ include:
packages: packages:
MatrixRustSDK: MatrixRustSDK:
url: https://github.com/matrix-org/matrix-rust-components-swift url: https://github.com/matrix-org/matrix-rust-components-swift
exactVersion: 1.0.43-alpha exactVersion: 1.0.45-alpha
# path: ../matrix-rust-sdk # path: ../matrix-rust-sdk
DesignKit: DesignKit:
path: DesignKit path: DesignKit