// // CryptoChatControllerChats.swift // Wallet // // Created by Saveliy Stavitsky on 8/17/20. // Copyright © 2020 List. All rights reserved. // import UIKit import IQKeyboardManagerSwift import EosioSwift import Branch import Combine import WalletKit import WalletUIComponents import struct RealmSwift.Results final class CryptoChatControllerChats: UIViewController { @IBOutlet private weak var tableView: UITableView! @IBOutlet private weak var headerContainerView: UIView! @IBOutlet private weak var addChatButton: UIButton! @IBOutlet private weak var chatsListLabel: UILabel! private var bottomSpace: CGFloat = 0 private var msgsHistoryService: CryptoChat.Service.MsgsHistory? private weak var createChatView: CryptoChatViewCreateChat? private var query = "" private let refreshControl = UIRefreshControl() private var header: CryptoChatViewChatsHeader { CryptoChatViewChatsHeader(width: UIScreen.main.bounds.width, viewModel: self.viewModel) } private let viewModel = TextFieldViewModel(placeholder: L10n.CryptoChat.Chats.search) private var cancellables = Set() private var chats: Results? private var msgs: Results? private lazy var noAccountsView: CommonViewEmpty = { CommonViewEmpty( title: L10n.CryptoChat.Chats.NoAccounts.title, text: L10n.Account.Empty.text, image: Asset.accountEmpty.image, backgroundColor: Asset.snow.color, submit: L10n.Account.Empty.submit ) { [weak self] in guard let self = self else { return } AccountController.showPopup(in: self) } }() private lazy var emptyView: CommonViewEmpty = { CommonViewEmpty( title: L10n.CryptoChat.Chats.Empty.title, text: L10n.CryptoChat.Chats.Empty.text, image: Asset.chatsEmpty.image, submit: L10n.CryptoChat.Chats.Empty.submit, submitRealtion: .top ) { [weak self] in guard let self = self else { return } self.showSelectChatAddMethodPopup() } }() private lazy var encodedView: CommonViewEmpty = { CommonViewEmpty( title: L10n.CryptoChat.Chats.Encoded.title, text: L10n.CryptoChat.Chats.encoded, image: Asset.chatsLocked.image, submit: L10n.CryptoChat.Chats.encodedButton, submitRealtion: .top ) { [weak self] in guard let self = self else { return } self.onChange(wallet: Accounts().current) } }() override func viewDidLoad() { super.viewDidLoad() self.navigationController?.interactivePopGestureRecognizer?.delegate = nil self.navigationItem.title = L10n.CryptoChat.Chats.title self.tableView.showsVerticalScrollIndicator = false self.tableView.separatorStyle = .none (self.tableView as UIScrollView).delegate = self self.tableView.register(cell: CryptoChatCellChat.self) self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged) self.tableView.refreshControl = self.refreshControl Notification.subscribe(name: .didUpdateHistory, { guard Accounts().current?.name == $0.userInfo?["username"] as? String else { return } self.msgsHistoryService?.fetchFromLocalHistory() }) self.addChatButton.addTarget(self, action: #selector(self.addButtonPressed(_:)), for: .touchUpInside) self.setupView() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Accounts().current.isExist ? self.noAccountsView.removeFromSuperview() : self.view.addSubview(noAccountsView) Accounts().bank.activePublisher .receive(on: DispatchQueue.main) .sink { self.onChange(wallet: $0) } .store(in: &self.cancellables) self.viewModel.objectWillChange .receive(on: DispatchQueue.main) .sink { [weak self] in guard let self else { return } self.textFieldDidChange(self.viewModel.text) if self.viewModel.shouldClear { self.textFieldShouldClear() } } .store(in: &self.cancellables) self.navigationController?.setNavigationBarHidden(true, animated: animated) // IQKeyboardManager.shared.enable = false self.refresh(self) guard let window = UIApplication.shared.windows.first, let controller = window.rootViewController as? MainController else { return } if (controller.barNav.items?.count ?? 0) > 1 { controller.barNav.popItem(animated: true) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.cancellables.removeAll() self.msgsHistoryService = nil self.chats = nil self.msgs = nil self.tableView.reloadData() self.navigationController?.setNavigationBarHidden(false, animated: animated) // IQKeyboardManager.shared.enable = true } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if self.bottomSpace == 0 { self.bottomSpace += (self.tabBarController?.tabBar.frameHeight ?? 0) /*tab bar*/ + 40 /*button height*/ + 24 /*button top space*/ } self.noAccountsView.frame = self.tableView.frame self.emptyView.frame = self.tableView.frame self.encodedView.frame = self.tableView.frame } // MARK: - Private private func setup(username: String) { AccountViewAuthorize.showGetPrivateKey(in: self) { [weak self] privateKey in guard let self else { return } self.msgsHistoryService = try? CryptoChat.Service.MsgsHistory(username: username, encryptionKey: privateKey) self.msgsHistoryService?.updatePublisher .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshControl.endRefreshing() self?.tableView.reloadData() } .store(in: &self.cancellables) self.chats = self.msgsHistoryService?.chats self.msgs = self.msgsHistoryService?.filterMsgs(query: "") self.header.searchTextField.viewModel.text = "" self.tableView.reloadData() Accounts().messageShelf.activeBook?.fetch() } } private func onChange(wallet: WalletKit.Wallet?) { guard let wallet = Accounts().current else { self.view.addSubview(self.noAccountsView) return } self.msgsHistoryService = nil self.chats = nil self.msgs = nil self.tableView.reloadData() self.setup(username: wallet.name) self.noAccountsView.removeFromSuperview() } private func openChat(username: String, unreadCount: Int) { guard let service = self.msgsHistoryService else { return } let chatViewController = CryptoChatControllerChat(username: username, unreadCount: unreadCount, service: service) mainController.content.push(chatViewController, animated: true) } private func textFieldShouldClear() { self.chats = self.msgsHistoryService?.chats self.msgs = self.msgsHistoryService?.filterMsgs(query: "") self.query = "" self.tableView.reloadData() } private func textFieldDidChange(_ text: String?) { if let text { self.chats = text.isEmpty ? self.msgsHistoryService?.chats : self.msgsHistoryService?.filterChats(query: text) self.msgs = text.isEmpty ? self.msgsHistoryService?.filterMsgs(query: "") : self.msgsHistoryService?.filterMsgs(query: text) self.query = text.isEmpty ? "" : text } else { self.chats = self.msgsHistoryService?.chats self.msgs = self.msgsHistoryService?.filterMsgs(query: "") self.query = "" } self.tableView.reloadData() } private func setupView() { self.addChatButton.layer.borderColor = Asset.deepWater.color.withAlphaComponent(0.2).cgColor self.addChatButton.layer.borderWidth = 1.0 self.addChatButton.layer.cornerRadius = 8.0 } @objc private func addButtonPressed(_ sender: Any) { self.showSelectChatAddMethodPopup() } @objc private func refresh(_ sender: AnyObject) { // TODO: look for better solution for refresh DispatchQueue.main.async { [weak self] in self?.refreshControl.beginRefreshing() } guard let username = Accounts().current?.name else { return } if self.msgsHistoryService == nil { self.setup(username: username) } else { Accounts().messageShelf.activeBook?.fetch() } DispatchQueue.main.async { [weak self] in self?.refreshControl.endRefreshing() } } } // MARK: - UITableViewDelegate, UITableViewDataSource extension CryptoChatControllerChats: UITableViewDelegate, UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { 1 + ((self.msgs?.count ?? 0) > 0 ? 1 : 0) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let wallet = Accounts().current { if self.msgsHistoryService == nil { self.emptyView.removeFromSuperview() Loader.hide(in: tableView) self.view.addSubview(self.encodedView) } else { if !UserDefaults.standard.bool(forKey: "\(wallet.name)_firstMsgsSyncDone") { Loader.show(in: tableView) } else { Loader.hide(in: tableView) (self.chats?.count ?? 0) == 0 && self.query.isEmpty ? self.view.addSubview(self.emptyView) : self.emptyView.removeFromSuperview() } self.encodedView.removeFromSuperview() } [ self.addChatButton, self.header ] .forEach { $0?.isHidden = (self.msgsHistoryService == nil) || !UserDefaults.standard.bool(forKey: "\(wallet.name)_firstMsgsSyncDone") || ((self.chats?.count ?? 0) == 0 && self.query.isEmpty) } } else { Loader.hide(in: tableView) self.emptyView.removeFromSuperview() self.encodedView.removeFromSuperview() } return section == 0 ? (self.chats?.count ?? 0) : (self.msgs?.count ?? 0) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(CryptoChatCellChat.self, indexPath: indexPath) if indexPath.section == 0 { guard let chat = self.chats?[indexPath.row] else { return cell } cell.titleLabel.text = chat.chatName cell.titleShortLabel.text = String(chat.chatName.prefix(3)) cell.descriptionLabel.text = chat.textDescription cell.timeLabel.text = chat.time cell.setUnread(count: chat.unreadCount) } else { guard let msg = self.msgs?[indexPath.row] else { return cell } cell.titleLabel.text = msg.chatName cell.titleShortLabel.text = String(msg.chatName.prefix(3)) cell.descriptionLabel.text = msg.textDescription cell.timeLabel.text = msg.time cell.setUnread(count: nil) } return cell } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { if section == 0 { return self.header } else { let button = CommonButtonAction(width: UIScreen.main.bounds.width, height: 40) button.style = .secondary button.backgroundColor = Asset.deepWater.color.withAlphaComponent(0.1) button.isUserInteractionEnabled = false button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24) button.cornerRadius = 0 button.setTitle(L10n.CryptoChat.Chats.Search.msgsSection, for: .normal) button.setTitleColor(Asset.textDeepWater.color, for: .normal) button.titleLabel?.font = Font.font(style: .bold, size: 12) button.contentHorizontalAlignment = .left return UIStackView(subviews: [UIView(height: 16, color: .white), button], axis: .vertical, distribution: .fill, alignment: .fill, spacing: 0) } } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { section == 0 ? 65 : 56 } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) if indexPath.section == 0 { guard let chat = self.chats?[indexPath.row] else { return } self.openChat(username: chat.chatName, unreadCount: chat.unreadCount) } else { guard let msg = self.msgs?[indexPath.row] else { return } self.openChat(username: msg.chatName, unreadCount: 0) } } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { section == 1 ? 56 : 0 } func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { if !self.query.isEmpty { return nil } guard let chats = self.chats else { return nil } let chat = chats[indexPath.row] let contextItem = UIContextualAction(style: .destructive, title: L10n.Common.Button.delete) { [weak self] (_, _, _) in self?.msgsHistoryService?.hideChatMsgs(chatName: chat.chatName) } let actions = UISwipeActionsConfiguration(actions: [contextItem]) return actions } } // MARK: - Opening QR to select New Chat Add Method extension CryptoChatControllerChats: UIImagePickerControllerDelegate, UINavigationControllerDelegate { func showSelectChatAddMethodPopup() { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let openScannerAction: ((UIAlertAction) -> Void) = { [weak self] _ in self?.openScannerQrViewController { [weak self] in self?.dismiss(animated: true) let username = self?.injectUsername(string: $0) ?? .init() guard !username.isEmpty else { return } switch username { case let username where URL(string: username)?.absoluteString.contains("app.link") ?? false: Branch.getInstance().application(UIApplication.shared, open: URL(string: username), options: nil) default: self?.validateEosAccount(username: username) } } } let cryptoChatCreateAction: ((UIAlertAction) -> Void) = { [weak self] _ in self?.openCryptoChatCreatePopup() } let imagePickerAction: ((UIAlertAction) -> Void) = { [weak self] _ in self?.openImagePickerViewController() } alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.findAccount, style: .default, handler: cryptoChatCreateAction)) if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) { alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.qrLibrary, style: .default, handler: imagePickerAction)) } if UIImagePickerController.isSourceTypeAvailable(.camera) { alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.qrCamera, style: .default, handler: openScannerAction)) } alert.addAction(.init(title: L10n.Common.Button.cancel, style: .cancel, handler: nil)) self.present(alert, animated: true) } private func injectUsername(string: String) -> String? { guard let stringData = string.data(using: .utf8) else { return string } let addressDecoder = try? JSONDecoder().decode(WalletTansferQr.self, from: stringData) return addressDecoder?.address ?? string } private func validateEosAccount(username: String) { let environment = ApplicationEnvironment.shared().current guard let node = environment.node, let url = URL(string: node) else { Alert.error(text: L10n.CryptoChat.Chats.createTextFieldError) return } EosioRpcProvider(endpoint: url, headers: environment.headers).getAccount(requestParameters: EosioRpcAccountRequest(accountName: username), completion: { [weak self] in switch $0 { case .success: self?.openChat(username: username, unreadCount: 0) case .failure: Alert.error(text: L10n.CryptoChat.Chats.createTextFieldError) } }) } // MARK: - UIImagePickerControllerDelegate & UINavigationControllerDelegate delegate methods func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { self.dismiss(animated: true, completion: nil) if let selectedImage = info[.originalImage] as? UIImage, let detector = CIDetector(ofType: CIDetectorTypeQRCode, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]), let ciImage = CIImage(image: selectedImage) { let qrCodeString = (detector.features(in: ciImage) as? [CIQRCodeFeature])? .compactMap(\.messageString) .joined() ?? String.init() guard let username = self.injectUsername(string: qrCodeString) else { return } guard !username.isEmpty else { Alert.error(text: L10n.CryptoChat.Chats.noQr, delay: 1) return } switch username { case let username where URL(string: username)?.absoluteString.contains("app.link") ?? false: Branch.getInstance().application(UIApplication.shared, open: URL(string: username), options: nil) default: self.validateEosAccount(username: username) } } } } // MARK: - Routing extension CryptoChatControllerChats { fileprivate func openImagePickerViewController() { let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary self.present(imagePicker, animated: true, completion: nil) } fileprivate func openCryptoChatCreatePopup() { let view = CryptoChatViewCreateChat() Popup.show( content: CommonViewControllerViewPopUp( title: L10n.CryptoChat.Chats.craateDescription, image: Asset.chatCreate.image, view: view ), in: self ) view.succeedPublisher .receive(on: DispatchQueue.main) .sink { [weak self] username in guard let self else { return } Popup.hide(in: self) { [weak self] in self?.openChat(username: username, unreadCount: 0) } } .store(in: &self.cancellables) } fileprivate func openScannerQrViewController(onAction: @escaping (String) -> Void) { let scanner = ScannerViewController() scanner.navigationItem.leftBarButtonItem = .close { [weak self] in self?.dismiss(animated: true) } scanner.didCapture = onAction let navCtrl = UINavigationController(rootViewController: scanner) navCtrl.modalPresentationStyle = .fullScreen self.navigationController?.tabBarController?.present(navCtrl, animated: true) } }