// // CryptoChatControllerChat.swift // Wallet // // Created by Saveliy Stavitsky on 8/19/20. // Copyright © 2020 List. All rights reserved. // import UIKit import MessageKit import InputBarAccessoryView import EosioSwift import IQKeyboardManagerSwift import Combine import struct RealmSwift.Results final class CryptoChatControllerChat: MessagesViewController, InputBarAccessoryViewDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate { typealias HistoryService = CryptoChat.Service.MsgsHistory let unreadCount: Int let historyService: HistoryService let infoView = CommonViewWarning() var loadedMsgsCount = 0 lazy var messages: Results = { self.historyService.openChatWith(self.username) }() private enum Constants { static let bottomInset: CGFloat = 38 } private let username: String private var publicKey: String? private var lastMsgs: [String: String] = [:] private let refreshControl = UIRefreshControl() private var cancellables = Set() private var topbarHeight: CGFloat { let statusBarFrame = UIApplication.shared.statusBarFrame let navigationbarFrame = self.navigationController?.navigationBar.frame ?? .zero return statusBarFrame.size.height + navigationbarFrame.size.height } private lazy var createButton: UIButton = { let button = UIButton(width: UIScreen.main.bounds.width, height: 32) button.frameOrigin = CGPoint(x: 0, y: 44) button.titleLabel?.font = FontFamily.GolosUI.medium.font(size: 12) button.setImage(Asset.walletInheritanceRefresh.image.withRenderingMode(.alwaysTemplate).tinted(with: .white), for: .normal) button.setTitle(L10n.CryptoChat.Chat.refresh, for: .normal) button.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: -16) button.backgroundColor = Asset.dark.color button.addTarget(self, action: #selector(self.refresh(_:)), for: .touchUpInside) button.contentHorizontalAlignment = .center button.setTitleColor(.white, for: .normal) return button }() private lazy var buttonsView: CryptoChatButtonsArrayView = { let buttonsView = CryptoChatButtonsArrayView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 56)) buttonsView.qrPressed = { [weak self] in self?.showQrPopup() } buttonsView.transferPressed = { [weak self] in var dictionary: [String: Any] = [:] dictionary["address"] = self?.username ?? "" let viewController = StoryboardScene.Wallet.transfer.instantiate() viewController.hidesBack = true viewController.dictionary = dictionary viewController.completion = { [weak self] in self?.becomeFirstResponder() } self?.navigationController?.pushViewController(viewController, animated: true) } buttonsView.receivePressed = { [weak self] in let viewController = StoryboardScene.Wallet.receive.instantiate() viewController.hidesBack = true self?.navigationController?.pushViewController(viewController, animated: true) } return buttonsView }() init(username: String, unreadCount: Int, service: HistoryService) { self.username = username self.unreadCount = unreadCount self.historyService = service super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.messagesCollectionView.register(InfoHeaderCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) self.view.addSubview(self.buttonsView) Network.accounts.getChatPublicKey(username, completion: { [weak self] in self?.publicKey = $0.value }) let navigationItem = UINavigationItem() navigationItem.title = username navigationItem.leftBarButtonItem = .pop(self) navigationItem.rightBarButtonItem = UIBarButtonItem(customView: (UIApplication.shared.windows.first!.rootViewController as? MainController)!.resourcesSwitch) (UIApplication.shared.windows.first!.rootViewController as? MainController)? .barNav.pushItem(navigationItem, animated: true) self.infoView.text = L10n.CryptoChat.Chat.info self.view.addSubview(self.infoView) self.infoView.translatesAutoresizingMaskIntoConstraints = false self.infoView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true self.infoView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true self.historyService.updatePublisher .sink { [weak self] historyService in guard let self = self else { return } DispatchQueue.main.async { self.loadedMsgsCount = min(self.messages.count, self.loadedMsgsCount + 100) self.messagesCollectionView.reloadData() self.messagesCollectionView.scrollFullDown() historyService.markChatAsRead(username: self.username) } } .store(in: &self.cancellables) self.historyService.markChatAsRead(username: self.username) navigationItem.title = self.username navigationItem.leftBarButtonItem = .pop(self) self.messagesCollectionView.messagesDataSource = self self.messagesCollectionView.messagesLayoutDelegate = self self.messagesCollectionView.messagesDisplayDelegate = self self.messagesCollectionView.keyboardDismissMode = .onDrag self.additionalBottomInset = Constants.bottomInset self.navigationController?.navigationBar.barTintColor = .white self.messagesCollectionView.backgroundColor = .white self.configureMessageCollectionView() self.configureMessageInputBar() self.messagesCollectionView.scrollFullDown() self.messagesCollectionView.addSubview(refreshControl) self.refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) NotificationCenter.default.publisher(for: .didUpdateHistory) .sink { [weak self] in guard let self, Accounts().current?.name == $0.userInfo?["username"] as? String else { return } self.historyService.fetchFromLocalHistory() } .store(in: &self.cancellables) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) Accounts().isLocked = true IQKeyboardManager.shared.enable = false self.becomeFirstResponder() self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false self.navigationController?.setNavigationBarHidden(true, animated: false) self.messageInputBar.inputTextView.text = self.lastMsgs[self.username] ?? "" } override func viewWillDisappear(_ animated: Bool) { Accounts().isLocked = false IQKeyboardManager.shared.enable = true self.navigationController?.setNavigationBarHidden(false, animated: false) self.lastMsgs[self.username] = self.messageInputBar.inputTextView.text // TODO: - bad hack! Search good solution! DispatchQueue.main.async { [weak self] in self?.refreshControl.beginRefreshing() self?.refreshControl.endRefreshing() } super.viewWillDisappear(animated) } override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { guard action == #selector(self.delete(_:)) else { return super.collectionView(collectionView, canPerformAction: action, forItemAt: indexPath, withSender: sender) } return true } override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { guard action == #selector(self.delete(_:)) else { super.collectionView(collectionView, performAction: action, forItemAt: indexPath, withSender: sender) return } // 1.) Remove from datasource // insert your code here let msgIndex = self.messages.count - self.loadedMsgsCount + indexPath.section guard msgIndex >= 0 && msgIndex < self.messages.count else { return } let msg = self.messages[msgIndex] self.historyService.hideMsg(ephemPublicKey: msg.id) self.loadedMsgsCount -= 1 // 2.) Delete sections DispatchQueue.main.async { [weak self] in self?.messagesCollectionView.reloadData() } } private func configureMessageCollectionView() { self.messagesCollectionView.messagesDataSource = self self.messagesCollectionView.messageCellDelegate = self self.scrollsToBottomOnKeyboardBeginsEditing = true // default false self.maintainPositionOnKeyboardFrameChanged = true // default false if let layout = self.messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout { layout.setMessageIncomingAccessoryViewSize(CGSize(width: 24, height: 24)) layout.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 20, right: 0)) layout.setMessageIncomingAvatarSize(.zero) layout.setMessageIncomingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 0))) layout.setMessageIncomingCellTopLabelAlignment(LabelAlignment(textAlignment: .center, textInsets: .zero)) layout.setMessageIncomingCellBottomLabelAlignment(LabelAlignment(textAlignment: .center, textInsets: .zero)) layout.setMessageOutgoingAccessoryViewSize(CGSize(width: 24, height: 24)) layout.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 20)) layout.setMessageOutgoingAvatarSize(.zero) layout.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) layout.setMessageOutgoingCellTopLabelAlignment(LabelAlignment(textAlignment: .center, textInsets: .zero)) layout.setMessageOutgoingCellBottomLabelAlignment(LabelAlignment(textAlignment: .center, textInsets: .zero)) } } private func configureMessageInputBar() { self.messageInputBar.delegate = self self.messageInputBar.sendButton.image = Asset.chatSend.image self.messageInputBar.sendButton.title = nil self.messageInputBar.inputTextView.placeholderTextColor = Asset.textPebble.color self.messageInputBar.inputTextView.textColor = Asset.textGranite.color self.messageInputBar.inputTextView.font = Font.font(style: .regular, size: 16) self.messageInputBar.separatorLine.backgroundColor = Asset.marble.color self.messageInputBar.textViewPadding = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) self.messageInputBar.maxTextViewHeight = 140 self.messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.emptyInputPlaceholder self.messageInputBar.backgroundView.backgroundColor = .white self.messageInputBar.inputTextView.isImagePasteEnabled = false } private func showQrPopup() { self.dismissKeyboard() guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { let ctrl = ScannerViewController() ctrl.navigationItem.leftBarButtonItem = .close { [weak self] in DispatchQueue.main.async { self?.dismiss(animated: true) } } ctrl.didCapture = { [weak self] string in DispatchQueue.main.async { self?.dismiss(animated: true) } self?.sendMsg(msgText: "\(string)") } let navCtrl = UINavigationController(rootViewController: ctrl) navCtrl.modalPresentationStyle = .fullScreen self.navigationController?.tabBarController?.present(navCtrl, animated: true) return } let ctrl = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) ctrl.addAction(.init(title: L10n.Wallet.Send.qrCamera, style: .default) { [weak self] _ in let ctrl = ScannerViewController() ctrl.navigationItem.leftBarButtonItem = .close { [weak self] in DispatchQueue.main.async { self?.dismiss(animated: true) } } ctrl.didCapture = { [weak self] string in DispatchQueue.main.async { self?.dismiss(animated: true) } self?.sendMsg(msgText: "\(string)") } let navCtrl = UINavigationController(rootViewController: ctrl) navCtrl.modalPresentationStyle = .fullScreen self?.navigationController?.tabBarController?.present(navCtrl, animated: true) }) ctrl.addAction(.init(title: L10n.Wallet.Send.qrLibrary, style: .default) { [weak self] _ in let imagePicker = UIImagePickerController() imagePicker.delegate = self imagePicker.sourceType = .photoLibrary self?.present(imagePicker, animated: true, completion: nil) }) ctrl.addAction(.init(title: L10n.Common.Button.cancel, style: .cancel, handler: nil)) self.present(ctrl, animated: true) } @objc private func dismissKeyboard() { self.messageInputBar.inputTextView.resignFirstResponder() } @objc private func refresh(_ sender: AnyObject) { Accounts().messageShelf.activeBook?.fetch() } @objc private func loadMoreMessages() { self.loadedMsgsCount = min(self.messages.count, self.loadedMsgsCount + 100) self.messagesCollectionView.reloadDataAndKeepOffset() self.refreshControl.endRefreshing() } // MARK: - InputBarAccessoryViewDelegate func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { let components = inputBar.inputTextView.components self.messageInputBar.inputTextView.text = String() self.messageInputBar.invalidatePlugins() var msgText = "" for component in components { if let text = component as? String { msgText += "\(text)</plaintext>" } else if let img = (component as? UIImage)?.pngData() { msgText += "<image>\(img.base64EncodedString())</image>" } } self.sendMsg(msgText: msgText) } func sendMsgTo(publicKey: String, msg msgValue: CryptoChat.Model.Msg) { do { var msg = msgValue try msg.encrypt(receiverPublicKey: publicKey) AccountViewAuthorize.showGetPrivateKey(in: self) { Network.Service.Blockchain.execute(contract: .chat, action: .chatSendDm, data: msg, privateKeys: [$0]) { switch $0 { case .success: self.historyService.setMsgSynced(ephemPublicKey: msg.ephemPublicKey) case let .failure(error): self.dismissKeyboard() self.historyService.markMsgUnsent(ephemPublicKey: msg.ephemPublicKey) Alert.error(error) } } } } catch { self.dismissKeyboard() Alert.error(text: "Error while encrypt msg: \(error.localizedDescription)") self.historyService.markMsgUnsent(ephemPublicKey: msgValue.ephemPublicKey) } } func sendMsg(msgText: String) { guard !self.username.isEmpty else { return } guard let user = Accounts().current else { return } // Send button activity animation self.messageInputBar.sendButton.startAnimating() self.messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.sendingInputPlaceholder let msg = CryptoChat.Model.Msg(from: user.name, to: username, msg: msgText) self.historyService.saveMsg(CryptoChatModelRealmMessage(id: msg.ephemPublicKey, from: msg.from, to: msg.to, chatName: username, timestamp: Date(), data: msgText), ignoreTimestamp: true) self.loadedMsgsCount += 1 if let receiverPublicKey = self.publicKey, !receiverPublicKey.isEmpty { self.messageInputBar.sendButton.stopAnimating() self.messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.emptyInputPlaceholder self.messagesCollectionView.scrollToLastItem(animated: true) self.sendMsgTo(publicKey: receiverPublicKey, msg: msg) } else { Network.accounts.getChatPublicKey(username, completion: { [weak self] in self?.publicKey = $0.value self?.messageInputBar.sendButton.stopAnimating() self?.messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.emptyInputPlaceholder self?.messagesCollectionView.scrollToLastItem(animated: true) if let receiverPublicKey = $0.value, !receiverPublicKey.isEmpty { self?.sendMsgTo(publicKey: receiverPublicKey, msg: msg) } else { self?.historyService.markMsgUnsent(ephemPublicKey: msg.ephemPublicKey) } }) } } private func insertMessages(_ data: [Any]) {} // MARK: - UIImagePickerControllerDelegate, UINavigationControllerDelegate 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) { var qrCodeLink = "" for feature in (detector.features(in: ciImage) as? [CIQRCodeFeature]) ?? [] { qrCodeLink += feature.messageString ?? "" } print(qrCodeLink) // Your result from QR Code if !qrCodeLink.isEmpty { self.sendMsg(msgText: "<qr>\(qrCodeLink)</qr>") } else { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { Alert.error(text: L10n.CryptoChat.Chat.noQr) } } } } } // MARK: - MessageCollectionViewCell extension MessageCollectionViewCell { override open func delete(_ sender: Any?) { if let collectionView = self.superview as? UICollectionView, let indexPath = collectionView.indexPath(for: self) { collectionView.delegate?.collectionView?(collectionView, performAction: #selector(self.delete(_:)), forItemAt: indexPath, withSender: sender) } } }