Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

don't screenshot dictation placeholders as pasted images (#10313)

authored by

Spence Pope and committed by
GitHub
3931b908 985129dd

+158
+136
patches/expo-paste-input+0.1.15.patch
··· 1 + diff --git a/node_modules/expo-paste-input/ios/ExpoPasteInputView.swift b/node_modules/expo-paste-input/ios/ExpoPasteInputView.swift 2 + index 2164aec4ec1d..d216db6d2927 100644 3 + --- a/node_modules/expo-paste-input/ios/ExpoPasteInputView.swift 4 + +++ b/node_modules/expo-paste-input/ios/ExpoPasteInputView.swift 5 + @@ -511,14 +511,17 @@ class ExpoPasteInputView: ExpoView { 6 + var attachmentRanges: [NSRange] = [] 7 + var mediaPayloads: [MediaPayload] = [] 8 + 9 + + // Only track ranges for attachments we successfully extract a real payload 10 + + // from. Attachments without a payload (e.g. iOS dictation placeholders) 11 + + // are left alone — sanitizing them would delete characters the system 12 + + // manages itself, and emitting "unsupported" would raise a spurious error. 13 + attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, _ in 14 + guard let attachment = value as? NSTextAttachment else { 15 + return 16 + } 17 + 18 + - attachmentRanges.append(range) 19 + - 20 + if let payload = self.extractMediaPayload(from: attachment, textView: textView, range: range) { 21 + + attachmentRanges.append(range) 22 + mediaPayloads.append(payload) 23 + } 24 + } 25 + @@ -529,9 +532,8 @@ class ExpoPasteInputView: ExpoView { 26 + return 27 + } 28 + 29 + - attachmentRanges.append(range) 30 + - 31 + if let payload = self.extractMediaPayload(from: adaptiveGlyph) { 32 + + attachmentRanges.append(range) 33 + mediaPayloads.append(payload) 34 + } 35 + } 36 + @@ -539,17 +541,12 @@ class ExpoPasteInputView: ExpoView { 37 + 38 + attachmentRanges = uniqueRanges(attachmentRanges) 39 + 40 + - guard !attachmentRanges.isEmpty else { 41 + - return 42 + - } 43 + - 44 + - sanitizeAttachments(in: textView, ranges: attachmentRanges) 45 + - 46 + guard !mediaPayloads.isEmpty else { 47 + - handleUnsupportedPaste() 48 + return 49 + } 50 + 51 + + sanitizeAttachments(in: textView, ranges: attachmentRanges) 52 + + 53 + emitImagesAsync(for: mediaPayloads) 54 + } 55 + 56 + @@ -651,6 +648,11 @@ class ExpoPasteInputView: ExpoView { 57 + } 58 + 59 + private func extractMediaPayload(from attachment: NSTextAttachment, textView: UITextView, range: NSRange) -> MediaPayload? { 60 + + // Only accept attachments that carry real image payloads. We intentionally 61 + + // do not fall back to `image(forBounds:)` or rendering the text view's 62 + + // hierarchy, because system-inserted attachments (e.g. the iOS dictation 63 + + // placeholder) draw themselves via those paths and would cause us to 64 + + // emit a screenshot of the composer as a "pasted image". 65 + if let fileWrapperData = attachment.fileWrapper?.regularFileContents, 66 + let payload = extractMediaPayload(fromData: fileWrapperData) { 67 + return payload 68 + @@ -667,20 +669,6 @@ class ExpoPasteInputView: ExpoView { 69 + return .image(image) 70 + } 71 + 72 + - let attachmentBounds = attachment.bounds.size.width > 0 && attachment.bounds.size.height > 0 73 + - ? attachment.bounds 74 + - : CGRect(origin: .zero, size: CGSize(width: 128, height: 128)) 75 + - 76 + - if let image = attachment.image(forBounds: attachmentBounds, textContainer: textView.textContainer, characterIndex: range.location), 77 + - image.size.width > 0, 78 + - image.size.height > 0 { 79 + - return .image(image) 80 + - } 81 + - 82 + - if let renderedImage = renderTextAttachment(in: textView, range: range) { 83 + - return .image(renderedImage) 84 + - } 85 + - 86 + return nil 87 + } 88 + 89 + @@ -701,47 +689,6 @@ class ExpoPasteInputView: ExpoView { 90 + return .imageData(data) 91 + } 92 + 93 + - private func renderTextAttachment(in textView: UITextView, range: NSRange) -> UIImage? { 94 + - let glyphRange = textView.layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) 95 + - var rect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textView.textContainer) 96 + - 97 + - rect.origin.x += textView.textContainerInset.left - textView.contentOffset.x 98 + - rect.origin.y += textView.textContainerInset.top - textView.contentOffset.y 99 + - rect = rect.integral 100 + - 101 + - guard rect.width > 1, rect.height > 1 else { 102 + - return nil 103 + - } 104 + - 105 + - let format = UIGraphicsImageRendererFormat.default() 106 + - format.scale = textView.window?.screen.scale ?? UIScreen.main.scale 107 + - format.opaque = false 108 + - 109 + - let image = UIGraphicsImageRenderer(size: rect.size, format: format).image { _ in 110 + - let drawRect = CGRect( 111 + - origin: CGPoint(x: -rect.origin.x, y: -rect.origin.y), 112 + - size: textView.bounds.size 113 + - ) 114 + - 115 + - if textView.window != nil { 116 + - textView.drawHierarchy(in: drawRect, afterScreenUpdates: false) 117 + - } else { 118 + - guard let context = UIGraphicsGetCurrentContext() else { 119 + - return 120 + - } 121 + - 122 + - context.translateBy(x: -rect.origin.x, y: -rect.origin.y) 123 + - textView.layer.render(in: context) 124 + - } 125 + - } 126 + - 127 + - guard image.size.width > 0, image.size.height > 0 else { 128 + - return nil 129 + - } 130 + - 131 + - return image 132 + - } 133 + - 134 + @available(iOS 18.0, *) 135 + private func handleAdaptiveImageGlyphInsertion(_ adaptiveGlyph: NSAdaptiveImageGlyph) -> Bool { 136 + guard let payload = extractMediaPayload(from: adaptiveGlyph) else {
+22
patches/expo-paste-input+0.1.15.patch.md
··· 1 + # Expo Paste Input Patch 2 + 3 + `expo-paste-input` observes `UITextView.textDidChangeNotification` and treats any 4 + `NSTextAttachment` in the text view's `attributedText` as a pasted image. When 5 + it can't find a real image payload on an attachment, it falls back to 6 + `image(forBounds:)` and, failing that, to a `drawHierarchy` screenshot of the 7 + text view at the attachment's glyph rect. 8 + 9 + iOS Dictation inserts its own `NSTextAttachment` (the shimmer/cursor indicator) 10 + into the text view during dictation. Those attachments don't carry real image 11 + data, so the fallbacks would fire — emitting a zoomed-in screenshot of the 12 + composer as if the user had pasted an image at the end of dictation. 13 + 14 + This patch: 15 + 16 + - Removes the `image(forBounds:)` and `renderTextAttachment` fallbacks in 17 + `extractMediaPayload` so the library only accepts attachments carrying a real 18 + payload (`fileWrapper`, `contents`, or `image`). 19 + - Only sanitizes (deletes) attachment ranges that produced a payload, and 20 + skips the "unsupported" toast when an attachment has no payload. Unknown 21 + system attachments like the dictation placeholder are left alone rather 22 + than being ripped out from under iOS.