624 lines
24 KiB
Swift
624 lines
24 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import PromiseKit
|
|
|
|
// MARK: -
|
|
|
|
@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 selectedConversations: [ConversationItem] = []
|
|
|
|
@objc
|
|
public init(shareViewDelegate: ShareViewDelegate) {
|
|
self.shareViewDelegate = shareViewDelegate
|
|
super.init()
|
|
delegate = self
|
|
}
|
|
|
|
public func presentActionSheetOnNavigationController(_ alert: ActionSheetController) {
|
|
if let navigationController = shareViewDelegate?.shareViewNavigationController {
|
|
navigationController.presentActionSheet(alert)
|
|
} else {
|
|
super.presentActionSheet(alert)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
selectedConversations.append(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
|
|
let (promise, resolver) = Promise<Void>.pending()
|
|
|
|
let message = self.databaseStorage.read { transaction in
|
|
return ThreadUtil.sendMessageNonDurably(
|
|
body: body,
|
|
thread: thread,
|
|
quotedReplyModel: nil,
|
|
linkPreviewDraft: linkPreviewDraft,
|
|
transaction: transaction
|
|
) { error in
|
|
if let error = error {
|
|
resolver.reject(error)
|
|
} else {
|
|
resolver.fulfill(())
|
|
}
|
|
}
|
|
}
|
|
|
|
self.outgoingMessages.append(message)
|
|
|
|
return promise
|
|
}
|
|
} else if isContactShare {
|
|
guard let contactShare = approvedContactShare else {
|
|
throw OWSAssertionError("Missing contactShare.")
|
|
}
|
|
|
|
sendToThreads { thread in
|
|
let (promise, resolver) = Promise<Void>.pending()
|
|
|
|
if let avatarImage = contactShare.avatarImage {
|
|
self.databaseStorage.write { transaction in
|
|
contactShare.dbRecord.saveAvatarImage(avatarImage, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
let message = ThreadUtil.sendMessageNonDurably(
|
|
contactShare: contactShare.dbRecord,
|
|
thread: thread
|
|
) { error in
|
|
if let error = error {
|
|
resolver.reject(error)
|
|
} else {
|
|
resolver.fulfill(())
|
|
}
|
|
}
|
|
|
|
self.outgoingMessages.append(message)
|
|
|
|
return promise
|
|
}
|
|
} else {
|
|
guard let approvedAttachments = approvedAttachments else {
|
|
throw OWSAssertionError("Missing approvedAttachments.")
|
|
}
|
|
|
|
sendToConversations { conversations in
|
|
return AttachmentMultisend.sendApprovedMediaNonDurably(
|
|
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 seenAttachmentIds = Set<String>()
|
|
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 }
|
|
|
|
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 }
|
|
|
|
seenAttachmentIds.insert(notificationAttachmentId)
|
|
|
|
// Attachments upload one at a time, so we can infer that
|
|
// the number of attachments we've seen progress updates
|
|
// for is which attachment we're uploading.
|
|
progressLabel.text = String(
|
|
format: progressFormat,
|
|
OWSFormat.formatInt(seenAttachmentIds.count),
|
|
OWSFormat.formatInt(attachmentIds.count)
|
|
)
|
|
progressView.progress = progress.floatValue
|
|
}
|
|
|
|
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.selectedConversationsForConversationPicker
|
|
firstly {
|
|
enqueueBlock(conversations)
|
|
}.done { threads in
|
|
for thread in threads {
|
|
// We're sending a message to this thread, approve any pending message request
|
|
ThreadUtil.addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(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.selectedConversationsForConversationPicker
|
|
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.addToProfileWhitelistIfEmptyOrPendingRequestWithSneakyTransaction(thread: thread)
|
|
}
|
|
return 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(withTansaction: transaction)
|
|
}
|
|
}
|
|
self.shareViewDelegate?.shareViewWasCancelled()
|
|
}
|
|
|
|
let failureTitle = NSLocalizedString("SHARE_EXTENSION_SENDING_FAILURE_TITLE", comment: "Alert title")
|
|
|
|
let nsError = error as NSError
|
|
if nsError.domain == OWSSignalServiceKitErrorDomain, nsError.code == OWSErrorCode.untrustedIdentity.rawValue {
|
|
guard let untrustedAddress = nsError.userInfo[OWSErrorRecipientAddressKey] as? SignalServiceAddress else {
|
|
return owsFailDebug("Missing 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>]()
|
|
for message in outgoingMessages {
|
|
promises.append(messageSender.sendMessage(.promise, message.asPreparer))
|
|
}
|
|
|
|
let dismissSendProgress = showSendProgress()
|
|
when(fulfilled: promises).done {
|
|
dismissSendProgress {}
|
|
self.shareViewDelegate?.shareViewWasCompleted()
|
|
}.catch { error in
|
|
dismissSendProgress { self.showSendFailure(error: error) }
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension SharingThreadPickerViewController: ConversationPickerDelegate {
|
|
var selectedConversationsForConversationPicker: [ConversationItem] {
|
|
return selectedConversations
|
|
}
|
|
|
|
func conversationPicker(_ conversationPickerViewController: ConversationPickerViewController,
|
|
didSelectConversation conversation: ConversationItem) {
|
|
selectedConversations.append(conversation)
|
|
}
|
|
|
|
func conversationPicker(_ conversationPickerViewController: ConversationPickerViewController,
|
|
didDeselectConversation conversation: ConversationItem) {
|
|
selectedConversations.removeAll { $0.messageRecipient == conversation.messageRecipient }
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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 = selectedConversationsForConversationPicker
|
|
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 = selectedConversationsForConversationPicker
|
|
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] {
|
|
selectedConversationsForConversationPicker.map { $0.title }
|
|
}
|
|
|
|
var attachmentApprovalMentionableAddresses: [SignalServiceAddress] {
|
|
guard selectedConversationsForConversationPicker.count == 1,
|
|
case .group(let groupThreadId) = selectedConversationsForConversationPicker.first?.messageRecipient,
|
|
let groupThread = databaseStorage.uiRead(block: { transaction in
|
|
return TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: transaction)
|
|
}),
|
|
Mention.threadAllowsMentionSend(groupThread) else { return [] }
|
|
return groupThread.recipientAddresses
|
|
}
|
|
}
|