diff --git a/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift b/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift index 50ca5d654..c7d6b0aae 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift @@ -9,6 +9,7 @@ import CommonKit protocol ChatPreservationProtocol: AnyObject, Sendable { + var updateNotifier: ObservableSender { get } func preserveMessage(_ message: String, forAddress address: String) func getPreservedMessageFor(address: String, thenRemoveIt: Bool) -> String? func setReplyMessage(_ message: MessageModel?, forAddress address: String) diff --git a/Adamant/Modules/ChatsList/ChatListFactory.swift b/Adamant/Modules/ChatsList/ChatListFactory.swift index c830143a5..58b6f3b1f 100644 --- a/Adamant/Modules/ChatsList/ChatListFactory.swift +++ b/Adamant/Modules/ChatsList/ChatListFactory.swift @@ -24,7 +24,8 @@ struct ChatListFactory { dialogService: assembler.resolve(DialogService.self)!, addressBook: assembler.resolve(AddressBookService.self)!, avatarService: assembler.resolve(AvatarService.self)!, - walletServiceCompose: assembler.resolve(WalletServiceCompose.self)! + walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, + chatPreservation: assembler.resolve(ChatPreservationProtocol.self)! ) } diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index b4a80927a..cf9712a1c 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -57,6 +57,7 @@ final class ChatListViewController: KeyboardObservingViewController { private let addressBook: AddressBookService private let avatarService: AvatarService private let walletServiceCompose: WalletServiceCompose + private let chatPreservation: ChatPreservationProtocol // MARK: IBOutlet @IBOutlet weak var tableView: UITableView! @@ -142,7 +143,8 @@ final class ChatListViewController: KeyboardObservingViewController { dialogService: DialogService, addressBook: AddressBookService, avatarService: AvatarService, - walletServiceCompose: WalletServiceCompose + walletServiceCompose: WalletServiceCompose, + chatPreservation: ChatPreservationProtocol ) { self.accountService = accountService self.chatsProvider = chatsProvider @@ -153,6 +155,7 @@ final class ChatListViewController: KeyboardObservingViewController { self.addressBook = addressBook self.avatarService = avatarService self.walletServiceCompose = walletServiceCompose + self.chatPreservation = chatPreservation super.init(nibName: "ChatListViewController", bundle: nil) } @@ -193,7 +196,6 @@ final class ChatListViewController: KeyboardObservingViewController { tableView.deselectRow(at: indexPath, animated: animated) } } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() @@ -331,6 +333,12 @@ final class ChatListViewController: KeyboardObservingViewController { .sink { @MainActor [weak self] in self?.setIsStateUpdating($0) } .store(in: &subscriptions) } + chatPreservation.updateNotifier + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.tableView.reloadData() + } + .store(in: &subscriptions) } private func closeDetailVC() { @@ -681,10 +689,15 @@ extension ChatListViewController { cell.accountLabel.text = chatroom.getName(addressBookService: addressBook) cell.hasUnreadMessages = chatroom.hasUnreadMessages - - if let lastTransaction = chatroom.lastTransaction { + if let address = chatroom.partner?.address, + let preservedMessage = shortDescription(for: address) { + cell.hasUnreadMessages = chatroom.hasUnreadMessages + cell.lastMessageLabel.attributedText = preservedMessage + cell.isClockVisible = false + } else if let lastTransaction = chatroom.lastTransaction { cell.hasUnreadMessages = lastTransaction.isUnread cell.lastMessageLabel.attributedText = shortDescription(for: lastTransaction) + cell.isClockVisible = lastTransaction.statusEnum == .pending } else { cell.lastMessageLabel.text = nil } @@ -1034,7 +1047,53 @@ extension ChatListViewController { return nil } } - + private func shortDescription(for address: String) -> NSAttributedString? { + var descriptionParts: [NSAttributedString] = [] + + if chatPreservation.getReplyMessage(address: address, thenRemoveIt: false) != nil { + let replyImageAttachment = NSTextAttachment() + replyImageAttachment.image = UIImage(systemName: "arrowshape.turn.up.left")?.withTintColor(.adamant.primary) + replyImageAttachment.bounds = CGRect(x: .zero, y: -3, width: 23, height: 20) + + descriptionParts.append(NSAttributedString(attachment: replyImageAttachment)) + } + if let files = chatPreservation.getPreservedFiles(for: address, thenRemoveIt: false), !files.isEmpty { + let mediaCount = files.count(where: { $0.type.isMedia }) + let otherCount = files.count(where: { !$0.type.isMedia }) + + let fileParts = [ + mediaCount > 0 ? "📸" + (mediaCount >= 2 ? "\(mediaCount)" : "") : nil, + otherCount > 0 ? "📄" + (otherCount >= 2 ? "\(otherCount)" : "") : nil + ].compactMap { $0 } + + if !fileParts.isEmpty { + let parsedFileParts = fileParts + .map { markdownParser.parse($0).resolveLinkColor() } + .reduce(NSMutableAttributedString()) { result, part in + if !result.string.isEmpty { result.append(NSAttributedString(string: " ")) } + result.append(part) + return result + } + descriptionParts.append(parsedFileParts) + } + } + + if let preservedMessage = chatPreservation.getPreservedMessageFor(address: address, thenRemoveIt: false) { + let processedMessage = MessageProcessHelper.process(preservedMessage) + descriptionParts.append(NSAttributedString(string: processedMessage)) + } + guard descriptionParts.contains(where: { !$0.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty }) else { + return nil + } + + let result = NSMutableAttributedString(string: "✏️: ") + for (index, part) in descriptionParts.enumerated() { + if index > 0 { result.append(NSAttributedString(string: " ")) } + result.append(part) + } + + return result + } private func getRawReplyPresentation(isOutgoing: Bool, text: String) -> NSMutableAttributedString { let prefix = isOutgoing ? "\(String.adamant.chatList.sentMessagePrefix)" diff --git a/Adamant/Modules/ChatsList/ChatTableViewCell.swift b/Adamant/Modules/ChatsList/ChatTableViewCell.swift index fd1fd98bb..01bb92200 100644 --- a/Adamant/Modules/ChatsList/ChatTableViewCell.swift +++ b/Adamant/Modules/ChatsList/ChatTableViewCell.swift @@ -21,11 +21,17 @@ final class ChatTableViewCell: UITableViewCell { @IBOutlet weak var lastMessageLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var badgeView: UIView! + @IBOutlet weak var clockView: UIImageView! + @IBOutlet weak var lastMessageLeadingAnchor: NSLayoutConstraint! override func awakeFromNib() { - Task { @MainActor in badgeView.layer.cornerRadius = badgeView.bounds.height / 2 } + Task { @MainActor in + badgeView.layer.cornerRadius = badgeView.bounds.height / 2 + clockView.contentMode = .scaleAspectFit + clockView.image = UIImage.asset(named: "status_pending") + clockView.tintColor = .adamant.secondary + } } - var avatarImage: UIImage? { get { return avatarImageView.image @@ -71,4 +77,21 @@ final class ChatTableViewCell: UITableViewCell { badgeView.backgroundColor = newValue } } + var isClockVisible: Bool { + get { + return !clockView.isHidden + } + set { + if newValue { + clockView.isHidden = false + lastMessageLeadingAnchor.constant = 27 + } else { + UIView.animate(withDuration: 0.3) { + self.clockView.isHidden = true + self.lastMessageLeadingAnchor.constant = 10 + self.layoutIfNeeded() + } + } + } + } } diff --git a/Adamant/Modules/ChatsList/ChatTableViewCell.xib b/Adamant/Modules/ChatsList/ChatTableViewCell.xib index e37ae780b..cf2775dcf 100644 --- a/Adamant/Modules/ChatsList/ChatTableViewCell.xib +++ b/Adamant/Modules/ChatsList/ChatTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -31,7 +31,7 @@