Files
Elena Nazarova 2a43809fac Merge branch 'feature/MALINKA-774/Incorrect_messages_order' into 'develop'
Resolve MALINKA-774 "Feature//incorrect messages order"

See merge request raspberry/mobile/ios!239
2023-02-06 22:49:01 +03:00

472 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
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] = [:]
final class CryptoChatControllerChat: MessagesViewController {
private enum Constants {
static let bottomInset: CGFloat = 38
}
typealias HistoryService = CryptoChat.Service.MsgsHistory
let username: String
let unreadCount: Int
let historyService: HistoryService
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")
}
var publicKey: String?
var loadedMsgsCount = 0
private let refreshControl = UIRefreshControl()
lazy var messages: Results<CryptoChatModelRealmMessage> = { self.historyService.openChatWith(self.username) }()
var bottomSpace: CGFloat = 0
private var cancellables = Set<AnyCancellable>()
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.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
}()
let infoView = CommonViewWarning()
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)
self.observers.append(contentsOf: [
Notification.subscribe(name: .didUpdateHistory, { [weak self] in
guard Accounts().current?.name == $0.userInfo?["username"] as? String else { return }
guard let self = self else { return }
self.historyService.fetchFromLocalHistory()
})
])
}
var observers: [AnyObject] = []
deinit { self.observers.forEach { Notification.unsubscribe(observer: $0) } }
@objc func dismissKeyboard() {
self.messageInputBar.inputTextView.resignFirstResponder()
}
@objc func refresh(_ sender: AnyObject) {
Accounts().messageShelf.activeBook?.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)
self.messageInputBar.inputTextView.text = lastMsgs[self.username] ?? ""
}
override func viewWillDisappear(_ animated: Bool) {
Accounts().isLocked = false
IQKeyboardManager.shared.enable = true
self.navigationController?.setNavigationBarHidden(false, animated: false)
lastMsgs[self.username] = self.messageInputBar.inputTextView.text
// TODO: - bad hack! Search good solution!
DispatchQueue.main.async {
self.refreshControl.beginRefreshing()
self.refreshControl.endRefreshing()
}
super.viewWillDisappear(animated)
}
@objc func loadMoreMessages() {
self.loadedMsgsCount = min(self.messages.count, self.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 = self.messages[self.messages.count - self.loadedMsgsCount + indexPath.section]
self.historyService.hideMsg(ephemPublicKey: msg.id)
self.loadedMsgsCount -= 1
// 2.) Delete sections
DispatchQueue.main.async { [weak self] in
self?.messagesCollectionView.reloadData()
}
} else {
super.collectionView(collectionView, performAction: action, forItemAt: indexPath, withSender: sender)
}
}
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))
}
}
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))
present(ctrl, animated: true)
}
}
extension CryptoChatControllerChat: 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 let .success(trxId):
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]) {
}
}
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)
})
}
}
}
}