// // CryptoChatControllerChats.swift // PayCash // // Created by Saveliy Stavitsky on 8/17/20. // Copyright © 2020 List. All rights reserved. // import UIKit import IQKeyboardManagerSwift import EosioSwift import Branch import struct RealmSwift.Results class CryptoChatControllerChats: UIViewController { @IBOutlet private weak var tableView: UITableView! @IBOutlet weak var headerContainerView: UIView! @IBOutlet weak var addChatButton: UIButton! @IBOutlet weak var chatsListLabel: UILabel! var msgsHistoryService: CryptoChat.Service.MsgsHistory? private let refreshControl = UIRefreshControl() let header = CryptoChatViewChatsHeader(width: UIScreen.main.bounds.width, height: 120) var chats: Results? 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.onAccountChange(Notification(name: .didChangeAccount)) } }() var bottomSpace: CGFloat = 0 override func viewDidLoad() { super.viewDidLoad() navigationController?.interactivePopGestureRecognizer?.delegate = nil navigationItem.title = L10n.CryptoChat.Chats.title tableView.showsVerticalScrollIndicator = false tableView.separatorStyle = .none (tableView as UIScrollView).delegate = self tableView.register(cell: CryptoChatCellChat.self) refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged) tableView.addSubview(refreshControl) Notification.subscribe(name: .didUpdateHistory, { guard Accounts().current?.username == $0.userInfo?["username"] as? String else { return } self.msgsHistoryService?.fetchFromLocalHistory() }) self.addChatButton.addTarget(self, action: #selector(self.addButtonPressed(_:)), for: .touchUpInside) self.setupView() } func setup(username: String) { AccountViewAuthorize.showGetPrivateKey(in: self, { [weak self] privateKey in self?.msgsHistoryService = try? CryptoChat.Service .MsgsHistory(username: username, encryptionKey: privateKey) self?.msgsHistoryService?.didUpdate = { [weak self] in DispatchQueue.main.async { self?.refreshControl.endRefreshing() self?.tableView.reloadData() } } self?.chats = self?.msgsHistoryService?.chats self?.msgs = self?.msgsHistoryService?.filterMsgs(query: "") self?.header.searchTextField.text = "" self?.tableView.reloadData() Account.Service.History.shared.current?.fetch() }) } @objc func addButtonPressed(_ sender: Any) { self.showSelectChatAddMethodPopup() } @objc func onAccountChange(_ notification: Notification) { if let username = Accounts().current?.username { msgsHistoryService = nil chats = nil msgs = nil tableView.reloadData() setup(username: username) } Accounts().exists ? noAccountsView.removeFromSuperview() : view.addSubview(noAccountsView) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Accounts().exists ? noAccountsView.removeFromSuperview() : view.addSubview(noAccountsView) NotificationCenter.default.addObserver(self, selector: #selector(onAccountChange(_:)), name: .didChangeAccount, object: nil) navigationController?.setNavigationBarHidden(true, animated: animated) // IQKeyboardManager.shared.enable = false refresh(self) if ((UIApplication.shared.windows.first!.rootViewController as? MainController)?.barNav.items?.count ?? 0) > 1 { (UIApplication.shared.windows.first!.rootViewController as? MainController)?.barNav.popItem(animated: true) } } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) NotificationCenter.default.removeObserver(self, name: .didChangeAccount, object: nil) msgsHistoryService = nil chats = nil msgs = nil tableView.reloadData() navigationController?.setNavigationBarHidden(false, animated: animated) // IQKeyboardManager.shared.enable = true } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if bottomSpace == 0 { bottomSpace += (self.tabBarController?.tabBar.frameHeight ?? 0) /*tab bar*/ + 40 /*button height*/ + 24 /*button top space*/ } noAccountsView.frame = tableView.frame emptyView.frame = tableView.frame encodedView.frame = tableView.frame } 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 func refresh(_ sender: AnyObject) { guard let username = Accounts().current?.username else { return } if msgsHistoryService == nil { setup(username: username) } else { Account.Service.History.shared.current?.fetch() } } func openChat(username: String, unreadCount: Int) { guard username.count > 0, msgsHistoryService != nil else { return } let chatViewController = CryptoChatControllerChat() chatViewController.username = username chatViewController.msgsHistoryService = msgsHistoryService chatViewController.unreadCount = unreadCount mainController.content.push(chatViewController, animated: true) } var query = "" } extension CryptoChatControllerChats: UITableViewDelegate, UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { 1 + ((msgs?.count ?? 0) > 0 ? 1 : 0) } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if Accounts().current != nil { if msgsHistoryService == nil { emptyView.removeFromSuperview() Loader.hide(in: tableView) view.addSubview(encodedView) } else { if !UserDefaults.standard.bool(forKey: "\(Accounts().current!.username)_firstMsgsSyncDone") { Loader.show(in: tableView) } else { Loader.hide(in: tableView) (chats?.count ?? 0) == 0 && query.isEmpty ? view.addSubview(emptyView) : emptyView.removeFromSuperview() } encodedView.removeFromSuperview() } [ self.addChatButton, self.header.searchTextField ] .forEach { $0?.isHidden = (msgsHistoryService == nil) || !UserDefaults.standard.bool(forKey: "\(Accounts().current!.username)_firstMsgsSyncDone") || ((chats?.count ?? 0) == 0 && query.isEmpty) } } else { Loader.hide(in: tableView) emptyView.removeFromSuperview() encodedView.removeFromSuperview() } return section == 0 ? (chats?.count ?? 0) : (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 = 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 = 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 { header.parent = self header.searchTextField.delegate = self return 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 = chats?[indexPath.row] else { return } openChat(username: chat.chatName, unreadCount: chat.unreadCount) } else { guard let msg = msgs?[indexPath.row] else { return } openChat(username: msg.chatName, unreadCount: 0) } } func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { if (msgs?.count ?? 0) > 0 { return section == 1 ? UIView(height: 56, color: .white) : UIView(height: 0) } else { return UIView(height: 56, color: .white) } } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { section == 1 ? 56 : 0 } func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { if !query.isEmpty { return nil } guard let chat = chats?[indexPath.row] else { return nil } let delete = UITableViewRowAction(style: .destructive, title: L10n.Common.Button.delete) { [weak self] (action, indexPath) in self?.msgsHistoryService?.hideChatMsgs(chatName: chat.chatName) } return [delete] } } extension CryptoChatControllerChats: CommonTextFieldSearchDelegate { func textField(_ textField: CommonTextFieldSearch, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let text = textField.text { let updatedText = (text as NSString).replacingCharacters(in: range, with: string) if updatedText.isEmpty { chats = msgsHistoryService?.chats msgs = msgsHistoryService?.filterMsgs(query: "") query = "" } else { chats = msgsHistoryService?.filterChats(query: updatedText) msgs = msgsHistoryService?.filterMsgs(query: updatedText) query = updatedText } } else { chats = msgsHistoryService?.chats msgs = msgsHistoryService?.filterMsgs(query: "") query = "" } tableView.reloadData() return true } func textFieldShouldClear(_ textField: CommonTextFieldSearch) -> Bool { chats = msgsHistoryService?.chats msgs = msgsHistoryService?.filterMsgs(query: "") query = "" tableView.reloadData() return true } } // 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) { EosioRpcProvider(endpoint: URL(string: Network.servers.current.node)!).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() view.parent = self Popup.show(content: CommonViewControllerViewPopUp( title: L10n.CryptoChat.Chats.craateDescription, image: Asset.chatCreate.image, view: view )) } 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) } }