From b333bed28df0b1c80c77662a49e068fa7ebb9db3 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 02:33:15 -0700 Subject: [PATCH 1/3] fix(mobile): support dragged images in the composer --- .../ios/T3ComposerEditorView.swift | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 180da203008..9a36a36f41f 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -141,7 +141,7 @@ private final class ComposerTextView: UITextView { replace(textRange, withText: "") } - private func loadImages(from providers: [NSItemProvider]) { + func loadImages(from providers: [NSItemProvider]) { let group = DispatchGroup() let lock = NSLock() var images = [UIImage?](repeating: nil, count: providers.count) @@ -282,7 +282,7 @@ private final class ComposerTextView: UITextView { } } -public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { +public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDropDelegate { private let textView = ComposerTextView() private let placeholderLabel = UILabel() private var value = "" @@ -326,6 +326,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { clipsToBounds = false textView.delegate = self + textView.textDropDelegate = self textView.backgroundColor = .clear textView.textContainerInset = .zero textView.textContainer.lineFragmentPadding = 0 @@ -500,6 +501,38 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { return true } + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + proposalForDrop drop: UITextDropRequest + ) -> UITextDropProposal { + let hasImages = drop.dropSession.items.contains { + $0.itemProvider.canLoadObject(ofClass: UIImage.self) + } + guard hasImages else { + return drop.suggestedProposal + } + + // The composer owns image drops so UIKit does not insert NSTextAttachments + // that the controlled plain-text value cannot represent. + let proposal = UITextDropProposal(operation: .copy) + proposal.dropAction = .insert + proposal.dropPerformer = .delegate + return proposal + } + + public func textDroppableView( + _ textDroppableView: UIView & UITextDroppable, + willPerformDrop drop: UITextDropRequest + ) { + let imageProviders = drop.dropSession.items + .map(\.itemProvider) + .filter { $0.canLoadObject(ofClass: UIImage.self) } + guard !imageProviders.isEmpty else { + return + } + textView.loadImages(from: imageProviders) + } + public func textViewDidBeginEditing(_ textView: UITextView) { onComposerFocus() } From 9aa97f85d74d349e2ea6cf7edffe0091894a6c4e Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 02:36:10 -0700 Subject: [PATCH 2/3] fix(mobile): preserve mixed iOS drops --- .../t3-composer-editor/ios/T3ComposerEditorView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 9a36a36f41f..2c77bfe799f 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -505,10 +505,11 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro _ textDroppableView: UIView & UITextDroppable, proposalForDrop drop: UITextDropRequest ) -> UITextDropProposal { - let hasImages = drop.dropSession.items.contains { + let items = drop.dropSession.items + let containsOnlyImages = !items.isEmpty && items.allSatisfy { $0.itemProvider.canLoadObject(ofClass: UIImage.self) } - guard hasImages else { + guard containsOnlyImages else { return drop.suggestedProposal } From d85134f5c28013ec6f74d3aebe18c8aa07d50cf3 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Thu, 30 Jul 2026 02:47:23 -0700 Subject: [PATCH 3/3] fix(mobile): handle mixed iOS drops once --- .../ios/T3ComposerEditorView.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index 2c77bfe799f..ec5b54aa8f1 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -505,11 +505,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro _ textDroppableView: UIView & UITextDroppable, proposalForDrop drop: UITextDropRequest ) -> UITextDropProposal { - let items = drop.dropSession.items - let containsOnlyImages = !items.isEmpty && items.allSatisfy { - $0.itemProvider.canLoadObject(ofClass: UIImage.self) - } - guard containsOnlyImages else { + guard droppedImageProviders(in: drop) != nil else { return drop.suggestedProposal } @@ -525,15 +521,21 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro _ textDroppableView: UIView & UITextDroppable, willPerformDrop drop: UITextDropRequest ) { - let imageProviders = drop.dropSession.items - .map(\.itemProvider) - .filter { $0.canLoadObject(ofClass: UIImage.self) } - guard !imageProviders.isEmpty else { + guard let imageProviders = droppedImageProviders(in: drop) else { return } textView.loadImages(from: imageProviders) } + private func droppedImageProviders(in drop: UITextDropRequest) -> [NSItemProvider]? { + let providers = drop.dropSession.items.map(\.itemProvider) + guard !providers.isEmpty, + providers.allSatisfy({ $0.canLoadObject(ofClass: UIImage.self) }) else { + return nil + } + return providers + } + public func textViewDidBeginEditing(_ textView: UITextView) { onComposerFocus() }