chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
The iOS app's inbox keeps your place when you come back to it
ios/Knotless/Views/InboxView.swift+18−5
| @@ -11,6 +11,8 @@ struct InboxView: View { | ||
@State private var items: [InboxItem] = [] | ||
@State private var cursor: String? | ||
@State private var phase = Phase.loading | ||
/// What `items` are of. | ||
@State private var read: ReloadKey? | ||
| ||
enum Phase { case loading, ready, signedOut, failed(String) } | ||
private struct ReloadKey: Hashable { let filter: String?; let did: String? } | ||
| @@ -24,7 +26,12 @@ struct InboxView: View { | ||
content.frame(maxHeight: .infinity) | ||
} | ||
.navigationTitle(signedOut ? "" : "Inbox") | ||
.task(id: ReloadKey(filter: filter, did: account.person?.did)) { await reload() } | ||
.task(id: ReloadKey(filter: filter, did: account.person?.did)) { | ||
// Shown again, it keeps the pages read of it. Another filter or account starts anew. | ||
let key = ReloadKey(filter: filter, did: account.person?.did) | ||
// Only once it's read: with none, `items` are still of what came before. | ||
if await reload(keepingPages: key == read) { read = key } | ||
} | ||
} | ||
| ||
private var signedOut: Bool { | ||
| @@ -91,11 +98,13 @@ struct InboxView: View { | ||
try await Client.shared.query("listNotifications", ["limit": "30", "kind": filter, "cursor": cursor]) | ||
} | ||
| ||
private func reload() async { | ||
/// Whether it was read. | ||
@discardableResult private func reload(keepingPages: Bool = false) async -> Bool { | ||
do { | ||
let answer = try await fetch(cursor: nil) | ||
items = answer.notifications | ||
cursor = answer.cursor | ||
let old = keepingPages ? items : [] | ||
cursor = old.count > answer.notifications.count ? cursor : answer.cursor | ||
items = rereading(answer.notifications, over: old) | ||
phase = .ready | ||
// Seen, once all of them were on offer. The dots stay until the next | ||
// look, so someone can see what was new this time. | ||
| @@ -103,16 +112,20 @@ struct InboxView: View { | ||
try? await Client.shared.send(Writes.seen(at: newest)) | ||
Pushes.shared.clearBadge() | ||
} | ||
return true | ||
} catch let error as APIError where error.needsSignIn { | ||
phase = .signedOut | ||
} catch is CancellationError { | ||
} catch let error as URLError where error.code == .cancelled { | ||
} catch { | ||
phase = .failed(error.localizedDescription) | ||
} | ||
return false | ||
} | ||
| ||
private func more() async { | ||
guard let cursor, let answer = try? await fetch(cursor: cursor) else { return } | ||
let asked = filter | ||
guard let cursor, let answer = try? await fetch(cursor: cursor), filter == asked else { return } | ||
self.cursor = answer.cursor | ||
items += answer.notifications.filter { !items.contains($0) } | ||
} | ||