pageta-native/Sources/Core/PendingShareStore.swift
Till JS 6ec9e19dca Geteilte Links local-first speichern statt Login-Gate
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>
2026-05-26 15:50:47 +02:00

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
}