authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-07-30 03:01:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-30 03:01:56-07:00
log2652fee49bdb0a7d286124cd2bd318155d4a7931
tree59312eb6d8402f836e2b471dd65f0e76da96388d
parent2f466ffc9ca21b2d536082dd821a1f57e0f354b1
signature Signed by PGP key B5690EEEBB952194

fix(mobile): support dragged images in the composer (#4953)


1 files changed, 38 insertions(+), 2 deletions(-)

apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift+38-2
...@@ -141,7 +141,7 @@ private final class ComposerTextView: UITextView {...@@ -141,7 +141,7 @@ private final class ComposerTextView: UITextView {
141 replace(textRange, withText: "")141 replace(textRange, withText: "")
142 }142 }
143143
144 private func loadImages(from providers: [NSItemProvider]) {144 func loadImages(from providers: [NSItemProvider]) {
145 let group = DispatchGroup()145 let group = DispatchGroup()
146 let lock = NSLock()146 let lock = NSLock()
147 var images = [UIImage?](repeating: nil, count: providers.count)147 var images = [UIImage?](repeating: nil, count: providers.count)
...@@ -282,7 +282,7 @@ private final class ComposerTextView: UITextView {...@@ -282,7 +282,7 @@ private final class ComposerTextView: UITextView {
282 }282 }
283}283}
284284
285public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {285public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDropDelegate {
286 private let textView = ComposerTextView()286 private let textView = ComposerTextView()
287 private let placeholderLabel = UILabel()287 private let placeholderLabel = UILabel()
288 private var value = ""288 private var value = ""
...@@ -326,6 +326,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {...@@ -326,6 +326,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
326326
327 clipsToBounds = false327 clipsToBounds = false
328 textView.delegate = self328 textView.delegate = self
329 textView.textDropDelegate = self
329 textView.backgroundColor = .clear330 textView.backgroundColor = .clear
330 textView.textContainerInset = .zero331 textView.textContainerInset = .zero
331 textView.textContainer.lineFragmentPadding = 0332 textView.textContainer.lineFragmentPadding = 0
...@@ -500,6 +501,41 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {...@@ -500,6 +501,41 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
500 return true501 return true
501 }502 }
502503
504 public func textDroppableView(
505 _ textDroppableView: UIView & UITextDroppable,
506 proposalForDrop drop: UITextDropRequest
507 ) -> UITextDropProposal {
508 guard droppedImageProviders(in: drop) != nil else {
509 return drop.suggestedProposal
510 }
511
512 // The composer owns image drops so UIKit does not insert NSTextAttachments
513 // that the controlled plain-text value cannot represent.
514 let proposal = UITextDropProposal(operation: .copy)
515 proposal.dropAction = .insert
516 proposal.dropPerformer = .delegate
517 return proposal
518 }
519
520 public func textDroppableView(
521 _ textDroppableView: UIView & UITextDroppable,
522 willPerformDrop drop: UITextDropRequest
523 ) {
524 guard let imageProviders = droppedImageProviders(in: drop) else {
525 return
526 }
527 textView.loadImages(from: imageProviders)
528 }
529
530 private func droppedImageProviders(in drop: UITextDropRequest) -> [NSItemProvider]? {
531 let providers = drop.dropSession.items.map(\.itemProvider)
532 guard !providers.isEmpty,
533 providers.allSatisfy({ $0.canLoadObject(ofClass: UIImage.self) }) else {
534 return nil
535 }
536 return providers
537 }
538
503 public func textViewDidBeginEditing(_ textView: UITextView) {539 public func textViewDidBeginEditing(_ textView: UITextView) {
504 onComposerFocus()540 onComposerFocus()
505 }541 }