Files
2023-03-10 11:23:56 +03:00

453 lines
20 KiB
Swift

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