diff --git a/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift b/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift index 503617ce2..aa152d5b1 100644 --- a/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift +++ b/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift @@ -8,6 +8,7 @@ import SwiftUI import UIKit +import Combine final class CircularProgressState: ObservableObject { let lineWidth: CGFloat @@ -15,7 +16,9 @@ final class CircularProgressState: ObservableObject { let progressColor: UIColor @Published var progress: Double = 0 @Published var hidden: Bool = false - + @Published var isSpinning: Bool = false + @Published var backgroundGradient: LinearGradient? = nil + init( lineWidth: CGFloat = 6, backgroundColor: UIColor = .lightGray, @@ -33,18 +36,31 @@ final class CircularProgressState: ObservableObject { struct CircularProgressView: View { @StateObject private var state: CircularProgressState - + @State private var rotationAngle: Double = 0 + @State private var timer: Timer.TimerPublisher = Timer.publish(every: 0.016, on: .main, in: .common) + @State private var timerCancellable: Cancellable? + init(state: @escaping () -> CircularProgressState) { _state = .init(wrappedValue: state()) } - + var body: some View { ZStack { - Circle() - .stroke( - Color(uiColor: state.backgroundColor), - lineWidth: state.lineWidth - ) + if let gradient = state.backgroundGradient { + Circle() + .stroke( + gradient, + lineWidth: state.lineWidth + ) + .rotationEffect(.degrees(rotationAngle)) + } else { + Circle() + .stroke( + Color(uiColor: state.backgroundColor), + lineWidth: state.lineWidth + ) + } + Circle() .trim(from: 0, to: state.progress) .stroke( @@ -58,5 +74,20 @@ struct CircularProgressView: View { .animation(.easeOut, value: state.progress) } .opacity(state.hidden ? .zero : 1.0) + .onReceive(timer) { _ in + if state.isSpinning { + rotationAngle += 2 + if rotationAngle >= 360 { rotationAngle -= 360 } + } + } + .onReceive(state.$isSpinning) { spinning in + if spinning { + timer = Timer.publish(every: 0.016, on: .main, in: .common) + timerCancellable = timer.connect() + } else { + timerCancellable?.cancel() + timerCancellable = nil + } + } } } diff --git a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift index b5270ed44..ed628239c 100644 --- a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift +++ b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift @@ -9,17 +9,23 @@ import CommonKit import UIKit -enum FileMessageStatus: Equatable { - case busy - case needToDownload(failed: Bool) - case failed +enum FileMessageStatus: Hashable { + case uploading + case downloading case success + case failed + case needToDownload(failed: Bool) var image: UIImage { switch self { - case .busy: return .asset(named: "status_failed") ?? .init() - case .success: return .asset(named: "status_success") ?? .init() - case .failed: return .asset(named: "status_failed") ?? .init() + case .uploading: + return UIImage(systemName: "square.fill") ?? UIImage() + case .downloading: + return .asset(named: "status_pending") ?? .init() + case .success: + return .asset(named: "status_success") ?? .init() + case .failed: + return .asset(named: "status_failed") ?? .init() case let .needToDownload(failed): guard !failed else { return .asset(named: "download-circular-error") ?? .init() @@ -30,8 +36,10 @@ enum FileMessageStatus: Equatable { var imageTintColor: UIColor { switch self { - case .busy, .needToDownload, .success: return .adamant.primary - case .failed: return .adamant.attention + case .uploading, .downloading, .needToDownload, .success: + return .adamant.primary + case .failed: + return .adamant.attention } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift index 2398b0e96..b951db691 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift @@ -106,7 +106,7 @@ final class ChatMediaContainerView: UIView { private var statusProgressState = CircularProgressState( lineWidth: 2.0, - backgroundColor: .clear, + backgroundColor: .lightGray, progressColor: .adamant.primary, progress: .zero, hidden: true @@ -143,7 +143,7 @@ final class ChatMediaContainerView: UIView { } @objc func onStatusButtonTap() { - if model.status == .busy { + if model.status == .uploading { actionHandler(.cancelUploading(messageId: model.id)) return } @@ -185,7 +185,7 @@ extension ChatMediaContainerView { reactionsStack.insertSubview(progressRingHostingView, aboveSubview: statusButton) progressRingHostingView.snp.makeConstraints { make in make.center.equalTo(statusButton) - make.size.equalTo(31) + make.size.equalTo(30) } } @@ -222,9 +222,18 @@ extension ChatMediaContainerView { func updateProgressRing() { let averageProgress = averageUploadProgress() - + + if averageProgress == 0 { + statusProgressState.backgroundGradient = LinearGradient( + gradient: Gradient(colors: [.white, .black]), + startPoint: .topLeading, + endPoint: .bottomTrailing + )} else { + statusProgressState.backgroundGradient = nil + } statusProgressState.progress = averageProgress / 100 - statusProgressState.hidden = averageProgress == 0 || averageProgress == 100 + statusProgressState.hidden = averageProgress == 100 || !(model.status == .uploading) + statusProgressState.isSpinning = (model.status == .uploading && averageProgress == 0) } func updateLayout() { diff --git a/Adamant/Modules/Chat/ViewModel/ChatFileService.swift b/Adamant/Modules/Chat/ViewModel/ChatFileService.swift index 28781cd23..a9d02ff01 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatFileService.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatFileService.swift @@ -209,6 +209,9 @@ final class ChatFileService: ChatFileProtocol, Sendable { func cancelUpload(messageId: String) async { guard let tasks = uploadTasks[messageId] else { return } + uploadTasks[messageId] = nil + uploadingFilesDictionary[messageId] = nil + var fileIdsToRemove: [String] = [] for (fileId, task) in tasks { @@ -221,9 +224,6 @@ final class ChatFileService: ChatFileProtocol, Sendable { oldIds: fileIdsToRemove, txId: messageId ) - - uploadTasks[messageId] = nil - uploadingFilesDictionary[messageId] = nil } func isDownloadPreviewLimitReached(for fileId: String) -> Bool { @@ -993,6 +993,7 @@ extension ChatFileService { do { let result = try await uploadTask.value + try Task.checkCancellation() sendProgress( for: result.file.cid, diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index 25e2e0723..f6799df22 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -1728,8 +1728,12 @@ extension ChatViewModel { return .failed } - if model.content.fileModel.files.first(where: { $0.isBusy }) != nil { - return .busy + if model.content.fileModel.files.contains(where: { $0.isUploading }) { + return .uploading + } + + if model.content.fileModel.files.contains(where: { $0.isDownloading }) { + return .downloading } if model.content.fileModel.files.contains(where: { diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index d789cc4b5..62343d3c8 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -1357,8 +1357,12 @@ extension AdamantChatsProvider { do { let locallyID = signedTransaction.generateId() ?? UUID().uuidString - transaction.transactionId = locallyID - transaction.chatMessageId = locallyID + if transaction.transactionId.isEmpty { + transaction.transactionId = locallyID + } + if transaction.chatMessageId?.isEmpty ?? true { + transaction.chatMessageId = locallyID + } let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get()