mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Fixed an issue where tapping a remote notification wasn't opening the conversation Fixed an issue where the Share Extension could fail due to not having a snode pool loaded Fixed an issue where the Theme may not be applied at the right time in the Share extension
359 lines
15 KiB
Swift
359 lines
15 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import Combine
|
|
import GRDB
|
|
import DifferenceKit
|
|
import SessionUIKit
|
|
import SignalUtilitiesKit
|
|
import SessionMessagingKit
|
|
import SessionSnodeKit
|
|
|
|
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?
|
|
|
|
// MARK: - Intialization
|
|
|
|
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
|
|
}()
|
|
|
|
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
|
|
}()
|
|
|
|
// MARK: - Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
navigationItem.titleView = titleLabel
|
|
|
|
view.themeBackgroundColor = .backgroundPrimary
|
|
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()
|
|
}
|
|
|
|
// 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
|
|
|
|
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)
|
|
|
|
ShareNavController.attachmentPrepPublisher?
|
|
.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)
|
|
}
|
|
)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
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) {
|
|
}
|
|
}
|