Korrigiert den vorigen Ansatz: extract/preview ist server-auth-gated, ein ausgeloggt geteilter Link konnte deshalb nicht gespeichert werden und erschien erst nach Login. Statt einen Anmelde-Hinweis zu zeigen, speichert Pageta den Link jetzt local-first. - flushPendingShares: kein .signedIn-Gate mehr. saveSharedArticle legt einen Stub an (ArticleSaved, Host-Titel, leerer Inhalt) — reiner lokaler Emit, funktioniert anonym. Artikel erscheint sofort in der Lese-Liste. - Neuer Event-Typ ArticleEnriched (+ non-destruktiver Reducer-Case) füllt Titel/Inhalt/Metadaten nach, ohne Status/Progress/Favorit/Notiz/Tags zu berühren. - enrichPendingArticles-Sweep: läuft nach Flush, bei App-Active und Login; pro Artikel best-effort, gescheiterte Versuche je Session nicht wiederholt. claimOnSignIn liftet die anonymen Events beim Login in den Account. - ShareExtension-Toast wieder ehrlich "Zur Lese-Liste hinzugefügt"; das nun überflüssige SharedSessionFlag + der Login-Prompt entfernt. - Reader zeigt für noch nicht extrahierte Artikel einen "Inhalt wird noch geladen"-Hinweis statt einer leeren Seite. Offener Punkt: Web-Reducer kennt ArticleEnriched noch nicht -> auf Web bleiben so geteilte Artikel bis zum Web-Follow-up inhaltsleer. Builds: iOS grün, macOS kompiliert (nur Provisioning blockt Signing). Tests: 38/38 grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
Swift
76 lines
2.9 KiB
Swift
import Foundation
|
|
|
|
/// Inbox-Queue für URLs, die die ShareExtension empfangen hat und die
|
|
/// noch nicht zum Server gepostet wurden. Lebt in App-Group-UserDefaults
|
|
/// (`group.ev.mana.pageta`), damit App und ShareExt denselben Bucket
|
|
/// sehen.
|
|
///
|
|
/// **Ablauf:**
|
|
/// 1. ShareExt nimmt URL von Safari, ruft `enqueue(_:)`, dismissed.
|
|
/// 2. App wird gestartet/foregrounded, ruft `drain()`, postet jede URL
|
|
/// via `PagetaAPI.saveArticle(url:)`. Bei Success: aus Queue raus.
|
|
/// 3. Bei Network-Fehler: bleibt in Queue, nächster Launch versucht es.
|
|
///
|
|
/// Begründung: ShareExt darf nicht synchron zum Server reden — die
|
|
/// 30s-Time-Limit für app-extensions, kein JWT-Refresh ohne den vollen
|
|
/// AuthClient-Pfad. Queue → Asynchron im App-Context ist sauberer.
|
|
struct PendingShareStore: @unchecked Sendable {
|
|
// UserDefaults ist thread-safe (Apple Docs), aber nicht als Sendable
|
|
// markiert in der SDK — explicit @unchecked statt Wrapper-Aufwand.
|
|
private let defaults: UserDefaults
|
|
private let key: String
|
|
|
|
static let storageKey = "pageta.pendingShares.v1"
|
|
|
|
init(appGroup: String = "group.ev.mana.pageta", key: String = PendingShareStore.storageKey) {
|
|
// Wenn der App-Group nicht registriert ist (Dev-Setup ohne
|
|
// Entitlement), fällt UserDefaults(suiteName:) auf nil zurück.
|
|
// Dann nutzen wir standardUserDefaults — Queue funktioniert App-
|
|
// intern, aber ShareExt sieht sie nicht. Logging im Caller.
|
|
self.defaults = UserDefaults(suiteName: appGroup) ?? .standard
|
|
self.key = key
|
|
}
|
|
|
|
/// URLs in Reihenfolge des `enqueue` (oldest first). Dedup auf
|
|
/// String-Basis, damit ein doppeltes Share nicht zweimal landet.
|
|
func currentQueue() -> [URL] {
|
|
let raw = defaults.array(forKey: key) as? [String] ?? []
|
|
return raw.compactMap { URL(string: $0) }
|
|
}
|
|
|
|
/// Anhängen + dedupen. Returns true wenn neu, false bei dup.
|
|
@discardableResult
|
|
func enqueue(_ url: URL) -> Bool {
|
|
var raw = defaults.array(forKey: key) as? [String] ?? []
|
|
let str = url.absoluteString
|
|
guard !raw.contains(str) else { return false }
|
|
raw.append(str)
|
|
defaults.set(raw, forKey: key)
|
|
return true
|
|
}
|
|
|
|
/// Eine URL als verarbeitet markieren — aus der Queue entfernen.
|
|
func remove(_ url: URL) {
|
|
var raw = defaults.array(forKey: key) as? [String] ?? []
|
|
raw.removeAll { $0 == url.absoluteString }
|
|
defaults.set(raw, forKey: key)
|
|
}
|
|
|
|
/// Alles wegwerfen (nur für Tests + Manual-Reset).
|
|
func clear() {
|
|
defaults.removeObject(forKey: key)
|
|
}
|
|
}
|
|
|
|
/// Ergebnis eines Share-Versuchs — steuert das Toast in beiden
|
|
/// ShareExtensions (iOS + macOS).
|
|
///
|
|
/// Es gibt bewusst keinen „erst anmelden"-Fall: die App speichert geteilte
|
|
/// Links local-first (auch ausgeloggt), darum ist „hinzugefügt" immer ehrlich,
|
|
/// sobald eine URL gefunden wurde.
|
|
enum ShareOutcome: Equatable {
|
|
/// URL gefunden → lokal gespeichert (Extraktion + Claim laufen später nach).
|
|
case saved
|
|
/// Nichts Brauchbares im Share.
|
|
case noURL
|
|
}
|