~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

The iOS app's issue and pull lists refresh and hold still as they load

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:ios/list-refresh
ios/Knotless/Views/RepoView.swift+64−33
@@ -506,6 +506,13 @@ private struct BranchPicker: View {
}
/// A list a page at a time, for those screens that are only that.
ios/Knotless/Views/RepoView.swiftline 508
review.knotless.dev

This doc comment described PagedList but now sits above rereading. Move it back down to PagedList.

/// A list's first page read again, over all that was read of it: the rest stay,
/// so a list scrolled far down stays where it was, but for any the new page moved up.
func rereading<Item: Identifiable>(_ first: [Item], over old: [Item]) -> [Item] {
let new = Set(first.map(\.id))
return first + old.dropFirst(first.count).filter { !new.contains($0.id) }
}
struct PagedList<Item: Identifiable & Equatable & Sendable, Row: View>: View {
struct Page: Sendable {
let items: [Item]
@@ -535,18 +542,27 @@ struct PagedList<Item: Identifiable & Equatable & Sendable, Row: View>: View {
}
}
.listStyle(.plain)
.task {
do {
let page = try await fetch(nil)
items = page.items
cursor = page.cursor
} catch is CancellationError {
return
} catch {
failure = error.localizedDescription
}
loading = false
// Each time it's shown again, as a pull may have changed while it wasn't.
.task { await read(keepingPages: true) }
.refreshable { await read(keepingPages: false) }
}
private func read(keepingPages: Bool) async {
do {
let page = try await fetch(nil)
let old = keepingPages ? items : []
cursor = old.count > page.items.count ? cursor : page.cursor
items = rereading(page.items, over: old)
failure = nil
} catch is CancellationError {
return
} catch let error as URLError where error.code == .cancelled {
return
} catch {
// What's on screen stays, rather than a list gone for a lost connection.
if items.isEmpty { failure = error.localizedDescription }
}
loading = false
}
private func more() async {
@@ -680,62 +696,77 @@ struct ItemsView: View {
let kind: String
@State private var state = "open"
@State private var answer = ItemList()
@State private var loading = true
/// What's been read of each state, so going back to one shows it at once.
@State private var answers: [String: ItemList] = [:]
@State private var failure: String?
private var shown: ItemList? { answers[state] }
/// The counts, which every answer has, while this state's is on its way.
private var counts: ItemList? { shown ?? answers.values.first }
var body: some View {
List {
Picker("State", selection: $state) {
Text(label("Open", answer.openCount)).tag("open")
Text(label("Closed", answer.closedCount)).tag("closed")
Text(label("Open", counts?.openCount)).tag("open")
Text(label("Closed", counts?.closedCount)).tag("closed")
}
.pickerStyle(.segmented)
.listRowSeparator(.hidden)
if let failure {
if let shown {
if shown.items.isEmpty {
Text(state == "open" ? "Nothing open." : "Nothing closed.").foregroundStyle(.secondary)
}
ForEach(shown.items) { item in
NavigationLink(value: Route.thread(repo, uri: item.uri)) { ItemRow(item: item) }
.onAppear { if item == shown.items.last { Task { await more() } } }
}
} else if let failure {
Text(failure).foregroundStyle(.secondary)
} else if loading {
} else {
ProgressView().frame(maxWidth: .infinity)
} else if answer.items.isEmpty {
Text(state == "open" ? "Nothing open." : "Nothing closed.").foregroundStyle(.secondary)
}
ForEach(answer.items) { item in
NavigationLink(value: Route.thread(repo, uri: item.uri)) { ItemRow(item: item) }
.onAppear { if item == answer.items.last { Task { await more() } } }
}
}
.listStyle(.plain)
.navigationTitle(kind == "pull" ? "Pulls" : "Issues")
.navigationBarTitleDisplayMode(.inline)
.task(id: state) { await reload() }
// Each time it's shown again too, as a pull may have changed while it wasn't.
.task(id: state) { await read(keepingPages: true) }
.refreshable { await read(keepingPages: false) }
}
private func label(_ name: String, _ count: Int?) -> String {
count.map { "\(name) \($0)" } ?? name
}
private func fetch(_ cursor: String?) async throws -> ItemList {
private func fetch(_ state: String, _ cursor: String?) async throws -> ItemList {
try await Client.shared.query("listItems", ["repo": repo.uri, "kind": kind, "state": state, "limit": "30", "cursor": cursor])
}
private func reload() async {
loading = true
private func read(keepingPages: Bool) async {
let asked = state
failure = nil
do {
answer = try await fetch(nil)
var page = try await fetch(asked, nil)
if keepingPages, let old = answers[asked], old.items.count > page.items.count {
page.cursor = old.cursor
page.items = rereading(page.items, over: old.items)
}
answers[asked] = page
} catch is CancellationError {
return
} catch let error as URLError where error.code == .cancelled {
} catch {
// What's on screen stays, rather than a list gone for a lost connection.
failure = error.localizedDescription
}
loading = false
}
private func more() async {
guard let cursor = answer.cursor, let next = try? await fetch(cursor) else { return }
answer.cursor = next.cursor
answer.items += next.items.filter { !answer.items.contains($0) }
let asked = state
guard let cursor = answers[asked]?.cursor, let next = try? await fetch(asked, cursor) else { return }
answers[asked]?.cursor = next.cursor
let have = answers[asked]?.items ?? []
answers[asked]?.items = have + next.items.filter { !have.contains($0) }
}
}