mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
11 KiB
Swift
291 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import GRDB
|
|
import PromiseKit
|
|
import DifferenceKit
|
|
import Sodium
|
|
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: [SessionThreadViewModel]) {
|
|
// 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, target: updatedViewData),
|
|
with: .automatic,
|
|
interrupt: { $0.changeCount > 100 } // Prevent too many changes from causing performance issues
|
|
) { [weak self] updatedData in
|
|
self?.viewModel.updateData(updatedData)
|
|
}
|
|
}
|
|
|
|
// MARK: - UITableViewDataSource
|
|
|
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
return self.viewModel.viewData.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[indexPath.row])
|
|
|
|
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[indexPath.row].threadId,
|
|
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
|
|
.writeAsync { [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 interaction: Interaction = try Interaction(
|
|
threadId: threadId,
|
|
authorId: getUserHexEncodedPublicKey(db),
|
|
variant: .standardOutgoing,
|
|
body: body,
|
|
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
|
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
|
|
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: LinkPreviewDraft = 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?) {
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
|
|
}
|
|
|
|
func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
}
|
|
}
|