Files
2022-05-27 20:21:01 +03:00

484 lines
21 KiB
Swift

//
// 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<CryptoChatModelRealmMessage> = { 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: "<qr>\(string)</qr>")
}
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: "<qr>\(string)</qr>")
}
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 += "<plaintext>\(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)
})
}
}
}
}