Randall hit an issue where his device would take a few seconds to start up. His logs indicate that his device is spending time running LaunchJobs. The first change here is to make sure we explicitly set the launch job work at an ultra-high priority. This is okay, since we don't actually present UI until after we finish these jobs. Running this work at UserInteractive is appropriate since it quite literally prevents the user from interacting with the app. There's also no other time-sensitive UI work we need to be doing (like running animations) that we could be contending with. The second change is to add a bit more logging that allows us to monitor the amount of work these jobs are doing. This will allow us to see if these jobs are performing an excessive amount of work. Finally, I moved these LaunchJobs to Swift. Mostly because the ObjC implementations were block based. The additional code was going to indent things further and our linter aggressiely indents blocks to begin with. Moving this to Swift is much more readable.
641 lines
25 KiB
Swift
641 lines
25 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import Foundation
|
|
import SignalUI
|
|
|
|
@objc
|
|
class SharingThreadPickerViewController: ConversationPickerViewController {
|
|
|
|
weak var shareViewDelegate: ShareViewDelegate?
|
|
|
|
var attachments: [SignalAttachment]? {
|
|
didSet { updateApprovalMode() }
|
|
}
|
|
|
|
lazy var isTextMessage: Bool = {
|
|
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
|
|
return attachment.isConvertibleToTextMessage && attachment.dataLength < kOversizeTextMessageSizeThreshold
|
|
}()
|
|
|
|
lazy var isContactShare: Bool = {
|
|
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
|
|
return attachment.isConvertibleToContactShare
|
|
}()
|
|
|
|
var approvedAttachments: [SignalAttachment]?
|
|
var approvedContactShare: ContactShareViewModel?
|
|
var approvalMessageBody: MessageBody?
|
|
var approvalLinkPreviewDraft: OWSLinkPreviewDraft?
|
|
|
|
var outgoingMessages = [TSOutgoingMessage]() {
|
|
didSet { AssertIsOnMainThread() }
|
|
}
|
|
|
|
var mentionCandidates: [SignalServiceAddress] = []
|
|
|
|
var selectedConversations: [ConversationItem] { selection.conversations }
|
|
|
|
@objc
|
|
public init(shareViewDelegate: ShareViewDelegate) {
|
|
self.shareViewDelegate = shareViewDelegate
|
|
|
|
super.init(selection: ConversationPickerSelection())
|
|
|
|
pickerDelegate = self
|
|
}
|
|
|
|
public func presentActionSheetOnNavigationController(_ alert: ActionSheetController) {
|
|
if let navigationController = shareViewDelegate?.shareViewNavigationController {
|
|
navigationController.presentActionSheet(alert)
|
|
} else {
|
|
super.presentActionSheet(alert)
|
|
}
|
|
}
|
|
|
|
private func updateMentionCandidates() {
|
|
AssertIsOnMainThread()
|
|
|
|
guard selectedConversations.count == 1,
|
|
case .group(let groupThreadId) = selectedConversations.first?.messageRecipient else {
|
|
mentionCandidates = []
|
|
return
|
|
}
|
|
|
|
let groupThread = databaseStorage.read { readTx in
|
|
TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: readTx)
|
|
}
|
|
|
|
owsAssertDebug(groupThread != nil)
|
|
if let groupThread = groupThread, Mention.threadAllowsMentionSend(groupThread) {
|
|
mentionCandidates = groupThread.recipientAddresses
|
|
} else {
|
|
mentionCandidates = []
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Approval
|
|
|
|
extension SharingThreadPickerViewController {
|
|
|
|
func approve() {
|
|
do {
|
|
let vc = try buildApprovalViewController()
|
|
navigationController?.pushViewController(vc, animated: true)
|
|
} catch {
|
|
shareViewDelegate?.shareViewFailed(error: error)
|
|
}
|
|
}
|
|
|
|
func buildApprovalViewController(for thread: TSThread) throws -> UIViewController {
|
|
AssertIsOnMainThread()
|
|
loadViewIfNeeded()
|
|
guard let conversationItem = conversation(for: thread) else {
|
|
throw OWSAssertionError("Unexpectedly missing conversation for selected thread")
|
|
}
|
|
selection.add(conversationItem)
|
|
return try buildApprovalViewController()
|
|
}
|
|
|
|
func buildApprovalViewController() throws -> UIViewController {
|
|
guard let attachments = attachments, let firstAttachment = attachments.first else {
|
|
throw OWSAssertionError("Unexpectedly missing attachments")
|
|
}
|
|
|
|
let approvalVC: UIViewController
|
|
|
|
if isTextMessage {
|
|
guard let messageText = String(data: firstAttachment.data, encoding: .utf8)?.filterForDisplay else {
|
|
throw OWSAssertionError("Missing or invalid message text for text attachment")
|
|
}
|
|
let approvalView = TextApprovalViewController(messageBody: MessageBody(text: messageText, ranges: .empty))
|
|
approvalVC = approvalView
|
|
approvalView.delegate = self
|
|
|
|
} else if isContactShare {
|
|
guard let cnContact = Contact.cnContact(withVCardData: firstAttachment.data),
|
|
let contactShareRecord = OWSContacts.contact(forSystemContact: cnContact) else {
|
|
throw OWSAssertionError("Missing or invalid contact data for contact share attachment")
|
|
}
|
|
|
|
var avatarImageData = contactsManager.avatarData(forCNContactId: cnContact.identifier)
|
|
|
|
if avatarImageData == nil {
|
|
let contact = Contact(systemContact: cnContact)
|
|
for address in contact.registeredAddresses() {
|
|
guard let data = contactsManagerImpl.profileImageDataForAddress(
|
|
withSneakyTransaction: address
|
|
) else { continue }
|
|
avatarImageData = data
|
|
contactShareRecord.isProfileAvatar = true
|
|
break
|
|
}
|
|
}
|
|
|
|
let contactShare = ContactShareViewModel(contactShareRecord: contactShareRecord, avatarImageData: avatarImageData)
|
|
let approvalView = ContactShareApprovalViewController(contactShare: contactShare)
|
|
approvalVC = approvalView
|
|
approvalView.delegate = self
|
|
|
|
} else {
|
|
let approvalView = AttachmentApprovalViewController(options: .hasCancel, sendButtonImageName: "send-solid-24", attachmentApprovalItems: attachments.map { AttachmentApprovalItem(attachment: $0, canSave: false) })
|
|
approvalVC = approvalView
|
|
approvalView.approvalDelegate = self
|
|
}
|
|
|
|
return approvalVC
|
|
}
|
|
}
|
|
|
|
// MARK: - Sending
|
|
|
|
extension SharingThreadPickerViewController {
|
|
|
|
func send() {
|
|
do {
|
|
try tryToSend()
|
|
} catch {
|
|
shareViewDelegate?.shareViewFailed(error: error)
|
|
}
|
|
}
|
|
|
|
func tryToSend() throws {
|
|
outgoingMessages.removeAll()
|
|
|
|
if isTextMessage {
|
|
guard let body = approvalMessageBody, body.text.count > 0 else {
|
|
throw OWSAssertionError("Missing body.")
|
|
}
|
|
|
|
let linkPreviewDraft = approvalLinkPreviewDraft
|
|
|
|
sendToThreads { thread in
|
|
return firstly(on: .global()) { () -> Promise<Void> in
|
|
return self.databaseStorage.write { transaction in
|
|
let preparer = OutgoingMessagePreparer(
|
|
messageBody: body,
|
|
mediaAttachments: [],
|
|
thread: thread,
|
|
quotedReplyModel: nil,
|
|
transaction: transaction
|
|
)
|
|
preparer.insertMessage(linkPreviewDraft: linkPreviewDraft, transaction: transaction)
|
|
self.outgoingMessages.append(preparer.unpreparedMessage)
|
|
return ThreadUtil.enqueueMessagePromise(
|
|
message: preparer.unpreparedMessage,
|
|
isHighPriority: true,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} else if isContactShare {
|
|
guard let contactShare = approvedContactShare else {
|
|
throw OWSAssertionError("Missing contactShare.")
|
|
}
|
|
|
|
sendToThreads { thread in
|
|
return firstly(on: .global()) { () -> Promise<Void> in
|
|
return self.databaseStorage.write { transaction in
|
|
let builder = TSOutgoingMessageBuilder(thread: thread)
|
|
builder.contactShare = contactShare.dbRecord
|
|
let dmConfiguration = thread.disappearingMessagesConfiguration(with: transaction)
|
|
builder.expiresInSeconds = dmConfiguration.isEnabled ? dmConfiguration.durationSeconds : 0
|
|
let message = builder.build()
|
|
message.anyInsert(transaction: transaction)
|
|
self.outgoingMessages.append(message)
|
|
return ThreadUtil.enqueueMessagePromise(
|
|
message: message,
|
|
isHighPriority: true,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
guard let approvedAttachments = approvedAttachments else {
|
|
throw OWSAssertionError("Missing approvedAttachments.")
|
|
}
|
|
|
|
sendToConversations { conversations in
|
|
return AttachmentMultisend.sendApprovedMediaFromShareExtension(
|
|
conversations: conversations,
|
|
approvalMessageBody: self.approvalMessageBody,
|
|
approvedAttachments: approvedAttachments,
|
|
messagesReadyToSend: { messages in
|
|
DispatchQueue.main.async { self.outgoingMessages = messages }
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func showSendProgress() -> (((() -> Void)?) -> Void) {
|
|
AssertIsOnMainThread()
|
|
|
|
let actionSheet = ActionSheetController()
|
|
|
|
let headerWithProgress = UIView()
|
|
headerWithProgress.backgroundColor = Theme.actionSheetBackgroundColor
|
|
headerWithProgress.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
|
|
|
let progressLabel = UILabel()
|
|
progressLabel.textAlignment = .center
|
|
progressLabel.numberOfLines = 0
|
|
progressLabel.lineBreakMode = .byWordWrapping
|
|
progressLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped.ows_semibold
|
|
progressLabel.textColor = Theme.primaryTextColor
|
|
progressLabel.text = NSLocalizedString("SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", comment: "Alert title")
|
|
|
|
headerWithProgress.addSubview(progressLabel)
|
|
progressLabel.autoPinWidthToSuperviewMargins()
|
|
progressLabel.autoPinTopToSuperviewMargin()
|
|
|
|
let progressView = UIProgressView(progressViewStyle: .default)
|
|
headerWithProgress.addSubview(progressView)
|
|
progressView.autoPinWidthToSuperviewMargins()
|
|
progressView.autoPinEdge(.top, to: .bottom, of: progressLabel, withOffset: 8)
|
|
progressView.autoPinBottomToSuperviewMargin()
|
|
|
|
actionSheet.customHeader = headerWithProgress
|
|
|
|
let cancelAction = ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel) { [weak self] _ in
|
|
self?.shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
actionSheet.addAction(cancelAction)
|
|
|
|
presentActionSheetOnNavigationController(actionSheet)
|
|
|
|
let progressFormat = NSLocalizedString("SHARE_EXTENSION_SENDING_IN_PROGRESS_FORMAT",
|
|
comment: "Send progress for share extension. Embeds {{ %1$@ number of attachments uploaded, %2$@ total number of attachments}}")
|
|
|
|
var progressPerAttachment = [String: Float]()
|
|
let observer = NotificationCenter.default.addObserver(
|
|
forName: .attachmentUploadProgress,
|
|
object: nil,
|
|
queue: nil
|
|
) { notification in
|
|
// We can safely show the progress for just the first message,
|
|
// all the messages share the same attachment upload progress.
|
|
guard let attachmentIds = self.outgoingMessages.first?.attachmentIds else { return }
|
|
|
|
// Populate the initial progress for all attachments at 0
|
|
if progressPerAttachment.isEmpty {
|
|
progressPerAttachment = Dictionary(uniqueKeysWithValues: attachmentIds.map { ($0, 0) })
|
|
}
|
|
|
|
guard let notificationAttachmentId = notification.userInfo?[kAttachmentUploadAttachmentIDKey] as? String else {
|
|
owsFailDebug("Missing notificationAttachmentId.")
|
|
return
|
|
}
|
|
guard let progress = notification.userInfo?[kAttachmentUploadProgressKey] as? NSNumber else {
|
|
owsFailDebug("Missing progress.")
|
|
return
|
|
}
|
|
|
|
guard attachmentIds.contains(notificationAttachmentId) else { return }
|
|
|
|
progressPerAttachment[notificationAttachmentId] = progress.floatValue
|
|
|
|
// Attachments can upload in parallel, so we show the progress
|
|
// of the average of all the individual attachment's progress.
|
|
progressView.progress = progressPerAttachment.values.reduce(0, +) / Float(attachmentIds.count)
|
|
|
|
// In order to indicate approximately how many attachments remain
|
|
// to upload, we look at the number that have had their progress
|
|
// reach 100%.
|
|
let totalCompleted = progressPerAttachment.values.filter { $0 == 1 }.count
|
|
|
|
progressLabel.text = String(
|
|
format: progressFormat,
|
|
OWSFormat.formatInt(min(totalCompleted + 1, attachmentIds.count)),
|
|
OWSFormat.formatInt(attachmentIds.count)
|
|
)
|
|
}
|
|
|
|
return { completion in
|
|
NotificationCenter.default.removeObserver(observer)
|
|
actionSheet.dismiss(animated: true, completion: completion)
|
|
}
|
|
}
|
|
|
|
func sendToConversations(enqueueBlock: @escaping ([ConversationItem]) -> Promise<[TSThread]>) {
|
|
AssertIsOnMainThread()
|
|
|
|
let dismissSendProgress = showSendProgress()
|
|
let conversations = self.selectedConversations
|
|
firstly {
|
|
enqueueBlock(conversations)
|
|
}.done { threads in
|
|
for thread in threads {
|
|
// We're sending a message to this thread, approve any pending message request
|
|
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread: thread)
|
|
}
|
|
|
|
dismissSendProgress {}
|
|
self.shareViewDelegate?.shareViewWasCompleted()
|
|
}.catch { error in
|
|
dismissSendProgress { self.showSendFailure(error: error) }
|
|
}
|
|
}
|
|
|
|
func sendToThreads(enqueueBlock: @escaping (TSThread) -> Promise<Void>) {
|
|
AssertIsOnMainThread()
|
|
|
|
let dismissSendProgress = showSendProgress()
|
|
let conversations = self.selectedConversations
|
|
firstly {
|
|
self.threads(for: conversations)
|
|
}.then { (threads: [TSThread]) -> Promise<Void> in
|
|
var sendPromises = [Promise<Void>]()
|
|
for thread in threads {
|
|
sendPromises.append(enqueueBlock(thread))
|
|
|
|
// We're sending a message to this thread, approve any pending message request
|
|
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(thread: thread)
|
|
}
|
|
return Promise.when(fulfilled: sendPromises)
|
|
}.done {
|
|
dismissSendProgress {}
|
|
self.shareViewDelegate?.shareViewWasCompleted()
|
|
}.catch { error in
|
|
dismissSendProgress { self.showSendFailure(error: error) }
|
|
}
|
|
}
|
|
|
|
func threads(for conversationItems: [ConversationItem]) -> Promise<[TSThread]> {
|
|
return firstly(on: .sharedUserInteractive) {
|
|
guard conversationItems.count > 0 else {
|
|
throw OWSAssertionError("No recipients.")
|
|
}
|
|
|
|
var threads: [TSThread] = []
|
|
|
|
self.databaseStorage.write { transaction in
|
|
for conversation in conversationItems {
|
|
guard let thread = conversation.thread(transaction: transaction) else {
|
|
owsFailDebug("Missing thread for conversation")
|
|
continue
|
|
}
|
|
threads.append(thread)
|
|
}
|
|
}
|
|
return threads
|
|
}
|
|
}
|
|
|
|
func showSendFailure(error: Error) {
|
|
AssertIsOnMainThread()
|
|
|
|
owsFailDebug("Error: \(error)")
|
|
|
|
let cancelAction = ActionSheetAction(
|
|
title: CommonStrings.cancelButton,
|
|
style: .cancel
|
|
) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
self.databaseStorage.write { transaction in
|
|
for message in self.outgoingMessages {
|
|
// If we sent the message to anyone, mark it as failed
|
|
message.updateWithAllSendingRecipientsMarkedAsFailed(with: transaction)
|
|
}
|
|
}
|
|
self.shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
let failureTitle = NSLocalizedString("SHARE_EXTENSION_SENDING_FAILURE_TITLE", comment: "Alert title")
|
|
|
|
if let untrustedIdentityError = error as? UntrustedIdentityError {
|
|
let untrustedAddress = untrustedIdentityError.address
|
|
let failureFormat = NSLocalizedString("SHARE_EXTENSION_FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_FORMAT",
|
|
comment: "alert body when sharing file failed because of untrusted/changed identity keys")
|
|
let displayName = self.contactsManager.displayName(for: untrustedAddress)
|
|
let failureMessage = String(format: failureFormat, displayName)
|
|
|
|
let actionSheet = ActionSheetController(title: failureTitle, message: failureMessage)
|
|
actionSheet.addAction(cancelAction)
|
|
|
|
let confirmAction = ActionSheetAction(
|
|
title: SafetyNumberStrings.confirmSendButton,
|
|
style: .default
|
|
) { [weak self] _ in
|
|
guard let self = self else { return }
|
|
|
|
// Confirm Identity
|
|
self.databaseStorage.write { transaction in
|
|
let verificationState = self.identityManager.verificationState(
|
|
for: untrustedAddress,
|
|
transaction: transaction
|
|
)
|
|
switch verificationState {
|
|
case .default:
|
|
// If we learned of a changed SN during send, then we've already recorded the new identity
|
|
// and there's nothing else we need to do for the resend to succeed.
|
|
// We don't want to redundantly set status to "default" because we would create a
|
|
// "You marked Alice as unverified" notice, which wouldn't make sense if Alice was never
|
|
// marked as "Verified".
|
|
Logger.info("recipient has acceptable verification status. Next send will succeed.")
|
|
case .noLongerVerified:
|
|
Logger.info("marked recipient: \(untrustedAddress) as default verification status.")
|
|
guard let indentityKey = self.identityManager.identityKey(
|
|
for: untrustedAddress,
|
|
transaction: transaction
|
|
) else { return owsFailDebug("missing identity key") }
|
|
|
|
self.identityManager.setVerificationState(
|
|
.default,
|
|
identityKey: indentityKey,
|
|
address: untrustedAddress,
|
|
isUserInitiatedChange: true,
|
|
transaction: transaction
|
|
)
|
|
case .verified:
|
|
owsFailDebug("Unexpected state")
|
|
}
|
|
}
|
|
|
|
// Resend
|
|
self.resendMessages()
|
|
}
|
|
actionSheet.addAction(confirmAction)
|
|
|
|
presentActionSheetOnNavigationController(actionSheet)
|
|
} else {
|
|
let actionSheet = ActionSheetController(title: failureTitle)
|
|
actionSheet.addAction(cancelAction)
|
|
|
|
let retryAction = ActionSheetAction(title: CommonStrings.retryButton, style: .default) { [weak self] _ in
|
|
self?.resendMessages()
|
|
}
|
|
actionSheet.addAction(retryAction)
|
|
|
|
presentActionSheetOnNavigationController(actionSheet)
|
|
}
|
|
}
|
|
|
|
func resendMessages() {
|
|
AssertIsOnMainThread()
|
|
owsAssertDebug(!outgoingMessages.isEmpty)
|
|
|
|
var promises = [Promise<Void>]()
|
|
databaseStorage.write { transaction in
|
|
for message in outgoingMessages {
|
|
promises.append(messageSenderJobQueue.add(
|
|
.promise,
|
|
message: message.asPreparer,
|
|
isHighPriority: true,
|
|
transaction: transaction
|
|
))
|
|
}
|
|
}
|
|
|
|
let dismissSendProgress = showSendProgress()
|
|
Promise.when(fulfilled: promises).done {
|
|
dismissSendProgress {}
|
|
self.shareViewDelegate?.shareViewWasCompleted()
|
|
}.catch { error in
|
|
dismissSendProgress { self.showSendFailure(error: error) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SharingThreadPickerViewController: ConversationPickerDelegate {
|
|
func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
|
|
updateMentionCandidates()
|
|
}
|
|
|
|
func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
|
|
approve()
|
|
}
|
|
|
|
func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool {
|
|
return true
|
|
}
|
|
|
|
func conversationPickerDidCancel(_ conversationPickerViewController: ConversationPickerViewController) {
|
|
shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode {
|
|
return attachments?.isEmpty != false ? .loading : .next
|
|
}
|
|
|
|
func conversationPickerDidBeginEditingText() {}
|
|
|
|
func conversationPickerSearchBarActiveDidChange(_ conversationPickerViewController: ConversationPickerViewController) {}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SharingThreadPickerViewController: TextApprovalViewControllerDelegate {
|
|
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?) {
|
|
assert(messageBody?.text.count ?? 0 > 0)
|
|
|
|
approvalMessageBody = messageBody
|
|
approvalLinkPreviewDraft = linkPreviewDraft
|
|
|
|
send()
|
|
}
|
|
|
|
func textApprovalDidCancel(_ textApproval: TextApprovalViewController) {
|
|
shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
func textApprovalCustomTitle(_ textApproval: TextApprovalViewController) -> String? {
|
|
return NSLocalizedString("FORWARD_MESSAGE", comment: "Label and title for 'message forwarding' views.")
|
|
}
|
|
|
|
func textApprovalRecipientsDescription(_ textApproval: TextApprovalViewController) -> String? {
|
|
let conversations = selectedConversations
|
|
guard conversations.count > 0 else {
|
|
return nil
|
|
}
|
|
return conversations.map { $0.title }.joined(separator: ", ")
|
|
}
|
|
|
|
func textApprovalMode(_ textApproval: TextApprovalViewController) -> ApprovalMode {
|
|
return .send
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SharingThreadPickerViewController: ContactShareApprovalViewControllerDelegate {
|
|
func approveContactShare(_ approveContactShare: ContactShareApprovalViewController,
|
|
didApproveContactShare contactShare: ContactShareViewModel) {
|
|
approvedContactShare = contactShare
|
|
|
|
send()
|
|
}
|
|
|
|
func approveContactShare(_ approveContactShare: ContactShareApprovalViewController,
|
|
didCancelContactShare contactShare: ContactShareViewModel) {
|
|
shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
func contactApprovalCustomTitle(_ contactApproval: ContactShareApprovalViewController) -> String? {
|
|
return NSLocalizedString("FORWARD_CONTACT", comment: "Label and title for 'contact forwarding' views.")
|
|
}
|
|
|
|
func contactApprovalRecipientsDescription(_ contactApproval: ContactShareApprovalViewController) -> String? {
|
|
let conversations = selectedConversations
|
|
guard conversations.count > 0 else {
|
|
return nil
|
|
}
|
|
return conversations.map { $0.title }.joined(separator: ", ")
|
|
}
|
|
|
|
func contactApprovalMode(_ contactApproval: ContactShareApprovalViewController) -> ApprovalMode {
|
|
return .send
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDelegate {
|
|
|
|
func attachmentApprovalDidAppear(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
// We can ignore this event.
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageBody newMessageBody: MessageBody?) {
|
|
self.approvalMessageBody = newMessageBody
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {
|
|
// We can ignore this event.
|
|
}
|
|
|
|
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageBody: MessageBody?) {
|
|
self.approvedAttachments = attachments
|
|
self.approvalMessageBody = messageBody
|
|
|
|
send()
|
|
}
|
|
|
|
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) {
|
|
owsFailDebug("Cannot add more to message forwards.")
|
|
}
|
|
|
|
var attachmentApprovalTextInputContextIdentifier: String? {
|
|
return nil
|
|
}
|
|
|
|
var attachmentApprovalRecipientNames: [String] {
|
|
selectedConversations.map { $0.title }
|
|
}
|
|
|
|
var attachmentApprovalMentionableAddresses: [SignalServiceAddress] {
|
|
mentionCandidates
|
|
}
|
|
}
|