diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 38bbe20e7..5e750c965 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -70,28 +70,31 @@ final class HomeScreenCoordinator: Coordinator, Presentable { } } - parameters.userSession.callbacks.sink { [weak self] result in - switch result { - case .updatedRoomsList: - self?.updateRoomsList() - } - }.store(in: &cancellables) + parameters.userSession + .callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] result in + switch result { + case .updatedRoomsList: + self?.updateRoomsList() + } + }.store(in: &cancellables) updateRoomsList() - parameters.userSession.userAvatarURL { [weak self] result in - if case let .success(avatarURL) = result { - self?.parameters.mediaProvider.loadImageFromURL(avatarURL) { result in - if case let .success(avatar) = result { - self?.viewModel.updateWithUserAvatar(avatar) + Task { + if case let .success(userAvatarURL) = await parameters.userSession.loadUserAvatarURL() { + if case let .success(avatar) = await parameters.mediaProvider.loadImageFromURL(userAvatarURL) { + await MainActor.run { + self.viewModel.updateWithUserAvatar(avatar) } } } - } - - parameters.userSession.userDisplayName { [weak self] result in - if case let .success(displayName) = result { - self?.viewModel.updateWithUserDisplayName(displayName) + + if case let .success(userDisplayName) = await parameters.userSession.loadUserDisplayName() { + await MainActor.run { + self.viewModel.updateWithUserDisplayName(userDisplayName) + } } } } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 94a3d2284..1498466ea 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -65,19 +65,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol roomUpdateListeners.removeAll() roomList.forEach({ roomSummary in - roomSummary.callbacks.sink { [weak self] callback in - guard let self = self else { - return - } - - switch callback { - case .updatedData: - if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) { - self.state.rooms[index] = self.buildOrUpdateRoomFromSummary(roomSummary) + roomSummary.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self = self else { + return + } + + switch callback { + case .updatedData: + if let index = self.state.rooms.firstIndex(where: { $0.id == roomSummary.id }) { + self.state.rooms[index] = self.buildOrUpdateRoomFromSummary(roomSummary) + } } } - } - .store(in: &roomUpdateListeners) + .store(in: &roomUpdateListeners) }) } @@ -98,7 +100,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - roomSummary.loadData() + Task { + await roomSummary.loadDetails() + } } private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom { diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index ca87bece0..106bb6aab 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -152,7 +152,7 @@ struct RoomCell: View { } .animation(.easeInOut, value: room) .frame(minHeight: 60.0) - .onAppear { + .task { context.send(viewAction: .loadRoomData(roomIdentifier: room.id)) } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 5d82219a1..d863b7f8a 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -38,7 +38,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥", bindings: RoomScreenViewStateBindings(composerText: ""))) - timelineController.callbacks.sink { [weak self] callback in + timelineController.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in guard let self = self else { return } switch callback { @@ -62,25 +64,37 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol // MARK: - Public override func process(viewAction: RoomScreenViewAction) { - switch viewAction { - case .loadPreviousPage: - state.isBackPaginating = true - timelineController.paginateBackwards(Constants.backPaginationPageSize) { [weak self] _ in - self?.state.isBackPaginating = false + Task { + switch viewAction { + case .loadPreviousPage: + await MainActor.run { + state.isBackPaginating = true + } + + switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) { + default: + await MainActor.run { + state.isBackPaginating = false + } + } + + case .itemAppeared(let id): + await timelineController.processItemAppearance(id) + case .itemDisappeared(let id): + await timelineController.processItemDisappearance(id) + case .linkClicked(let url): + MXLog.warning("Link clicked: \(url)") + case .sendMessage: + guard state.bindings.composerText.count > 0 else { + return + } + + await timelineController.sendMessage(state.bindings.composerText) + + await MainActor.run { + state.bindings.composerText = "" + } } - case .itemAppeared(let id): - timelineController.processItemAppearance(id) - case .itemDisappeared(let id): - timelineController.processItemDisappearance(id) - case .linkClicked(let url): - MXLog.warning("Link clicked: \(url)") - case .sendMessage: - guard state.bindings.composerText.count > 0 else { - return - } - - timelineController.sendMessage(state.bindings.composerText) - state.bindings.composerText = "" } } diff --git a/ElementX/Sources/Services/Authentication/UserSession.swift b/ElementX/Sources/Services/Authentication/UserSession.swift index a29c2fe91..258dc17f1 100644 --- a/ElementX/Sources/Services/Authentication/UserSession.swift +++ b/ElementX/Sources/Services/Authentication/UserSession.swift @@ -28,16 +28,13 @@ private class WeakUserSessionWrapper: ClientDelegate { } func didReceiveSyncUpdate() { - DispatchQueue.main.async { - self.userSession?.didReceiveSyncUpdate() - } + self.userSession?.didReceiveSyncUpdate() } } class UserSession: ClientDelegate { private let client: Client - private let processingQueue: DispatchQueue private(set) var rooms: [RoomProxy] = [] { didSet { @@ -55,7 +52,6 @@ class UserSession: ClientDelegate { init(client: Client) { self.client = client - self.processingQueue = DispatchQueue(label: "UserSessionProcessingQueue") self.mediaProvider = MediaProvider(client: client, imageCache: ImageCache.default) client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self)) @@ -63,7 +59,9 @@ class UserSession: ClientDelegate { Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.") client.startSync() - updateRooms() + Task { + await updateRooms() + } } var userIdentifier: String { @@ -75,34 +73,25 @@ class UserSession: ClientDelegate { } } - func userDisplayName(_ completion: @escaping (Result) -> Void) { - processingQueue.async { + func loadUserDisplayName() async -> Result { + await withCheckedContinuation { continuation in do { let displayName = try self.client.displayName() - - DispatchQueue.main.async { - completion(.success(displayName)) - } + continuation.resume(returning: .success(displayName)) } catch { - DispatchQueue.main.async { - completion(.failure(UserSessionError.failedRetrievingDisplayName)) - } + continuation.resume(returning: .failure(.failedRetrievingDisplayName)) } + } } - - func userAvatarURL(_ completion: @escaping (Result) -> Void) { - processingQueue.async { + + func loadUserAvatarURL() async -> Result { + await withCheckedContinuation { continuation in do { - let displayName = try self.client.avatarUrl() - - DispatchQueue.main.async { - completion(.success(displayName)) - } + let avatarURL = try self.client.avatarUrl() + continuation.resume(returning: .success(avatarURL)) } catch { - DispatchQueue.main.async { - completion(.failure(UserSessionError.failedRetrievingDisplayName)) - } + continuation.resume(returning: .failure(.failedRetrievingDisplayName)) } } } @@ -111,43 +100,38 @@ class UserSession: ClientDelegate { func didReceiveSyncUpdate() { Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update") - updateRooms() + + Task { + await updateRooms() + } } // MARK: Private - func updateRooms() { + private func updateRooms() async { var currentRooms = self.rooms - self.processingQueue.async { [weak self] in - guard let self = self else { - return - } - - Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms") - let sdkRooms = self.client.rooms() - Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms") - - Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms") - let diff = sdkRooms.map({ $0.id()}).difference(from: currentRooms.map({ $0.id })) - - for change in diff { - switch change { - case .insert(_, let id, _): - guard let sdkRoom = sdkRooms.first(where: { $0.id() == id }) else { - MXLog.error("Failed retrieving sdk room with id: \(id)") - break - } - currentRooms.append(RoomProxy(room: sdkRoom, messageFactory: RoomMessageFactory())) - case .remove(_, let id, _): - currentRooms.removeAll { $0.id == id } + Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms") + let sdkRooms = self.client.rooms() + Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms") + + Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms") + let diff = sdkRooms.map({ $0.id()}).difference(from: currentRooms.map({ $0.id })) + + for change in diff { + switch change { + case .insert(_, let id, _): + guard let sdkRoom = sdkRooms.first(where: { $0.id() == id }) else { + MXLog.error("Failed retrieving sdk room with id: \(id)") + break } - } - - Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms") - - DispatchQueue.main.async { - self.rooms = currentRooms + currentRooms.append(RoomProxy(room: sdkRoom, messageFactory: RoomMessageFactory())) + case .remove(_, let id, _): + currentRooms.removeAll { $0.id == id } } } + + Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms") + + self.rooms = currentRooms } } diff --git a/ElementX/Sources/Services/Media/MediaProvider.swift b/ElementX/Sources/Services/Media/MediaProvider.swift index 58b7f837c..6f0c91d52 100644 --- a/ElementX/Sources/Services/Media/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/MediaProvider.swift @@ -29,46 +29,6 @@ struct MediaProvider: MediaProviderProtocol { return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil) } - func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result) -> Void) { - if let image = imageFromSource(source) { - completion(.success(image)) - return - } - - imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in - if case let .success(cacheResult) = result, - let image = cacheResult.image { - completion(.success(image)) - return - } - - processingQueue.async { - do { - let imageData = try client.getMediaContent(source: source.underlyingSource) - - guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else { - MXLog.error("Invalid image data") - DispatchQueue.main.async { - completion(.failure(.invalidImageData)) - } - return - } - - imageCache.store(image, forKey: source.underlyingSource.url()) - - DispatchQueue.main.async { - completion(.success(image)) - } - } catch { - MXLog.error("Failed retrieving image with error: \(error)") - DispatchQueue.main.async { - completion(.failure(.failedRetrievingImage)) - } - } - } - } - } - func imageFromURL(_ url: String?) -> UIImage? { guard let url = url else { return nil @@ -77,7 +37,42 @@ struct MediaProvider: MediaProviderProtocol { return imageFromSource(MediaSource(source: mediaSourceFromUrl(url: url))) } - func loadImageFromURL(_ url: String, _ completion: @escaping (Result) -> Void) { - return loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)), completion) + func loadImageFromURL(_ url: String) async -> Result { + await loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url))) + } + + func loadImageFromSource(_ source: MediaSource) async -> Result { + if let image = imageFromSource(source) { + return .success(image) + } + + return await withCheckedContinuation { continuation in + imageCache.retrieveImage(forKey: source.underlyingSource.url()) { result in + if case let .success(cacheResult) = result, + let image = cacheResult.image { + continuation.resume(returning: .success(image)) + return + } + + processingQueue.async { + do { + let imageData = try client.getMediaContent(source: source.underlyingSource) + + guard let image = UIImage(data: Data(bytes: imageData, count: imageData.count)) else { + MXLog.error("Invalid image data") + continuation.resume(returning: .failure(.invalidImageData)) + return + } + + imageCache.store(image, forKey: source.underlyingSource.url()) + + continuation.resume(returning: .success(image)) + } catch { + MXLog.error("Failed retrieving image with error: \(error)") + continuation.resume(returning: .failure(.failedRetrievingImage)) + } + } + } + } } } diff --git a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift index 5768e98b8..13dbd2689 100644 --- a/ElementX/Sources/Services/Media/MediaProviderProtocol.swift +++ b/ElementX/Sources/Services/Media/MediaProviderProtocol.swift @@ -17,9 +17,9 @@ enum MediaProviderError: Error { protocol MediaProviderProtocol { func imageFromSource(_ source: MediaSource?) -> UIImage? - func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result) -> Void) + func loadImageFromSource(_ source: MediaSource) async -> Result func imageFromURL(_ url: String?) -> UIImage? - func loadImageFromURL(_ url: String, _ completion: @escaping (Result) -> Void) + func loadImageFromURL(_ url: String) async -> Result } diff --git a/ElementX/Sources/Services/Media/MockMediaProvider.swift b/ElementX/Sources/Services/Media/MockMediaProvider.swift index 384b2eb38..d6355f9a8 100644 --- a/ElementX/Sources/Services/Media/MockMediaProvider.swift +++ b/ElementX/Sources/Services/Media/MockMediaProvider.swift @@ -19,8 +19,8 @@ struct MockMediaProvider: MediaProviderProtocol { return nil } - func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result) -> Void) { - + func loadImageFromSource(_ source: MediaSource) async -> Result { + return .failure(.failedRetrievingImage) } func imageFromURL(_ url: String?) -> UIImage? { @@ -30,4 +30,8 @@ struct MockMediaProvider: MediaProviderProtocol { func loadImageFromURL(_ url: String, _ completion: @escaping (Result) -> Void) { } + + func loadImageFromURL(_ url: String) async -> Result { + return .failure(.failedRetrievingImage) + } } diff --git a/ElementX/Sources/Services/Room/Members/MemberDetailsProvider.swift b/ElementX/Sources/Services/Room/Members/MemberDetailsProvider.swift index 3bab47903..201a99cc8 100644 --- a/ElementX/Sources/Services/Room/Members/MemberDetailsProvider.swift +++ b/ElementX/Sources/Services/Room/Members/MemberDetailsProvider.swift @@ -9,7 +9,7 @@ import Foundation class MemberDetailProvider: MemberDetailProviderProtocol { - private let roomProxy: RoomProxyProtocol? + private let roomProxy: RoomProxyProtocol private var memberAvatars = [String: String]() private var memberDisplayNames = [String: String]() @@ -18,58 +18,38 @@ class MemberDetailProvider: MemberDetailProviderProtocol { } func avatarURLForUserId(_ userId: String) -> String? { - self.memberAvatars[userId] + memberAvatars[userId] } - func avatarURLForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - guard let roomProxy = roomProxy else { - return - } - + func loadAvatarURLForUserId(_ userId: String) async -> Result { if let avatarURL = avatarURLForUserId(userId) { - completion(.success(avatarURL)) + return .success(avatarURL) } - roomProxy.avatarURLForUserId(userId, completion: { [weak self] result in - guard let self = self else { - return - } - - switch result { - case .success(let avatarURL): - self.memberAvatars[userId] = avatarURL - completion(.success(avatarURL)) - case .failure: - completion(.failure(.failedRetrievingUserAvatarURL)) - } - }) + switch await roomProxy.loadAvatarURLForUserId(userId) { + case .success(let avatarURL): + memberAvatars[userId] = avatarURL + return .success(avatarURL) + case .failure: + return .failure(.failedRetrievingUserAvatarURL) + } } - + func displayNameForUserId(_ userId: String) -> String? { - self.memberDisplayNames[userId] + memberDisplayNames[userId] } - func displayNameForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - guard let roomProxy = roomProxy else { - return + func loadDisplayNameForUserId(_ userId: String) async -> Result { + if let displayName = displayNameForUserId(userId) { + return .success(displayName) } - if let avatarURL = displayNameForUserId(userId) { - completion(.success(avatarURL)) + switch await roomProxy.loadDisplayNameForUserId(userId) { + case .success(let displayName): + memberDisplayNames[userId] = displayName + return .success(displayName) + case .failure: + return .failure(.failedRetrievingUserDisplayName) } - - roomProxy.displayNameForUserId(userId, completion: { [weak self] result in - guard let self = self else { - return - } - - switch result { - case .success(let displayName): - self.memberDisplayNames[userId] = displayName - completion(.success(displayName)) - case .failure: - completion(.failure(.failedRetrievingUserDisplayName)) - } - }) } } diff --git a/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift b/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift index 219f5a859..0905107c7 100644 --- a/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift +++ b/ElementX/Sources/Services/Room/Members/MemberDetailsProviderProtocol.swift @@ -16,8 +16,8 @@ enum MemberDetailProviderError: Error { protocol MemberDetailProviderProtocol { func avatarURLForUserId(_ userId: String) -> String? - func avatarURLForUserId(_ userId: String, completion: @escaping (Result) -> Void) + func loadAvatarURLForUserId(_ userId: String) async -> Result func displayNameForUserId(_ userId: String) -> String? - func displayNameForUserId(_ userId: String, completion: @escaping (Result) -> Void) + func loadDisplayNameForUserId(_ userId: String) async -> Result } diff --git a/ElementX/Sources/Services/Room/MockRoomProxy.swift b/ElementX/Sources/Services/Room/MockRoomProxy.swift index 5ca494c87..e8a0d8545 100644 --- a/ElementX/Sources/Services/Room/MockRoomProxy.swift +++ b/ElementX/Sources/Services/Room/MockRoomProxy.swift @@ -27,27 +27,27 @@ struct MockRoomProxy: RoomProxyProtocol { var callbacks = PassthroughSubject() - func displayName(_ completion: @escaping (Result) -> Void) { - completion(.success("Room display name")) + func loadDisplayNameForUserId(_ userId: String) async -> Result { + return .failure(.failedRetrievingMemberDisplayName) + } + + func loadAvatarURLForUserId(_ userId: String) async -> Result { + return .failure(.failedRetrievingMemberAvatarURL) + } + + func loadDisplayName() async -> Result { + return .failure(.failedRetrievingDisplayName) } func startLiveEventListener() { } - func paginateBackwards(count: UInt, callback: ((Result) -> Void)?) { - + func paginateBackwards(count: UInt) async -> Result { + return .failure(.backwardStreamNotAvailable) } - - func avatarURLForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - - } - - func displayNameForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - - } - - func sendMessage(_ message: String, callback: ((Result) -> Void)?) { + func sendMessage(_ message: String) async -> Result { + return .failure(.failedSendingMessage) } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 6c9cfab37..616597a80 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -21,9 +21,7 @@ private class WeakRoomProxyWrapper: RoomDelegate { // MARK: - RoomDelegate func didReceiveMessage(message: AnyMessage) { - DispatchQueue.main.async { - self.roomProxy?.appendMessage(message) - } + self.roomProxy?.appendMessage(message) } } @@ -31,9 +29,6 @@ class RoomProxy: RoomProxyProtocol { private let room: Room private let messageFactory: RoomMessageFactory - private let generalProcessingQueue: DispatchQueue - private let messageProcessingQueue: DispatchQueue - private var backwardStream: BackwardsStreamProtocol? private(set) var displayName: String? @@ -45,18 +40,13 @@ class RoomProxy: RoomProxyProtocol { init(room: Room, messageFactory: RoomMessageFactory) { self.room = room self.messageFactory = messageFactory - generalProcessingQueue = DispatchQueue(label: "RoomProxyGeneralProcessingQueue") - messageProcessingQueue = DispatchQueue(label: "RoomProxyMessageProcessingQueue") messages = [] - messageProcessingQueue.async { - let backwardStream = room.startLiveEventListener() - DispatchQueue.main.async { - self.backwardStream = backwardStream - } - } - room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self)) + + Task { + backwardStream = room.startLiveEventListener() + } } var id: String { @@ -95,66 +85,50 @@ class RoomProxy: RoomProxyProtocol { room.avatarUrl() } - func avatarURLForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - generalProcessingQueue.async { + func loadAvatarURLForUserId(_ userId: String) async -> Result { + await withCheckedContinuation({ continuation in do { let avatarURL = try self.room.memberAvatarUrl(userId: userId) - - DispatchQueue.main.async { - completion(.success(avatarURL)) - } + continuation.resume(returning: .success(avatarURL)) } catch { - DispatchQueue.main.async { - completion(.failure(RoomProxyError.failedRetrievingMemberAvatarURL)) - } + continuation.resume(returning: .failure(.failedRetrievingMemberAvatarURL)) } - } + }) } - func displayNameForUserId(_ userId: String, completion: @escaping (Result) -> Void) { - generalProcessingQueue.async { + func loadDisplayNameForUserId(_ userId: String) async -> Result { + await withCheckedContinuation({ continuation in do { let displayName = try self.room.memberDisplayName(userId: userId) - - DispatchQueue.main.async { - completion(.success(displayName)) - } + continuation.resume(returning: .success(displayName)) } catch { - DispatchQueue.main.async { - completion(.failure(RoomProxyError.failedRetrievingMemberDisplayName)) - } + continuation.resume(returning: .failure(.failedRetrievingMemberDisplayName)) } - } + }) } - - func displayName(_ completion: @escaping (Result) -> Void) { - if let displayName = displayName { - completion(.success(displayName)) - return - } - generalProcessingQueue.async { + func loadDisplayName() async -> Result { + await withCheckedContinuation({ continuation in + if let displayName = displayName { + continuation.resume(returning: .success(displayName)) + return + } + do { let displayName = try self.room.displayName() self.displayName = displayName - - DispatchQueue.main.async { - completion(.success(displayName)) - } + + continuation.resume(returning: .success(displayName)) } catch { - DispatchQueue.main.async { - completion(.failure(.failedRetrievingDisplayName)) - } + continuation.resume(returning: .failure(.failedRetrievingDisplayName)) } - } + }) } - - func paginateBackwards(count: UInt, callback: ((Result) -> Void)?) { - messageProcessingQueue.async { + + func paginateBackwards(count: UInt) async -> Result { + await withCheckedContinuation { continuation in guard let backwardStream = self.backwardStream else { - DispatchQueue.main.async { - callback?(.failure(.backwardStreamNotAvailable)) - } + continuation.resume(returning: .failure(.backwardStreamNotAvailable)) return } @@ -166,28 +140,22 @@ class RoomProxy: RoomProxyProtocol { self.messageFactory.buildRoomMessageFrom(message) }.reversed() - DispatchQueue.main.async { - self.messages.insert(contentsOf: messages, at: 0) - callback?(.success(())) - } + self.messages.insert(contentsOf: messages, at: 0) + + continuation.resume(returning: .success(())) } } - func sendMessage(_ message: String, callback: ((Result) -> Void)?) { + func sendMessage(_ message: String) async -> Result { let messageContent = messageEventContentFromMarkdown(md: message) let transactionId = genTransactionId() - messageProcessingQueue.async { + return await withCheckedContinuation { continuation in do { try self.room.send(msg: messageContent, txnId: transactionId) - - DispatchQueue.main.async { - callback?(.success(())) - } + continuation.resume(returning: .success(())) } catch { - DispatchQueue.main.async { - callback?(.failure(.failedSendingMessage)) - } + continuation.resume(returning: .failure(.failedSendingMessage)) } } } diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 8727cff37..51f05d5b1 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -37,15 +37,15 @@ protocol RoomProxyProtocol { var avatarURL: String? { get } - func displayName(_ completion: @escaping (Result) -> Void) + func loadAvatarURLForUserId(_ userId: String) async -> Result - func avatarURLForUserId(_ userId: String, completion: @escaping (Result) -> Void) + func loadDisplayNameForUserId(_ userId: String) async -> Result - func displayNameForUserId(_ userId: String, completion: @escaping (Result) -> Void) + func loadDisplayName() async -> Result - func paginateBackwards(count: UInt, callback: ((Result) -> Void)?) + func paginateBackwards(count: UInt) async -> Result - func sendMessage(_ message: String, callback: ((Result) -> Void)?) + func sendMessage(_ message: String) async -> Result var callbacks: PassthroughSubject { get } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactory.swift b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactory.swift index 8a5725b4a..31fa4191f 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactory.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactory.swift @@ -16,21 +16,20 @@ struct EventBriefFactory: EventBriefFactoryProtocol { self.memberDetailProvider = memberDetailProvider } - func eventBriefForMessage(_ message: RoomMessageProtocol?, completion: @escaping ((EventBrief?) -> Void)) { + func eventBriefForMessage(_ message: RoomMessageProtocol?) async -> EventBrief? { guard let message = message else { - completion(nil) - return + return nil } switch message { case is ImageRoomMessage: - completion(nil) + return nil case let message as TextRoomMessage: - buildEventBrief(message: message, htmlBody: message.htmlBody, completion: completion) + return await buildEventBrief(message: message, htmlBody: message.htmlBody) case let message as NoticeRoomMessage: - buildEventBrief(message: message, htmlBody: message.htmlBody, completion: completion) + return await buildEventBrief(message: message, htmlBody: message.htmlBody) case let message as EmoteRoomMessage: - buildEventBrief(message: message, htmlBody: message.htmlBody, completion: completion) + return await buildEventBrief(message: message, htmlBody: message.htmlBody) default: fatalError("Unknown room message.") } @@ -38,26 +37,24 @@ struct EventBriefFactory: EventBriefFactoryProtocol { // MARK: - Private - private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?, completion: @escaping ((EventBrief?) -> Void)) { - memberDetailProvider.displayNameForUserId(message.sender) { result in - switch result { - case .success(let displayName): - completion(EventBrief(eventId: message.id, - senderId: message.sender, - senderDisplayName: displayName, - body: message.body, - htmlBody: htmlBody, - date: message.originServerTs)) - case .failure(let error): - MXLog.error("Failed fetching sender display name with error: \(error)") - - completion(EventBrief(eventId: message.id, - senderId: message.sender, - senderDisplayName: nil, - body: message.body, - htmlBody: htmlBody, - date: message.originServerTs)) - } + private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?) async -> EventBrief? { + switch await memberDetailProvider.loadDisplayNameForUserId(message.sender) { + case .success(let displayName): + return EventBrief(eventId: message.id, + senderId: message.sender, + senderDisplayName: displayName, + body: message.body, + htmlBody: htmlBody, + date: message.originServerTs) + case .failure(let error): + MXLog.error("Failed fetching sender display name with error: \(error)") + + return EventBrief(eventId: message.id, + senderId: message.sender, + senderDisplayName: nil, + body: message.body, + htmlBody: htmlBody, + date: message.originServerTs) } } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift index 21f2869af..806bcf1d4 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/EventBriefFactoryProtocol.swift @@ -9,5 +9,5 @@ import Foundation protocol EventBriefFactoryProtocol { - func eventBriefForMessage(_ message: RoomMessageProtocol?, completion: @escaping ((EventBrief?) -> Void)) + func eventBriefForMessage(_ message: RoomMessageProtocol?) async -> EventBrief? } diff --git a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummary.swift index 46bfa49dd..2ae1550a3 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/MockRoomSummary.swift @@ -30,7 +30,7 @@ struct MockRoomSummary: RoomSummaryProtocol { var avatar: UIImage? - func loadData() { + func loadDetails() { } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 690739bdb..e7afcb579 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -71,81 +71,81 @@ class RoomSummary: RoomSummaryProtocol { self.mediaProvider = mediaProvider self.eventBriefFactory = eventBriefFactory - eventBriefFactory.eventBriefForMessage(roomProxy.messages.last) { [weak self] result in - self?.lastMessage = result + Task { + lastMessage = await eventBriefFactory.eventBriefForMessage(roomProxy.messages.last) } - roomProxy.callbacks.sink { [weak self] callback in - guard let self = self else { - return - } - - switch callback { - case .updatedMessages: - self.eventBriefFactory.eventBriefForMessage(self.roomProxy.messages.last) { [weak self] result in - self?.lastMessage = result + roomProxy.callbacks + .receive(on: DispatchQueue.main) + .sink { [weak self] callback in + guard let self = self else { + return + } + + switch callback { + case .updatedMessages: + Task { + self.lastMessage = await eventBriefFactory.eventBriefForMessage(roomProxy.messages.last) + } } } - } - .store(in: &roomUpdateListeners) + .store(in: &roomUpdateListeners) } - func loadData() { + func loadDetails() async { if hasLoadedData { return } - loadDisplayName() - loadLastMessage() - loadAvatar() + await withTaskGroup(of: Void.self) { group in + group.addTask { + await self.loadDisplayName() + } + group.addTask { + await self.loadAvatar() + } + group.addTask { + await self.loadLastMessage() + } + } hasLoadedData = true } // MARK: - Private - private func loadDisplayName() { - roomProxy.displayName { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let displayName): - self.displayName = displayName - case .failure(let error): - MXLog.error("Failed fetching room display name with error: \(error)") - } + private func loadDisplayName() async { + switch await roomProxy.loadDisplayName() { + case .success(let displayName): + self.displayName = displayName + case .failure(let error): + MXLog.error("Failed fetching room display name with error: \(error)") } } - private func loadLastMessage() { - if roomProxy.messages.last == nil { - roomProxy.paginateBackwards(count: 1) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success: - self.eventBriefFactory.eventBriefForMessage(self.roomProxy.messages.last) { [weak self] result in - self?.lastMessage = result - } - case .failure(let error): - MXLog.error("Failed back paginating with error: \(error)") - } - } + private func loadAvatar() async { + guard let avatarURL = roomProxy.avatarURL else { + return + } + + switch await mediaProvider.loadImageFromURL(avatarURL) { + case .success(let avatar): + self.avatar = avatar + case .failure(let error): + MXLog.error("Failed fetching room avatar with error: \(error)") } } - private func loadAvatar() { - if let avatarURL = roomProxy.avatarURL { - mediaProvider.loadImageFromURL(avatarURL) { [weak self] result in - guard let self = self else { return } - - switch result { - case .success(let image): - self.avatar = image - case .failure(let error): - MXLog.error("Failed fetching room avatar with error: \(error)") - } - } + private func loadLastMessage() async { + guard roomProxy.messages.last == nil else { + return + } + + switch await roomProxy.paginateBackwards(count: 1) { + case .success: + self.lastMessage = await self.eventBriefFactory.eventBriefForMessage(self.roomProxy.messages.last) + case .failure(let error): + MXLog.error("Failed back paginating with error: \(error)") } } } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift index 840d20b11..5bc3dd7dc 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProtocol.swift @@ -28,5 +28,5 @@ protocol RoomSummaryProtocol { var callbacks: PassthroughSubject { get } - func loadData() + func loadDetails() async } diff --git a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift index 9763bcccc..cc724e069 100644 --- a/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/MockRoomTimelineController.swift @@ -19,19 +19,19 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"), TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")] - func paginateBackwards(_ count: UInt, callback: ((Result) -> Void)) { - callbacks.send(.updatedTimelineItems) + func paginateBackwards(_ count: UInt) async -> Result { + return .failure(.generic) } - func processItemAppearance(_ itemId: String) { + func processItemAppearance(_ itemId: String) async { } - func processItemDisappearance(_ itemId: String) { + func processItemDisappearance(_ itemId: String) async { } - func sendMessage(_ message: String) { + func sendMessage(_ message: String) async { } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift index d9357a022..ddc3feca7 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineController.swift @@ -45,31 +45,29 @@ class RoomTimelineController: RoomTimelineControllerProtocol { NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) } - func paginateBackwards(_ count: UInt, callback: @escaping ((Result) -> Void)) { - timelineProvider.paginateBackwards(count) { [weak self] result in - switch result { - case .success: - callback(.success(())) - self?.updateTimelineItems() - case .failure: - callback(.failure(.generic)) - } + func paginateBackwards(_ count: UInt) async -> Result { + switch await timelineProvider.paginateBackwards(count) { + case .success: + updateTimelineItems() + return .success(()) + case .failure: + return .failure(.generic) } } - func processItemAppearance(_ itemId: String) { + func processItemAppearance(_ itemId: String) async { guard let timelineItem = self.timelineItems.filter({ $0.id == itemId}).first else { return } if let item = timelineItem as? EventBasedTimelineItemProtocol { - loadUserAvatarForTimelineItem(item) - loadUserDisplayNameForTimelineItem(item) + await loadUserAvatarForTimelineItem(item) + await loadUserDisplayNameForTimelineItem(item) } switch timelineItem { case let item as ImageRoomTimelineItem: - loadImageForTimelineItem(item) + await loadImageForTimelineItem(item) default: break } @@ -79,8 +77,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } - func sendMessage(_ message: String) { - timelineProvider.sendMessage(message) + func sendMessage(_ message: String) async { + switch await timelineProvider.sendMessage(message) { + case .success: + break + case .failure: + break + } } // MARK: - Private @@ -124,7 +127,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs) } - private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) { + private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) async { if timelineItem.image != nil { return } @@ -133,65 +136,69 @@ class RoomTimelineController: RoomTimelineControllerProtocol { return } - mediaProvider.loadImageFromSource(source) { [weak self] result in - guard let self = self else { + switch await mediaProvider.loadImageFromSource(source) { + case .success(let image): + guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), + var item = self.timelineItems[index] as? ImageRoomTimelineItem else { return } - if case let .success(image) = result { + item.image = image + self.timelineItems[index] = item + self.callbacks.send(.updatedTimelineItem(timelineItem.id)) + case .failure: + break + } + } + + private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async { + if timelineItem.shouldShowSenderDetails == false { + return + } + + switch await memberDetailProvider.loadAvatarURLForUserId(timelineItem.senderId) { + case .success(let avatarURL): + guard let avatarURL = avatarURL else { + return + } + + switch await mediaProvider.loadImageFromURL(avatarURL) { + case .success(let avatar): guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), - var item = self.timelineItems[index] as? ImageRoomTimelineItem else { + var item = self.timelineItems[index] as? EventBasedTimelineItemProtocol else { return } - item.image = image + item.senderAvatar = avatar self.timelineItems[index] = item self.callbacks.send(.updatedTimelineItem(timelineItem.id)) + case .failure: + break } + + case .failure: + break } } - private func loadUserAvatarForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) { + private func loadUserDisplayNameForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) async { if timelineItem.shouldShowSenderDetails == false { return } - memberDetailProvider.avatarURLForUserId(timelineItem.senderId) { result in - if case let .success(avatarURL) = result, - let avatarURL = avatarURL { - self.mediaProvider.loadImageFromURL(avatarURL) { result in - if case let .success(image) = result { - guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), - var item = self.timelineItems[index] as? EventBasedTimelineItemProtocol else { - return - } - - item.senderAvatar = image - self.timelineItems[index] = item - self.callbacks.send(.updatedTimelineItem(timelineItem.id)) - } - } - } - } - } - - private func loadUserDisplayNameForTimelineItem(_ timelineItem: EventBasedTimelineItemProtocol) { - if timelineItem.shouldShowSenderDetails == false { - return - } - - memberDetailProvider.displayNameForUserId(timelineItem.senderId) { result in - if case let .success(displayName) = result, - let displayName = displayName { - guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), - var item = self.timelineItems[index] as? EventBasedTimelineItemProtocol else { - return - } - - item.senderDisplayName = displayName - self.timelineItems[index] = item - self.callbacks.send(.updatedTimelineItem(timelineItem.id)) + switch await memberDetailProvider.loadDisplayNameForUserId(timelineItem.senderId) { + case .success(let displayName): + guard let displayName = displayName, + let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), + var item = self.timelineItems[index] as? EventBasedTimelineItemProtocol else { + return } + + item.senderDisplayName = displayName + self.timelineItems[index] = item + self.callbacks.send(.updatedTimelineItem(timelineItem.id)) + case .failure: + break } } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift index 7cc3d8b5f..45fa0c66f 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineControllerProtocol.swift @@ -21,12 +21,12 @@ enum RoomTimelineControllerError: Error { protocol RoomTimelineControllerProtocol { var timelineItems: [RoomTimelineItemProtocol] { get } var callbacks: PassthroughSubject { get } - - func paginateBackwards(_ count: UInt, callback: @escaping ((Result) -> Void)) - func processItemAppearance(_ itemId: String) + func processItemAppearance(_ itemId: String) async - func processItemDisappearance(_ itemId: String) + func processItemDisappearance(_ itemId: String) async - func sendMessage(_ message: String) + func paginateBackwards(_ count: UInt) async -> Result + + func sendMessage(_ message: String) async } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift index 2d3d53e5e..378fbc7de 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProvider.swift @@ -13,7 +13,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { private let roomProxy: RoomProxyProtocol private var cancellables = Set() - let callbacks = PassthroughSubject() + let callbacks = PassthroughSubject() init(roomProxy: RoomProxyProtocol) { self.roomProxy = roomProxy @@ -32,25 +32,21 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol { roomProxy.messages } - func paginateBackwards(_ count: UInt, callback: ((Result) -> Void)?) { - self.roomProxy.paginateBackwards(count: count) { result in - switch result { - case .success: - callback?(.success(())) - case .failure: - callback?(.failure(.generic)) - } + func paginateBackwards(_ count: UInt) async -> Result { + switch await roomProxy.paginateBackwards(count: count) { + case .success: + return .success(()) + case .failure: + return .failure(.generic) } } - func sendMessage(_ message: String) { - roomProxy.sendMessage(message) { result in - switch result { - case .success: - break - case .failure(let error): - MXLog.error("Failed sending message with error: \(error)") - } + func sendMessage(_ message: String) async -> Result { + switch await roomProxy.sendMessage(message) { + case .success: + return .success(()) + case .failure: + return .failure(.failedSendingMessage) } } } diff --git a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift index ede71a352..cd61cbc5d 100644 --- a/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift +++ b/ElementX/Sources/Services/Timeline/RoomTimelineProviderProtocol.swift @@ -9,20 +9,21 @@ import Foundation import Combine -enum RoomTimelineCallback { +enum RoomTimelineProviderCallback { case updatedMessages } -enum RoomTimelineError: Error { +enum RoomTimelineProviderError: Error { + case failedSendingMessage case generic } protocol RoomTimelineProviderProtocol { - var callbacks: PassthroughSubject { get } + var callbacks: PassthroughSubject { get } var messages: [RoomMessageProtocol] { get } - func paginateBackwards(_ count: UInt, callback: ((Result) -> Void)?) + func paginateBackwards(_ count: UInt) async -> Result - func sendMessage(_ message: String) + func sendMessage(_ message: String) async -> Result }