mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-02 14:49:44 +08:00
feat(ios): improve share preview (#15538)
#### PR Dependency Tree * **PR #15538** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added rich link previews to mobile and iOS sharing, including images, metadata, transcripts, and selected text. * Share imports can now create structured content blocks, embeds, bookmarks, and transcript callouts. * Added workspace-aware preview handling for cloud, self-hosted, and signed-out modes. * **Accessibility** * Improved collapse/expand controls with semantic buttons and ARIA relationships. * **Bug Fixes** * Enhanced URL and error sanitization in server logs. * Improved link-preview CORS support, validation, and request handling. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -10,6 +10,9 @@ final class ShareViewModel: ObservableObject {
|
||||
@Published var isSaving = false
|
||||
@Published var hasSaved = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var linkPreviewState: ShareLinkPreviewState = .idle
|
||||
@Published var linkPreviewMediaImage: UIImage?
|
||||
@Published var linkPreviewFaviconImage: UIImage?
|
||||
|
||||
var actionTitle: String {
|
||||
"Open AFFiNE"
|
||||
@@ -23,10 +26,43 @@ final class ShareViewModel: ObservableObject {
|
||||
}
|
||||
|
||||
private var draft: SharePayloadDraft?
|
||||
private var previewRoute: SharePreviewRoute = .deferred
|
||||
private var previewTask: Task<Void, Never>?
|
||||
private var userEditedTitle: String?
|
||||
private let store: ShareInboxStore
|
||||
private let previewClient: ShareLinkPreviewClient
|
||||
|
||||
init(store: ShareInboxStore = .shared) {
|
||||
init(
|
||||
store: ShareInboxStore = .shared,
|
||||
previewClient: ShareLinkPreviewClient = ShareLinkPreviewClient()
|
||||
) {
|
||||
self.store = store
|
||||
self.previewClient = previewClient
|
||||
}
|
||||
|
||||
var linkPreview: ShareLinkPreview? {
|
||||
guard case let .loaded(preview) = linkPreviewState else { return nil }
|
||||
return preview
|
||||
}
|
||||
|
||||
var displayTitle: String {
|
||||
ShareInboxSafety.previewTitle(
|
||||
original: title,
|
||||
userEdited: userEditedTitle,
|
||||
serverTitle: linkPreview?.title
|
||||
)
|
||||
}
|
||||
|
||||
var sharedURL: String? { draft?.content?.url }
|
||||
|
||||
var selectedText: String? {
|
||||
guard draft?.content?.kind == .url else { return nil }
|
||||
return draft?.content?.text
|
||||
}
|
||||
|
||||
func updateTitle(_ value: String) {
|
||||
userEditedTitle = value
|
||||
title = value
|
||||
}
|
||||
|
||||
func load(from extensionContext: NSExtensionContext?) async {
|
||||
@@ -36,21 +72,54 @@ final class ShareViewModel: ObservableObject {
|
||||
let items = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
|
||||
let built = await SharePayloadBuilder.build(from: items)
|
||||
draft = built
|
||||
userEditedTitle = nil
|
||||
title = built.title
|
||||
previewText = built.previewText
|
||||
errorMessage = built.errorMessage
|
||||
linkPreviewMediaImage = nil
|
||||
linkPreviewFaviconImage = nil
|
||||
if let file = built.file {
|
||||
previewImage = UIImage(data: file.data)?
|
||||
.preparingThumbnail(of: CGSize(width: 480, height: 480))
|
||||
}
|
||||
guard built.content?.kind == .url, let url = built.content?.url else { return }
|
||||
previewRoute = ShareInboxSafety.previewRoute(mode: store.workspaceMode(), url: url)
|
||||
guard previewRoute == .official else {
|
||||
linkPreviewState = .deferred
|
||||
return
|
||||
}
|
||||
linkPreviewState = .loading
|
||||
previewTask = Task { [weak self] in
|
||||
guard let self else { return }
|
||||
do {
|
||||
let preview = try await previewClient.fetch(url: url)
|
||||
guard !Task.isCancelled else { return }
|
||||
linkPreviewState = .loaded(preview)
|
||||
async let media = previewClient.fetchImageIfPresent(url: preview.images?.first)
|
||||
async let favicon = previewClient.fetchImageIfPresent(url: preview.favicons?.first)
|
||||
let images = await (media, favicon)
|
||||
guard !Task.isCancelled else { return }
|
||||
linkPreviewMediaImage = images.0
|
||||
linkPreviewFaviconImage = images.1
|
||||
} catch is CancellationError {
|
||||
return
|
||||
} catch {
|
||||
guard !Task.isCancelled else { return }
|
||||
linkPreviewState = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save() async -> Bool {
|
||||
guard !isSaving, !hasSaved else { return false }
|
||||
previewTask?.cancel()
|
||||
isSaving = true
|
||||
defer { isSaving = false }
|
||||
|
||||
let trimmedTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let trimmedTitle = ShareInboxSafety.manifestTitle(
|
||||
original: draft?.title ?? title,
|
||||
userEdited: userEditedTitle
|
||||
)
|
||||
guard !trimmedTitle.isEmpty else {
|
||||
errorMessage = "Title is required."
|
||||
return false
|
||||
@@ -77,6 +146,7 @@ final class ShareViewModel: ObservableObject {
|
||||
id: itemId,
|
||||
title: trimmedTitle,
|
||||
content: content,
|
||||
previewRoute: previewRoute,
|
||||
previewText: draft.previewText,
|
||||
attachments: attachments
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user