Files
raspberry/iOS/Wallet/Sources/CryptoChat/Controllers/CryptoChatControllerChats.swift
2022-05-27 20:21:01 +03:00

482 lines
18 KiB
Swift

//
// CryptoChatControllerChats.swift
// PayCash
//
// Created by Saveliy Stavitsky on 8/17/20.
// Copyright © 2020 List. All rights reserved.
//
import UIKit
import IQKeyboardManagerSwift
import EosioSwift
import Branch
import struct RealmSwift.Results
class CryptoChatControllerChats: UIViewController {
@IBOutlet private weak var tableView: UITableView!
@IBOutlet weak var headerContainerView: UIView!
@IBOutlet weak var addChatButton: UIButton!
@IBOutlet weak var chatsListLabel: UILabel!
var msgsHistoryService: CryptoChat.Service.MsgsHistory?
private let refreshControl = UIRefreshControl()
let header = CryptoChatViewChatsHeader(width: UIScreen.main.bounds.width, height: 120)
var chats: Results<CryptoChatModelRealmChat>?
var msgs: Results<CryptoChatModelRealmMessage>?
private lazy var noAccountsView: CommonViewEmpty = {
CommonViewEmpty(
title: L10n.CryptoChat.Chats.NoAccounts.title,
text: L10n.Account.Empty.text,
image: Asset.accountEmpty.image,
backgroundColor: Asset.snow.color,
submit: L10n.Account.Empty.submit
) { [weak self] in
guard let self = self else { return }
AccountController.showPopup(in: self)
}
}()
private lazy var emptyView: CommonViewEmpty = {
CommonViewEmpty(
title: L10n.CryptoChat.Chats.Empty.title,
text: L10n.CryptoChat.Chats.Empty.text,
image: Asset.chatsEmpty.image,
submit: L10n.CryptoChat.Chats.Empty.submit,
submitRealtion: .top
) { [weak self] in
guard let self = self else { return }
self.showSelectChatAddMethodPopup()
}
}()
private lazy var encodedView: CommonViewEmpty = {
CommonViewEmpty(
title: L10n.CryptoChat.Chats.Encoded.title,
text: L10n.CryptoChat.Chats.encoded,
image: Asset.chatsLocked.image,
submit: L10n.CryptoChat.Chats.encodedButton,
submitRealtion: .top
) { [weak self] in
guard let self = self else { return }
self.onAccountChange(Notification(name: .didChangeAccount))
}
}()
var bottomSpace: CGFloat = 0
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.interactivePopGestureRecognizer?.delegate = nil
navigationItem.title = L10n.CryptoChat.Chats.title
tableView.showsVerticalScrollIndicator = false
tableView.separatorStyle = .none
(tableView as UIScrollView).delegate = self
tableView.register(cell: CryptoChatCellChat.self)
refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
tableView.addSubview(refreshControl)
Notification.subscribe(name: .didUpdateHistory, {
guard Accounts().current?.username == $0.userInfo?["username"] as? String else { return }
self.msgsHistoryService?.fetchFromLocalHistory()
})
self.addChatButton.addTarget(self, action: #selector(self.addButtonPressed(_:)), for: .touchUpInside)
self.setupView()
}
func setup(username: String) {
AccountViewAuthorize.showGetPrivateKey(in: self, { [weak self] privateKey in
self?.msgsHistoryService = try? CryptoChat.Service
.MsgsHistory(username: username, encryptionKey: privateKey)
self?.msgsHistoryService?.didUpdate = { [weak self] in
DispatchQueue.main.async {
self?.refreshControl.endRefreshing()
self?.tableView.reloadData()
}
}
self?.chats = self?.msgsHistoryService?.chats
self?.msgs = self?.msgsHistoryService?.filterMsgs(query: "")
self?.header.searchTextField.text = ""
self?.tableView.reloadData()
Account.Service.History.shared.current?.fetch()
})
}
@objc
func addButtonPressed(_ sender: Any) {
self.showSelectChatAddMethodPopup()
}
@objc func onAccountChange(_ notification: Notification) {
if let username = Accounts().current?.username {
msgsHistoryService = nil
chats = nil
msgs = nil
tableView.reloadData()
setup(username: username)
}
Accounts().exists ? noAccountsView.removeFromSuperview() : view.addSubview(noAccountsView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Accounts().exists ? noAccountsView.removeFromSuperview() : view.addSubview(noAccountsView)
NotificationCenter.default.addObserver(self, selector: #selector(onAccountChange(_:)), name: .didChangeAccount, object: nil)
navigationController?.setNavigationBarHidden(true, animated: animated)
// IQKeyboardManager.shared.enable = false
refresh(self)
if ((UIApplication.shared.windows.first!.rootViewController as? MainController)?.barNav.items?.count ?? 0) > 1 {
(UIApplication.shared.windows.first!.rootViewController as? MainController)?.barNav.popItem(animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
NotificationCenter.default.removeObserver(self, name: .didChangeAccount, object: nil)
msgsHistoryService = nil
chats = nil
msgs = nil
tableView.reloadData()
navigationController?.setNavigationBarHidden(false, animated: animated)
// IQKeyboardManager.shared.enable = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if bottomSpace == 0 {
bottomSpace += (self.tabBarController?.tabBar.frameHeight ?? 0) /*tab bar*/
+ 40 /*button height*/ + 24 /*button top space*/
}
noAccountsView.frame = tableView.frame
emptyView.frame = tableView.frame
encodedView.frame = tableView.frame
}
private func setupView() {
self.addChatButton.layer.borderColor = Asset.deepWater.color.withAlphaComponent(0.2).cgColor
self.addChatButton.layer.borderWidth = 1.0
self.addChatButton.layer.cornerRadius = 8.0
}
@objc func refresh(_ sender: AnyObject) {
guard let username = Accounts().current?.username else { return }
if msgsHistoryService == nil {
setup(username: username)
} else {
Account.Service.History.shared.current?.fetch()
}
}
func openChat(username: String, unreadCount: Int) {
guard username.count > 0, msgsHistoryService != nil else { return }
let chatViewController = CryptoChatControllerChat()
chatViewController.username = username
chatViewController.msgsHistoryService = msgsHistoryService
chatViewController.unreadCount = unreadCount
mainController.content.push(chatViewController, animated: true)
}
var query = ""
}
extension CryptoChatControllerChats: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
1 + ((msgs?.count ?? 0) > 0 ? 1 : 0)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if Accounts().current != nil {
if msgsHistoryService == nil {
emptyView.removeFromSuperview()
Loader.hide(in: tableView)
view.addSubview(encodedView)
} else {
if !UserDefaults.standard.bool(forKey: "\(Accounts().current!.username)_firstMsgsSyncDone") {
Loader.show(in: tableView)
} else {
Loader.hide(in: tableView)
(chats?.count ?? 0) == 0 && query.isEmpty ? view.addSubview(emptyView) : emptyView.removeFromSuperview()
}
encodedView.removeFromSuperview()
}
[
self.addChatButton,
self.header.searchTextField
]
.forEach {
$0?.isHidden = (msgsHistoryService == nil)
|| !UserDefaults.standard.bool(forKey: "\(Accounts().current!.username)_firstMsgsSyncDone")
|| ((chats?.count ?? 0) == 0 && query.isEmpty)
}
} else {
Loader.hide(in: tableView)
emptyView.removeFromSuperview()
encodedView.removeFromSuperview()
}
return section == 0 ? (chats?.count ?? 0) : (msgs?.count ?? 0)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(CryptoChatCellChat.self, indexPath: indexPath)
if indexPath.section == 0 {
guard let chat = chats?[indexPath.row] else { return cell }
cell.titleLabel.text = chat.chatName
cell.titleShortLabel.text = String(chat.chatName.prefix(3))
cell.descriptionLabel.text = chat.textDescription
cell.timeLabel.text = chat.time
cell.setUnread(count: chat.unreadCount)
} else {
guard let msg = msgs?[indexPath.row] else { return cell }
cell.titleLabel.text = msg.chatName
cell.titleShortLabel.text = String(msg.chatName.prefix(3))
cell.descriptionLabel.text = msg.textDescription
cell.timeLabel.text = msg.time
cell.setUnread(count: nil)
}
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 {
header.parent = self
header.searchTextField.delegate = self
return header
} else {
let button = CommonButtonAction(width: UIScreen.main.bounds.width, height: 40)
button.style = .secondary
button.backgroundColor = Asset.deepWater.color.withAlphaComponent(0.1)
button.isUserInteractionEnabled = false
button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 24, bottom: 0, right: 24)
button.cornerRadius = 0
button.setTitle(L10n.CryptoChat.Chats.Search.msgsSection, for: .normal)
button.setTitleColor(Asset.textDeepWater.color, for: .normal)
button.titleLabel?.font = Font.font(style: .bold, size: 12)
button.contentHorizontalAlignment = .left
return UIStackView(subviews: [UIView(height: 16, color: .white), button], axis: .vertical, distribution: .fill, alignment: .fill, spacing: 0)
}
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
section == 0 ? 65 : 56
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 0 {
guard let chat = chats?[indexPath.row] else { return }
openChat(username: chat.chatName, unreadCount: chat.unreadCount)
} else {
guard let msg = msgs?[indexPath.row] else { return }
openChat(username: msg.chatName, unreadCount: 0)
}
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
if (msgs?.count ?? 0) > 0 {
return section == 1 ? UIView(height: 56, color: .white) : UIView(height: 0)
} else {
return UIView(height: 56, color: .white)
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
section == 1 ? 56 : 0
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
if !query.isEmpty { return nil }
guard let chat = chats?[indexPath.row] else { return nil }
let delete = UITableViewRowAction(style: .destructive, title: L10n.Common.Button.delete) { [weak self] (action, indexPath) in
self?.msgsHistoryService?.hideChatMsgs(chatName: chat.chatName)
}
return [delete]
}
}
extension CryptoChatControllerChats: CommonTextFieldSearchDelegate {
func textField(_ textField: CommonTextFieldSearch, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text {
let updatedText = (text as NSString).replacingCharacters(in: range, with: string)
if updatedText.isEmpty {
chats = msgsHistoryService?.chats
msgs = msgsHistoryService?.filterMsgs(query: "")
query = ""
} else {
chats = msgsHistoryService?.filterChats(query: updatedText)
msgs = msgsHistoryService?.filterMsgs(query: updatedText)
query = updatedText
}
} else {
chats = msgsHistoryService?.chats
msgs = msgsHistoryService?.filterMsgs(query: "")
query = ""
}
tableView.reloadData()
return true
}
func textFieldShouldClear(_ textField: CommonTextFieldSearch) -> Bool {
chats = msgsHistoryService?.chats
msgs = msgsHistoryService?.filterMsgs(query: "")
query = ""
tableView.reloadData()
return true
}
}
// MARK: - Opening QR to select New Chat Add Method
extension CryptoChatControllerChats: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func showSelectChatAddMethodPopup() {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let openScannerAction: ((UIAlertAction) -> Void) = { [weak self] _ in
self?.openScannerQrViewController { [weak self] in
self?.dismiss(animated: true)
let username = self?.injectUsername(string: $0) ?? .init()
guard !username.isEmpty else { return }
switch username {
case let username where URL(string: username)?.absoluteString.contains("app.link") ?? false:
Branch.getInstance().application(UIApplication.shared, open: URL(string: username), options: nil)
default:
self?.validateEosAccount(username: username)
}
}
}
let cryptoChatCreateAction: ((UIAlertAction) -> Void) = { [weak self] _ in
self?.openCryptoChatCreatePopup()
}
let imagePickerAction: ((UIAlertAction) -> Void) = { [weak self] _ in
self?.openImagePickerViewController()
}
alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.findAccount, style: .default, handler: cryptoChatCreateAction))
if UIImagePickerController.isSourceTypeAvailable(.photoLibrary) {
alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.qrLibrary, style: .default, handler: imagePickerAction))
}
if UIImagePickerController.isSourceTypeAvailable(.camera) {
alert.addAction(.init(title: L10n.CryptoChat.Chats.Popup.qrCamera, style: .default, handler: openScannerAction))
}
alert.addAction(.init(title: L10n.Common.Button.cancel, style: .cancel, handler: nil))
self.present(alert, animated: true)
}
private func injectUsername(string: String) -> String? {
guard let stringData = string.data(using: .utf8) else { return string }
let addressDecoder = try? JSONDecoder().decode(WalletTansferQr.self, from: stringData)
return addressDecoder?.address ?? string
}
private func validateEosAccount(username: String) {
EosioRpcProvider(endpoint: URL(string: Network.servers.current.node)!).getAccount(requestParameters: EosioRpcAccountRequest(accountName: username), completion: { [weak self] in
switch $0 {
case .success:
self?.openChat(username: username, unreadCount: 0)
case .failure:
Alert.error(text: L10n.CryptoChat.Chats.createTextFieldError)
}
})
}
// MARK: UIImagePickerControllerDelegate & UINavigationControllerDelegate delegate methods
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) {
let qrCodeString =
(detector.features(in: ciImage) as? [CIQRCodeFeature])?
.compactMap(\.messageString)
.joined()
?? String.init()
guard let username = self.injectUsername(string: qrCodeString) else {
return
}
guard !username.isEmpty else {
Alert.error(text: L10n.CryptoChat.Chats.noQr, delay: 1)
return
}
switch username {
case let username where URL(string: username)?.absoluteString.contains("app.link") ?? false:
Branch.getInstance().application(UIApplication.shared, open: URL(string: username), options: nil)
default:
self.validateEosAccount(username: username)
}
}
}
}
// MARK: - Routing
extension CryptoChatControllerChats {
fileprivate func openImagePickerViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .photoLibrary
self.present(imagePicker, animated: true, completion: nil)
}
fileprivate func openCryptoChatCreatePopup() {
let view = CryptoChatViewCreateChat()
view.parent = self
Popup.show(content: CommonViewControllerViewPopUp(
title: L10n.CryptoChat.Chats.craateDescription,
image: Asset.chatCreate.image,
view: view
))
}
fileprivate func openScannerQrViewController(onAction: @escaping (String) -> Void) {
let scanner = ScannerViewController()
scanner.navigationItem.leftBarButtonItem = .close { [weak self] in
self?.dismiss(animated: true)
}
scanner.didCapture = onAction
let navCtrl = UINavigationController(rootViewController: scanner)
navCtrl.modalPresentationStyle = .fullScreen
self.navigationController?.tabBarController?.present(navCtrl, animated: true)
}
}