diff --git a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift index f1fc78b..776d63a 100644 --- a/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift +++ b/ConversationsClassic/AppCore/Middlewares/FileMiddleware.swift @@ -4,12 +4,21 @@ import UIKit final class FileMiddleware { static let shared = FileMiddleware() + private var downloadingMessageIDs = ThreadSafeSet() func middleware(state _: AppState, action: AppAction) -> AnyPublisher { switch action { case .conversationAction(.messagesUpdated(let messages)): - return Future { promise in + return Future { [weak self] promise in + guard let wSelf = self else { + promise(.success(.empty)) + return + } for message in messages where message.attachmentRemotePath != nil && message.attachmentLocalPath == nil { + if wSelf.downloadingMessageIDs.contains(message.id) { + continue + } + wSelf.downloadingMessageIDs.insert(message.id) DispatchQueue.main.async { // swiftlint:disable:next force_unwrapping store.dispatch(.fileAction(.downloadAttachmentFile(id: message.id, attachmentRemotePath: message.attachmentRemotePath!))) @@ -34,14 +43,19 @@ final class FileMiddleware { }.eraseToAnyPublisher() case .fileAction(.attachmentFileDownloaded(let id, let localUrl)): - return Just(.fileAction(.createAttachmentThumbnail(id: id, localUrl: localUrl))) - .eraseToAnyPublisher() + return Future { [weak self] promise in + self?.downloadingMessageIDs.remove(id) + promise(.success(.fileAction(.createAttachmentThumbnail(id: id, localUrl: localUrl)))) + } + .eraseToAnyPublisher() case .fileAction(.createAttachmentThumbnail(let id, let localUrl)): - return Future { promise in + return Future { [weak self] promise in if let thumbnailUrl = FileProcessing.shared.createThumbnail(id: id, localUrl: localUrl) { + self?.downloadingMessageIDs.remove(id) promise(.success(.fileAction(.attachmentThumbnailCreated(id: id, thumbnailUrl: thumbnailUrl)))) } else { + self?.downloadingMessageIDs.remove(id) promise(.success(.empty)) } } diff --git a/ConversationsClassic/Helpers/Set+Extensions.swift b/ConversationsClassic/Helpers/Set+Extensions.swift new file mode 100644 index 0000000..910f33b --- /dev/null +++ b/ConversationsClassic/Helpers/Set+Extensions.swift @@ -0,0 +1,26 @@ +import Foundation + +class ThreadSafeSet { + private var set: Set = [] + private let accessQueue = DispatchQueue(label: "com.example.ThreadSafeSet") + + func insert(_ newElement: T) { + _ = accessQueue.sync { + set.insert(newElement) + } + } + + func remove(_ element: T) { + _ = accessQueue.sync { + set.remove(element) + } + } + + var elements: Set { + accessQueue.sync { set } + } + + func contains(_ element: T) -> Bool { + accessQueue.sync { set.contains(element) } + } +} diff --git a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift index f670802..4dec962 100644 --- a/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift +++ b/ConversationsClassic/View/Screens/Conversation/ConversationMessageContainer.swift @@ -70,29 +70,33 @@ private struct AttachmentView: View { let message: Message var body: some View { - switch message.attachmentType { - case .image: - if let thumbnail = thumbnail() { - thumbnail - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) - } else { + if message.attachmentDownloadFailed { + failed + } else { + switch message.attachmentType { + case .image: + if let thumbnail = thumbnail() { + thumbnail + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) + } else { + placeholder + } + + case .movie: + if let file = message.attachmentLocalPath { + VideoPlayerView(url: file) + .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) + .cornerRadius(Const.attachmentPreviewSize / 10) + .overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1)) + } else { + placeholder + } + + default: placeholder } - - case .movie: - if let file = message.attachmentLocalPath { - VideoPlayerView(url: file) - .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) - .cornerRadius(Const.attachmentPreviewSize / 10) - .overlay(RoundedRectangle(cornerRadius: Const.attachmentPreviewSize / 10).stroke(Color.Material.Shape.separator, lineWidth: 1)) - } else { - placeholder - } - - default: - placeholder } } @@ -113,6 +117,19 @@ private struct AttachmentView: View { } } + @ViewBuilder private var failed: some View { + Rectangle() + .foregroundColor(.Material.Background.dark) + .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) + .overlay { + ZStack { + Image(systemName: "exclamationmark.triangle") + .font(.body1) + .foregroundColor(.Rainbow.red500) + } + } + } + private func progressImageName(_ type: MessageAttachmentType) -> String { switch type { case .image: