session-ios/SessionShareExtension/ThreadPickerVC.swift

359 lines
15 KiB
Swift
Raw Permalink Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import GRDB
import DifferenceKit
2021-05-06 02:36:58 +02:00
import SessionUIKit
import SignalUtilitiesKit
import SessionMessagingKit
import SessionSnodeKit
2021-05-06 02:36:58 +02:00
final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate {
private let viewModel: ThreadPickerViewModel = ThreadPickerViewModel()
private var dataChangeObservable: DatabaseCancellable? {
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
}
private var hasLoadedInitialData: Bool = false
var shareNavController: ShareNavController?
2021-05-06 02:36:58 +02:00
// MARK: - Intialization
2021-05-06 02:36:58 +02:00
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - UI
private lazy var titleLabel: UILabel = {
let titleLabel: UILabel = UILabel()
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
titleLabel.text = "vc_share_title".localized()
titleLabel.themeTextColor = .textPrimary
return titleLabel
}()
private lazy var databaseErrorLabel: UILabel = {
let result: UILabel = UILabel()
result.font = .systemFont(ofSize: Values.mediumFontSize)
result.text = "database_inaccessible_error".localized()
result.textAlignment = .center
result.themeTextColor = .textPrimary
result.numberOfLines = 0
result.isHidden = true
return result
}()
2021-05-06 02:36:58 +02:00
private lazy var tableView: UITableView = {
let tableView: UITableView = UITableView()
tableView.themeBackgroundColor = .backgroundPrimary
tableView.separatorStyle = .none
tableView.register(view: SimplifiedConversationCell.self)
tableView.showsVerticalScrollIndicator = false
tableView.dataSource = self
tableView.delegate = self
return tableView
2021-05-06 02:36:58 +02:00
}()
// MARK: - Lifecycle
2021-05-06 02:36:58 +02:00
override func viewDidLoad() {
super.viewDidLoad()
2021-05-06 02:36:58 +02:00
navigationItem.titleView = titleLabel
view.themeBackgroundColor = .backgroundPrimary
2021-05-06 02:36:58 +02:00
view.addSubview(tableView)
view.addSubview(databaseErrorLabel)
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)
stopObservingChanges()
}
@objc func applicationDidBecomeActive(_ notification: Notification) {
/// Need to dispatch to the next run loop to prevent a possible crash caused by the database resuming mid-query
DispatchQueue.main.async { [weak self] in
self?.startObservingChanges()
}
}
@objc func applicationDidResignActive(_ notification: Notification) {
stopObservingChanges()
2021-05-06 02:36:58 +02:00
}
// MARK: Layout
private func setupLayout() {
tableView.pin(to: view)
databaseErrorLabel.pin(.top, to: .top, of: view, withInset: Values.massiveSpacing)
databaseErrorLabel.pin(.leading, to: .leading, of: view, withInset: Values.veryLargeSpacing)
databaseErrorLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.veryLargeSpacing)
}
// MARK: - Updating
private func startObservingChanges() {
guard dataChangeObservable == nil else { return }
// Start observing for data changes
dataChangeObservable = Storage.shared.start(
viewModel.observableViewData,
onError: { [weak self] _ in self?.databaseErrorLabel.isHidden = Storage.shared.isValid },
onChange: { [weak self] viewData in
// The defaul scheduler emits changes on the main thread
self?.handleUpdates(viewData)
}
)
}
private func stopObservingChanges() {
dataChangeObservable = nil
}
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
2021-05-06 02:36:58 +02:00
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.viewData.count
2021-05-06 02:36:58 +02:00
}
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])
2021-05-06 02:36:58 +02:00
return cell
}
// MARK: - Interaction
2021-05-06 02:45:30 +02:00
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
ShareNavController.attachmentPrepPublisher?
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveValue: { [weak self] attachments in
guard let strongSelf = self else { return }
let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController(
threadId: strongSelf.viewModel.viewData[indexPath.row].threadId,
threadVariant: strongSelf.viewModel.viewData[indexPath.row].threadVariant,
attachments: attachments,
approvalDelegate: strongSelf
)
strongSelf.navigationController?.present(approvalVC, animated: true, completion: nil)
}
)
2021-05-06 03:09:54 +02:00
}
func attachmentApproval(
_ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
threadVariant: SessionThread.Variant,
messageText: String?,
using dependencies: Dependencies = Dependencies()
) {
// 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
)
shareNavController?.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
Storage.resumeDatabaseAccess()
/// When we prepare the message we set the timestamp to be the `SnodeAPI.currentOffsetTimestampMs()`
/// but won't actually have a value because the share extension won't have talked to a service node yet which can cause
/// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate
Just(())
.setFailureType(to: Error.self)
.flatMap { _ in
// We may not have sufficient snodes, so rather than failing we try to load/fetch
// them if needed
guard !SnodeAPI.hasCachedSnodesIncludingExpired() else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
return SnodeAPI.getSnodePool()
.map { _ in () }
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { _ in
SnodeAPI
.getSwarm(
for: {
switch threadVariant {
case .contact, .legacyGroup, .group: return threadId
case .community: return getUserHexEncodedPublicKey(using: dependencies)
}
}(),
using: dependencies
)
.tryFlatMapWithRandomSnode { SnodeAPI.getNetworkTime(from: $0, using: dependencies) }
.map { _ in () }
.eraseToAnyPublisher()
}
.flatMap { _ in
dependencies.storage.writePublisher { db -> MessageSender.PreparedSendData in
guard
let threadVariant: SessionThread.Variant = try SessionThread
.filter(id: threadId)
.select(.variant)
.asRequest(of: SessionThread.Variant.self)
.fetchOne(db)
else { throw MessageSenderError.noThread }
// Create the interaction
let interaction: Interaction = try Interaction(
threadId: threadId,
authorId: getUserHexEncodedPublicKey(db),
variant: .standardOutgoing,
body: body,
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
expiresInSeconds: try? DisappearingMessagesConfiguration
.select(.durationSeconds)
.filter(id: threadId)
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
.asRequest(of: TimeInterval.self)
.fetchOne(db),
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
throw StorageError.failedToSave
}
// 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
.generateAttachmentIfPossible(
imageData: linkPreviewDraft.jpegImageData,
mimeType: OWSMimeTypeImageJpeg
)?
.inserted(db)
.id
).insert(db)
}
// Prepare any attachments
try Attachment.process(
db,
data: Attachment.prepare(attachments: finalAttachments),
for: interactionId
)
// Prepare the message send data
return try MessageSender
.preparedSendData(
db,
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant,
using: dependencies
)
}
}
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0, using: dependencies) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { [weak self] result in
Storage.suspendDatabaseAccess()
activityIndicator.dismiss { }
switch result {
case .finished: self?.shareNavController?.shareViewWasCompleted()
case .failure(let error): self?.shareNavController?.shareViewFailed(error: error)
}
}
)
2021-05-06 04:48:49 +02:00
}
2021-05-06 03:09:54 +02:00
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
dismiss(animated: true, completion: nil)
2021-05-06 03:09:54 +02:00
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) {
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
}
func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
2021-05-06 02:45:30 +02:00
}
2021-05-06 02:36:58 +02:00
}