Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [*] [internal] Stop donating screen activities as Siri predictions now that App Shortcuts cover them [#25757]
* [*] Fix an issue where the Reader tab and Me tab show incorrect state after logging out [#25952]
* [*] Stats: Open the latest post when tapping the Latest Post Summary card in the Insights tab [#25896]
* [*] Share extension: fix text being dropped when a note containing both text and images is shared [#25987]

27.2
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct ExtractedShare {

// Build the returned string by doing the following:
// * 1: Look for imported text.
// * 2: Look for selected text, if it exists put it into a blockquote.
// * 2: Look for selected text, quoting it only when there is a source to attribute.
// * 3: No selected text, but we have a page description...use that.
// * 4: No selected text, but we have a page title...use that.
// * Finally, default to a simple link if nothing else is found
Expand All @@ -42,7 +42,13 @@ struct ExtractedShare {
}

guard selectedText.isEmpty else {
return "<blockquote><p>\(selectedText.escapeHtmlNamedEntities())\(readOnText)</p></blockquote>"
let paragraphs = ExtractedShare.paragraphsHTML(from: selectedText, appending: readOnText)

// Only quote text that arrived with a source to attribute.
guard url != nil else {
return paragraphs
}
return "<blockquote>\(paragraphs)</blockquote>"
}

if !description.isEmpty {
Expand All @@ -53,6 +59,38 @@ struct ExtractedShare {
return "<p>\(rawLink)</p>"
}
}

/// Converts plain text to paragraphs, treating a blank line as a paragraph break and a single
/// line break as a `<br>`. `suffix` is appended to the last paragraph.
private static func paragraphsHTML(from text: String, appending suffix: String) -> String {
var paragraphs = [[String]]()
var current = [String]()

for line in text.replacingOccurrences(of: "\r\n", with: "\n").components(separatedBy: "\n") {
if line.trimmingCharacters(in: .whitespaces).isEmpty {
if !current.isEmpty {
paragraphs.append(current)
current = []
}
} else {
current.append(line)
}
}

if !current.isEmpty {
paragraphs.append(current)
}

guard !paragraphs.isEmpty else {
return "<p>\(text.escapeHtmlNamedEntities())\(suffix)</p>"
}

return paragraphs.enumerated().map { index, lines in
let body = lines.map { $0.escapeHtmlNamedEntities() }.joined(separator: "<br>")
let tail = index == paragraphs.count - 1 ? suffix : ""
return "<p>\(body)\(tail)</p>"
}.joined()
}
}

struct ExtractedImage {
Expand Down Expand Up @@ -231,6 +269,10 @@ private protocol ExtensionContentExtractor {
private protocol TypeBasedExtensionContentExtractor: ExtensionContentExtractor, Sendable {
associatedtype Payload
var acceptedType: String { get }

/// The attachments this extractor will read. Defaults to everything matching `acceptedType`.
func itemProviders(in context: NSExtensionContext) -> [NSItemProvider]

func convert(payload: Payload) -> ExtractedItem?
}

Expand All @@ -243,12 +285,16 @@ private extension TypeBasedExtensionContentExtractor {
return CGSize(width: dimension, height: dimension)
}

func itemProviders(in context: NSExtensionContext) -> [NSItemProvider] {
return context.itemProviders(ofType: acceptedType)
}

func canHandle(context: NSExtensionContext) -> Bool {
return !context.itemProviders(ofType: acceptedType).isEmpty
return !itemProviders(in: context).isEmpty
}

func extract(context: NSExtensionContext, completion: @escaping ([ExtractedItem]) -> Void) {
let itemProviders = context.itemProviders(ofType: acceptedType)
let itemProviders = self.itemProviders(in: context)
print(acceptedType)
var results = [ExtractedItem]()
guard !itemProviders.isEmpty else {
Expand Down Expand Up @@ -340,6 +386,14 @@ private struct URLExtractor: TypeBasedExtensionContentExtractor {
typealias Payload = URL
let acceptedType = UTType.url.identifier

/// An image shared as a file also declares `public.url`, but `processLocalFile(url:)` cannot
/// convert it. Claiming it here would stop `PlainTextExtractor` reading the text beside it.
func itemProviders(in context: NSExtensionContext) -> [NSItemProvider] {
return context.itemProviders(ofType: acceptedType).filter { provider in
!provider.hasItemConformingToTypeIdentifier(UTType.image.identifier)
}
}

func convert(payload: URL) -> ExtractedItem? {
guard !payload.isFileURL else {
return processLocalFile(url: payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1008,7 +1008,10 @@ extension ShareExtensionEditorViewController {
}

func insertImageAttachment(with url: URL) {
let attachment = richTextView.replaceWithImage(at: self.richTextView.selectedRange, sourceURL: url, placeHolderImage: Assets.defaultMissingImage)
// `setHTML` leaves the caret at the start of the document, so append instead of inserting
// there, which would put shared images above the text they arrived with.
let endOfDocument = NSRange(location: richTextView.textStorage.length, length: 0)
let attachment = richTextView.replaceWithImage(at: endOfDocument, sourceURL: url, placeHolderImage: Assets.defaultMissingImage)

attachment.size = .full
attachment.uploadID = url.lastPathComponent // Use the filename as the uploadID here.
Expand Down Expand Up @@ -1265,7 +1268,8 @@ private extension ShareExtensionEditorViewController {
ShareExtractor(extensionContext: extensionContext)
.loadShare { [weak self] share in
self?.setTitleText(share.title)
self?.richTextView.setHTML(share.combinedContentHTML)
// The trailing paragraph keeps appended images off the end of the last sentence.
self?.richTextView.setHTML(share.combinedContentHTML + "<p></p>")

share.images.forEach({ extractedImage in
if extractedImage.insertionState == .requiresInsertion {
Expand Down