mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Concurrency Fixes (#283)
* Review places where MainActor used * Adapt to MainActor changes, avoid redundant task detachments * Move Rust client operations into a dedicated concurrent queue, make init async * Add changelog * Fixed small working for awaiting non-async method * Remove some redundant tasks * Put back some main actors * Fix tests * Fixed timeline updates when the number of items doesn't change. Keeps more previous home screen room data between reloads Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
@@ -96,12 +96,13 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
|
|||||||
|
|
||||||
init(initialViewState: State) {
|
init(initialViewState: State) {
|
||||||
context = Context(initialViewState: initialViewState)
|
context = Context(initialViewState: initialViewState)
|
||||||
context.viewActions.sink { [weak self] action in
|
context.viewActions
|
||||||
guard let self else { return }
|
.sink { [weak self] action in
|
||||||
|
guard let self else { return }
|
||||||
Task { await self.process(viewAction: action) }
|
|
||||||
}
|
Task { await self.process(viewAction: action) }
|
||||||
.store(in: &cancellables)
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
|
/// Override to handles incoming `ViewAction`s from the `ViewModel`.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
private let userSession: UserSessionProtocol
|
private let userSession: UserSessionProtocol
|
||||||
private let roomSummaryProvider: RoomSummaryProviderProtocol
|
private let roomSummaryProvider: RoomSummaryProviderProtocol
|
||||||
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
private let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||||
|
private var roomsForIdentifiers = [String: HomeScreenRoom]()
|
||||||
|
|
||||||
var callback: ((HomeScreenViewModelAction) -> Void)?
|
var callback: ((HomeScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
@@ -72,9 +73,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
roomSummaryProvider.roomListPublisher
|
roomSummaryProvider.roomListPublisher
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
Task {
|
self?.updateRooms()
|
||||||
await self?.updateRooms()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
@@ -91,10 +90,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
state.userDisplayName = userDisplayName
|
state.userDisplayName = userDisplayName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
updateRooms()
|
||||||
await updateRooms()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@@ -117,52 +114,58 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func loadDataForRoomIdentifier(_ identifier: String) {
|
private func loadDataForRoomIdentifier(_ identifier: String) {
|
||||||
guard let summary = roomSummaryProvider.roomListPublisher.value.first(where: { $0.asFilled?.id == identifier })?.asFilled,
|
guard let roomSummary = roomSummaryProvider.roomListPublisher.value.first(where: { $0.asFilled?.id == identifier })?.asFilled,
|
||||||
let homeRoomIndex = state.rooms.firstIndex(where: { $0.id == identifier }) else {
|
let roomIndex = state.rooms.firstIndex(where: { $0.id == identifier }) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var details = state.rooms[homeRoomIndex]
|
var room = state.rooms[roomIndex]
|
||||||
|
|
||||||
guard details.avatar == nil,
|
guard room.avatar == nil,
|
||||||
let avatarURLString = summary.avatarURLString else {
|
let avatarURLString = roomSummary.avatarURLString else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
|
if case let .success(image) = await userSession.mediaProvider.loadImageFromURLString(avatarURLString, avatarSize: .room(on: .home)) {
|
||||||
details.avatar = image
|
room.avatar = image
|
||||||
state.rooms[homeRoomIndex] = details
|
state.rooms[roomIndex] = room
|
||||||
|
roomsForIdentifiers[roomSummary.id] = room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRooms() async {
|
private func updateRooms() {
|
||||||
state.rooms = await Task.detached {
|
var rooms = [HomeScreenRoom]()
|
||||||
var rooms = [HomeScreenRoom]()
|
var newRoomsForIdentifiers = [String: HomeScreenRoom]()
|
||||||
|
|
||||||
for summary in self.roomSummaryProvider.roomListPublisher.value {
|
for summary in roomSummaryProvider.roomListPublisher.value {
|
||||||
switch summary {
|
switch summary {
|
||||||
case .empty(let id):
|
case .empty(let id):
|
||||||
rooms.append(HomeScreenRoom.placeholder(id: id))
|
rooms.append(HomeScreenRoom.placeholder(id: id))
|
||||||
case .filled(let summary):
|
case .filled(let summary):
|
||||||
let avatarImage = await self.userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
|
let oldRoom = roomsForIdentifiers[summary.id]
|
||||||
|
|
||||||
var timestamp: String?
|
let avatarImage = userSession.mediaProvider.imageFromURLString(summary.avatarURLString, avatarSize: .room(on: .home))
|
||||||
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
|
||||||
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
var timestamp: String?
|
||||||
}
|
if let lastMessageTimestamp = summary.lastMessageTimestamp {
|
||||||
|
timestamp = lastMessageTimestamp.formatted(date: .omitted, time: .shortened)
|
||||||
rooms.append(HomeScreenRoom(id: summary.id,
|
|
||||||
name: summary.name,
|
|
||||||
hasUnreads: summary.unreadNotificationCount > 0,
|
|
||||||
timestamp: timestamp,
|
|
||||||
lastMessage: summary.lastMessage,
|
|
||||||
avatar: avatarImage))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let room = HomeScreenRoom(id: summary.id,
|
||||||
|
name: summary.name,
|
||||||
|
hasUnreads: summary.unreadNotificationCount > 0,
|
||||||
|
timestamp: timestamp ?? oldRoom?.timestamp,
|
||||||
|
lastMessage: summary.lastMessage ?? oldRoom?.lastMessage,
|
||||||
|
avatar: avatarImage ?? oldRoom?.avatar)
|
||||||
|
|
||||||
|
rooms.append(room)
|
||||||
|
newRoomsForIdentifiers[summary.id] = room
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return rooms
|
|
||||||
}.value
|
state.rooms = rooms
|
||||||
|
roomsForIdentifiers = newRoomsForIdentifiers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ struct TimelineItemList: View {
|
|||||||
// Otherwise just update the items
|
// Otherwise just update the items
|
||||||
timelineItems = context.viewState.items
|
timelineItems = context.viewState.items
|
||||||
}
|
}
|
||||||
|
.onChange(of: context.viewState.items, perform: { items in
|
||||||
|
if timelineItems != items {
|
||||||
|
timelineItems = items
|
||||||
|
}
|
||||||
|
})
|
||||||
.background(GeometryReader { geo in
|
.background(GeometryReader { geo in
|
||||||
Color.clear.preference(key: ViewFramePreferenceKey.self, value: [geo.frame(in: .global)])
|
Color.clear.preference(key: ViewFramePreferenceKey.self, value: [geo.frame(in: .global)])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ enum AuthenticationServiceError: Error {
|
|||||||
case failedLoggingIn
|
case failedLoggingIn
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol AuthenticationServiceProxyProtocol {
|
protocol AuthenticationServiceProxyProtocol {
|
||||||
var homeserver: LoginHomeserver { get }
|
var homeserver: LoginHomeserver { get }
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
protocol BackgroundTaskServiceProtocol {
|
protocol BackgroundTaskServiceProtocol {
|
||||||
func startBackgroundTask(withName name: String,
|
func startBackgroundTask(withName name: String,
|
||||||
isReusable: Bool,
|
isReusable: Bool,
|
||||||
|
|||||||
@@ -31,23 +31,17 @@ private class WeakClientProxyWrapper: ClientDelegate, SlidingSyncObserver {
|
|||||||
func didReceiveSyncUpdate() { }
|
func didReceiveSyncUpdate() { }
|
||||||
|
|
||||||
func didReceiveAuthError(isSoftLogout: Bool) {
|
func didReceiveAuthError(isSoftLogout: Bool) {
|
||||||
Task {
|
clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
||||||
await clientProxy?.didReceiveAuthError(isSoftLogout: isSoftLogout)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didUpdateRestoreToken() {
|
func didUpdateRestoreToken() {
|
||||||
Task {
|
clientProxy?.didUpdateRestoreToken()
|
||||||
await clientProxy?.didUpdateRestoreToken()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SlidingSyncDelegate
|
// MARK: - SlidingSyncDelegate
|
||||||
|
|
||||||
func didReceiveSyncUpdate(summary: UpdateSummary) {
|
func didReceiveSyncUpdate(summary: UpdateSummary) {
|
||||||
Task {
|
clientProxy?.didReceiveSlidingSyncUpdate(summary: summary)
|
||||||
await self.clientProxy?.didReceiveSlidingSyncUpdate(summary: summary)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,11 +52,18 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
private let client: ClientProtocol
|
private let client: ClientProtocol
|
||||||
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
private let backgroundTaskService: BackgroundTaskServiceProtocol
|
||||||
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
private var sessionVerificationControllerProxy: SessionVerificationControllerProxy?
|
||||||
|
private let clientQueue: DispatchQueue
|
||||||
|
|
||||||
private var slidingSyncObserverToken: StoppableSpawn?
|
private var slidingSyncObserverToken: StoppableSpawn?
|
||||||
private let slidingSync: SlidingSync
|
private var slidingSync: SlidingSync!
|
||||||
|
|
||||||
let roomSummaryProvider: RoomSummaryProviderProtocol
|
var roomSummaryProviderInternal: RoomSummaryProviderProtocol!
|
||||||
|
var roomSummaryProvider: RoomSummaryProviderProtocol {
|
||||||
|
guard let roomSummaryProviderInternal else {
|
||||||
|
fatalError("There is an issue with ClientProxy object initialization")
|
||||||
|
}
|
||||||
|
return roomSummaryProviderInternal
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
// These need to be inlined instead of using stopSync()
|
// These need to be inlined instead of using stopSync()
|
||||||
@@ -75,33 +76,37 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
let callbacks = PassthroughSubject<ClientProxyCallback, Never>()
|
||||||
|
|
||||||
init(client: ClientProtocol,
|
init(client: ClientProtocol,
|
||||||
backgroundTaskService: BackgroundTaskServiceProtocol) {
|
backgroundTaskService: BackgroundTaskServiceProtocol) async {
|
||||||
self.client = client
|
self.client = client
|
||||||
self.backgroundTaskService = backgroundTaskService
|
self.backgroundTaskService = backgroundTaskService
|
||||||
|
clientQueue = .init(label: "ClientProxyQueue",
|
||||||
do {
|
attributes: .concurrent)
|
||||||
let slidingSyncBuilder = try client.slidingSync().homeserver(url: BuildSettings.slidingSyncProxyBaseURL.absoluteString)
|
|
||||||
|
await Task.dispatch(on: clientQueue) {
|
||||||
let slidingSyncView = try SlidingSyncViewBuilder()
|
do {
|
||||||
.timelineLimit(limit: 10)
|
let slidingSyncBuilder = try client.slidingSync().homeserver(url: BuildSettings.slidingSyncProxyBaseURL.absoluteString)
|
||||||
.requiredState(requiredState: [RequiredState(key: "m.room.avatar", value: ""),
|
|
||||||
RequiredState(key: "m.room.encryption", value: "")])
|
let slidingSyncView = try SlidingSyncViewBuilder()
|
||||||
.name(name: "HomeScreenView")
|
.timelineLimit(limit: 10)
|
||||||
.syncMode(mode: .fullSync)
|
.requiredState(requiredState: [RequiredState(key: "m.room.avatar", value: ""),
|
||||||
.build()
|
RequiredState(key: "m.room.encryption", value: "")])
|
||||||
|
.name(name: "HomeScreenView")
|
||||||
slidingSync = try slidingSyncBuilder
|
.syncMode(mode: .fullSync)
|
||||||
.addView(view: slidingSyncView)
|
.build()
|
||||||
.withCommonExtensions()
|
|
||||||
.build()
|
self.slidingSync = try slidingSyncBuilder
|
||||||
|
.addView(view: slidingSyncView)
|
||||||
roomSummaryProvider = RoomSummaryProvider(slidingSyncController: slidingSync,
|
.withCommonExtensions()
|
||||||
slidingSyncView: slidingSyncView,
|
.build()
|
||||||
roomMessageFactory: RoomMessageFactory())
|
|
||||||
} catch {
|
self.roomSummaryProviderInternal = RoomSummaryProvider(slidingSyncController: self.slidingSync,
|
||||||
fatalError("Failed configuring sliding sync")
|
slidingSyncView: slidingSyncView,
|
||||||
|
roomMessageFactory: RoomMessageFactory())
|
||||||
|
} catch {
|
||||||
|
fatalError("Failed configuring sliding sync")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
client.setDelegate(delegate: WeakClientProxyWrapper(clientProxy: self))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,27 +161,22 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
slidingSync.setObserver(observer: nil)
|
slidingSync.setObserver(observer: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
||||||
do {
|
let (slidingSyncRoom, room) = await Task.dispatch(on: clientQueue) {
|
||||||
guard let slidingSyncRoom = try slidingSync.getRoom(roomId: identifier),
|
self.roomTupleForIdentifier(identifier)
|
||||||
let room = slidingSyncRoom.fullRoom() else {
|
}
|
||||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
|
||||||
return nil
|
guard let slidingSyncRoom, let room else {
|
||||||
}
|
|
||||||
|
|
||||||
let roomProxy = RoomProxy(slidingSyncRoom: slidingSyncRoom,
|
|
||||||
room: room,
|
|
||||||
backgroundTaskService: backgroundTaskService)
|
|
||||||
|
|
||||||
return roomProxy
|
|
||||||
} catch {
|
|
||||||
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await RoomProxy(slidingSyncRoom: slidingSyncRoom,
|
||||||
|
room: room,
|
||||||
|
backgroundTaskService: backgroundTaskService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
|
||||||
await Task.dispatch(on: .global()) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
do {
|
||||||
let displayName = try self.client.displayName()
|
let displayName = try self.client.displayName()
|
||||||
return .success(displayName)
|
return .success(displayName)
|
||||||
@@ -187,7 +187,7 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
func loadUserAvatarURLString() async -> Result<String, ClientProxyError> {
|
||||||
await Task.dispatch(on: .global()) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
do {
|
||||||
let avatarURL = try self.client.avatarUrl()
|
let avatarURL = try self.client.avatarUrl()
|
||||||
return .success(avatarURL)
|
return .success(avatarURL)
|
||||||
@@ -198,11 +198,15 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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)
|
await Task.dispatch(on: clientQueue) {
|
||||||
|
.failure(.failedRetrievingAccountData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError> {
|
func setAccountData<Content: Encodable>(content: Content, type: String) async -> Result<Void, ClientProxyError> {
|
||||||
.failure(.failedSettingAccountData)
|
await Task.dispatch(on: clientQueue) {
|
||||||
|
.failure(.failedSettingAccountData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
|
func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource {
|
||||||
@@ -210,21 +214,21 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) async throws -> Data {
|
||||||
try await Task.dispatch(on: .global()) {
|
try await Task.dispatch(on: clientQueue) {
|
||||||
let bytes = try self.client.getMediaContent(source: source)
|
let bytes = try self.client.getMediaContent(source: source)
|
||||||
return Data(bytes: bytes, count: bytes.count)
|
return Data(bytes: bytes, count: bytes.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
func loadMediaThumbnailForSource(_ source: MatrixRustSDK.MediaSource, width: UInt, height: UInt) async throws -> Data {
|
||||||
try await Task.dispatch(on: .global()) {
|
try await Task.dispatch(on: clientQueue) {
|
||||||
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
|
let bytes = try self.client.getMediaThumbnail(source: source, width: UInt64(width), height: UInt64(height))
|
||||||
return Data(bytes: bytes, count: bytes.count)
|
return Data(bytes: bytes, count: bytes.count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
func sessionVerificationControllerProxy() async -> Result<SessionVerificationControllerProxyProtocol, ClientProxyError> {
|
||||||
await Task.dispatch(on: .global()) {
|
await Task.dispatch(on: clientQueue) {
|
||||||
do {
|
do {
|
||||||
let sessionVerificationController = try self.client.getSessionVerificationController()
|
let sessionVerificationController = try self.client.getSessionVerificationController()
|
||||||
return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController))
|
return .success(SessionVerificationControllerProxy(sessionVerificationController: sessionVerificationController))
|
||||||
@@ -235,14 +239,28 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func logout() async {
|
func logout() async {
|
||||||
do {
|
await Task.dispatch(on: clientQueue) {
|
||||||
try client.logout()
|
do {
|
||||||
} catch {
|
try self.client.logout()
|
||||||
MXLog.error("Failed logging out with error: \(error)")
|
} catch {
|
||||||
|
MXLog.error("Failed logging out with error: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private
|
// MARK: Private
|
||||||
|
|
||||||
|
private func roomTupleForIdentifier(_ identifier: String) -> (SlidingSyncRoom?, Room?) {
|
||||||
|
do {
|
||||||
|
let slidingSyncRoom = try slidingSync.getRoom(roomId: identifier)
|
||||||
|
let fullRoom = slidingSyncRoom?.fullRoom()
|
||||||
|
|
||||||
|
return (slidingSyncRoom, fullRoom)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed retrieving room with identifier: \(identifier)")
|
||||||
|
return (nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
fileprivate func didReceiveAuthError(isSoftLogout: Bool) {
|
||||||
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
callbacks.send(.receivedAuthError(isSoftLogout: isSoftLogout))
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ enum ClientProxyError: Error {
|
|||||||
case failedLoadingMedia
|
case failedLoadingMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol ClientProxyProtocol {
|
protocol ClientProxyProtocol {
|
||||||
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
var callbacks: PassthroughSubject<ClientProxyCallback, Never> { get }
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ protocol ClientProxyProtocol {
|
|||||||
|
|
||||||
func stopSync()
|
func stopSync()
|
||||||
|
|
||||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol?
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol?
|
||||||
|
|
||||||
func loadUserDisplayName() async -> Result<String, ClientProxyError>
|
func loadUserDisplayName() async -> Result<String, ClientProxyError>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct MockClientProxy: ClientProxyProtocol {
|
|||||||
|
|
||||||
func stopSync() { }
|
func stopSync() { }
|
||||||
|
|
||||||
func roomForIdentifier(_ identifier: String) -> RoomProxyProtocol? {
|
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,43 +55,38 @@ struct MediaProvider: MediaProviderProtocol {
|
|||||||
return .success(image)
|
return .success(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
let loadImageBgTask = backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.underlyingSource.url().hashValue)")
|
let loadImageBgTask = await backgroundTaskService.startBackgroundTask(withName: "LoadImage: \(source.url.hashValue)")
|
||||||
defer {
|
defer {
|
||||||
loadImageBgTask?.stop()
|
loadImageBgTask?.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
let cacheKey = cacheKeyForURLString(source.underlyingSource.url(), avatarSize: avatarSize)
|
let cacheKey = cacheKeyForURLString(source.url, avatarSize: avatarSize)
|
||||||
|
|
||||||
return await Task.detached { () -> Result<UIImage, MediaProviderError> in
|
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
||||||
if case let .success(cacheResult) = await imageCache.retrieveImage(forKey: cacheKey),
|
let image = cacheResult.image {
|
||||||
let image = cacheResult.image {
|
return .success(image)
|
||||||
return .success(image)
|
}
|
||||||
}
|
|
||||||
|
do {
|
||||||
do {
|
let imageData: Data
|
||||||
let imageData = try await Task.detached { () -> Data in
|
if let avatarSize {
|
||||||
if let avatarSize {
|
imageData = try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
|
||||||
return try await clientProxy.loadMediaThumbnailForSource(source.underlyingSource, width: UInt(avatarSize.scaledValue), height: UInt(avatarSize.scaledValue))
|
} else {
|
||||||
} else {
|
imageData = try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
||||||
return try await clientProxy.loadMediaContentForSource(source.underlyingSource)
|
}
|
||||||
}
|
|
||||||
|
guard let image = UIImage(data: imageData) else {
|
||||||
}.value
|
MXLog.error("Invalid image data")
|
||||||
|
return .failure(.invalidImageData)
|
||||||
guard let image = UIImage(data: imageData) else {
|
}
|
||||||
MXLog.error("Invalid image data")
|
|
||||||
return .failure(.invalidImageData)
|
imageCache.store(image, forKey: cacheKey)
|
||||||
}
|
|
||||||
|
return .success(image)
|
||||||
imageCache.store(image, forKey: cacheKey)
|
} catch {
|
||||||
|
MXLog.error("Failed retrieving image with error: \(error)")
|
||||||
return .success(image)
|
return .failure(.failedRetrievingImage)
|
||||||
} catch {
|
|
||||||
MXLog.error("Failed retrieving image with error: \(error)")
|
|
||||||
return .failure(.failedRetrievingImage)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ enum MediaProviderError: Error {
|
|||||||
case invalidImageData
|
case invalidImageData
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol MediaProviderProtocol {
|
protocol MediaProviderProtocol {
|
||||||
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage?
|
func imageFromSource(_ source: MediaSource?, avatarSize: AvatarSize?) -> UIImage?
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import MatrixRustSDK
|
|||||||
|
|
||||||
struct MediaSource: Equatable {
|
struct MediaSource: Equatable {
|
||||||
let underlyingSource: MatrixRustSDK.MediaSource
|
let underlyingSource: MatrixRustSDK.MediaSource
|
||||||
|
|
||||||
|
var url: String {
|
||||||
|
underlyingSource.url()
|
||||||
|
}
|
||||||
|
|
||||||
init(source: MatrixRustSDK.MediaSource) {
|
init(source: MatrixRustSDK.MediaSource) {
|
||||||
underlyingSource = source
|
underlyingSource = source
|
||||||
|
|||||||
@@ -101,36 +101,34 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateRoomsWithIdentifiers(_ identifiers: [String]) {
|
func updateRoomsWithIdentifiers(_ identifiers: [String]) {
|
||||||
Task.detached {
|
guard statePublisher.value == .live else {
|
||||||
guard self.statePublisher.value == .live else {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var changes = [CollectionDifference<RoomSummary>.Change]()
|
|
||||||
for identifier in identifiers {
|
|
||||||
guard let index = self.rooms.firstIndex(where: { $0.id == identifier }) else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldRoom = self.rooms[index]
|
|
||||||
let newRoom = self.buildRoomSummaryForIdentifier(identifier)
|
|
||||||
|
|
||||||
changes.append(.remove(offset: index, element: oldRoom, associatedWith: nil))
|
|
||||||
changes.append(.insert(offset: index, element: newRoom, associatedWith: nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let diff = CollectionDifference(changes) else {
|
|
||||||
MXLog.error("Failed creating diff from changes: \(changes)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let newSummaries = self.rooms.applying(diff) else {
|
|
||||||
MXLog.error("Failed applying diff: \(diff)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.rooms = newSummaries
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var changes = [CollectionDifference<RoomSummary>.Change]()
|
||||||
|
for identifier in identifiers {
|
||||||
|
guard let index = rooms.firstIndex(where: { $0.id == identifier }) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldRoom = rooms[index]
|
||||||
|
let newRoom = buildRoomSummaryForIdentifier(identifier)
|
||||||
|
|
||||||
|
changes.append(.remove(offset: index, element: oldRoom, associatedWith: nil))
|
||||||
|
changes.append(.insert(offset: index, element: newRoom, associatedWith: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let diff = CollectionDifference(changes) else {
|
||||||
|
MXLog.error("Failed creating diff from changes: \(changes)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newSummaries = rooms.applying(diff) else {
|
||||||
|
MXLog.error("Failed applying diff: \(diff)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms = newSummaries
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ enum UserSessionCallback {
|
|||||||
case updateRestoreTokenNeeded
|
case updateRestoreTokenNeeded
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol UserSessionProtocol {
|
protocol UserSessionProtocol {
|
||||||
var userID: String { get }
|
var userID: String { get }
|
||||||
var isSoftLogout: Bool { get }
|
var isSoftLogout: Bool { get }
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
|||||||
init(roomProxy: RoomProxyProtocol) {
|
init(roomProxy: RoomProxyProtocol) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
itemProxies = []
|
itemProxies = []
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
let roomTimelineListener = RoomTimelineListener()
|
let roomTimelineListener = RoomTimelineListener()
|
||||||
await roomProxy.addTimelineListener(listener: roomTimelineListener)
|
await roomProxy.addTimelineListener(listener: roomTimelineListener)
|
||||||
|
|
||||||
roomTimelineListener
|
roomTimelineListener
|
||||||
.itemsUpdatePublisher
|
.itemsUpdatePublisher
|
||||||
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
.collect(.byTime(DispatchQueue.global(qos: .background), 0.5))
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
|||||||
return .failure(.failedSettingUpSession)
|
return .failure(.failedSettingUpSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientProxy = ClientProxy(client: client, backgroundTaskService: backgroundTaskService)
|
let clientProxy = await ClientProxy(client: client, backgroundTaskService: backgroundTaskService)
|
||||||
|
|
||||||
return .success(clientProxy)
|
return .success(clientProxy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ enum UserSessionStoreError: Error {
|
|||||||
case failedRefreshingRestoreToken
|
case failedRefreshingRestoreToken
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol UserSessionStoreProtocol {
|
protocol UserSessionStoreProtocol {
|
||||||
/// Whether or not there are sessions in the store.
|
/// Whether or not there are sessions in the store.
|
||||||
var hasSessions: Bool { get }
|
var hasSessions: Bool { get }
|
||||||
|
|||||||
@@ -139,34 +139,36 @@ class UserSessionFlowCoordinator: Coordinator {
|
|||||||
// MARK: Rooms
|
// MARK: Rooms
|
||||||
|
|
||||||
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
private func presentRoomWithIdentifier(_ roomIdentifier: String) {
|
||||||
guard let roomProxy = userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
|
Task { @MainActor in
|
||||||
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomIdentifier) else {
|
||||||
return
|
MXLog.error("Invalid room identifier: \(roomIdentifier)")
|
||||||
}
|
return
|
||||||
let userId = userSession.clientProxy.userIdentifier
|
}
|
||||||
|
let userId = userSession.clientProxy.userIdentifier
|
||||||
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
|
||||||
mediaProvider: userSession.mediaProvider,
|
|
||||||
roomProxy: roomProxy,
|
|
||||||
attributedStringBuilder: AttributedStringBuilder())
|
|
||||||
|
|
||||||
let timelineController = RoomTimelineController(userId: userId,
|
|
||||||
roomId: roomIdentifier,
|
|
||||||
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
|
|
||||||
timelineItemFactory: timelineItemFactory,
|
|
||||||
mediaProvider: userSession.mediaProvider,
|
|
||||||
roomProxy: roomProxy)
|
|
||||||
|
|
||||||
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
let timelineItemFactory = RoomTimelineItemFactory(userID: userId,
|
||||||
mediaProvider: userSession.mediaProvider,
|
mediaProvider: userSession.mediaProvider,
|
||||||
roomName: roomProxy.displayName ?? roomProxy.name,
|
roomProxy: roomProxy,
|
||||||
roomAvatarUrl: roomProxy.avatarURL)
|
attributedStringBuilder: AttributedStringBuilder())
|
||||||
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
|
||||||
|
|
||||||
add(childCoordinator: coordinator)
|
let timelineController = RoomTimelineController(userId: userId,
|
||||||
navigationRouter.push(coordinator) { [weak self] in
|
roomId: roomIdentifier,
|
||||||
guard let self else { return }
|
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
|
||||||
self.stateMachine.processEvent(.dismissedRoomScreen)
|
timelineItemFactory: timelineItemFactory,
|
||||||
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
roomProxy: roomProxy)
|
||||||
|
|
||||||
|
let parameters = RoomScreenCoordinatorParameters(timelineController: timelineController,
|
||||||
|
mediaProvider: userSession.mediaProvider,
|
||||||
|
roomName: roomProxy.displayName ?? roomProxy.name,
|
||||||
|
roomAvatarUrl: roomProxy.avatarURL)
|
||||||
|
let coordinator = RoomScreenCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
navigationRouter.push(coordinator) { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.stateMachine.processEvent(.dismissedRoomScreen)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,25 +224,23 @@ class UserSessionFlowCoordinator: Coordinator {
|
|||||||
// MARK: Session verification
|
// MARK: Session verification
|
||||||
|
|
||||||
private func presentSessionVerification() {
|
private func presentSessionVerification() {
|
||||||
Task {
|
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
||||||
guard let sessionVerificationController = userSession.sessionVerificationController else {
|
fatalError("The sessionVerificationController should aways be valid at this point")
|
||||||
fatalError("The sessionVerificationController should aways be valid at this point")
|
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
|
|
||||||
|
|
||||||
let coordinator = SessionVerificationCoordinator(parameters: parameters)
|
|
||||||
|
|
||||||
coordinator.callback = { [weak self] in
|
|
||||||
self?.navigationRouter.dismissModule()
|
|
||||||
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
|
|
||||||
}
|
|
||||||
|
|
||||||
add(childCoordinator: coordinator)
|
|
||||||
navigationRouter.present(coordinator)
|
|
||||||
|
|
||||||
coordinator.start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: sessionVerificationController)
|
||||||
|
|
||||||
|
let coordinator = SessionVerificationCoordinator(parameters: parameters)
|
||||||
|
|
||||||
|
coordinator.callback = { [weak self] in
|
||||||
|
self?.navigationRouter.dismissModule()
|
||||||
|
self?.stateMachine.processEvent(.dismissedSessionVerificationScreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(childCoordinator: coordinator)
|
||||||
|
navigationRouter.present(coordinator)
|
||||||
|
|
||||||
|
coordinator.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tearDownDismissedSessionVerificationScreen() {
|
private func tearDownDismissedSessionVerificationScreen() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import XCTest
|
|||||||
|
|
||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class BackgroundTaskTests: XCTestCase {
|
class BackgroundTaskTests: XCTestCase {
|
||||||
private enum Constants {
|
private enum Constants {
|
||||||
static let bgTaskName = "test"
|
static let bgTaskName = "test"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Move Rust client operations into a dedicated concurrent queue, make sure not used on main thread.
|
||||||
Reference in New Issue
Block a user