// // CryptoChatControllerChat.swift // PayCash // // 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 struct RealmSwift.Results extension MessageCollectionViewCell { override open func delete(_ sender: Any?) { // Get the collectionView if let collectionView = self.superview as? UICollectionView { // Get indexPath if let indexPath = collectionView.indexPath(for: self) { // Trigger action collectionView.delegate?.collectionView?(collectionView, performAction: NSSelectorFromString("delete:"), forItemAt: indexPath, withSender: sender) } } } } var lastMsgs: [String: String] = [:] class CryptoChatControllerChat: MessagesViewController { var username: String! var unreadCount = 0 var msgsHistoryService: CryptoChat.Service.MsgsHistory! var loadedMsgsCount = 0 let refreshControl = UIRefreshControl() lazy var messages: Results = { msgsHistoryService.openChatWith(username) }() var bottomSpace: CGFloat = 0 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 }() var topbarHeight: CGFloat { UIApplication.shared.statusBarFrame.size.height + (self.navigationController?.navigationBar.frame.size.height ?? 0.0) } private lazy var buttonsView: CryptoChatButtonsArrayView = { let buttonsView = CryptoChatButtonsArrayView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 56)) // buttonsView.frameOrigin = CGPoint(x: 0, y: 0) buttonsView.qrPressed = { [weak self] in self?.showQrPopup() } buttonsView.transferPressed = { [weak self] in let viewController = StoryboardScene.Wallet.transfer.instantiate() viewController.hidesBack = true var dictionary: [String: Any] = [:] dictionary["address"] = self?.username ?? "" viewController.dictionary = dictionary 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 }() let infoView = CommonViewWarning() override func viewDidLoad() { super.viewDidLoad() messagesCollectionView.register(InfoHeaderCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader) // view.addSubview(createButton) view.addSubview(buttonsView) 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) infoView.text = L10n.CryptoChat.Chat.info view.addSubview(infoView) infoView.translatesAutoresizingMaskIntoConstraints = false infoView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true infoView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true msgsHistoryService?.didUpdate = { [weak self] in guard let self = self else { return } DispatchQueue.main.async { self.loadedMsgsCount = min(self.messages.count, self.loadedMsgsCount + 100) self.messagesCollectionView.reloadData() self.messagesCollectionView.scrollToBottom() self.msgsHistoryService?.markChatAsRead(username: self.username) } } msgsHistoryService.markChatAsRead(username: username) navigationItem.title = username navigationItem.leftBarButtonItem = .pop(self) // navigationItem.rightBarButtonItem = .info { // Alert.hint(text: L10n.CryptoChat.Chat.infoHelpText) // } messagesCollectionView.messagesDataSource = self messagesCollectionView.messagesLayoutDelegate = self messagesCollectionView.messagesDisplayDelegate = self // let tap = UITapGestureRecognizer(target: self, action: #selector(CryptoChatControllerChat.dismissKeyboard)) // messagesCollectionView.addGestureRecognizer(tap) messagesCollectionView.keyboardDismissMode = .onDrag navigationController?.navigationBar.barTintColor = .white self.messagesCollectionView.backgroundColor = .white configureMessageCollectionView() configureMessageInputBar() messagesCollectionView.scrollToBottom() messagesCollectionView.addSubview(refreshControl) refreshControl.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged) observers.append(contentsOf: [ Notification.subscribe(name: .didUpdateHistory, { [weak self] in guard Accounts().current?.username == $0.userInfo?["username"] as? String else { return } guard let self = self else { return } self.msgsHistoryService?.fetchFromLocalHistory() }) ]) } var observers: [AnyObject] = [] deinit { observers.forEach({ Notification.unsubscribe(observer: $0) }) } @objc func dismissKeyboard() { self.messageInputBar.inputTextView.resignFirstResponder() } @objc func refresh(_ sender: AnyObject) { Account.Service.History.shared.current?.fetch() } 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) messageInputBar.inputTextView.text = lastMsgs[username] ?? "" } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.becomeFirstResponder() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) Accounts().isLocked = false IQKeyboardManager.shared.enable = true self.navigationController?.setNavigationBarHidden(false, animated: false) lastMsgs[username] = messageInputBar.inputTextView.text } @objc func loadMoreMessages() { loadedMsgsCount = min(messages.count, loadedMsgsCount + 100) self.messagesCollectionView.reloadDataAndKeepOffset() self.refreshControl.endRefreshing() } override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool { if action == NSSelectorFromString("delete:") { return true } else { return super.collectionView(collectionView, canPerformAction: action, forItemAt: indexPath, withSender: sender) } } override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) { if action == NSSelectorFromString("delete:") { // 1.) Remove from datasource // insert your code here let msg = messages[messages.count - loadedMsgsCount + indexPath.section] self.msgsHistoryService.hideMsg(ephemPublicKey: msg.id) self.loadedMsgsCount -= 1 // 2.) Delete sections collectionView.deleteSections([indexPath.section]) self.messagesCollectionView.reloadData() } else { super.collectionView(collectionView, performAction: action, forItemAt: indexPath, withSender: sender) } } func configureMessageCollectionView() { messagesCollectionView.messagesDataSource = self messagesCollectionView.messageCellDelegate = self scrollsToBottomOnKeyboardBeginsEditing = true // default false maintainPositionOnKeyboardFrameChanged = true // default false if let layout = 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)) } } func configureMessageInputBar() { messageInputBar.delegate = self messageInputBar.sendButton.image = Asset.chatSend.image messageInputBar.sendButton.title = nil messageInputBar.inputTextView.placeholderTextColor = Asset.textPebble.color messageInputBar.inputTextView.textColor = Asset.textGranite.color messageInputBar.inputTextView.font = Font.font(style: .regular, size: 16) // messageInputBar.inputTextView.layer.cornerRadius = 12.0 // messageInputBar.inputTextView.layer.borderWidth = 1.0 // messageInputBar.inputTextView.layer.borderColor = Asset.lightGray.color.cgColor // messageInputBar.separatorLine.isHidden = true messageInputBar.separatorLine.backgroundColor = Asset.marble.color messageInputBar.textViewPadding = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0) messageInputBar.maxTextViewHeight = 140 // messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 5, left: 12, bottom: 4, right: 32) // messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 5, left: 16, bottom: 4, right: 12) messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.emptyInputPlaceholder messageInputBar.backgroundView.backgroundColor = .white messageInputBar.inputTextView.isImagePasteEnabled = false return () let items = [ InputBarButtonItem() .configure { $0.spacing = .fixed(20) $0.image = Asset.chatQr.image $0.setSize(CGSize(width: 32, height: 32), animated: false) }.onSelected { [weak self] _ in self?.showQrPopup() } // .onDeselected { // $0.tintColor = UIColor.lightGray // } , messageInputBar .sendButton .configure { $0.spacing = .fixed(0) $0.image = Asset.chatSend.image $0.title = nil $0.setSize(CGSize(width: 32, height: 32), animated: false) } ] messageInputBar.middleContentViewPadding.right = -46 messageInputBar.setRightStackViewWidthConstant(to: 82 , animated: true) messageInputBar.setStackViewItems(items, forStack: .right, animated: true) messageInputBar.invalidateIntrinsicContentSize() reloadInputViews() messageInputBar.layoutIfNeeded() } private func showQrPopup() { 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)) present(ctrl, animated: true) } } extension CryptoChatControllerChat: InputBarAccessoryViewDelegate { func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { let components = inputBar.inputTextView.components messageInputBar.inputTextView.text = String() 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>" } } sendMsg(msgText: msgText) } func sendMsg(msgText: String) { guard let username = username, username.count > 0 else { return } guard let user = Accounts().current else { return } // Send button activity animation messageInputBar.sendButton.startAnimating() messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.sendingInputPlaceholder EosioRpcProvider(endpoint: URL(string: Network.servers.current.node)!).getAccount(requestParameters: EosioRpcAccountRequest(accountName: username), completion: { [weak self] in guard let self = self else { return } self.messageInputBar.sendButton.stopAnimating() self.messageInputBar.inputTextView.placeholder = L10n.CryptoChat.Chat.emptyInputPlaceholder // self.insertMessages(components) self.messagesCollectionView.scrollToBottom(animated: true) switch $0 { case let .success(account): var receiverPublicKey: String = "" account.permissions.forEach({ if $0.permName == "me.chat" { if let key = $0.requiredAuth.keys.first?.key, $0.requiredAuth.keys.count > 0 { receiverPublicKey = key } else { Alert.error(text: "Found more than one key in me.chat permissoin of \(username)") } } }) if receiverPublicKey.count == 0 { print("Could not find me.chat permission, looking up active key") account.permissions.forEach({ if $0.permName == "active" { if let key = $0.requiredAuth.keys.first?.key, $0.requiredAuth.keys.count > 0 { receiverPublicKey = key } else { Alert.error(text: "Found more than one key in active permissoin of \(username)") } } }) } if receiverPublicKey.count == 0 { Alert.error(text: "Cannot find me.chat or active keys for account \(username)") return } do { let msg = try CryptoChat.Model.Msg(from: user.username, to: username, msg: msgText, receiverPublicKey: receiverPublicKey) AccountViewAuthorize.showGetPrivateKey(in: self) { self.msgsHistoryService.saveMsg(CryptoChatModelRealmMessage(id: msg.ephemPublicKey, from: msg.from, to: msg.to, chatName: username, timestamp: Date(), data: msgText), ignoreTimestamp: true) self.loadedMsgsCount += 1 self.messagesCollectionView.reloadData() self.messagesCollectionView.scrollToBottom() Network.Service.Blockchain.execute(contract: .chat, action: .chatSendDm, data: msg, privateKeys: [$0]) { switch $0 { case let .success(trxId): // UserDefaults.standard.setValue(<#T##value: Any?##Any?#>, forKey: "chatsReadCount") // AccountHistory().current?.fetchUntil(trxId: trxId) self.msgsHistoryService.setMsgSynced(ephemPublicKey: msg.ephemPublicKey) self.msgsHistoryService.didUpdate?() case let .failure(error): Alert.error(text: "\(error)") self.msgsHistoryService.markMsgUnsent(ephemPublicKey: msg.ephemPublicKey) self.msgsHistoryService.didUpdate?() } } } } catch { Alert.error(text: "Error while encrypt msg: \(error.localizedDescription)") } case let .failure(error): Alert.error(text: error.description) } }) } private func insertMessages(_ data: [Any]) { } } extension CryptoChatControllerChat: 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 { sendMsg(msgText: "<qr>\(qrCodeLink)</qr>") } else { DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { Alert.error(text: L10n.CryptoChat.Chat.noQr) }) } } } }