Files
raspberry/iOS/Wallet/Sources/CryptoChat/Controllers/CryptoChatControllerChats.swift
2023-03-10 11:23:56 +03:00

539 lines
20 KiB
Swift

//
// CryptoChatControllerChats.swift
// Wallet
//
// Created by Saveliy Stavitsky on 8/17/20.
// Copyright © 2020 List. All rights reserved.
//
import UIKit
import IQKeyboardManagerSwift
import EosioSwift
import Branch
import Combine
import WalletKit
import WalletUIComponents
import struct RealmSwift.Results
final class CryptoChatControllerChats: UIViewController {
@IBOutlet private weak var tableView: UITableView!
@IBOutlet private weak var headerContainerView: UIView!
@IBOutlet private weak var addChatButton: UIButton!
@IBOutlet private weak var chatsListLabel: UILabel!
private var bottomSpace: CGFloat = 0
private var msgsHistoryService: CryptoChat.Service.MsgsHistory?
private weak var createChatView: CryptoChatViewCreateChat?
private var query = ""
private let refreshControl = UIRefreshControl()
private var header: CryptoChatViewChatsHeader { CryptoChatViewChatsHeader(width: UIScreen.main.bounds.width,
viewModel: self.viewModel) }
private let viewModel = TextFieldViewModel(placeholder: L10n.CryptoChat.Chats.search)
private var cancellables = Set<AnyCancellable>()
private var chats: Results<CryptoChatModelRealmChat>?
private 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.onChange(wallet: Accounts().current)
}
}()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.interactivePopGestureRecognizer?.delegate = nil
self.navigationItem.title = L10n.CryptoChat.Chats.title
self.tableView.showsVerticalScrollIndicator = false
self.tableView.separatorStyle = .none
(self.tableView as UIScrollView).delegate = self
self.tableView.register(cell: CryptoChatCellChat.self)
self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged)
self.tableView.refreshControl = self.refreshControl
Notification.subscribe(name: .didUpdateHistory, {
guard Accounts().current?.name == $0.userInfo?["username"] as? String else { return }
self.msgsHistoryService?.fetchFromLocalHistory()
})
self.addChatButton.addTarget(self, action: #selector(self.addButtonPressed(_:)), for: .touchUpInside)
self.setupView()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Accounts().current.isExist ? self.noAccountsView.removeFromSuperview() : self.view.addSubview(noAccountsView)
Accounts().bank.activePublisher
.receive(on: DispatchQueue.main)
.sink {
self.onChange(wallet: $0)
}
.store(in: &self.cancellables)
self.viewModel.objectWillChange
.receive(on: DispatchQueue.main)
.sink { [weak self] in
guard let self else { return }
self.textFieldDidChange(self.viewModel.text)
if self.viewModel.shouldClear { self.textFieldShouldClear() }
}
.store(in: &self.cancellables)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
// IQKeyboardManager.shared.enable = false
self.refresh(self)
guard let window = UIApplication.shared.windows.first,
let controller = window.rootViewController as? MainController else { return }
if (controller.barNav.items?.count ?? 0) > 1 {
controller.barNav.popItem(animated: true)
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.cancellables.removeAll()
self.msgsHistoryService = nil
self.chats = nil
self.msgs = nil
self.tableView.reloadData()
self.navigationController?.setNavigationBarHidden(false, animated: animated)
// IQKeyboardManager.shared.enable = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if self.bottomSpace == 0 {
self.bottomSpace += (self.tabBarController?.tabBar.frameHeight ?? 0) /*tab bar*/
+ 40 /*button height*/ + 24 /*button top space*/
}
self.noAccountsView.frame = self.tableView.frame
self.emptyView.frame = self.tableView.frame
self.encodedView.frame = self.tableView.frame
}
// MARK: - Private
private func setup(username: String) {
AccountViewAuthorize.showGetPrivateKey(in: self) { [weak self] privateKey in
guard let self else { return }
self.msgsHistoryService = try? CryptoChat.Service.MsgsHistory(username: username, encryptionKey: privateKey)
self.msgsHistoryService?.updatePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.refreshControl.endRefreshing()
self?.tableView.reloadData()
}
.store(in: &self.cancellables)
self.chats = self.msgsHistoryService?.chats
self.msgs = self.msgsHistoryService?.filterMsgs(query: "")
self.header.searchTextField.viewModel.text = ""
self.tableView.reloadData()
Accounts().messageShelf.activeBook?.fetch()
}
}
private func onChange(wallet: WalletKit.Wallet?) {
guard let wallet = Accounts().current else {
self.view.addSubview(self.noAccountsView)
return
}
self.msgsHistoryService = nil
self.chats = nil
self.msgs = nil
self.tableView.reloadData()
self.setup(username: wallet.name)
self.noAccountsView.removeFromSuperview()
}
private func openChat(username: String, unreadCount: Int) {
guard let service = self.msgsHistoryService else { return }
let chatViewController = CryptoChatControllerChat(username: username,
unreadCount: unreadCount,
service: service)
mainController.content.push(chatViewController, animated: true)
}
private func textFieldShouldClear() {
self.chats = self.msgsHistoryService?.chats
self.msgs = self.msgsHistoryService?.filterMsgs(query: "")
self.query = ""
self.tableView.reloadData()
}
private func textFieldDidChange(_ text: String?) {
if let text {
self.chats = text.isEmpty ? self.msgsHistoryService?.chats : self.msgsHistoryService?.filterChats(query: text)
self.msgs = text.isEmpty ? self.msgsHistoryService?.filterMsgs(query: "") : self.msgsHistoryService?.filterMsgs(query: text)
self.query = text.isEmpty ? "" : text
} else {
self.chats = self.msgsHistoryService?.chats
self.msgs = self.msgsHistoryService?.filterMsgs(query: "")
self.query = ""
}
self.tableView.reloadData()
}
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
private func addButtonPressed(_ sender: Any) {
self.showSelectChatAddMethodPopup()
}
@objc
private func refresh(_ sender: AnyObject) {
// TODO: look for better solution for refresh
DispatchQueue.main.async { [weak self] in
self?.refreshControl.beginRefreshing()
}
guard let username = Accounts().current?.name else { return }
if self.msgsHistoryService == nil {
self.setup(username: username)
} else {
Accounts().messageShelf.activeBook?.fetch()
}
DispatchQueue.main.async { [weak self] in
self?.refreshControl.endRefreshing()
}
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension CryptoChatControllerChats: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
1 + ((self.msgs?.count ?? 0) > 0 ? 1 : 0)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let wallet = Accounts().current {
if self.msgsHistoryService == nil {
self.emptyView.removeFromSuperview()
Loader.hide(in: tableView)
self.view.addSubview(self.encodedView)
} else {
if !UserDefaults.standard.bool(forKey: "\(wallet.name)_firstMsgsSyncDone") {
Loader.show(in: tableView)
} else {
Loader.hide(in: tableView)
(self.chats?.count ?? 0) == 0 && self.query.isEmpty
? self.view.addSubview(self.emptyView)
: self.emptyView.removeFromSuperview()
}
self.encodedView.removeFromSuperview()
}
[
self.addChatButton,
self.header
]
.forEach {
$0?.isHidden = (self.msgsHistoryService == nil)
|| !UserDefaults.standard.bool(forKey: "\(wallet.name)_firstMsgsSyncDone")
|| ((self.chats?.count ?? 0) == 0 && self.query.isEmpty)
}
} else {
Loader.hide(in: tableView)
self.emptyView.removeFromSuperview()
self.encodedView.removeFromSuperview()
}
return section == 0
? (self.chats?.count ?? 0)
: (self.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 = self.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 = self.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 {
return self.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 = self.chats?[indexPath.row] else { return }
self.openChat(username: chat.chatName, unreadCount: chat.unreadCount)
} else {
guard let msg = self.msgs?[indexPath.row] else { return }
self.openChat(username: msg.chatName, unreadCount: 0)
}
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
section == 1 ? 56 : 0
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
if !self.query.isEmpty { return nil }
guard let chats = self.chats else { return nil }
let chat = chats[indexPath.row]
let contextItem = UIContextualAction(style: .destructive, title: L10n.Common.Button.delete) { [weak self] (_, _, _) in
self?.msgsHistoryService?.hideChatMsgs(chatName: chat.chatName)
}
let actions = UISwipeActionsConfiguration(actions: [contextItem])
return actions
}
}
// 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) {
let environment = ApplicationEnvironment.shared().current
guard let node = environment.node,
let url = URL(string: node) else {
Alert.error(text: L10n.CryptoChat.Chats.createTextFieldError)
return
}
EosioRpcProvider(endpoint: url, headers: environment.headers).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()
Popup.show(
content: CommonViewControllerViewPopUp(
title: L10n.CryptoChat.Chats.craateDescription,
image: Asset.chatCreate.image,
view: view
), in: self
)
view.succeedPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] username in
guard let self else { return }
Popup.hide(in: self) { [weak self] in
self?.openChat(username: username, unreadCount: 0)
}
}
.store(in: &self.cancellables)
}
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)
}
}