453 lines
20 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|