Files
openclaw/apps/ios/ShareExtension/ShareContentExtractor.swift
Peter Steinberger d962a3d9c1 fix(ios): avoid duplicate URLs in shared drafts (#116430)
* fix(ios): deduplicate shared URLs

* test(ios): prove share provider extraction
2026-07-30 09:09:35 -07:00

143 lines
5.9 KiB
Swift

import Foundation
import OpenClawKit
import UniformTypeIdentifiers
struct ExtractedShareProviderContent {
var payload: SharedContentPayload
var imageProviders: [NSItemProvider]
var attachmentSummary: ShareAttachmentSummary
}
@MainActor
enum ShareContentExtractor {
static func extract(from items: [NSExtensionItem]) async -> ExtractedShareProviderContent {
var title: String?
var attributedContentText: String?
var providers: [NSItemProvider] = []
for item in items {
if title == nil { title = item.attributedTitle?.string }
if attributedContentText == nil { attributedContentText = item.attributedContentText?.string }
providers.append(contentsOf: item.attachments ?? [])
}
var providersWithLoadedContent = Set<ObjectIdentifier>()
var sharedURL: URL?
for provider in providers {
guard let providerURL = await self.loadURL(from: provider) else { continue }
sharedURL = providerURL
providersWithLoadedContent.insert(ObjectIdentifier(provider))
break
}
var sharedText: String?
for provider in providers {
guard let providerText = await self.loadText(from: provider) else { continue }
providersWithLoadedContent.insert(ObjectIdentifier(provider))
guard let distinctText = SharePayloadNormalizer.distinctProviderText(
providerText,
sharedURL: sharedURL)
else { continue }
sharedText = distinctText
break
}
var imageProviders: [NSItemProvider] = []
var attachmentSummary = ShareAttachmentSummary()
for provider in providers {
if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
attachmentSummary.selectedImageCount += 1
imageProviders.append(provider)
} else if provider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
attachmentSummary.videoCount += 1
} else if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) {
attachmentSummary.fileCount += 1
} else {
// UTI conformance only promises a representation exists; count it as handled
// only after the provider successfully delivers content we can send.
attachmentSummary.recordUnclassifiedProvider(
didLoadContent: providersWithLoadedContent.contains(ObjectIdentifier(provider)))
}
}
let supplementalTitle = SharePayloadNormalizer.distinctAttributedText(
attributedContentText,
sharedText: sharedText,
sharedURL: sharedURL)
return ExtractedShareProviderContent(
payload: SharedContentPayload(title: title ?? supplementalTitle, url: sharedURL, text: sharedText),
imageProviders: imageProviders,
attachmentSummary: attachmentSummary)
}
private static func loadURL(from provider: NSItemProvider) async -> URL? {
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier),
let url = await self.loadURLValue(from: provider, typeIdentifier: UTType.url.identifier)
{
return url
}
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier),
let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.text.identifier),
let url = SharePayloadNormalizer.webURL(from: text)
{
return url
}
return nil
}
private static func loadText(from provider: NSItemProvider) async -> String? {
if provider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier),
let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.plainText.identifier)
{
return text
}
if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier),
let text = await self.loadTextValue(from: provider, typeIdentifier: UTType.text.identifier)
{
return text
}
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier),
let url = await self.loadURLValue(from: provider, typeIdentifier: UTType.url.identifier)
{
return url.absoluteString
}
return nil
}
private static func loadURLValue(from provider: NSItemProvider, typeIdentifier: String) async -> URL? {
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
if let url = item as? URL {
continuation.resume(returning: url)
} else if let value = item as? String, let url = URL(string: value) {
continuation.resume(returning: url)
} else if let value = item as? NSString, let url = URL(string: value as String) {
continuation.resume(returning: url)
} else {
continuation.resume(returning: nil)
}
}
}
}
private static func loadTextValue(from provider: NSItemProvider, typeIdentifier: String) async -> String? {
await withCheckedContinuation { continuation in
provider.loadItem(forTypeIdentifier: typeIdentifier, options: nil) { item, _ in
if let text = item as? String {
continuation.resume(returning: text)
} else if let text = item as? NSString {
continuation.resume(returning: text as String)
} else if let text = item as? NSAttributedString {
continuation.resume(returning: text.string)
} else {
continuation.resume(returning: nil)
}
}
}
}
}