mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Updated to the latest version of GRDB Fixed an issue with db reentrant behaviour with the Attachment upload function Finished up the updated 'sendNonDurability' functions
294 lines
11 KiB
Swift
294 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import GRDB
|
|
import PromiseKit
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
import SessionMessagingKit
|
|
|
|
final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
|
|
private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel()
|
|
private var dataChangeObservable: DatabaseCancellable?
|
|
private var hasLoadedInitialData: Bool = false
|
|
|
|
var shareVC: ShareVC?
|
|
|
|
// MARK: - Intialization
|
|
|
|
deinit {
|
|
NotificationCenter.default.removeObserver(self)
|
|
}
|
|
|
|
// MARK: - UI
|
|
|
|
private lazy var titleLabel: UILabel = {
|
|
let titleLabel: UILabel = UILabel()
|
|
titleLabel.text = "vc_share_title".localized()
|
|
titleLabel.textColor = Colors.text
|
|
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
|
|
|
return titleLabel
|
|
}()
|
|
|
|
private lazy var tableView: UITableView = {
|
|
let tableView: UITableView = UITableView()
|
|
tableView.backgroundColor = .clear
|
|
tableView.separatorStyle = .none
|
|
tableView.register(view: SimplifiedConversationCell.self)
|
|
tableView.showsVerticalScrollIndicator = false
|
|
tableView.dataSource = self
|
|
tableView.delegate = self
|
|
|
|
return tableView
|
|
}()
|
|
|
|
private lazy var fadeView: UIView = {
|
|
let view = UIView()
|
|
let gradient = Gradients.homeVCFade
|
|
view.setGradient(gradient)
|
|
view.isUserInteractionEnabled = false
|
|
|
|
return view
|
|
}()
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
setupNavBar()
|
|
|
|
// Gradient
|
|
view.backgroundColor = .clear
|
|
view.setGradient(Gradients.defaultBackground)
|
|
|
|
// Title
|
|
navigationItem.titleView = titleLabel
|
|
|
|
// Table view
|
|
|
|
view.addSubview(tableView)
|
|
view.addSubview(fadeView)
|
|
|
|
setupLayout()
|
|
|
|
// Notifications
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidBecomeActive(_:)),
|
|
name: UIApplication.didBecomeActiveNotification,
|
|
object: nil
|
|
)
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidResignActive(_:)),
|
|
name: UIApplication.didEnterBackgroundNotification, object: nil
|
|
)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
startObservingChanges()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
// Stop observing database changes
|
|
dataChangeObservable?.cancel()
|
|
}
|
|
|
|
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
|
startObservingChanges()
|
|
}
|
|
|
|
@objc func applicationDidResignActive(_ notification: Notification) {
|
|
// Stop observing database changes
|
|
dataChangeObservable?.cancel()
|
|
}
|
|
|
|
private func setupNavBar() {
|
|
guard let navigationBar = navigationController?.navigationBar else { return }
|
|
if #available(iOS 15.0, *) {
|
|
let appearance = UINavigationBarAppearance()
|
|
appearance.configureWithOpaqueBackground()
|
|
appearance.backgroundColor = Colors.navigationBarBackground
|
|
navigationBar.standardAppearance = appearance;
|
|
navigationBar.scrollEdgeAppearance = navigationBar.standardAppearance
|
|
}
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
view.setGradient(Gradients.defaultBackground)
|
|
fadeView.setGradient(Gradients.homeVCFade)
|
|
}
|
|
|
|
// MARK: Layout
|
|
|
|
private func setupLayout() {
|
|
let topInset = 0.15 * view.height()
|
|
|
|
tableView.pin(to: view)
|
|
fadeView.pin(.leading, to: .leading, of: view)
|
|
fadeView.pin(.top, to: .top, of: view, withInset: topInset)
|
|
fadeView.pin(.trailing, to: .trailing, of: view)
|
|
fadeView.pin(.bottom, to: .bottom, of: view)
|
|
}
|
|
|
|
// MARK: - Updating
|
|
|
|
private func startObservingChanges() {
|
|
// Start observing for data changes
|
|
dataChangeObservable = GRDBStorage.shared.start(
|
|
viewModel.observableViewData,
|
|
onError: { _ in },
|
|
onChange: { [weak self] viewData in
|
|
// The defaul scheduler emits changes on the main thread
|
|
self?.handleUpdates(viewData)
|
|
}
|
|
)
|
|
}
|
|
|
|
private func handleUpdates(_ updatedViewData: ThreadPickerViewModel.ViewData) {
|
|
// Ensure the first load runs without animations (if we don't do this the cells will animate
|
|
// in from a frame of CGRect.zero)
|
|
guard hasLoadedInitialData else {
|
|
hasLoadedInitialData = true
|
|
UIView.performWithoutAnimation { handleUpdates(updatedViewData) }
|
|
return
|
|
}
|
|
|
|
// Reload the table content (animate changes after the first load)
|
|
tableView.reload(
|
|
using: StagedChangeset(source: viewModel.viewData.items, target: updatedViewData.items),
|
|
with: .automatic,
|
|
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
|
) { [weak self] updatedData in
|
|
self?.viewModel.updateData(
|
|
ThreadPickerViewModel.ViewData(
|
|
currentUserProfile: updatedViewData.currentUserProfile,
|
|
items: updatedData
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableViewDataSource
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return self.viewModel.viewData.items.count
|
|
}
|
|
|
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath)
|
|
cell.update(
|
|
with: self.viewModel.viewData.items[indexPath.row],
|
|
currentUserProfile: self.viewModel.viewData.currentUserProfile
|
|
)
|
|
|
|
return cell
|
|
}
|
|
|
|
// MARK: - Interaction
|
|
|
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
tableView.deselectRow(at: indexPath, animated: true)
|
|
|
|
guard let attachments: [SignalAttachment] = ShareVC.attachmentPrepPromise?.value else { return }
|
|
|
|
let approvalVC: OWSNavigationController = AttachmentApprovalViewController.wrappedInNavController(
|
|
threadId: self.viewModel.viewData.items[indexPath.row].id,
|
|
attachments: attachments,
|
|
approvalDelegate: self
|
|
)
|
|
self.navigationController?.present(approvalVC, animated: true, completion: nil)
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
|
|
// Sharing a URL or plain text will populate the 'messageText' field so in those
|
|
// cases we should ignore the attachments
|
|
let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl)
|
|
let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText)
|
|
let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments)
|
|
let body: String? = (
|
|
isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ?
|
|
(
|
|
(messageText?.isEmpty == true || (attachments[0].text() == messageText) ?
|
|
attachments[0].text() :
|
|
"\(attachments[0].text() ?? "")\n\n\(messageText ?? "")"
|
|
)
|
|
) :
|
|
messageText
|
|
)
|
|
|
|
shareVC?.dismiss(animated: true, completion: nil)
|
|
|
|
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
|
|
GRDBStorage.shared
|
|
.write { [weak self] db -> Promise<Void> in
|
|
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
|
|
activityIndicator.dismiss { }
|
|
self?.shareVC?.shareViewFailed(error: MessageSenderError.noThread)
|
|
return Promise(error: MessageSenderError.noThread)
|
|
}
|
|
|
|
// Create the interaction
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
let interaction: Interaction = try Interaction(
|
|
threadId: threadId,
|
|
authorId: getUserHexEncodedPublicKey(db),
|
|
variant: .standardOutgoing,
|
|
body: body,
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
|
hasMention: (body?.contains("@\(userPublicKey)") == true),
|
|
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
|
|
).inserted(db)
|
|
|
|
// If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing
|
|
// one then add it now
|
|
if
|
|
isSharingUrl,
|
|
let linkPreviewDraft: OWSLinkPreviewDraft = attachments.first?.linkPreviewDraft,
|
|
(try? interaction.linkPreview.isEmpty(db)) == true
|
|
{
|
|
try LinkPreview(
|
|
url: linkPreviewDraft.urlString,
|
|
title: linkPreviewDraft.title,
|
|
attachmentId: LinkPreview.saveAttachmentIfPossible(
|
|
db,
|
|
imageData: linkPreviewDraft.jpegImageData,
|
|
mimeType: OWSMimeTypeImageJpeg
|
|
)
|
|
).insert(db)
|
|
}
|
|
|
|
return try MessageSender.sendNonDurably(
|
|
db,
|
|
interaction: interaction,
|
|
with: finalAttachments,
|
|
in: thread
|
|
)
|
|
}
|
|
.done { [weak self] _ in
|
|
activityIndicator.dismiss { }
|
|
self?.shareVC?.shareViewWasCompleted()
|
|
}
|
|
.catch { [weak self] error in
|
|
activityIndicator.dismiss { }
|
|
self?.shareVC?.shareViewFailed(error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
dismiss(animated: true, completion: nil)
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
|
|
// Do nothing
|
|
}
|
|
}
|