From 964d2fdffa50c6c016cfb70e0d4e4883e867ad67 Mon Sep 17 00:00:00 2001 From: Nora Trapp Date: Mon, 7 Mar 2022 12:20:21 -0800 Subject: [PATCH] Fix forwarding of messages with mentions --- .../ForwardMessageViewController.swift | 100 +++++------------- .../src/Messages/MessageBody.swift | 44 +++++++- SignalUI/Utils/AttachmentMultisend.swift | 8 +- 3 files changed, 75 insertions(+), 77 deletions(-) diff --git a/Signal/src/ViewControllers/ForwardMessageViewController.swift b/Signal/src/ViewControllers/ForwardMessageViewController.swift index c14a72ace2..6e6da0c940 100644 --- a/Signal/src/ViewControllers/ForwardMessageViewController.swift +++ b/Signal/src/ViewControllers/ForwardMessageViewController.swift @@ -2,6 +2,8 @@ // Copyright (c) 2022 Open Whisper Systems. All rights reserved. // +import SignalServiceKit + public protocol ForwardMessageDelegate: AnyObject { func forwardMessageFlowDidComplete(items: [ForwardMessageItem], recipientThreads: [TSThread]) @@ -24,7 +26,6 @@ class ForwardMessageViewController: InteractiveSheetViewController { public typealias Item = ForwardMessageItem fileprivate typealias Content = ForwardMessageContent - fileprivate typealias RecipientThread = ForwardMessageRecipientThread fileprivate var content: Content @@ -33,8 +34,6 @@ class ForwardMessageViewController: InteractiveSheetViewController { 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) @@ -177,25 +176,6 @@ class ForwardMessageViewController: InteractiveSheetViewController { 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 @@ -269,11 +249,11 @@ extension ForwardMessageViewController { let recipientConversations = self.selectedConversations firstly(on: .global()) { self.recipientThreads(for: recipientConversations) - }.then(on: .main) { (recipientThreads: [RecipientThread]) -> Promise in + }.then(on: .main) { (recipientThreads: [TSThread]) -> Promise 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, + ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread: recipientThread, transaction: transaction) } @@ -314,16 +294,15 @@ extension ForwardMessageViewController { return self.send(toRecipientThreads: recipientThreads) { recipientThread in self.send(body: messageBody, linkPreviewDraft: nil, - thread: recipientThread.thread) + recipientThread: recipientThread) } } else { return Promise.value(()) } } }.map(on: .main) { - let threads = recipientThreads.map { $0.thread } self.forwardMessageDelegate?.forwardMessageFlowDidComplete(items: content.allItems, - recipientThreads: threads) + recipientThreads: recipientThreads) } }.catch(on: .main) { error in owsFailDebug("Error: \(error)") @@ -332,7 +311,7 @@ extension ForwardMessageViewController { } } - private func send(item: Item, toRecipientThreads recipientThreads: [RecipientThread]) -> Promise { + private func send(item: Item, toRecipientThreads recipientThreads: [TSThread]) -> Promise { AssertIsOnMainThread() let componentState = item.componentState @@ -341,7 +320,7 @@ extension ForwardMessageViewController { let stickerInfo = stickerMetadata.stickerInfo if StickerManager.isStickerInstalled(stickerInfo: stickerInfo) { return send(toRecipientThreads: recipientThreads) { recipientThread in - self.send(installedSticker: stickerInfo, thread: recipientThread.thread) + self.send(installedSticker: stickerInfo, thread: recipientThread) } } else { guard let stickerAttachment = componentState.stickerAttachment else { @@ -352,7 +331,7 @@ extension ForwardMessageViewController { return send(toRecipientThreads: recipientThreads) { recipientThread in self.send(uninstalledSticker: stickerMetadata, stickerData: stickerData, - thread: recipientThread.thread) + thread: recipientThread) } } catch { return Promise(error: error) @@ -365,7 +344,7 @@ extension ForwardMessageViewController { contactShare.dbRecord.saveAvatarImage(avatarImage, transaction: transaction) } } - return self.send(contactShare: contactShare, thread: recipientThread.thread) + return self.send(contactShare: contactShare, thread: recipientThread) } } else if let attachments = item.attachments, !attachments.isEmpty { @@ -379,20 +358,22 @@ extension ForwardMessageViewController { return send(toRecipientThreads: recipientThreads) { recipientThread in self.send(body: messageBody, linkPreviewDraft: linkPreviewDraft, - thread: recipientThread.thread) + recipientThread: recipientThread) } } else { return Promise(error: ForwardError.invalidInteraction) } } - fileprivate func send(body: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?, thread: TSThread) -> Promise { + fileprivate func send(body: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?, recipientThread: TSThread) -> Promise { databaseStorage.read { transaction in - ThreadUtil.enqueueMessage(body: body, - thread: thread, - quotedReplyModel: nil, - linkPreviewDraft: linkPreviewDraft, - transaction: transaction) + ThreadUtil.enqueueMessage( + body: body.forNewContext(recipientThread, transaction: transaction.unwrapGrdbRead), + thread: recipientThread, + quotedReplyModel: nil, + linkPreviewDraft: linkPreviewDraft, + transaction: transaction + ) } return Promise.value(()) } @@ -424,14 +405,14 @@ extension ForwardMessageViewController { return Promise.value(()) } - fileprivate func send(toRecipientThreads recipientThreads: [RecipientThread], - enqueueBlock: @escaping (RecipientThread) -> Promise) -> Promise { + fileprivate func send(toRecipientThreads recipientThreads: [TSThread], + enqueueBlock: @escaping (TSThread) -> Promise) -> Promise { AssertIsOnMainThread() return Promise.when(fulfilled: recipientThreads.map { thread in enqueueBlock(thread) }).asVoid() } - fileprivate func recipientThreads(for conversationItems: [ConversationItem]) -> Promise<[RecipientThread]> { + fileprivate func recipientThreads(for conversationItems: [ConversationItem]) -> Promise<[TSThread]> { firstly(on: .global()) { guard conversationItems.count > 0 else { throw OWSAssertionError("No recipients.") @@ -439,7 +420,10 @@ extension ForwardMessageViewController { return try self.databaseStorage.write { transaction in try conversationItems.map { - try RecipientThread.build(conversationItem: $0, transaction: transaction) + guard let thread = $0.getOrCreateThread(transaction: transaction) else { + throw ForwardError.missingThread + } + return thread } } } @@ -450,7 +434,6 @@ extension ForwardMessageViewController { extension ForwardMessageViewController: ConversationPickerDelegate { func conversationPickerSelectionDidChange(_ conversationPickerViewController: ConversationPickerViewController) { - updateCurrentMentionableAddresses() ensureBottomFooterVisibility() } @@ -814,37 +797,6 @@ private enum ForwardMessageContent { // MARK: - -private struct ForwardMessageRecipientThread { - let thread: TSThread - let mentionCandidates: [SignalServiceAddress] - - static func build(conversationItem: ConversationItem, - transaction: SDSAnyWriteTransaction) throws -> ForwardMessageRecipientThread { - - guard let thread = conversationItem.getOrCreateThread(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? diff --git a/SignalServiceKit/src/Messages/MessageBody.swift b/SignalServiceKit/src/Messages/MessageBody.swift index 32d09b4954..f760aef164 100644 --- a/SignalServiceKit/src/Messages/MessageBody.swift +++ b/SignalServiceKit/src/Messages/MessageBody.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2021 Open Whisper Systems. All rights reserved. +// Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import Foundation @@ -45,6 +45,48 @@ public class MessageBody: NSObject, NSCopying, NSSecureCoding { public func plaintextBody(transaction: GRDBReadTransaction) -> String { return ranges.plaintextBody(text: text, transaction: transaction) } + + public func forNewContext( + _ context: TSThread, + transaction: GRDBReadTransaction + ) -> MessageBody { + guard hasRanges else { return self } + guard let groupThread = context as? TSGroupThread, groupThread.isGroupV2Thread else { + return MessageBody(text: plaintextBody(transaction: transaction), ranges: .empty) + } + + let mutableText = NSMutableString(string: text) + var mentions = [NSRange: UUID]() + var rangeOffset = 0 + + func shouldPreserveMention(_ address: SignalServiceAddress) -> Bool { + return groupThread.recipientAddresses.contains(address) + } + + for (range, uuid) in ranges.orderedMentions { + guard range.location >= 0 && range.location + range.length <= (text as NSString).length else { + owsFailDebug("Ignoring invalid range in body ranges \(range)") + continue + } + + let mentionAddress = SignalServiceAddress(uuid: uuid) + let offsetRange = NSRange(location: range.location + rangeOffset, length: range.length) + + if shouldPreserveMention(mentionAddress) { + mentions[offsetRange] = uuid + } else { + let displayName = Self.contactsManager.displayName( + for: mentionAddress, + transaction: transaction.asAnyRead + ) + let mentionPlaintext = MessageBodyRanges.mentionPrefix + displayName + mutableText.replaceCharacters(in: offsetRange, with: mentionPlaintext) + rangeOffset += (mentionPlaintext as NSString).length - range.length + } + } + + return MessageBody(text: String(mutableText), ranges: MessageBodyRanges(mentions: mentions)) + } } @objcMembers diff --git a/SignalUI/Utils/AttachmentMultisend.swift b/SignalUI/Utils/AttachmentMultisend.swift index 2e05a0a559..13583e6770 100644 --- a/SignalUI/Utils/AttachmentMultisend.swift +++ b/SignalUI/Utils/AttachmentMultisend.swift @@ -43,7 +43,9 @@ public class AttachmentMultisend: Dependencies { ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread: thread, transaction: transaction) - let message = try! ThreadUtil.createUnsentMessage(body: approvalMessageBody, + let messageBodyForContext = approvalMessageBody?.forNewContext(thread, transaction: transaction.unwrapGrdbRead) + + let message = try! ThreadUtil.createUnsentMessage(body: messageBodyForContext, mediaAttachments: attachments, thread: thread, quotedReplyModel: nil, @@ -117,7 +119,9 @@ public class AttachmentMultisend: Dependencies { ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread: thread, transaction: transaction) - let message = try! ThreadUtil.createUnsentMessage(body: approvalMessageBody, + let messageBodyForContext = approvalMessageBody?.forNewContext(thread, transaction: transaction.unwrapGrdbRead) + + let message = try! ThreadUtil.createUnsentMessage(body: messageBodyForContext, mediaAttachments: attachments, thread: thread, quotedReplyModel: nil,