Asyncify AttachmentMultisend
This commit is contained in:
parent
2fc65f3f01
commit
215659ae55
@ -147,46 +147,42 @@ extension CameraFirstCaptureSendFlow: ConversationPickerDelegate {
|
||||
}
|
||||
|
||||
public func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
|
||||
if let textAttachment = textAttachment {
|
||||
if let textAttachment {
|
||||
let selectedStoryItems = selectedConversations.filter { $0 is StoryConversationItem }
|
||||
guard !selectedStoryItems.isEmpty else {
|
||||
owsFailDebug("Selection was unexpectedly empty.")
|
||||
delegate?.cameraFirstCaptureSendFlowDidCancel(self)
|
||||
return
|
||||
}
|
||||
|
||||
firstly {
|
||||
AttachmentMultisend.sendTextAttachment(
|
||||
textAttachment,
|
||||
to: selectedStoryItems
|
||||
).enqueuedPromise
|
||||
}.done { _ in
|
||||
self.delegate?.cameraFirstCaptureSendFlowDidComplete(self)
|
||||
}.catch { error in
|
||||
owsFailDebug("Error: \(error)")
|
||||
Task { @MainActor in
|
||||
do {
|
||||
_ = try await AttachmentMultisend.enqueueTextAttachment(textAttachment, to: selectedStoryItems)
|
||||
self.delegate?.cameraFirstCaptureSendFlowDidComplete(self)
|
||||
} catch {
|
||||
owsFailDebug("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
guard let approvedAttachments = self.approvedAttachments else {
|
||||
owsFailDebug("approvedAttachments was unexpectedly nil")
|
||||
delegate?.cameraFirstCaptureSendFlowDidCancel(self)
|
||||
if let approvedAttachments {
|
||||
let approvedMessageBody = self.approvedMessageBody
|
||||
let selectedConversations = self.selectedConversations
|
||||
Task { @MainActor in
|
||||
do {
|
||||
_ = try await AttachmentMultisend.enqueueApprovedMedia(
|
||||
conversations: selectedConversations,
|
||||
approvedMessageBody: approvedMessageBody,
|
||||
approvedAttachments: approvedAttachments
|
||||
)
|
||||
self.delegate?.cameraFirstCaptureSendFlowDidComplete(self)
|
||||
} catch {
|
||||
owsFailDebug("\(error)")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let conversations = selectedConversations
|
||||
firstly {
|
||||
AttachmentMultisend.sendApprovedMedia(
|
||||
conversations: conversations,
|
||||
approvedMessageBody: self.approvedMessageBody,
|
||||
approvedAttachments: approvedAttachments
|
||||
).enqueuedPromise
|
||||
}.done { _ in
|
||||
self.delegate?.cameraFirstCaptureSendFlowDidComplete(self)
|
||||
}.catch { error in
|
||||
owsFailDebug("Error: \(error)")
|
||||
}
|
||||
owsFailDebug("completed without anything to send")
|
||||
delegate?.cameraFirstCaptureSendFlowDidCancel(self)
|
||||
}
|
||||
|
||||
public func conversationPickerCanCancel(_ conversationPickerViewController: ConversationPickerViewController) -> Bool {
|
||||
|
||||
@ -342,28 +342,26 @@ extension ForwardMessageViewController {
|
||||
} else if !item.attachments.isEmpty {
|
||||
// TODO: What about link previews in this case?
|
||||
let conversations = selectedConversations
|
||||
_ = try await AttachmentMultisend.sendApprovedMedia(
|
||||
_ = try await AttachmentMultisend.enqueueApprovedMedia(
|
||||
conversations: conversations,
|
||||
approvedMessageBody: item.messageBody,
|
||||
approvedAttachments: ApprovedAttachments(nonViewOnceAttachments: item.attachments),
|
||||
).enqueuedPromise.awaitable()
|
||||
)
|
||||
} else if let textAttachment = item.textAttachment {
|
||||
// TODO: we want to reuse the uploaded link preview image attachment instead of re-uploading
|
||||
// if the original was sent recently (if not the image could be stale)
|
||||
_ = try await AttachmentMultisend.sendTextAttachment(
|
||||
_ = try await AttachmentMultisend.enqueueTextAttachment(
|
||||
textAttachment.asUnsentAttachment(), to: selectedConversations
|
||||
).enqueuedPromise.awaitable()
|
||||
)
|
||||
} else if let messageBody = item.messageBody {
|
||||
let linkPreviewDraft = item.linkPreviewDraft
|
||||
await enqueueMessageViaThreadUtil(toRecipientThreads: outgoingMessageRecipientThreads) { recipientThread in
|
||||
self.send(body: messageBody, linkPreviewDraft: linkPreviewDraft, recipientThread: recipientThread)
|
||||
}
|
||||
|
||||
// Send the text message to any selected story recipients
|
||||
// as a text story with default styling.
|
||||
let storyConversations = selectedConversations.filter { $0.outgoingMessageType == .storyMessage }
|
||||
let storySendResult = StorySharing.sendTextStory(with: messageBody, linkPreviewDraft: linkPreviewDraft, to: storyConversations)
|
||||
_ = try await storySendResult?.enqueuedPromise.awaitable()
|
||||
// Send the text message to any selected story recipients as a text story
|
||||
// with default styling.
|
||||
_ = try await StorySharing.enqueueTextStory(with: messageBody, linkPreviewDraft: linkPreviewDraft, to: selectedConversations)
|
||||
} else {
|
||||
throw ForwardError.invalidInteraction
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ public class HydratedMessageBody: Equatable, Hashable {
|
||||
mentionHydrator: MentionHydrator,
|
||||
isRTL: Bool = CurrentAppContext().isRTL
|
||||
) {
|
||||
guard messageBody.text.isEmpty.negated else {
|
||||
if messageBody.text.isEmpty {
|
||||
self.hydratedText = ""
|
||||
self.unhydratedMentions = []
|
||||
self.mentionAttributes = []
|
||||
|
||||
@ -193,28 +193,28 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
selectedConversations: selectedConversations,
|
||||
approvedSend: approvedSend,
|
||||
) {
|
||||
case .success:
|
||||
case nil:
|
||||
self.dismissSendProgressSheet {}
|
||||
self.shareViewDelegate?.shareViewWasCompleted()
|
||||
case .failure(let error):
|
||||
self.dismissSendProgressSheet { self.showSendFailure(error: error) }
|
||||
case .some(let failure):
|
||||
self.dismissSendProgressSheet { self.showSendFailure(failure) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SendError: Error {
|
||||
private struct SendFailure {
|
||||
let outgoingMessages: [PreparedOutgoingMessage]
|
||||
let error: Error
|
||||
}
|
||||
|
||||
private nonisolated func tryToSend(
|
||||
private func tryToSend(
|
||||
selectedConversations: [ConversationItem],
|
||||
approvedSend: ApprovedSend,
|
||||
) async -> Result<Void, SendError> {
|
||||
) async -> SendFailure? {
|
||||
switch approvedSend {
|
||||
case .text(let messageBody, let linkPreview):
|
||||
guard !messageBody.text.isEmpty else {
|
||||
return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing body.")))
|
||||
return SendFailure(outgoingMessages: [], error: OWSAssertionError("Missing body."))
|
||||
}
|
||||
|
||||
let linkPreviewDataSource: LinkPreviewDataSource?
|
||||
@ -238,13 +238,13 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
)
|
||||
return try unpreparedMessage.prepare(tx: tx)
|
||||
},
|
||||
storySendBlock: { storyConversations in
|
||||
enqueueStory: { conversations in
|
||||
// Send the text message to any selected story recipients
|
||||
// as a text story with default styling.
|
||||
StorySharing.sendTextStory(
|
||||
try await StorySharing.enqueueTextStory(
|
||||
with: messageBody,
|
||||
linkPreviewDraft: linkPreview,
|
||||
to: storyConversations
|
||||
to: conversations
|
||||
)
|
||||
}
|
||||
)
|
||||
@ -254,7 +254,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
let contactShareManager = DependenciesBridge.shared.contactShareManager
|
||||
contactShareForSending = try await contactShareManager.validateAndPrepare(draft: contactShare)
|
||||
} catch {
|
||||
return .failure(.init(outgoingMessages: [], error: error))
|
||||
return SendFailure(outgoingMessages: [], error: error)
|
||||
}
|
||||
return await self.sendToOutgoingMessageThreads(
|
||||
selectedConversations: selectedConversations,
|
||||
@ -277,32 +277,35 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
return try unpreparedMessage.prepare(tx: tx)
|
||||
},
|
||||
// We don't send contact shares to stories
|
||||
storySendBlock: nil
|
||||
enqueueStory: { _ in [] },
|
||||
)
|
||||
case .other(let attachments, let messageBody):
|
||||
// This method will also add threads to the profile whitelist.
|
||||
let sendResult = AttachmentMultisend.sendApprovedMedia(
|
||||
conversations: selectedConversations,
|
||||
approvedMessageBody: messageBody,
|
||||
approvedAttachments: attachments
|
||||
)
|
||||
|
||||
let preparedMessages: [PreparedOutgoingMessage]
|
||||
let enqueueResults: [AttachmentMultisend.EnqueueResult]
|
||||
do {
|
||||
preparedMessages = try await sendResult.preparedPromise.awaitable()
|
||||
} catch let error {
|
||||
return .failure(.init(outgoingMessages: [], error: error))
|
||||
}
|
||||
await MainActor.run {
|
||||
self.presentOrUpdateSendProgressSheet(outgoingMessages: preparedMessages)
|
||||
enqueueResults = try await AttachmentMultisend.enqueueApprovedMedia(
|
||||
conversations: selectedConversations,
|
||||
approvedMessageBody: messageBody,
|
||||
approvedAttachments: attachments,
|
||||
)
|
||||
} catch {
|
||||
return SendFailure(outgoingMessages: [], error: error)
|
||||
}
|
||||
|
||||
self.presentOrUpdateSendProgressSheet(outgoingMessages: enqueueResults.map(\.preparedMessage))
|
||||
|
||||
do {
|
||||
_ = try await sendResult.sentPromise.awaitable()
|
||||
} catch let error {
|
||||
return .failure(.init(outgoingMessages: preparedMessages, error: error))
|
||||
try await withThrowingTaskGroup { taskGroup in
|
||||
for sendPromise in enqueueResults.map(\.sendPromise) {
|
||||
taskGroup.addTask { try await sendPromise.awaitable() }
|
||||
}
|
||||
try await taskGroup.waitForAll()
|
||||
}
|
||||
} catch {
|
||||
return SendFailure(outgoingMessages: enqueueResults.map(\.preparedMessage), error: error)
|
||||
}
|
||||
return .success(())
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -339,26 +342,25 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func sendToOutgoingMessageThreads(
|
||||
private func sendToOutgoingMessageThreads(
|
||||
selectedConversations: [ConversationItem],
|
||||
messageBody: MessageBody?,
|
||||
messageBlock: @escaping (AttachmentMultisend.Destination, DBWriteTransaction) throws -> PreparedOutgoingMessage,
|
||||
storySendBlock: (([ConversationItem]) -> AttachmentMultisend.Result?)?
|
||||
) async -> Result<Void, SendError> {
|
||||
messageBlock: (AttachmentMultisend.Destination, DBWriteTransaction) throws -> PreparedOutgoingMessage,
|
||||
enqueueStory: (_ conversations: [ConversationItem]) async throws -> [AttachmentMultisend.EnqueueResult],
|
||||
) async -> SendFailure? {
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
|
||||
let conversations = selectedConversations.filter { $0.outgoingMessageType == .message }
|
||||
|
||||
let preparedNonStoryMessages: [PreparedOutgoingMessage]
|
||||
let nonStorySendPromises: [Promise<Void>]
|
||||
|
||||
do {
|
||||
let destinations = try await AttachmentMultisend.prepareForSending(
|
||||
messageBody,
|
||||
to: conversations,
|
||||
db: SSKEnvironment.shared.databaseStorageRef,
|
||||
attachmentValidator: DependenciesBridge.shared.attachmentContentValidator
|
||||
let destinations = try await AttachmentMultisend.prepareDestinations(
|
||||
forSendingMessageBody: messageBody,
|
||||
toConversations: conversations,
|
||||
)
|
||||
|
||||
(preparedNonStoryMessages, nonStorySendPromises) = try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
|
||||
(preparedNonStoryMessages, nonStorySendPromises) = try await databaseStorage.awaitableWrite { tx in
|
||||
let preparedMessages = try destinations.map { destination in
|
||||
return try messageBlock(destination, tx)
|
||||
}
|
||||
@ -380,41 +382,32 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
}
|
||||
return (preparedMessages, sendPromises)
|
||||
}
|
||||
} catch let error {
|
||||
return .failure(.init(outgoingMessages: [], error: error))
|
||||
} catch {
|
||||
return SendFailure(outgoingMessages: [], error: error)
|
||||
}
|
||||
|
||||
let storyConversations = selectedConversations.filter { $0.outgoingMessageType == .storyMessage }
|
||||
let storySendResult = storySendBlock?(storyConversations)
|
||||
|
||||
let preparedStoryMessages: [PreparedOutgoingMessage]
|
||||
let enqueueStoryResults: [AttachmentMultisend.EnqueueResult]
|
||||
do {
|
||||
preparedStoryMessages = try await storySendResult?.preparedPromise.awaitable() ?? []
|
||||
enqueueStoryResults = try await enqueueStory(selectedConversations)
|
||||
} catch let error {
|
||||
return .failure(.init(outgoingMessages: [], error: error))
|
||||
return SendFailure(outgoingMessages: [], error: error)
|
||||
}
|
||||
|
||||
let preparedMessages = preparedNonStoryMessages + preparedStoryMessages
|
||||
await MainActor.run {
|
||||
self.presentOrUpdateSendProgressSheet(outgoingMessages: preparedMessages)
|
||||
}
|
||||
let preparedMessages = preparedNonStoryMessages + enqueueStoryResults.map(\.preparedMessage)
|
||||
self.presentOrUpdateSendProgressSheet(outgoingMessages: preparedMessages)
|
||||
|
||||
do {
|
||||
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
nonStorySendPromises.forEach { promise in
|
||||
taskGroup.addTask(operation: {
|
||||
try await promise.awaitable()
|
||||
})
|
||||
try await withThrowingTaskGroup { taskGroup in
|
||||
for sendPromise in nonStorySendPromises + enqueueStoryResults.map(\.sendPromise) {
|
||||
taskGroup.addTask { try await sendPromise.awaitable() }
|
||||
}
|
||||
taskGroup.addTask(operation: {
|
||||
try await _ = storySendResult?.sentPromise.awaitable()
|
||||
})
|
||||
try await taskGroup.waitForAll()
|
||||
}
|
||||
return .success(())
|
||||
} catch let error {
|
||||
return .failure(.init(outgoingMessages: preparedMessages, error: error))
|
||||
} catch {
|
||||
return SendFailure(outgoingMessages: preparedMessages, error: error)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private nonisolated func threads(for conversationItems: [ConversationItem], tx: DBWriteTransaction) -> [TSThread] {
|
||||
@ -427,10 +420,10 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func showSendFailure(error: SendError) {
|
||||
private func showSendFailure(_ failure: SendFailure) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
owsFailDebug("Error: \(error.error)")
|
||||
Logger.warn("\(failure.error)")
|
||||
|
||||
let cancelAction = ActionSheetAction(
|
||||
title: CommonStrings.cancelButton,
|
||||
@ -438,7 +431,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
||||
for message in error.outgoingMessages {
|
||||
for message in failure.outgoingMessages {
|
||||
// If we sent the message to anyone, mark it as failed
|
||||
message.updateWithAllSendingRecipientsMarkedAsFailed(tx: transaction)
|
||||
}
|
||||
@ -448,7 +441,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
|
||||
let failureTitle = OWSLocalizedString("SHARE_EXTENSION_SENDING_FAILURE_TITLE", comment: "Alert title")
|
||||
|
||||
if let untrustedIdentityError = error as? UntrustedIdentityError {
|
||||
if let untrustedIdentityError = failure.error as? UntrustedIdentityError {
|
||||
let untrustedServiceId = untrustedIdentityError.serviceId
|
||||
let failureFormat = OWSLocalizedString(
|
||||
"SHARE_EXTENSION_FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_FORMAT",
|
||||
@ -501,7 +494,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
}
|
||||
|
||||
// Resend
|
||||
self.resendMessages(error.outgoingMessages)
|
||||
self.resendMessages(failure.outgoingMessages)
|
||||
}
|
||||
actionSheet.addAction(confirmAction)
|
||||
|
||||
@ -511,7 +504,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
actionSheet.addAction(cancelAction)
|
||||
|
||||
let retryAction = ActionSheetAction(title: CommonStrings.retryButton, style: .default) { [weak self] _ in
|
||||
self?.resendMessages(error.outgoingMessages)
|
||||
self?.resendMessages(failure.outgoingMessages)
|
||||
}
|
||||
actionSheet.addAction(retryAction)
|
||||
|
||||
@ -521,16 +514,15 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
|
||||
func resendMessages(_ outgoingMessages: [PreparedOutgoingMessage]) {
|
||||
AssertIsOnMainThread()
|
||||
owsAssertDebug(outgoingMessages.count > 0)
|
||||
owsAssertDebug(!outgoingMessages.isEmpty)
|
||||
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueueRef
|
||||
|
||||
var promises = [Promise<Void>]()
|
||||
SSKEnvironment.shared.databaseStorageRef.write { transaction in
|
||||
databaseStorage.write { tx in
|
||||
for message in outgoingMessages {
|
||||
promises.append(SSKEnvironment.shared.messageSenderJobQueueRef.add(
|
||||
.promise,
|
||||
message: message,
|
||||
transaction: transaction
|
||||
))
|
||||
promises.append(messageSenderJobQueue.add(.promise, message: message, transaction: tx))
|
||||
}
|
||||
}
|
||||
|
||||
@ -540,7 +532,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
self.shareViewDelegate?.shareViewWasCompleted()
|
||||
}.catch { error in
|
||||
self.dismissSendProgressSheet {
|
||||
self.showSendFailure(error: .init(outgoingMessages: outgoingMessages, error: error))
|
||||
self.showSendFailure(SendFailure(outgoingMessages: outgoingMessages, error: error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,13 +16,10 @@ extension AttachmentMultisend {
|
||||
public let messageBody: ValidatedMessageBody?
|
||||
}
|
||||
|
||||
public static func prepareForSending(
|
||||
_ messageBody: MessageBody?,
|
||||
to conversations: [ConversationItem],
|
||||
db: SDSDatabaseStorage,
|
||||
attachmentValidator: AttachmentContentValidator
|
||||
public static func prepareDestinations(
|
||||
forSendingMessageBody messageBody: MessageBody?,
|
||||
toConversations conversations: [ConversationItem],
|
||||
) async throws -> [Destination] {
|
||||
|
||||
// If the message body has no mentions, we can "hydrate" once across all threads
|
||||
// and share it. We only need to re-generate per-thread if there are mentions.
|
||||
let canShareMessageBody = !(messageBody?.ranges.hasMentions ?? false)
|
||||
@ -33,7 +30,7 @@ extension AttachmentMultisend {
|
||||
let messageBody: HydratedMessageBody?
|
||||
}
|
||||
|
||||
let preDestinations: [PreDestination] = try await db.awaitableWrite { tx in
|
||||
let preDestinations: [PreDestination] = try await deps.databaseStorage.awaitableWrite { tx in
|
||||
return try conversations.map { conversation in
|
||||
guard let thread = conversation.getOrCreateThread(transaction: tx) else {
|
||||
throw OWSAssertionError("Missing thread for conversation")
|
||||
@ -57,7 +54,7 @@ extension AttachmentMultisend {
|
||||
// We only prepare the single shared body.
|
||||
let validatedMessageBody: ValidatedMessageBody?
|
||||
if let messageBody {
|
||||
validatedMessageBody = try await attachmentValidator.prepareOversizeTextIfNeeded(
|
||||
validatedMessageBody = try await deps.attachmentValidator.prepareOversizeTextIfNeeded(
|
||||
messageBody
|
||||
)
|
||||
} else {
|
||||
@ -83,7 +80,7 @@ extension AttachmentMultisend {
|
||||
))
|
||||
continue
|
||||
}
|
||||
let validatedMessageBody = try await attachmentValidator.prepareOversizeTextIfNeeded(
|
||||
let validatedMessageBody = try await deps.attachmentValidator.prepareOversizeTextIfNeeded(
|
||||
hydratedMessageBody.asMessageBodyForForwarding()
|
||||
)
|
||||
destinations.append(.init(
|
||||
|
||||
@ -9,166 +9,88 @@ import LibSignalClient
|
||||
|
||||
public class AttachmentMultisend {
|
||||
|
||||
public struct Result {
|
||||
/// Resolved when the messages are prepared but before uploading/sending.
|
||||
public let preparedPromise: Promise<[PreparedOutgoingMessage]>
|
||||
/// Resolved when sending is durably enqueued but before uploading/sending.
|
||||
public let enqueuedPromise: Promise<[TSThread]>
|
||||
/// Resolved when the message is sent.
|
||||
public let sentPromise: Promise<[TSThread]>
|
||||
public struct EnqueueResult {
|
||||
public let preparedMessage: PreparedOutgoingMessage
|
||||
public let sendPromise: Promise<Void>
|
||||
}
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - API
|
||||
|
||||
public class func sendApprovedMedia(
|
||||
public class func enqueueApprovedMedia(
|
||||
conversations: [ConversationItem],
|
||||
approvedMessageBody: MessageBody?,
|
||||
approvedAttachments: ApprovedAttachments,
|
||||
) -> AttachmentMultisend.Result {
|
||||
let (preparedPromise, preparedFuture) = Promise<[PreparedOutgoingMessage]>.pending()
|
||||
let (enqueuedPromise, enqueuedFuture) = Promise<[TSThread]>.pending()
|
||||
) async throws -> [EnqueueResult] {
|
||||
let destinations = try await prepareDestinations(
|
||||
forSendingMessageBody: approvedMessageBody,
|
||||
toConversations: conversations,
|
||||
)
|
||||
|
||||
let sentPromise = Promise<[TSThread]>.wrapAsync {
|
||||
let threads: [TSThread]
|
||||
let preparedMessages: [PreparedOutgoingMessage]
|
||||
let sendPromises: [Promise<Void>]
|
||||
do {
|
||||
let destinations = try await Self.prepareForSending(
|
||||
approvedMessageBody,
|
||||
to: conversations,
|
||||
db: deps.databaseStorage,
|
||||
attachmentValidator: deps.attachmentValidator
|
||||
)
|
||||
|
||||
var hasNonStoryDestination = false
|
||||
var hasStoryDestination = false
|
||||
destinations.forEach { destination in
|
||||
switch destination.conversationItem.outgoingMessageType {
|
||||
case .message:
|
||||
hasNonStoryDestination = true
|
||||
case .storyMessage:
|
||||
hasStoryDestination = true
|
||||
}
|
||||
}
|
||||
|
||||
let segmentedAttachments = try await segmentAttachmentsIfNecessary(
|
||||
for: conversations,
|
||||
approvedAttachments: approvedAttachments,
|
||||
hasNonStoryDestination: hasNonStoryDestination,
|
||||
hasStoryDestination: hasStoryDestination
|
||||
)
|
||||
|
||||
(threads, preparedMessages, sendPromises) = try await deps.databaseStorage.awaitableWrite { tx in
|
||||
let threads: [TSThread]
|
||||
let preparedMessages: [PreparedOutgoingMessage]
|
||||
(threads, preparedMessages) = try prepareForSending(
|
||||
destinations: destinations,
|
||||
// Stories get an untruncated message body
|
||||
messageBodyForStories: approvedMessageBody,
|
||||
approvedAttachments: segmentedAttachments,
|
||||
isViewOnce: approvedAttachments.isViewOnce,
|
||||
tx: tx
|
||||
)
|
||||
|
||||
let sendPromises: [Promise<Void>] = preparedMessages.map {
|
||||
deps.messageSenderJobQueue.add(
|
||||
.promise,
|
||||
message: $0,
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
return (threads, preparedMessages, sendPromises)
|
||||
}
|
||||
} catch let error {
|
||||
preparedFuture.reject(error)
|
||||
enqueuedFuture.reject(error)
|
||||
throw error
|
||||
var hasNonStoryDestination = false
|
||||
var hasStoryDestination = false
|
||||
destinations.forEach { destination in
|
||||
switch destination.conversationItem.outgoingMessageType {
|
||||
case .message:
|
||||
hasNonStoryDestination = true
|
||||
case .storyMessage:
|
||||
hasStoryDestination = true
|
||||
}
|
||||
preparedFuture.resolve(preparedMessages)
|
||||
enqueuedFuture.resolve(threads)
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
sendPromises.forEach { promise in
|
||||
taskGroup.addTask {
|
||||
try await promise.awaitable()
|
||||
}
|
||||
}
|
||||
try await taskGroup.waitForAll()
|
||||
}
|
||||
return threads
|
||||
}
|
||||
|
||||
return .init(
|
||||
preparedPromise: preparedPromise,
|
||||
enqueuedPromise: enqueuedPromise,
|
||||
sentPromise: sentPromise
|
||||
let segmentedAttachments = try await segmentAttachmentsIfNecessary(
|
||||
for: conversations,
|
||||
approvedAttachments: approvedAttachments,
|
||||
hasNonStoryDestination: hasNonStoryDestination,
|
||||
hasStoryDestination: hasStoryDestination,
|
||||
)
|
||||
|
||||
return try await deps.databaseStorage.awaitableWrite { tx in
|
||||
let preparedMessages = try prepareMessages(
|
||||
// Stories get an untruncated message body
|
||||
forSendingMessageBodyForStories: approvedMessageBody,
|
||||
approvedAttachments: segmentedAttachments,
|
||||
isViewOnce: approvedAttachments.isViewOnce,
|
||||
toDestinations: destinations,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
return preparedMessages.map {
|
||||
let sendPromise = deps.messageSenderJobQueue.add(.promise, message: $0, transaction: tx)
|
||||
return EnqueueResult(preparedMessage: $0, sendPromise: sendPromise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class func sendTextAttachment(
|
||||
public class func enqueueTextAttachment(
|
||||
_ textAttachment: UnsentTextAttachment,
|
||||
to conversations: [ConversationItem]
|
||||
) -> AttachmentMultisend.Result {
|
||||
let (preparedPromise, preparedFuture) = Promise<[PreparedOutgoingMessage]>.pending()
|
||||
let (enqueuedPromise, enqueuedFuture) = Promise<[TSThread]>.pending()
|
||||
|
||||
let sentPromise = Promise<[TSThread]>.wrapAsync {
|
||||
// Prepare the text attachment
|
||||
let textAttachment = try await textAttachment.validateAndPrepareForSending()
|
||||
|
||||
let threads: [TSThread]
|
||||
let preparedMessages: [PreparedOutgoingMessage]
|
||||
let sendPromises: [Promise<Void>]
|
||||
do {
|
||||
(threads, preparedMessages, sendPromises) = try await deps.databaseStorage.awaitableWrite { tx in
|
||||
let threads: [TSThread]
|
||||
let preparedMessages: [PreparedOutgoingMessage]
|
||||
(threads, preparedMessages) = try prepareForSending(
|
||||
conversations: conversations,
|
||||
textAttachment,
|
||||
tx: tx
|
||||
)
|
||||
|
||||
let sendPromises: [Promise<Void>] = preparedMessages.map {
|
||||
deps.messageSenderJobQueue.add(
|
||||
.promise,
|
||||
message: $0,
|
||||
transaction: tx
|
||||
)
|
||||
}
|
||||
return (threads, preparedMessages, sendPromises)
|
||||
}
|
||||
} catch let error {
|
||||
preparedFuture.reject(error)
|
||||
enqueuedFuture.reject(error)
|
||||
throw error
|
||||
}
|
||||
preparedFuture.resolve(preparedMessages)
|
||||
enqueuedFuture.resolve(threads)
|
||||
|
||||
try await withThrowingTaskGroup(of: Void.self) { taskGroup in
|
||||
sendPromises.forEach { promise in
|
||||
taskGroup.addTask {
|
||||
try await promise.awaitable()
|
||||
}
|
||||
}
|
||||
try await taskGroup.waitForAll()
|
||||
}
|
||||
return threads
|
||||
) async throws -> [EnqueueResult] {
|
||||
if conversations.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
return .init(
|
||||
preparedPromise: preparedPromise,
|
||||
enqueuedPromise: enqueuedPromise,
|
||||
sentPromise: sentPromise
|
||||
)
|
||||
// Prepare the text attachment
|
||||
let textAttachment = try await textAttachment.validateAndPrepareForSending()
|
||||
|
||||
return try await deps.databaseStorage.awaitableWrite { tx in
|
||||
let preparedMessages = try prepareMessages(
|
||||
forSendingTextAttachment: textAttachment,
|
||||
toConversations: conversations,
|
||||
tx: tx,
|
||||
)
|
||||
|
||||
return preparedMessages.map {
|
||||
let sendPromise = deps.messageSenderJobQueue.add(.promise, message: $0, transaction: tx)
|
||||
return EnqueueResult(preparedMessage: $0, sendPromise: sendPromise)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private struct Dependencies {
|
||||
struct Dependencies {
|
||||
let attachmentManager: AttachmentManager
|
||||
let attachmentValidator: AttachmentContentValidator
|
||||
let contactsMentionHydrator: ContactsMentionHydrator.Type
|
||||
@ -178,7 +100,7 @@ public class AttachmentMultisend {
|
||||
let tsAccountManager: TSAccountManager
|
||||
}
|
||||
|
||||
private static var deps = Dependencies(
|
||||
static let deps = Dependencies(
|
||||
attachmentManager: DependenciesBridge.shared.attachmentManager,
|
||||
attachmentValidator: DependenciesBridge.shared.attachmentContentValidator,
|
||||
contactsMentionHydrator: ContactsMentionHydrator.self,
|
||||
@ -300,13 +222,13 @@ public class AttachmentMultisend {
|
||||
|
||||
// MARK: - Preparing messages
|
||||
|
||||
private class func prepareForSending(
|
||||
destinations: [Destination],
|
||||
messageBodyForStories: MessageBody?,
|
||||
private class func prepareMessages(
|
||||
forSendingMessageBodyForStories messageBodyForStories: MessageBody?,
|
||||
approvedAttachments: [SegmentAttachmentResult],
|
||||
isViewOnce: Bool,
|
||||
tx: DBWriteTransaction
|
||||
) throws -> ([TSThread], [PreparedOutgoingMessage]) {
|
||||
toDestinations destinations: [Destination],
|
||||
tx: DBWriteTransaction,
|
||||
) throws -> [PreparedOutgoingMessage] {
|
||||
let segmentedAttachments = approvedAttachments.reduce([], { arr, segmented in
|
||||
return arr + segmented.segmentedOrOriginal.map { ($0, segmented.renderingFlag == .shouldLoop) }
|
||||
})
|
||||
@ -366,17 +288,14 @@ public class AttachmentMultisend {
|
||||
builders: storyMessageBuilders,
|
||||
tx: tx
|
||||
)
|
||||
let preparedMessages = nonStoryMessages + groupStoryMessages + privateStoryMessages
|
||||
let allThreads = nonStoryThreads.map(\.thread) + groupStoryThreads + privateStoryThreads
|
||||
return (allThreads, preparedMessages)
|
||||
return nonStoryMessages + groupStoryMessages + privateStoryMessages
|
||||
}
|
||||
|
||||
private class func prepareForSending(
|
||||
conversations: [ConversationItem],
|
||||
_ textAttachment: UnsentTextAttachment.ForSending,
|
||||
tx: DBWriteTransaction
|
||||
) throws -> ([TSThread], [PreparedOutgoingMessage]) {
|
||||
var allStoryThreads = [TSThread]()
|
||||
private class func prepareMessages(
|
||||
forSendingTextAttachment textAttachment: UnsentTextAttachment.ForSending,
|
||||
toConversations conversations: [ConversationItem],
|
||||
tx: DBWriteTransaction,
|
||||
) throws -> [PreparedOutgoingMessage] {
|
||||
var privateStoryThreads = [TSPrivateStoryThread]()
|
||||
var groupStoryThreads = [TSGroupThread]()
|
||||
for conversation in conversations {
|
||||
@ -393,7 +312,6 @@ public class AttachmentMultisend {
|
||||
case .storyMessage:
|
||||
throw OWSAssertionError("Invalid story message target!")
|
||||
}
|
||||
allStoryThreads.append(thread)
|
||||
}
|
||||
|
||||
let storyMessageBuilder = try storyMessageBuilder(
|
||||
@ -413,8 +331,7 @@ public class AttachmentMultisend {
|
||||
builders: [storyMessageBuilder],
|
||||
tx: tx
|
||||
)
|
||||
let preparedMessages = groupStoryMessages + privateStoryMessages
|
||||
return (allStoryThreads, preparedMessages)
|
||||
return groupStoryMessages + privateStoryMessages
|
||||
}
|
||||
|
||||
// MARK: Preparing Non-Story Messages
|
||||
|
||||
@ -6,19 +6,15 @@
|
||||
public import SignalServiceKit
|
||||
|
||||
public enum StorySharing {
|
||||
public static func sendTextStory(
|
||||
public static func enqueueTextStory(
|
||||
with messageBody: MessageBody,
|
||||
linkPreviewDraft: OWSLinkPreviewDraft?,
|
||||
to conversations: [ConversationItem]
|
||||
) -> AttachmentMultisend.Result? {
|
||||
to conversations: [ConversationItem],
|
||||
) async throws -> [AttachmentMultisend.EnqueueResult] {
|
||||
let storyConversations = conversations.filter { $0.outgoingMessageType == .storyMessage }
|
||||
owsAssertDebug(conversations.count == storyConversations.count)
|
||||
|
||||
guard !storyConversations.isEmpty else { return nil }
|
||||
|
||||
return AttachmentMultisend.sendTextAttachment(
|
||||
return try await AttachmentMultisend.enqueueTextAttachment(
|
||||
buildTextAttachment(with: messageBody, linkPreviewDraft: linkPreviewDraft),
|
||||
to: storyConversations
|
||||
to: storyConversations,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user