Signal-iOS/Signal/src/ViewControllers/ForwardMessageViewController.swift
2021-09-03 11:41:34 -07:00

888 lines
35 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
public protocol ForwardMessageDelegate: AnyObject {
func forwardMessageFlowDidComplete(items: [ForwardMessageItem],
recipientThreads: [TSThread])
func forwardMessageFlowDidCancel()
}
// MARK: -
@objc
class ForwardMessageViewController: InteractiveSheetViewController {
private let pickerVC: ForwardPickerViewController
private let forwardNavigationViewController = ForwardNavigationViewController()
private let handle = UIView()
override var interactiveScrollViews: [UIScrollView] { [ pickerVC.tableView ] }
public weak var forwardMessageDelegate: ForwardMessageDelegate?
public typealias Item = ForwardMessageItem
fileprivate typealias Content = ForwardMessageContent
fileprivate typealias RecipientThread = ForwardMessageRecipientThread
fileprivate var content: Content
fileprivate var textMessage: String?
private let selection = ConversationPickerSelection()
var selectedConversations: [ConversationItem] { selection.conversations }
fileprivate var currentMentionableAddresses: [SignalServiceAddress] = []
private init(content: Content) {
self.content = content
self.pickerVC = ForwardPickerViewController(selection: selection)
super.init()
selectRecipientsStep()
}
required init() {
fatalError("init() has not been implemented")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public class func present(forItemViewModels itemViewModels: [CVItemViewModelImpl],
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate) {
do {
let content: Content = try Self.databaseStorage.read { transaction in
try Content.build(itemViewModels: itemViewModels, transaction: transaction)
}
present(content: content, from: fromViewController, delegate: delegate)
} catch {
ForwardMessageViewController.showAlertForForwardError(error: error,
forwardedInteractionCount: itemViewModels.count)
}
}
public class func present(forSelectionItems selectionItems: [CVSelectionItem],
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate) {
do {
let content: Content = try Self.databaseStorage.read { transaction in
try Content.build(selectionItems: selectionItems, transaction: transaction)
}
present(content: content, from: fromViewController, delegate: delegate)
} catch {
ForwardMessageViewController.showAlertForForwardError(error: error,
forwardedInteractionCount: selectionItems.count)
}
}
private class func present(content: Content,
from fromViewController: UIViewController,
delegate: ForwardMessageDelegate) {
let sheet = ForwardMessageViewController(content: content)
sheet.forwardMessageDelegate = delegate
fromViewController.present(sheet, animated: true) {
UIApplication.shared.hideKeyboard()
}
}
public override func themeDidChange() {
super.themeDidChange()
}
public override func applyTheme() {
AssertIsOnMainThread()
super.applyTheme()
contentView.backgroundColor = pickerVC.tableBackgroundColor
handle.backgroundColor = Theme.tableView2PresentedSeparatorColor
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
applyTheme()
}
private func selectRecipientsStep() {
handle.autoSetDimensions(to: CGSize(width: 36, height: 5))
handle.layer.cornerRadius = 5 / 2
let handleContainer = UIView()
handleContainer.addSubview(handle)
handle.autoPinHeightToSuperview(withMargin: 12)
handle.autoHCenterInSuperview()
pickerVC.forwardMessageViewController = self
pickerVC.shouldShowSearchBar = false
pickerVC.shouldHideSearchBarIfCancelled = true
pickerVC.pickerDelegate = self
forwardNavigationViewController.forwardMessageViewController = self
forwardNavigationViewController.viewControllers = [ pickerVC ]
self.addChild(forwardNavigationViewController)
let navView = forwardNavigationViewController.view!
let stackView = UIStackView(arrangedSubviews: [
handleContainer,
navView ])
stackView.axis = .vertical
stackView.alignment = .fill
self.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
applyTheme()
}
fileprivate func selectSearchBar() {
AssertIsOnMainThread()
pickerVC.selectSearchBar()
ensureHeaderVisibility()
}
fileprivate func ensureHeaderVisibility() {
AssertIsOnMainThread()
forwardNavigationViewController.setNavigationBarHidden(pickerVC.isSearchBarActive, animated: false)
if pickerVC.isSearchBarActive {
maximizeHeight()
}
}
public override func willDismissInteractively() {
AssertIsOnMainThread()
forwardMessageDelegate?.forwardMessageFlowDidCancel()
}
override var renderExternalHandle: Bool { false }
override var minHeight: CGFloat { 576 }
fileprivate func updateCurrentMentionableAddresses() {
guard selectedConversations.count == 1,
let conversationItem = selectedConversations.first else {
self.currentMentionableAddresses = []
return
}
do {
try databaseStorage.write { transaction in
let recipientThread = try RecipientThread.build(conversationItem: conversationItem,
transaction: transaction)
self.currentMentionableAddresses = recipientThread.mentionCandidates
}
} catch {
owsFailDebug("Error: \(error)")
self.currentMentionableAddresses = []
}
}
}
// MARK: - Sending
extension ForwardMessageViewController {
private static let keyValueStore = SDSKeyValueStore(collection: "ForwardMessageViewController")
private static let hasForwardedKey = "hasForwardedKey"
private var hasForwardedWithSneakyTransaction: Bool {
databaseStorage.read { transaction in
Self.keyValueStore.getBool(Self.hasForwardedKey, defaultValue: false, transaction: transaction)
}
}
private static func markHasForwardedWithSneakyTransaction() {
databaseStorage.write { transaction in
Self.keyValueStore.setBool(true, key: Self.hasForwardedKey, transaction: transaction)
}
}
func sendStep() {
if hasForwardedWithSneakyTransaction {
tryToSend()
} else {
showFirstForwardAlert()
}
}
private func showFirstForwardAlert() {
let actionSheet = ActionSheetController(
title: NSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_TITLE",
comment: "Title for alert with information about forwarding messages."),
message: NSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_MESSAGE",
comment: "Message for alert with information about forwarding messages.")
)
let actionTitle: String
if content.allItems.count > 1 {
let format = NSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_PROCEED_N_FORMAT",
comment: "Format for label for button to proceed with forwarding multiple messages. Embeds: {{ the number of forwarded messages. }}")
actionTitle = String(format: format, OWSFormat.formatInt(content.allItems.count))
} else {
actionTitle = NSLocalizedString("FORWARD_MESSAGE_FIRST_FORWARD_PROCEED_1",
comment: "Label for button to proceed with forwarding a single message.")
}
actionSheet.addAction(ActionSheetAction(title: actionTitle) { [weak self] _ in
Self.markHasForwardedWithSneakyTransaction()
self?.tryToSend()
})
actionSheet.addAction(OWSActionSheets.cancelAction)
presentActionSheet(actionSheet)
}
private func tryToSend() {
AssertIsOnMainThread()
do {
try tryToSendThrows()
} catch {
owsFailDebug("Error: \(error)")
self.forwardMessageDelegate?.forwardMessageFlowDidCancel()
}
}
private func tryToSendThrows() throws {
let content = self.content
let textMessage = self.textMessage?.strippedOrNil
let recipientConversations = self.selectedConversations
firstly(on: .global()) {
self.recipientThreads(for: recipientConversations)
}.then(on: .main) { (recipientThreads: [RecipientThread]) -> Promise<Void> in
try Self.databaseStorage.write { transaction in
for recipientThread in recipientThreads {
// We're sending a message to this thread, approve any pending message request
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread: recipientThread.thread,
transaction: transaction)
}
func hasRenderableContent(interaction: TSInteraction) -> Bool {
guard let message = interaction as? TSMessage else {
return false
}
return message.hasRenderableContent()
}
// Make sure the message and its content haven't been deleted (view-once
// messages, remove delete, disappearing messages, manual deletion, etc.).
for item in content.allItems {
let interactionId = item.interaction.uniqueId
guard let latestInteraction = TSInteraction.anyFetch(uniqueId: interactionId, transaction: transaction),
hasRenderableContent(interaction: latestInteraction) else {
throw ForwardError.missingInteraction
}
}
}
// TODO: Ideally we would enqueue all with a single write tranasction.
return firstly { () -> Promise<Void> in
// Maintain order of interactions.
let sortedItems = content.allItems.sorted { lhs, rhs in
lhs.interaction.timestamp < rhs.interaction.timestamp
}
let promises: [Promise<Void>] = sortedItems.map { item in
self.send(item: item, toRecipientThreads: recipientThreads)
}
return firstly(on: .main) { () -> Promise<Void> in
Promise.when(resolved: promises).asVoid()
}.then(on: .main) { _ -> Promise<Void> in
// The user may have added an additional text message to the forward.
// It should be sent last.
if let textMessage = textMessage {
let messageBody = MessageBody(text: textMessage, ranges: .empty)
return self.send(toRecipientThreads: recipientThreads) { recipientThread in
self.send(body: messageBody,
linkPreviewDraft: nil,
thread: recipientThread.thread)
}
} else {
return Promise.value(())
}
}
}.map(on: .main) {
let threads = recipientThreads.map { $0.thread }
self.forwardMessageDelegate?.forwardMessageFlowDidComplete(items: content.allItems,
recipientThreads: threads)
}
}.catch(on: .main) { error in
owsFailDebug("Error: \(error)")
Self.showAlertForForwardError(error: error, forwardedInteractionCount: content.allItems.count)
}
}
private func send(item: Item, toRecipientThreads recipientThreads: [RecipientThread]) -> Promise<Void> {
AssertIsOnMainThread()
let componentState = item.componentState
if let stickerMetadata = item.stickerMetadata {
let stickerInfo = stickerMetadata.stickerInfo
if StickerManager.isStickerInstalled(stickerInfo: stickerInfo) {
return send(toRecipientThreads: recipientThreads) { recipientThread in
self.send(installedSticker: stickerInfo, thread: recipientThread.thread)
}
} else {
guard let stickerAttachment = componentState.stickerAttachment else {
return Promise(error: OWSAssertionError("Missing stickerAttachment."))
}
do {
let stickerData = try stickerAttachment.readDataFromFile()
return send(toRecipientThreads: recipientThreads) { recipientThread in
self.send(uninstalledSticker: stickerMetadata,
stickerData: stickerData,
thread: recipientThread.thread)
}
} catch {
return Promise(error: error)
}
}
} else if let contactShare = item.contactShare {
return send(toRecipientThreads: recipientThreads) { recipientThread in
if let avatarImage = contactShare.avatarImage {
self.databaseStorage.write { transaction in
contactShare.dbRecord.saveAvatarImage(avatarImage, transaction: transaction)
}
}
return self.send(contactShare: contactShare, thread: recipientThread.thread)
}
} else if let attachments = item.attachments,
!attachments.isEmpty {
// TODO: What about link previews in this case?
let conversations = selectedConversations
return AttachmentMultisend.sendApprovedMedia(conversations: conversations,
approvalMessageBody: item.messageBody,
approvedAttachments: attachments).asVoid()
} else if let messageBody = item.messageBody {
let linkPreviewDraft = item.linkPreviewDraft
return send(toRecipientThreads: recipientThreads) { recipientThread in
self.send(body: messageBody,
linkPreviewDraft: linkPreviewDraft,
thread: recipientThread.thread)
}
} else {
return Promise(error: ForwardError.invalidInteraction)
}
}
fileprivate func send(body: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?, thread: TSThread) -> Promise<Void> {
databaseStorage.read { transaction in
ThreadUtil.enqueueMessage(with: body,
thread: thread,
quotedReplyModel: nil,
linkPreviewDraft: linkPreviewDraft,
transaction: transaction)
}
return Promise.value(())
}
fileprivate func send(contactShare: ContactShareViewModel, thread: TSThread) -> Promise<Void> {
ThreadUtil.enqueueMessage(withContactShare: contactShare.dbRecord, thread: thread)
return Promise.value(())
}
fileprivate func send(body: MessageBody?, attachment: SignalAttachment, thread: TSThread) -> Promise<Void> {
databaseStorage.read { transaction in
ThreadUtil.enqueueMessage(with: body,
mediaAttachments: [attachment],
thread: thread,
quotedReplyModel: nil,
linkPreviewDraft: nil,
transaction: transaction)
}
return Promise.value(())
}
fileprivate func send(installedSticker stickerInfo: StickerInfo, thread: TSThread) -> Promise<Void> {
ThreadUtil.enqueueMessage(withInstalledSticker: stickerInfo, thread: thread)
return Promise.value(())
}
fileprivate func send(uninstalledSticker stickerMetadata: StickerMetadata, stickerData: Data, thread: TSThread) -> Promise<Void> {
ThreadUtil.enqueueMessage(withUninstalledSticker: stickerMetadata, stickerData: stickerData, thread: thread)
return Promise.value(())
}
fileprivate func send(toRecipientThreads recipientThreads: [RecipientThread],
enqueueBlock: @escaping (RecipientThread) -> Promise<Void>) -> Promise<Void> {
AssertIsOnMainThread()
return Promise.when(fulfilled: recipientThreads.map { thread in enqueueBlock(thread) }).asVoid()
}
fileprivate func recipientThreads(for conversationItems: [ConversationItem]) -> Promise<[RecipientThread]> {
firstly(on: .global()) {
guard conversationItems.count > 0 else {
throw OWSAssertionError("No recipients.")
}
return try self.databaseStorage.write { transaction in
try conversationItems.map {
try RecipientThread.build(conversationItem: $0, transaction: transaction)
}
}
}
}
}
// MARK: -
extension ForwardMessageViewController: ConversationPickerDelegate {
func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
updateCurrentMentionableAddresses()
}
func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
self.textMessage = conversationPickerViewController.textInput?.strippedOrNil
sendStep()
}
func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool {
false
}
func conversationPickerDidCancel(_ conversationPickerViewController: ConversationPickerViewController) {
forwardMessageDelegate?.forwardMessageFlowDidCancel()
}
func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode {
.send
}
var conversationPickerHasTextInput: Bool { true }
var conversationPickerTextInputDefaultText: String? {
NSLocalizedString("FORWARD_MESSAGE_TEXT_PLACEHOLDER",
comment: "Indicates that the user can add a text message to forwarded messages.")
}
func conversationPickerDidBeginEditingText() {
AssertIsOnMainThread()
maximizeHeight()
}
func conversationPickerSearchBarActiveDidChange(_ conversationPickerViewController: ConversationPickerViewController) {
ensureHeaderVisibility()
}
}
// MARK: -
extension TSAttachmentStream {
func cloneAsSignalAttachment() throws -> SignalAttachment {
guard let sourceUrl = originalMediaURL else {
throw OWSAssertionError("Missing originalMediaURL.")
}
guard let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) else {
throw OWSAssertionError("Missing dataUTI.")
}
let newUrl = OWSFileSystem.temporaryFileUrl(fileExtension: sourceUrl.pathExtension)
try FileManager.default.copyItem(at: sourceUrl, to: newUrl)
let clonedDataSource = try DataSourcePath.dataSource(with: newUrl,
shouldDeleteOnDeallocation: true)
clonedDataSource.sourceFilename = sourceFilename
var signalAttachment: SignalAttachment
if isVoiceMessage {
signalAttachment = SignalAttachment.voiceMessageAttachment(dataSource: clonedDataSource, dataUTI: dataUTI)
} else {
signalAttachment = SignalAttachment.attachment(dataSource: clonedDataSource, dataUTI: dataUTI)
}
signalAttachment.captionText = caption
signalAttachment.isBorderless = isBorderless
signalAttachment.isLoopingVideo = isLoopingVideo
return signalAttachment
}
}
// MARK: -
extension ForwardMessageViewController {
public static func finalizeForward(items: [Item],
recipientThreads: [TSThread],
fromViewController: UIViewController) {
let toast: String
if items.count > 1 {
toast = NSLocalizedString("FORWARD_MESSAGE_MESSAGES_SENT_N",
comment: "Indicates that multiple messages were forwarded.")
} else {
toast = NSLocalizedString("FORWARD_MESSAGE_MESSAGES_SENT_1",
comment: "Indicates that a single message was forwarded.")
}
fromViewController.presentToast(text: toast)
}
}
// MARK: -
public enum ForwardError: Error {
case missingInteraction
case missingThread
case invalidInteraction
}
// MARK: -
extension ForwardMessageViewController {
public static func showAlertForForwardError(error: Error,
forwardedInteractionCount: Int) {
let genericErrorMessage = (forwardedInteractionCount > 1
? NSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_N",
comment: "Error indicating that messages could not be forwarded.")
: NSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_1",
comment: "Error indicating that a message could not be forwarded."))
guard let forwardError = error as? ForwardError else {
owsFailDebug("Error: \(error).")
OWSActionSheets.showErrorAlert(message: genericErrorMessage)
return
}
switch forwardError {
case .missingInteraction:
let message = (forwardedInteractionCount > 1
? NSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_MISSING_N",
comment: "Error indicating that messages could not be forwarded.")
: NSLocalizedString("ERROR_COULD_NOT_FORWARD_MESSAGES_MISSING_1",
comment: "Error indicating that a message could not be forwarded."))
OWSActionSheets.showErrorAlert(message: message)
case .missingThread, .invalidInteraction:
owsFailDebug("Error: \(error).")
OWSActionSheets.showErrorAlert(message: genericErrorMessage)
}
}
}
// MARK: -
public struct ForwardMessageItem {
fileprivate typealias Item = ForwardMessageItem
let interaction: TSInteraction
let componentState: CVComponentState
let attachments: [SignalAttachment]?
let contactShare: ContactShareViewModel?
let messageBody: MessageBody?
let linkPreviewDraft: OWSLinkPreviewDraft?
let stickerMetadata: StickerMetadata?
fileprivate class Builder {
let interaction: TSInteraction
let componentState: CVComponentState
var attachments: [SignalAttachment]?
var contactShare: ContactShareViewModel?
var messageBody: MessageBody?
var linkPreviewDraft: OWSLinkPreviewDraft?
var stickerMetadata: StickerMetadata?
init(interaction: TSInteraction, componentState: CVComponentState) {
self.interaction = interaction
self.componentState = componentState
}
func build() -> ForwardMessageItem {
ForwardMessageItem(interaction: interaction,
componentState: componentState,
attachments: attachments,
contactShare: contactShare,
messageBody: messageBody,
linkPreviewDraft: linkPreviewDraft,
stickerMetadata: stickerMetadata)
}
}
fileprivate var asBuilder: Builder {
let builder = Builder(interaction: interaction, componentState: componentState)
builder.attachments = attachments
builder.contactShare = contactShare
builder.messageBody = messageBody
builder.linkPreviewDraft = linkPreviewDraft
builder.stickerMetadata = stickerMetadata
return builder
}
var isEmpty: Bool {
if let attachments = attachments,
!attachments.isEmpty {
return false
}
if contactShare != nil ||
messageBody != nil ||
stickerMetadata != nil {
return false
}
return true
}
fileprivate static func build(interaction: TSInteraction,
componentState: CVComponentState,
selectionType: CVSelectionType,
transaction: SDSAnyReadTransaction) throws -> Item {
let builder = Builder(interaction: interaction, componentState: componentState)
let shouldHaveText = (selectionType == .allContent ||
selectionType == .secondaryContent)
let shouldHaveAttachments = (selectionType == .allContent ||
selectionType == .primaryContent)
guard shouldHaveText || shouldHaveAttachments else {
throw ForwardError.invalidInteraction
}
if shouldHaveText,
let displayableBodyText = componentState.displayableBodyText,
!displayableBodyText.fullAttributedText.isEmpty {
let attributedText = displayableBodyText.fullAttributedText
builder.messageBody = MessageBody(attributedString: attributedText)
if let linkPreview = componentState.linkPreviewModel {
builder.linkPreviewDraft = Self.tryToCloneLinkPreview(linkPreview: linkPreview,
transaction: transaction)
}
}
if shouldHaveAttachments {
if let oldContactShare = componentState.contactShareModel {
builder.contactShare = oldContactShare.copyForResending()
}
var attachmentStreams = [TSAttachmentStream]()
attachmentStreams.append(contentsOf: componentState.bodyMediaAttachmentStreams)
if let attachmentStream = componentState.audioAttachmentStream {
attachmentStreams.append(attachmentStream)
}
if let attachmentStream = componentState.genericAttachmentStream {
attachmentStreams.append(attachmentStream)
}
if !attachmentStreams.isEmpty {
builder.attachments = try attachmentStreams.map { attachmentStream in
try attachmentStream.cloneAsSignalAttachment()
}
}
if let stickerMetadata = componentState.stickerMetadata {
builder.stickerMetadata = stickerMetadata
}
}
let item = builder.build()
guard !item.isEmpty else {
throw ForwardError.invalidInteraction
}
return item
}
private static func tryToCloneLinkPreview(linkPreview: OWSLinkPreview,
transaction: SDSAnyReadTransaction) -> OWSLinkPreviewDraft? {
guard let urlString = linkPreview.urlString,
let url = URL(string: urlString) else {
owsFailDebug("Missing or invalid urlString.")
return nil
}
struct LinkPreviewImage {
let imageData: Data
let mimetype: String
static func load(attachmentId: String,
transaction: SDSAnyReadTransaction) -> LinkPreviewImage? {
guard let attachment = TSAttachmentStream.anyFetchAttachmentStream(uniqueId: attachmentId,
transaction: transaction) else {
owsFailDebug("Missing attachment.")
return nil
}
guard let mimeType = attachment.contentType.nilIfEmpty else {
owsFailDebug("Missing mimeType.")
return nil
}
do {
let imageData = try attachment.readDataFromFile()
return LinkPreviewImage(imageData: imageData, mimetype: mimeType)
} catch {
owsFailDebug("Error: \(error).")
return nil
}
}
}
var linkPreviewImage: LinkPreviewImage?
if let imageAttachmentId = linkPreview.imageAttachmentId,
let image = LinkPreviewImage.load(attachmentId: imageAttachmentId,
transaction: transaction) {
linkPreviewImage = image
}
let draft = OWSLinkPreviewDraft(url: url,
title: linkPreview.title,
imageData: linkPreviewImage?.imageData,
imageMimeType: linkPreviewImage?.mimetype)
draft.previewDescription = linkPreview.previewDescription
draft.date = linkPreview.date
return draft
}
}
// MARK: -
private enum ForwardMessageContent {
fileprivate typealias Item = ForwardMessageItem
case single(item: Item)
case multiple(items: [Item])
var allItems: [Item] {
switch self {
case .single(let item):
return [item]
case .multiple(let items):
return items
}
}
static func build(items: [Item]) -> ForwardMessageContent {
if items.count == 1, let item = items.first {
return .single(item: item)
} else {
return .multiple(items: items)
}
}
static func build(itemViewModels: [CVItemViewModelImpl],
transaction: SDSAnyReadTransaction) throws -> ForwardMessageContent {
let items: [Item] = try itemViewModels.map { itemViewModel in
try Item.build(interaction: itemViewModel.interaction,
componentState: itemViewModel.renderItem.componentState,
selectionType: .allContent,
transaction: transaction)
}
return build(items: items)
}
static func build(selectionItems: [CVSelectionItem],
transaction: SDSAnyReadTransaction) throws -> ForwardMessageContent {
let items: [Item] = try selectionItems.map { selectionItem in
let interactionId = selectionItem.interactionId
guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId,
transaction: transaction) else {
throw ForwardError.invalidInteraction
}
let componentState = try buildComponentState(interactionId: interactionId,
transaction: transaction)
return try Item.build(interaction: interaction,
componentState: componentState,
selectionType: selectionItem.selectionType,
transaction: transaction)
}
return build(items: items)
}
private static func buildComponentState(interactionId: String,
transaction: SDSAnyReadTransaction) throws -> CVComponentState {
guard let interaction = TSInteraction.anyFetch(uniqueId: interactionId,
transaction: transaction) else {
throw ForwardError.missingInteraction
}
guard let componentState = CVLoader.buildStandaloneComponentState(interaction: interaction,
transaction: transaction) else {
throw ForwardError.invalidInteraction
}
return componentState
}
}
// MARK: -
private struct ForwardMessageRecipientThread {
let thread: TSThread
let mentionCandidates: [SignalServiceAddress]
static func build(conversationItem: ConversationItem,
transaction: SDSAnyWriteTransaction) throws -> ForwardMessageRecipientThread {
guard let thread = conversationItem.thread(transaction: transaction) else {
owsFailDebug("Missing thread for conversation")
throw ForwardError.missingThread
}
let mentionCandidates = self.mentionCandidates(conversationItem: conversationItem,
thread: thread,
transaction: transaction)
return ForwardMessageRecipientThread(thread: thread, mentionCandidates: mentionCandidates)
}
private static func mentionCandidates(conversationItem: ConversationItem,
thread: TSThread,
transaction: SDSAnyReadTransaction) -> [SignalServiceAddress] {
guard let groupThread = thread as? TSGroupThread,
Mention.threadAllowsMentionSend(groupThread) else {
return []
}
return groupThread.recipientAddresses
}
}
// MARK: -
private class ForwardNavigationViewController: OWSNavigationController {
weak var forwardMessageViewController: ForwardMessageViewController?
}
// MARK: -
private class ForwardPickerViewController: ConversationPickerViewController {
weak var forwardMessageViewController: ForwardMessageViewController?
@objc
public override func viewDidLoad() {
super.viewDidLoad()
updateNavigationItem()
}
public func updateNavigationItem() {
title = NSLocalizedString("FORWARD_MESSAGE_TITLE",
comment: "Title for the 'forward message(s)' view.")
navigationItem.leftBarButtonItem = UIBarButtonItem(image: Theme.iconImage(.cancel20),
style: .plain,
target: self,
action: #selector(didPressCancel))
navigationItem.rightBarButtonItem = UIBarButtonItem(image: Theme.iconImage(.settingsSearch),
style: .plain,
target: self,
action: #selector(didPressSearch))
}
@objc
private func didPressCancel() {
AssertIsOnMainThread()
pickerDelegate?.conversationPickerDidCancel(self)
}
@objc
private func didPressSearch() {
AssertIsOnMainThread()
forwardMessageViewController?.selectSearchBar()
}
}