In most cases when working with a ConversationItem we already have a write transaction, but sometimes we don't. In this case, we know you can only be blocked in a conversation if there's history in it (at least as far as the local device knows), and so we can ignore any threads that don't exist.
641 lines
25 KiB
Swift
641 lines
25 KiB
Swift
//
|
|
// Copyright (c) 2022 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.getOrCreateThread(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
|
|
}
|
|
}
|