Some checks failed
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 4s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Analyze (javascript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
198 lines
5.7 KiB
Swift
198 lines
5.7 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
@MainActor
|
|
final class ShareViewModel: ObservableObject {
|
|
@Published var title = ""
|
|
@Published var previewText = ""
|
|
@Published var previewImage: UIImage?
|
|
@Published var isLoading = true
|
|
@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"
|
|
}
|
|
|
|
var canSave: Bool {
|
|
!isLoading
|
|
&& !isSaving
|
|
&& !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
&& draft?.content != nil
|
|
}
|
|
|
|
private var previewRoute: SharePreviewRoute = .deferred
|
|
private var draft: SharePayloadDraft?
|
|
private var userEditedTitle: String?
|
|
private var loadGeneration = 0
|
|
private var previewTask: Task<Void, Never>?
|
|
private let store: ShareInboxStore
|
|
private let previewClient: ShareLinkPreviewClient
|
|
private let buildPayload: ([NSExtensionItem]) async -> SharePayloadDraft
|
|
|
|
init(
|
|
store: ShareInboxStore = .shared,
|
|
previewClient: ShareLinkPreviewClient = ShareLinkPreviewClient(),
|
|
buildPayload: @escaping ([NSExtensionItem]) async -> SharePayloadDraft = { items in
|
|
await SharePayloadBuilder.build(from: items)
|
|
}
|
|
) {
|
|
self.store = store
|
|
self.previewClient = previewClient
|
|
self.buildPayload = buildPayload
|
|
}
|
|
|
|
deinit {
|
|
draft?.discardStagingFiles()
|
|
}
|
|
|
|
var displayTitle: String {
|
|
ShareInboxSafety.previewTitle(
|
|
original: title,
|
|
userEdited: userEditedTitle,
|
|
serverTitle: linkPreview?.title
|
|
)
|
|
}
|
|
|
|
var linkPreview: ShareLinkPreview? {
|
|
guard case let .loaded(preview) = linkPreviewState else { return nil }
|
|
return preview
|
|
}
|
|
|
|
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 {
|
|
loadGeneration &+= 1
|
|
let generation = loadGeneration
|
|
previewTask?.cancel()
|
|
isLoading = true
|
|
|
|
let items = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
|
|
let built = await buildPayload(items)
|
|
guard generation == loadGeneration else {
|
|
built.discardStagingFiles()
|
|
return
|
|
}
|
|
draft?.discardStagingFiles()
|
|
draft = built
|
|
userEditedTitle = nil
|
|
title = built.title
|
|
previewText = built.previewText
|
|
errorMessage = built.errorMessage
|
|
linkPreviewState = .idle
|
|
linkPreviewMediaImage = nil
|
|
linkPreviewFaviconImage = nil
|
|
if let file = built.file {
|
|
previewImage = UIImage(data: file.thumbnailData)?
|
|
.preparingThumbnail(of: CGSize(width: 480, height: 480))
|
|
} else {
|
|
previewImage = nil
|
|
}
|
|
isLoading = false
|
|
previewRoute = built.content?.url.map { ShareInboxSafety.previewRoute(url: $0, mode: store.workspaceMode()) } ?? .deferred
|
|
guard built.content?.kind == .url, let url = built.content?.url, previewRoute == .official else {
|
|
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, generation == loadGeneration 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, generation == loadGeneration else { return }
|
|
linkPreviewMediaImage = images.0
|
|
linkPreviewFaviconImage = images.1
|
|
} catch is CancellationError {
|
|
return
|
|
} catch {
|
|
guard !Task.isCancelled, generation == loadGeneration else { return }
|
|
linkPreviewState = .failed
|
|
}
|
|
}
|
|
}
|
|
|
|
func discard() {
|
|
loadGeneration &+= 1
|
|
previewTask?.cancel()
|
|
draft?.discardStagingFiles()
|
|
draft = nil
|
|
previewImage = nil
|
|
linkPreviewState = .idle
|
|
linkPreviewMediaImage = nil
|
|
linkPreviewFaviconImage = nil
|
|
isLoading = false
|
|
}
|
|
|
|
func save() async -> Bool {
|
|
guard !isSaving, !hasSaved else { return false }
|
|
isSaving = true
|
|
defer { isSaving = false }
|
|
|
|
let trimmedTitle = ShareInboxSafety.manifestTitle(
|
|
original: draft?.title ?? title,
|
|
userEdited: userEditedTitle
|
|
)
|
|
guard !trimmedTitle.isEmpty else {
|
|
errorMessage = "Title is required."
|
|
return false
|
|
}
|
|
guard let draft, let content = draft.content else {
|
|
errorMessage = draft?.errorMessage ?? "Nothing to share."
|
|
return false
|
|
}
|
|
|
|
let itemId = UUID().uuidString
|
|
var attachments: [ShareInboxAttachment] = []
|
|
var attachmentFiles: [(ShareInboxAttachment, URL)] = []
|
|
if let file = draft.file {
|
|
let attachment = ShareInboxAttachment(
|
|
fileName: file.name,
|
|
mimeType: file.mimeType,
|
|
relativePath: "\(itemId)/\(file.name)"
|
|
)
|
|
attachments = [attachment]
|
|
attachmentFiles = [(attachment, file.ownedStagingURL)]
|
|
}
|
|
|
|
let item = ShareInboxItem(
|
|
id: itemId,
|
|
title: trimmedTitle,
|
|
content: content,
|
|
previewRoute: previewRoute,
|
|
previewText: draft.previewText,
|
|
attachments: attachments
|
|
)
|
|
|
|
do {
|
|
try store.enqueue(item, attachmentFiles: attachmentFiles)
|
|
hasSaved = true
|
|
previewTask?.cancel()
|
|
draft.discardStagingFiles()
|
|
return true
|
|
} catch {
|
|
errorMessage = "Failed to save shared content."
|
|
return false
|
|
}
|
|
}
|
|
}
|