vector-im/element-x-ios/issues/53 - Converted most callback based methods to async/await

This commit is contained in:
Stefan Ceriu
2022-05-24 16:59:01 +03:00
committed by Stefan Ceriu
parent f2fe39eca7
commit dde7786152
23 changed files with 407 additions and 454 deletions
@@ -70,28 +70,31 @@ final class HomeScreenCoordinator: Coordinator, Presentable {
} }
} }
parameters.userSession.callbacks.sink { [weak self] result in parameters.userSession
switch result { .callbacks
case .updatedRoomsList: .receive(on: DispatchQueue.main)
self?.updateRoomsList() .sink { [weak self] result in
} switch result {
}.store(in: &cancellables) case .updatedRoomsList:
self?.updateRoomsList()
}
}.store(in: &cancellables)
updateRoomsList() updateRoomsList()
parameters.userSession.userAvatarURL { [weak self] result in Task {
if case let .success(avatarURL) = result { if case let .success(userAvatarURL) = await parameters.userSession.loadUserAvatarURL() {
self?.parameters.mediaProvider.loadImageFromURL(avatarURL) { result in if case let .success(avatar) = await parameters.mediaProvider.loadImageFromURL(userAvatarURL) {
if case let .success(avatar) = result { await MainActor.run {
self?.viewModel.updateWithUserAvatar(avatar) self.viewModel.updateWithUserAvatar(avatar)
} }
} }
} }
}
if case let .success(userDisplayName) = await parameters.userSession.loadUserDisplayName() {
parameters.userSession.userDisplayName { [weak self] result in await MainActor.run {
if case let .success(displayName) = result { self.viewModel.updateWithUserDisplayName(userDisplayName)
self?.viewModel.updateWithUserDisplayName(displayName) }
} }
} }
} }
@@ -65,19 +65,21 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
roomUpdateListeners.removeAll() roomUpdateListeners.removeAll()
roomList.forEach({ roomSummary in roomList.forEach({ roomSummary in
roomSummary.callbacks.sink { [weak self] callback in roomSummary.callbacks
guard let self = self else { .receive(on: DispatchQueue.main)
return .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 }) { switch callback {
self.state.rooms[index] = self.buildOrUpdateRoomFromSummary(roomSummary) 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 return
} }
roomSummary.loadData() Task {
await roomSummary.loadDetails()
}
} }
private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom { private func buildOrUpdateRoomFromSummary(_ roomSummary: RoomSummaryProtocol) -> HomeScreenRoom {
@@ -152,7 +152,7 @@ struct RoomCell: View {
} }
.animation(.easeInOut, value: room) .animation(.easeInOut, value: room)
.frame(minHeight: 60.0) .frame(minHeight: 60.0)
.onAppear { .task {
context.send(viewAction: .loadRoomData(roomIdentifier: room.id)) context.send(viewAction: .loadRoomData(roomIdentifier: room.id))
} }
} }
@@ -38,7 +38,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥", bindings: RoomScreenViewStateBindings(composerText: ""))) 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 } guard let self = self else { return }
switch callback { switch callback {
@@ -62,25 +64,37 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Public // MARK: - Public
override func process(viewAction: RoomScreenViewAction) { override func process(viewAction: RoomScreenViewAction) {
switch viewAction { Task {
case .loadPreviousPage: switch viewAction {
state.isBackPaginating = true case .loadPreviousPage:
timelineController.paginateBackwards(Constants.backPaginationPageSize) { [weak self] _ in await MainActor.run {
self?.state.isBackPaginating = false 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 = ""
} }
} }
@@ -28,16 +28,13 @@ private class WeakUserSessionWrapper: ClientDelegate {
} }
func didReceiveSyncUpdate() { func didReceiveSyncUpdate() {
DispatchQueue.main.async { self.userSession?.didReceiveSyncUpdate()
self.userSession?.didReceiveSyncUpdate()
}
} }
} }
class UserSession: ClientDelegate { class UserSession: ClientDelegate {
private let client: Client private let client: Client
private let processingQueue: DispatchQueue
private(set) var rooms: [RoomProxy] = [] { private(set) var rooms: [RoomProxy] = [] {
didSet { didSet {
@@ -55,7 +52,6 @@ class UserSession: ClientDelegate {
init(client: Client) { init(client: Client) {
self.client = client self.client = client
self.processingQueue = DispatchQueue(label: "UserSessionProcessingQueue")
self.mediaProvider = MediaProvider(client: client, imageCache: ImageCache.default) self.mediaProvider = MediaProvider(client: client, imageCache: ImageCache.default)
client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self)) client.setDelegate(delegate: WeakUserSessionWrapper(userSession: self))
@@ -63,7 +59,9 @@ class UserSession: ClientDelegate {
Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.") Benchmark.startTrackingForIdentifier("ClientSync", message: "Started sync.")
client.startSync() client.startSync()
updateRooms() Task {
await updateRooms()
}
} }
var userIdentifier: String { var userIdentifier: String {
@@ -75,34 +73,25 @@ class UserSession: ClientDelegate {
} }
} }
func userDisplayName(_ completion: @escaping (Result<String, UserSessionError>) -> Void) { func loadUserDisplayName() async -> Result<String, UserSessionError> {
processingQueue.async { await withCheckedContinuation { continuation in
do { do {
let displayName = try self.client.displayName() let displayName = try self.client.displayName()
continuation.resume(returning: .success(displayName))
DispatchQueue.main.async {
completion(.success(displayName))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedRetrievingDisplayName))
completion(.failure(UserSessionError.failedRetrievingDisplayName))
}
} }
} }
} }
func userAvatarURL(_ completion: @escaping (Result<String, UserSessionError>) -> Void) { func loadUserAvatarURL() async -> Result<String, UserSessionError> {
processingQueue.async { await withCheckedContinuation { continuation in
do { do {
let displayName = try self.client.avatarUrl() let avatarURL = try self.client.avatarUrl()
continuation.resume(returning: .success(avatarURL))
DispatchQueue.main.async {
completion(.success(displayName))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedRetrievingDisplayName))
completion(.failure(UserSessionError.failedRetrievingDisplayName))
}
} }
} }
} }
@@ -111,43 +100,38 @@ class UserSession: ClientDelegate {
func didReceiveSyncUpdate() { func didReceiveSyncUpdate() {
Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update") Benchmark.logElapsedDurationForIdentifier("ClientSync", message: "Received sync update")
updateRooms()
Task {
await updateRooms()
}
} }
// MARK: Private // MARK: Private
func updateRooms() { private func updateRooms() async {
var currentRooms = self.rooms var currentRooms = self.rooms
self.processingQueue.async { [weak self] in Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms")
guard let self = self else { let sdkRooms = self.client.rooms()
return Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms")
}
Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms")
Benchmark.startTrackingForIdentifier("ClientRooms", message: "Fetching available rooms") let diff = sdkRooms.map({ $0.id()}).difference(from: currentRooms.map({ $0.id }))
let sdkRooms = self.client.rooms()
Benchmark.endTrackingForIdentifier("ClientRooms", message: "Retrieved \(sdkRooms.count) rooms") for change in diff {
switch change {
Benchmark.startTrackingForIdentifier("ProcessingRooms", message: "Started processing \(sdkRooms.count) rooms") case .insert(_, let id, _):
let diff = sdkRooms.map({ $0.id()}).difference(from: currentRooms.map({ $0.id })) guard let sdkRoom = sdkRooms.first(where: { $0.id() == id }) else {
MXLog.error("Failed retrieving sdk room with id: \(id)")
for change in diff { break
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 }
} }
} currentRooms.append(RoomProxy(room: sdkRoom, messageFactory: RoomMessageFactory()))
case .remove(_, let id, _):
Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms") currentRooms.removeAll { $0.id == id }
DispatchQueue.main.async {
self.rooms = currentRooms
} }
} }
Benchmark.endTrackingForIdentifier("ProcessingRooms", message: "Finished processing \(sdkRooms.count) rooms")
self.rooms = currentRooms
} }
} }
@@ -29,46 +29,6 @@ struct MediaProvider: MediaProviderProtocol {
return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil) return imageCache.retrieveImageInMemoryCache(forKey: source.underlyingSource.url(), options: nil)
} }
func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> 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? { func imageFromURL(_ url: String?) -> UIImage? {
guard let url = url else { guard let url = url else {
return nil return nil
@@ -77,7 +37,42 @@ struct MediaProvider: MediaProviderProtocol {
return imageFromSource(MediaSource(source: mediaSourceFromUrl(url: url))) return imageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)))
} }
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) { func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> {
return loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)), completion) await loadImageFromSource(MediaSource(source: mediaSourceFromUrl(url: url)))
}
func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
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))
}
}
}
}
} }
} }
@@ -17,9 +17,9 @@ enum MediaProviderError: Error {
protocol MediaProviderProtocol { protocol MediaProviderProtocol {
func imageFromSource(_ source: MediaSource?) -> UIImage? func imageFromSource(_ source: MediaSource?) -> UIImage?
func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError>
func imageFromURL(_ url: String?) -> UIImage? func imageFromURL(_ url: String?) -> UIImage?
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError>
} }
@@ -19,8 +19,8 @@ struct MockMediaProvider: MediaProviderProtocol {
return nil return nil
} }
func loadImageFromSource(_ source: MediaSource, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) { func loadImageFromSource(_ source: MediaSource) async -> Result<UIImage, MediaProviderError> {
return .failure(.failedRetrievingImage)
} }
func imageFromURL(_ url: String?) -> UIImage? { func imageFromURL(_ url: String?) -> UIImage? {
@@ -30,4 +30,8 @@ struct MockMediaProvider: MediaProviderProtocol {
func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) { func loadImageFromURL(_ url: String, _ completion: @escaping (Result<UIImage, MediaProviderError>) -> Void) {
} }
func loadImageFromURL(_ url: String) async -> Result<UIImage, MediaProviderError> {
return .failure(.failedRetrievingImage)
}
} }
@@ -9,7 +9,7 @@
import Foundation import Foundation
class MemberDetailProvider: MemberDetailProviderProtocol { class MemberDetailProvider: MemberDetailProviderProtocol {
private let roomProxy: RoomProxyProtocol? private let roomProxy: RoomProxyProtocol
private var memberAvatars = [String: String]() private var memberAvatars = [String: String]()
private var memberDisplayNames = [String: String]() private var memberDisplayNames = [String: String]()
@@ -18,58 +18,38 @@ class MemberDetailProvider: MemberDetailProviderProtocol {
} }
func avatarURLForUserId(_ userId: String) -> String? { func avatarURLForUserId(_ userId: String) -> String? {
self.memberAvatars[userId] memberAvatars[userId]
} }
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, MemberDetailProviderError>) -> Void) { func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
guard let roomProxy = roomProxy else {
return
}
if let avatarURL = avatarURLForUserId(userId) { if let avatarURL = avatarURLForUserId(userId) {
completion(.success(avatarURL)) return .success(avatarURL)
} }
roomProxy.avatarURLForUserId(userId, completion: { [weak self] result in switch await roomProxy.loadAvatarURLForUserId(userId) {
guard let self = self else { case .success(let avatarURL):
return memberAvatars[userId] = avatarURL
} return .success(avatarURL)
case .failure:
switch result { return .failure(.failedRetrievingUserAvatarURL)
case .success(let avatarURL): }
self.memberAvatars[userId] = avatarURL
completion(.success(avatarURL))
case .failure:
completion(.failure(.failedRetrievingUserAvatarURL))
}
})
} }
func displayNameForUserId(_ userId: String) -> String? { func displayNameForUserId(_ userId: String) -> String? {
self.memberDisplayNames[userId] memberDisplayNames[userId]
} }
func displayNameForUserId(_ userId: String, completion: @escaping (Result<String?, MemberDetailProviderError>) -> Void) { func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError> {
guard let roomProxy = roomProxy else { if let displayName = displayNameForUserId(userId) {
return return .success(displayName)
} }
if let avatarURL = displayNameForUserId(userId) { switch await roomProxy.loadDisplayNameForUserId(userId) {
completion(.success(avatarURL)) 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))
}
})
} }
} }
@@ -16,8 +16,8 @@ enum MemberDetailProviderError: Error {
protocol MemberDetailProviderProtocol { protocol MemberDetailProviderProtocol {
func avatarURLForUserId(_ userId: String) -> String? func avatarURLForUserId(_ userId: String) -> String?
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, MemberDetailProviderError>) -> Void) func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
func displayNameForUserId(_ userId: String) -> String? func displayNameForUserId(_ userId: String) -> String?
func displayNameForUserId(_ userId: String, completion: @escaping (Result<String?, MemberDetailProviderError>) -> Void) func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, MemberDetailProviderError>
} }
@@ -27,27 +27,27 @@ struct MockRoomProxy: RoomProxyProtocol {
var callbacks = PassthroughSubject<RoomProxyCallback, Never>() var callbacks = PassthroughSubject<RoomProxyCallback, Never>()
func displayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void) { func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
completion(.success("Room display name")) return .failure(.failedRetrievingMemberDisplayName)
}
func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
return .failure(.failedRetrievingMemberAvatarURL)
}
func loadDisplayName() async -> Result<String, RoomProxyError> {
return .failure(.failedRetrievingDisplayName)
} }
func startLiveEventListener() { func startLiveEventListener() {
} }
func paginateBackwards(count: UInt, callback: ((Result<Void, RoomProxyError>) -> Void)?) { func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
return .failure(.backwardStreamNotAvailable)
} }
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) {
}
func displayNameForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) {
}
func sendMessage(_ message: String, callback: ((Result<Void, RoomProxyError>) -> Void)?) {
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
return .failure(.failedSendingMessage)
} }
} }
+37 -69
View File
@@ -21,9 +21,7 @@ private class WeakRoomProxyWrapper: RoomDelegate {
// MARK: - RoomDelegate // MARK: - RoomDelegate
func didReceiveMessage(message: AnyMessage) { 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 room: Room
private let messageFactory: RoomMessageFactory private let messageFactory: RoomMessageFactory
private let generalProcessingQueue: DispatchQueue
private let messageProcessingQueue: DispatchQueue
private var backwardStream: BackwardsStreamProtocol? private var backwardStream: BackwardsStreamProtocol?
private(set) var displayName: String? private(set) var displayName: String?
@@ -45,18 +40,13 @@ class RoomProxy: RoomProxyProtocol {
init(room: Room, messageFactory: RoomMessageFactory) { init(room: Room, messageFactory: RoomMessageFactory) {
self.room = room self.room = room
self.messageFactory = messageFactory self.messageFactory = messageFactory
generalProcessingQueue = DispatchQueue(label: "RoomProxyGeneralProcessingQueue")
messageProcessingQueue = DispatchQueue(label: "RoomProxyMessageProcessingQueue")
messages = [] messages = []
messageProcessingQueue.async {
let backwardStream = room.startLiveEventListener()
DispatchQueue.main.async {
self.backwardStream = backwardStream
}
}
room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self)) room.setDelegate(delegate: WeakRoomProxyWrapper(roomProxy: self))
Task {
backwardStream = room.startLiveEventListener()
}
} }
var id: String { var id: String {
@@ -95,66 +85,50 @@ class RoomProxy: RoomProxyProtocol {
room.avatarUrl() room.avatarUrl()
} }
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) { func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
generalProcessingQueue.async { await withCheckedContinuation({ continuation in
do { do {
let avatarURL = try self.room.memberAvatarUrl(userId: userId) let avatarURL = try self.room.memberAvatarUrl(userId: userId)
continuation.resume(returning: .success(avatarURL))
DispatchQueue.main.async {
completion(.success(avatarURL))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedRetrievingMemberAvatarURL))
completion(.failure(RoomProxyError.failedRetrievingMemberAvatarURL))
}
} }
} })
} }
func displayNameForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) { func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError> {
generalProcessingQueue.async { await withCheckedContinuation({ continuation in
do { do {
let displayName = try self.room.memberDisplayName(userId: userId) let displayName = try self.room.memberDisplayName(userId: userId)
continuation.resume(returning: .success(displayName))
DispatchQueue.main.async {
completion(.success(displayName))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedRetrievingMemberDisplayName))
completion(.failure(RoomProxyError.failedRetrievingMemberDisplayName))
}
} }
} })
} }
func displayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void) {
if let displayName = displayName {
completion(.success(displayName))
return
}
generalProcessingQueue.async { func loadDisplayName() async -> Result<String, RoomProxyError> {
await withCheckedContinuation({ continuation in
if let displayName = displayName {
continuation.resume(returning: .success(displayName))
return
}
do { do {
let displayName = try self.room.displayName() let displayName = try self.room.displayName()
self.displayName = displayName self.displayName = displayName
DispatchQueue.main.async { continuation.resume(returning: .success(displayName))
completion(.success(displayName))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedRetrievingDisplayName))
completion(.failure(.failedRetrievingDisplayName))
}
} }
} })
} }
func paginateBackwards(count: UInt, callback: ((Result<Void, RoomProxyError>) -> Void)?) { func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError> {
messageProcessingQueue.async { await withCheckedContinuation { continuation in
guard let backwardStream = self.backwardStream else { guard let backwardStream = self.backwardStream else {
DispatchQueue.main.async { continuation.resume(returning: .failure(.backwardStreamNotAvailable))
callback?(.failure(.backwardStreamNotAvailable))
}
return return
} }
@@ -166,28 +140,22 @@ class RoomProxy: RoomProxyProtocol {
self.messageFactory.buildRoomMessageFrom(message) self.messageFactory.buildRoomMessageFrom(message)
}.reversed() }.reversed()
DispatchQueue.main.async { self.messages.insert(contentsOf: messages, at: 0)
self.messages.insert(contentsOf: messages, at: 0)
callback?(.success(())) continuation.resume(returning: .success(()))
}
} }
} }
func sendMessage(_ message: String, callback: ((Result<Void, RoomProxyError>) -> Void)?) { func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
let messageContent = messageEventContentFromMarkdown(md: message) let messageContent = messageEventContentFromMarkdown(md: message)
let transactionId = genTransactionId() let transactionId = genTransactionId()
messageProcessingQueue.async { return await withCheckedContinuation { continuation in
do { do {
try self.room.send(msg: messageContent, txnId: transactionId) try self.room.send(msg: messageContent, txnId: transactionId)
continuation.resume(returning: .success(()))
DispatchQueue.main.async {
callback?(.success(()))
}
} catch { } catch {
DispatchQueue.main.async { continuation.resume(returning: .failure(.failedSendingMessage))
callback?(.failure(.failedSendingMessage))
}
} }
} }
} }
@@ -37,15 +37,15 @@ protocol RoomProxyProtocol {
var avatarURL: String? { get } var avatarURL: String? { get }
func displayName(_ completion: @escaping (Result<String, RoomProxyError>) -> Void) func loadAvatarURLForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
func avatarURLForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
func displayNameForUserId(_ userId: String, completion: @escaping (Result<String?, RoomProxyError>) -> Void) func loadDisplayName() async -> Result<String, RoomProxyError>
func paginateBackwards(count: UInt, callback: ((Result<Void, RoomProxyError>) -> Void)?) func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError>
func sendMessage(_ message: String, callback: ((Result<Void, RoomProxyError>) -> Void)?) func sendMessage(_ message: String) async -> Result<Void, RoomProxyError>
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get } var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
} }
@@ -16,21 +16,20 @@ struct EventBriefFactory: EventBriefFactoryProtocol {
self.memberDetailProvider = memberDetailProvider self.memberDetailProvider = memberDetailProvider
} }
func eventBriefForMessage(_ message: RoomMessageProtocol?, completion: @escaping ((EventBrief?) -> Void)) { func eventBriefForMessage(_ message: RoomMessageProtocol?) async -> EventBrief? {
guard let message = message else { guard let message = message else {
completion(nil) return nil
return
} }
switch message { switch message {
case is ImageRoomMessage: case is ImageRoomMessage:
completion(nil) return nil
case let message as TextRoomMessage: 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: 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: case let message as EmoteRoomMessage:
buildEventBrief(message: message, htmlBody: message.htmlBody, completion: completion) return await buildEventBrief(message: message, htmlBody: message.htmlBody)
default: default:
fatalError("Unknown room message.") fatalError("Unknown room message.")
} }
@@ -38,26 +37,24 @@ struct EventBriefFactory: EventBriefFactoryProtocol {
// MARK: - Private // MARK: - Private
private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?, completion: @escaping ((EventBrief?) -> Void)) { private func buildEventBrief(message: RoomMessageProtocol, htmlBody: String?) async -> EventBrief? {
memberDetailProvider.displayNameForUserId(message.sender) { result in switch await memberDetailProvider.loadDisplayNameForUserId(message.sender) {
switch result { case .success(let displayName):
case .success(let displayName): return EventBrief(eventId: message.id,
completion(EventBrief(eventId: message.id, senderId: message.sender,
senderId: message.sender, senderDisplayName: displayName,
senderDisplayName: displayName, body: message.body,
body: message.body, htmlBody: htmlBody,
htmlBody: htmlBody, date: message.originServerTs)
date: message.originServerTs)) case .failure(let error):
case .failure(let error): MXLog.error("Failed fetching sender display name with error: \(error)")
MXLog.error("Failed fetching sender display name with error: \(error)")
return EventBrief(eventId: message.id,
completion(EventBrief(eventId: message.id, senderId: message.sender,
senderId: message.sender, senderDisplayName: nil,
senderDisplayName: nil, body: message.body,
body: message.body, htmlBody: htmlBody,
htmlBody: htmlBody, date: message.originServerTs)
date: message.originServerTs))
}
} }
} }
} }
@@ -9,5 +9,5 @@
import Foundation import Foundation
protocol EventBriefFactoryProtocol { protocol EventBriefFactoryProtocol {
func eventBriefForMessage(_ message: RoomMessageProtocol?, completion: @escaping ((EventBrief?) -> Void)) func eventBriefForMessage(_ message: RoomMessageProtocol?) async -> EventBrief?
} }
@@ -30,7 +30,7 @@ struct MockRoomSummary: RoomSummaryProtocol {
var avatar: UIImage? var avatar: UIImage?
func loadData() { func loadDetails() {
} }
@@ -71,81 +71,81 @@ class RoomSummary: RoomSummaryProtocol {
self.mediaProvider = mediaProvider self.mediaProvider = mediaProvider
self.eventBriefFactory = eventBriefFactory self.eventBriefFactory = eventBriefFactory
eventBriefFactory.eventBriefForMessage(roomProxy.messages.last) { [weak self] result in Task {
self?.lastMessage = result lastMessage = await eventBriefFactory.eventBriefForMessage(roomProxy.messages.last)
} }
roomProxy.callbacks.sink { [weak self] callback in roomProxy.callbacks
guard let self = self else { .receive(on: DispatchQueue.main)
return .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 switch callback {
self?.lastMessage = result case .updatedMessages:
Task {
self.lastMessage = await eventBriefFactory.eventBriefForMessage(roomProxy.messages.last)
}
} }
} }
} .store(in: &roomUpdateListeners)
.store(in: &roomUpdateListeners)
} }
func loadData() { func loadDetails() async {
if hasLoadedData { if hasLoadedData {
return return
} }
loadDisplayName() await withTaskGroup(of: Void.self) { group in
loadLastMessage() group.addTask {
loadAvatar() await self.loadDisplayName()
}
group.addTask {
await self.loadAvatar()
}
group.addTask {
await self.loadLastMessage()
}
}
hasLoadedData = true hasLoadedData = true
} }
// MARK: - Private // MARK: - Private
private func loadDisplayName() { private func loadDisplayName() async {
roomProxy.displayName { [weak self] result in switch await roomProxy.loadDisplayName() {
guard let self = self else { return } case .success(let displayName):
self.displayName = displayName
switch result { case .failure(let error):
case .success(let displayName): MXLog.error("Failed fetching room display name with error: \(error)")
self.displayName = displayName
case .failure(let error):
MXLog.error("Failed fetching room display name with error: \(error)")
}
} }
} }
private func loadLastMessage() { private func loadAvatar() async {
if roomProxy.messages.last == nil { guard let avatarURL = roomProxy.avatarURL else {
roomProxy.paginateBackwards(count: 1) { [weak self] result in return
guard let self = self else { return } }
switch result { switch await mediaProvider.loadImageFromURL(avatarURL) {
case .success: case .success(let avatar):
self.eventBriefFactory.eventBriefForMessage(self.roomProxy.messages.last) { [weak self] result in self.avatar = avatar
self?.lastMessage = result case .failure(let error):
} MXLog.error("Failed fetching room avatar with error: \(error)")
case .failure(let error):
MXLog.error("Failed back paginating with error: \(error)")
}
}
} }
} }
private func loadAvatar() { private func loadLastMessage() async {
if let avatarURL = roomProxy.avatarURL { guard roomProxy.messages.last == nil else {
mediaProvider.loadImageFromURL(avatarURL) { [weak self] result in return
guard let self = self else { return } }
switch result { switch await roomProxy.paginateBackwards(count: 1) {
case .success(let image): case .success:
self.avatar = image self.lastMessage = await self.eventBriefFactory.eventBriefForMessage(self.roomProxy.messages.last)
case .failure(let error): case .failure(let error):
MXLog.error("Failed fetching room avatar with error: \(error)") MXLog.error("Failed back paginating with error: \(error)")
}
}
} }
} }
} }
@@ -28,5 +28,5 @@ protocol RoomSummaryProtocol {
var callbacks: PassthroughSubject<RoomSummaryCallback, Never> { get } var callbacks: PassthroughSubject<RoomSummaryCallback, Never> { get }
func loadData() func loadDetails() async
} }
@@ -19,19 +19,19 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"), SeparatorRoomTimelineItem(id: UUID().uuidString, text: "Today"),
TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")] TextRoomTimelineItem(id: UUID().uuidString, text: "You too!", timestamp: "5 PM", shouldShowSenderDetails: true, senderId: "Bob")]
func paginateBackwards(_ count: UInt, callback: ((Result<Void, RoomTimelineControllerError>) -> Void)) { func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.updatedTimelineItems) 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 {
} }
} }
@@ -45,31 +45,29 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
} }
func paginateBackwards(_ count: UInt, callback: @escaping ((Result<Void, RoomTimelineControllerError>) -> Void)) { func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
timelineProvider.paginateBackwards(count) { [weak self] result in switch await timelineProvider.paginateBackwards(count) {
switch result { case .success:
case .success: updateTimelineItems()
callback(.success(())) return .success(())
self?.updateTimelineItems() case .failure:
case .failure: return .failure(.generic)
callback(.failure(.generic))
}
} }
} }
func processItemAppearance(_ itemId: String) { func processItemAppearance(_ itemId: String) async {
guard let timelineItem = self.timelineItems.filter({ $0.id == itemId}).first else { guard let timelineItem = self.timelineItems.filter({ $0.id == itemId}).first else {
return return
} }
if let item = timelineItem as? EventBasedTimelineItemProtocol { if let item = timelineItem as? EventBasedTimelineItemProtocol {
loadUserAvatarForTimelineItem(item) await loadUserAvatarForTimelineItem(item)
loadUserDisplayNameForTimelineItem(item) await loadUserDisplayNameForTimelineItem(item)
} }
switch timelineItem { switch timelineItem {
case let item as ImageRoomTimelineItem: case let item as ImageRoomTimelineItem:
loadImageForTimelineItem(item) await loadImageForTimelineItem(item)
default: default:
break break
} }
@@ -79,8 +77,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
} }
func sendMessage(_ message: String) { func sendMessage(_ message: String) async {
timelineProvider.sendMessage(message) switch await timelineProvider.sendMessage(message) {
case .success:
break
case .failure:
break
}
} }
// MARK: - Private // MARK: - Private
@@ -124,7 +127,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs) return Calendar.current.isDate(lhs.originServerTs, inSameDayAs: rhs.originServerTs)
} }
private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) { private func loadImageForTimelineItem(_ timelineItem: ImageRoomTimelineItem) async {
if timelineItem.image != nil { if timelineItem.image != nil {
return return
} }
@@ -133,65 +136,69 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
return return
} }
mediaProvider.loadImageFromSource(source) { [weak self] result in switch await mediaProvider.loadImageFromSource(source) {
guard let self = self else { case .success(let image):
guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
var item = self.timelineItems[index] as? ImageRoomTimelineItem else {
return 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 }), 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 return
} }
item.image = image item.senderAvatar = avatar
self.timelineItems[index] = item self.timelineItems[index] = item
self.callbacks.send(.updatedTimelineItem(timelineItem.id)) 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 { if timelineItem.shouldShowSenderDetails == false {
return return
} }
memberDetailProvider.avatarURLForUserId(timelineItem.senderId) { result in switch await memberDetailProvider.loadDisplayNameForUserId(timelineItem.senderId) {
if case let .success(avatarURL) = result, case .success(let displayName):
let avatarURL = avatarURL { guard let displayName = displayName,
self.mediaProvider.loadImageFromURL(avatarURL) { result in let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }),
if case let .success(image) = result { var item = self.timelineItems[index] as? EventBasedTimelineItemProtocol else {
guard let index = self.timelineItems.firstIndex(where: { $0.id == timelineItem.id }), return
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))
} }
item.senderDisplayName = displayName
self.timelineItems[index] = item
self.callbacks.send(.updatedTimelineItem(timelineItem.id))
case .failure:
break
} }
} }
} }
@@ -21,12 +21,12 @@ enum RoomTimelineControllerError: Error {
protocol RoomTimelineControllerProtocol { protocol RoomTimelineControllerProtocol {
var timelineItems: [RoomTimelineItemProtocol] { get } var timelineItems: [RoomTimelineItemProtocol] { get }
var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineControllerCallback, Never> { get }
func paginateBackwards(_ count: UInt, callback: @escaping ((Result<Void, RoomTimelineControllerError>) -> 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<Void, RoomTimelineControllerError>
func sendMessage(_ message: String) async
} }
@@ -13,7 +13,7 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
private let roomProxy: RoomProxyProtocol private let roomProxy: RoomProxyProtocol
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
let callbacks = PassthroughSubject<RoomTimelineCallback, Never>() let callbacks = PassthroughSubject<RoomTimelineProviderCallback, Never>()
init(roomProxy: RoomProxyProtocol) { init(roomProxy: RoomProxyProtocol) {
self.roomProxy = roomProxy self.roomProxy = roomProxy
@@ -32,25 +32,21 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
roomProxy.messages roomProxy.messages
} }
func paginateBackwards(_ count: UInt, callback: ((Result<Void, RoomTimelineError>) -> Void)?) { func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError> {
self.roomProxy.paginateBackwards(count: count) { result in switch await roomProxy.paginateBackwards(count: count) {
switch result { case .success:
case .success: return .success(())
callback?(.success(())) case .failure:
case .failure: return .failure(.generic)
callback?(.failure(.generic))
}
} }
} }
func sendMessage(_ message: String) { func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError> {
roomProxy.sendMessage(message) { result in switch await roomProxy.sendMessage(message) {
switch result { case .success:
case .success: return .success(())
break case .failure:
case .failure(let error): return .failure(.failedSendingMessage)
MXLog.error("Failed sending message with error: \(error)")
}
} }
} }
} }
@@ -9,20 +9,21 @@
import Foundation import Foundation
import Combine import Combine
enum RoomTimelineCallback { enum RoomTimelineProviderCallback {
case updatedMessages case updatedMessages
} }
enum RoomTimelineError: Error { enum RoomTimelineProviderError: Error {
case failedSendingMessage
case generic case generic
} }
protocol RoomTimelineProviderProtocol { protocol RoomTimelineProviderProtocol {
var callbacks: PassthroughSubject<RoomTimelineCallback, Never> { get } var callbacks: PassthroughSubject<RoomTimelineProviderCallback, Never> { get }
var messages: [RoomMessageProtocol] { get } var messages: [RoomMessageProtocol] { get }
func paginateBackwards(_ count: UInt, callback: ((Result<Void, RoomTimelineError>) -> Void)?) func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
func sendMessage(_ message: String) func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError>
} }