import HUD import UIKit import Models import Shared import Combine import XXLogger import Foundation import Integration import Permissions import DifferenceKit import DependencyInjection struct ReplyModel { var text: String var sender: String } enum SingleChatNavigationRoutes: Equatable { case none case camera case library case waitingRound case cameraPermission case libraryPermission case microphonePermission case webview(String) } final class SingleChatViewModel { @Dependency private var logger: XXLogger @Dependency private var session: SessionType @Dependency private var permissions: PermissionHandling var contact: Contact { contactSubject.value } private var stagedReply: Reply? private var cancellables = Set<AnyCancellable>() private let contactSubject: CurrentValueSubject<Contact, Never> private let replySubject = PassthroughSubject<ReplyModel, Never>() private let navigationRoutes = PassthroughSubject<SingleChatNavigationRoutes, Never>() private let sectionsRelay = CurrentValueSubject<[ArraySection<ChatSection, ChatItem>], Never>([]) var hud: AnyPublisher<HUDStatus, Never> { hudRelay.eraseToAnyPublisher() } private let hudRelay = CurrentValueSubject<HUDStatus, Never>(.none) var isOnline: AnyPublisher<Bool, Never> { session.isOnline } var contactPublisher: AnyPublisher<Contact, Never> { contactSubject.eraseToAnyPublisher() } var replyPublisher: AnyPublisher<ReplyModel, Never> { replySubject.eraseToAnyPublisher() } var navigation: AnyPublisher<SingleChatNavigationRoutes, Never> { navigationRoutes.eraseToAnyPublisher() } var shouldDisplayEmptyView: AnyPublisher<Bool, Never> { sectionsRelay.map { $0.isEmpty }.eraseToAnyPublisher() } var messages: AnyPublisher<[ArraySection<ChatSection, ChatItem>], Never> { sectionsRelay.map { sections -> [ArraySection<ChatSection, ChatItem>] in var snapshot = [ArraySection<ChatSection, ChatItem>]() sections.forEach { snapshot.append(.init(model: $0.model, elements: $0.elements)) } return snapshot }.eraseToAnyPublisher() } private func updateRecentState(_ contact: Contact) { if contact.isRecent == true { var contact = contact contact.isRecent = false session.update(contact) } } func viewDidAppear() { updateRecentState(contact) } init(_ contact: Contact) { self.contactSubject = .init(contact) updateRecentState(contact) session.contacts(.withUserId(contact.userId)) .compactMap { $0.first } .sink { [unowned self] in contactSubject.send($0) } .store(in: &cancellables) session.singleMessages(contact) .map { $0.sorted(by: { $0.timestamp < $1.timestamp }) } .map { messages in let domainModels = messages.map { ChatItem($0) } let groupedByDate = Dictionary(grouping: domainModels) { domainModel -> Date in let components = Calendar.current.dateComponents([.day, .month, .year], from: domainModel.date) return Calendar.current.date(from: components)! } return groupedByDate .map { .init(model: ChatSection(date: $0.key), elements: $0.value) } .sorted(by: { $0.model.date < $1.model.date }) }.receive(on: DispatchQueue.main) .sink { [unowned self] in sectionsRelay.send($0) } .store(in: &cancellables) } // MARK: Public func didSendAudio(url: URL) { let name = url.deletingPathExtension().lastPathComponent guard let file = FileManager.retrieve(name: name, type: Attachment.Extension.audio.written) else { return } let attachment = Attachment(name: name, data: file, _extension: .audio) let payload = Payload(text: "You sent a voice message", reply: nil, attachment: attachment) session.send(payload, toContact: contact) } func didSend(image: UIImage) { guard let imageData = image.orientedUp().jpegData(compressionQuality: 1.0) else { return } hudRelay.send(.on(nil)) session.send(imageData: imageData, to: contact) { [weak self] in switch $0 { case .success: self?.hudRelay.send(.none) case .failure(let error): self?.hudRelay.send(.error(.init(with: error))) } } } func readAll() { session.readAll(from: contact) } func didRequestDeleteAll() { session.deleteAll(from: contact) } func didRequestRetry(_ model: ChatItem) { session.retryMessage(model.identity) } func didNavigateSomewhere() { navigationRoutes.send(.none) } @discardableResult func didTest(permission: PermissionType) -> Bool { switch permission { case .camera: if permissions.isCameraAllowed { navigationRoutes.send(.camera) } else { navigationRoutes.send(.cameraPermission) } case .library: if permissions.isPhotosAllowed { navigationRoutes.send(.library) } else { navigationRoutes.send(.libraryPermission) } case .microphone: if permissions.isMicrophoneAllowed { return true } else { navigationRoutes.send(.microphonePermission) } } return false } func didRequestCopy(_ model: ChatItem) { UIPasteboard.general.string = model.payload.text } func didRequestDeleteSingle(_ model: ChatItem) { didRequestDelete([model]) } func abortReply() { stagedReply = nil } func send(_ string: String) { let text = string.trimmingCharacters(in: .whitespacesAndNewlines) let payload = Payload(text: text, reply: stagedReply, attachment: nil) session.send(payload, toContact: contact) stagedReply = nil } func didRequestReply(_ model: ChatItem) { guard let messageId = model.uniqueId else { return } let isIncoming = model.status == .received || model.status == .read stagedReply = Reply(messageId: messageId, senderId: isIncoming ? contact.userId : session.myId) replySubject.send(.init(text: model.payload.text, sender: isIncoming ? contact.nickname ?? contact.username : "You")) } func getText(from messageId: Data) -> String { session.getTextFromMessage(messageId: messageId) ?? "[DELETED]" } func showRoundFrom(_ roundURL: String?) { if let urlString = roundURL, !urlString.isEmpty { navigationRoutes.send(.webview(urlString)) } else { navigationRoutes.send(.waitingRound) } } func didRequestDelete(_ items: [ChatItem]) { session.delete(messages: items.map { $0.identity }) } func itemWith(id: Int64) -> ChatItem? { sectionsRelay.value.flatMap(\.elements).first(where: { $0.identity == id }) } func getName(from senderId: Data) -> String { senderId == session.myId ? "You" : contact.nickname ?? contact.username } func itemAt(indexPath: IndexPath) -> ChatItem? { guard sectionsRelay.value.count > indexPath.section else { return nil } let items = sectionsRelay.value[indexPath.section].elements return items.count > indexPath.row ? items[indexPath.row] : nil } func section(at index: Int) -> ChatSection? { sectionsRelay.value.count > 0 ? sectionsRelay.value[index].model : nil } }