Can't paste multiple photos into compose bar

This commit is contained in:
kate-signal 2025-07-30 13:28:02 -07:00 committed by GitHub
parent f9bf52ee7c
commit e52ee7828e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 63 additions and 38 deletions

View File

@ -40,6 +40,6 @@ extension ConversationViewController: BodyRangesTextViewDelegate {
public func textViewDidInsertMemoji(_ memojiGlyph: OWSAdaptiveImageGlyph) {
// Note: attachment might be nil or have an error at this point; that's fine.
let attachment = SignalAttachment.attachmentFromMemoji(memojiGlyph)
self.didPasteAttachment(attachment)
self.didPasteAttachments(attachment.map { [$0] })
}
}

View File

@ -556,7 +556,7 @@ public extension ConversationViewController {
message: errorMessage)
}
func showApprovalDialog(forAttachment attachment: SignalAttachment) {
func showApprovalDialog(forAttachments attachments: [SignalAttachment]) {
AssertIsOnMainThread()
guard hasViewWillAppearEverBegun else {
@ -569,7 +569,7 @@ public extension ConversationViewController {
}
let modal = AttachmentApprovalViewController.wrappedInNavController(
attachments: [attachment],
attachments: attachments,
initialMessageBody: inputToolbar.messageBodyForSending,
hasQuotedReplyDraft: inputToolbar.quotedReplyDraft != nil,
approvalDelegate: self,
@ -830,7 +830,7 @@ extension ConversationViewController: UIDocumentPickerDelegate {
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: contentType.identifier)
showApprovalDialog(forAttachment: attachment)
showApprovalDialog(forAttachments: [attachment])
}
private func showApprovalDialogAfterProcessingVideoURL(_ movieURL: URL, filename: String?) {
@ -864,7 +864,7 @@ extension ConversationViewController: UIDocumentPickerDelegate {
owsFailDebug("Invalid attachment: \(attachment.errorName ?? "Unknown error").")
self.showErrorAlert(forAttachment: attachment)
} else {
self.showApprovalDialog(forAttachment: attachment)
self.showApprovalDialog(forAttachments: [attachment])
}
}
}.catch(on: DispatchQueue.main) { error in

View File

@ -220,39 +220,39 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
// the pasteboard will be cleared as soon as paste() exits.
if SignalAttachment.pasteboardHasStickerAttachment() {
let attachment: SignalAttachment? = SignalAttachment.stickerAttachmentFromPasteboard()
self.didPasteAttachment(attachment)
self.didPasteAttachments(attachment.map { [$0] })
return
}
ModalActivityIndicatorViewController.present(fromViewController: self) { modal in
let attachment: SignalAttachment? = await SignalAttachment.attachmentFromPasteboard()
let attachments: [SignalAttachment]? = await SignalAttachment.attachmentsFromPasteboard()
await MainActor.run {
modal.dismiss {
// Note: attachment might be nil or have an error at this point; that's fine.
self.didPasteAttachment(attachment)
// Note: attachment array might be nil or have an error at this point; that's fine.
self.didPasteAttachments(attachments)
}
}
}
}
func didPasteAttachment(_ attachment: SignalAttachment?) {
func didPasteAttachments(_ attachments: [SignalAttachment]?) {
AssertIsOnMainThread()
guard let attachment = attachment else {
owsFailDebug("Missing attachment.")
guard let attachments, attachments.count > 0 else {
owsFailDebug("Missing attachments")
return
}
// If the thing we pasted is sticker-like, send it immediately
// and render it borderless.
if attachment.isBorderless {
if attachments.count == 1, let a = attachments.first, a.isBorderless {
Task {
await self.sendAttachments([attachment], from: self, messageBody: nil)
await self.sendAttachments([a], from: self, messageBody: nil)
}
} else {
dismissKeyBoard()
showApprovalDialog(forAttachment: attachment)
showApprovalDialog(forAttachments: attachments)
}
}

View File

@ -633,22 +633,50 @@ public class SignalAttachment: NSObject {
///
/// NOTE: The attachment returned by this method may not be valid.
/// Check the attachment's error property.
public class func attachmentFromPasteboard() async -> SignalAttachment? {
return await attachmentFromPasteboard(retrySinglePixelImages: true)
public class func attachmentsFromPasteboard() async -> [SignalAttachment]? {
guard UIPasteboard.general.numberOfItems >= 1,
let pasteboardUTITypes = UIPasteboard.general.types(forItemSet: nil)
else {
return nil
}
var attachments = [SignalAttachment]()
for (index, utiSet) in pasteboardUTITypes.enumerated() {
let attachment = await attachmentFromPasteboard(pasteboardUTIs: utiSet, index: IndexSet(integer: index), retrySinglePixelImages: true)
guard let attachment else {
owsFailDebug("Missing attachment")
continue
}
if attachments.isEmpty {
if attachment.allowMultipleAttachments() == false {
// If this is a non-visual-media attachment, we only allow 1 pasted item at a time.
return [attachment]
}
}
// Otherwise, continue with any visual media attachments, dropping
// any non-visual-media ones based on the first pasteboard item.
if attachment.allowMultipleAttachments() {
attachments.append(attachment)
} else {
Logger.warn("Dropping non-visual media attachment in paste action")
}
}
return attachments
}
private class func attachmentFromPasteboard(retrySinglePixelImages: Bool) async -> SignalAttachment? {
guard UIPasteboard.general.numberOfItems >= 1 else {
return nil
}
private func allowMultipleAttachments() -> Bool {
return !self.isBorderless
&& (MimeTypeUtil.isSupportedVideoMimeType(self.mimeType)
|| MimeTypeUtil.isSupportedDefinitelyAnimatedMimeType(self.mimeType)
|| MimeTypeUtil.isSupportedImageMimeType(self.mimeType))
}
// If pasteboard contains multiple items, use only the first.
let itemSet = IndexSet(integer: 0)
guard let pasteboardUTITypes = UIPasteboard.general.types(forItemSet: itemSet) else {
return nil
}
private class func attachmentFromPasteboard(pasteboardUTIs: [String], index: IndexSet, retrySinglePixelImages: Bool) async -> SignalAttachment? {
var pasteboardUTISet = Set<String>(filterDynamicUTITypes(pasteboardUTITypes[0]))
var pasteboardUTISet = Set<String>(filterDynamicUTITypes(pasteboardUTIs))
guard pasteboardUTISet.count > 0 else {
return nil
}
@ -664,7 +692,7 @@ public class SignalAttachment: NSObject {
for dataUTI in inputImageUTISet {
if pasteboardUTISet.contains(dataUTI) {
guard let data = dataForFirstPasteboardItem(dataUTI: dataUTI) else {
guard let data = dataForPasteboardItem(dataUTI: dataUTI, index: index) else {
owsFailDebug("Missing expected pasteboard data for UTI: \(dataUTI)")
return nil
}
@ -675,7 +703,7 @@ public class SignalAttachment: NSObject {
// pasteboard after a brief delay (once, then give up).
if dataSource?.imageMetadata.pixelSize == CGSize(square: 1), retrySinglePixelImages {
try? await Task.sleep(nanoseconds: NSEC_PER_MSEC * 50)
return await attachmentFromPasteboard(retrySinglePixelImages: false)
return await attachmentFromPasteboard(pasteboardUTIs: pasteboardUTIs, index: index, retrySinglePixelImages: false)
}
// If the data source is sticker like AND we're pasting the attachment,
@ -688,7 +716,7 @@ public class SignalAttachment: NSObject {
for dataUTI in videoUTISet {
if pasteboardUTISet.contains(dataUTI) {
guard
let data = dataForFirstPasteboardItem(dataUTI: dataUTI),
let data = dataForPasteboardItem(dataUTI: dataUTI, index: index),
let dataSource = DataSourceValue(data, utiType: dataUTI)
else {
owsFailDebug("Failed to build data source from pasteboard data for UTI: \(dataUTI)")
@ -703,7 +731,7 @@ public class SignalAttachment: NSObject {
}
for dataUTI in audioUTISet {
if pasteboardUTISet.contains(dataUTI) {
guard let data = dataForFirstPasteboardItem(dataUTI: dataUTI) else {
guard let data = dataForPasteboardItem(dataUTI: dataUTI, index: index) else {
owsFailDebug("Missing expected pasteboard data for UTI: \(dataUTI)")
return nil
}
@ -713,7 +741,7 @@ public class SignalAttachment: NSObject {
}
let dataUTI = pasteboardUTISet[pasteboardUTISet.startIndex]
guard let data = dataForFirstPasteboardItem(dataUTI: dataUTI) else {
guard let data = dataForPasteboardItem(dataUTI: dataUTI, index: index) else {
owsFailDebug("Missing expected pasteboard data for UTI: \(dataUTI)")
return nil
}
@ -741,7 +769,7 @@ public class SignalAttachment: NSObject {
for dataUTI in inputImageUTISet {
if pasteboardUTISet.contains(dataUTI) {
guard let data = dataForFirstPasteboardItem(dataUTI: dataUTI) else {
guard let data = dataForPasteboardItem(dataUTI: dataUTI, index: IndexSet(integer: 0)) else {
owsFailDebug("Missing expected pasteboard data for UTI: \(dataUTI)")
return nil
}
@ -785,11 +813,8 @@ public class SignalAttachment: NSObject {
return genericAttachment(dataSource: dataSource, dataUTI: dataUTI)
}
// This method should only be called for dataUTIs that
// are appropriate for the first pasteboard item.
private class func dataForFirstPasteboardItem(dataUTI: String) -> Data? {
let itemSet = IndexSet(integer: 0)
guard let datas = UIPasteboard.general.data(forPasteboardType: dataUTI, inItemSet: itemSet) else {
private class func dataForPasteboardItem(dataUTI: String, index: IndexSet) -> Data? {
guard let datas = UIPasteboard.general.data(forPasteboardType: dataUTI, inItemSet: index) else {
owsFailDebug("Missing expected pasteboard data for UTI: \(dataUTI)")
return nil
}