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:
ismailgulek
2022-11-02 18:25:46 +03:00
committed by GitHub
parent 965569aae2
commit f6a599a51b
19 changed files with 243 additions and 221 deletions
@@ -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"
+1
View File
@@ -0,0 +1 @@
Move Rust client operations into a dedicated concurrent queue, make sure not used on main thread.