Commit 558828a21c

558828a21cf2bdc4e784ee7d27455825c5fd7a6a

parent: a07148fa91

Unsigned

cmc <hello@cleberg.net> · 2026-04-14 00:13 UTC

fix(tickets): load all pages on tracker open for accurate Open tab

todo.sr.ht only exposes cursor-based `tracker.tickets` (no status filter),
so client-side Open filtering missed older open tickets when only the first
page was loaded. Paginate until the cursor is exhausted in loadTickets().

Fixes: https://todo.sr.ht/~ccleberg/hutch/62

Layout: unified · split

Hutch.xcodeproj/project.pbxproj +8 −8
@@ -515,7 +515,7 @@
515515 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
516516 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
517517 CODE_SIGN_STYLE = Automatic;
518 CURRENT_PROJECT_VERSION = 68;
518 CURRENT_PROJECT_VERSION = 69;
519519 DEVELOPMENT_TEAM = ZCNAX3VL9D;
520520 ENABLE_PREVIEWS = YES;
521521 GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
532532 "$(inherited)",
533533 "@executable_path/Frameworks",
534534 );
535 MARKETING_VERSION = 3.1.0;
535 MARKETING_VERSION = 3.1.1;
536536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
537537 PRODUCT_NAME = "$(TARGET_NAME)";
538538 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
552552 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
553553 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
554554 CODE_SIGN_STYLE = Automatic;
555 CURRENT_PROJECT_VERSION = 68;
555 CURRENT_PROJECT_VERSION = 69;
556556 DEVELOPMENT_TEAM = ZCNAX3VL9D;
557557 ENABLE_PREVIEWS = YES;
558558 GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
569569 "$(inherited)",
570570 "@executable_path/Frameworks",
571571 );
572 MARKETING_VERSION = 3.1.0;
572 MARKETING_VERSION = 3.1.1;
573573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
574574 PRODUCT_NAME = "$(TARGET_NAME)";
575575 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
632632 APPLICATION_EXTENSION_API_ONLY = YES;
633633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
634634 CODE_SIGN_STYLE = Automatic;
635 CURRENT_PROJECT_VERSION = 68;
635 CURRENT_PROJECT_VERSION = 69;
636636 DEVELOPMENT_TEAM = ZCNAX3VL9D;
637637 GENERATE_INFOPLIST_FILE = NO;
638638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
642642 "@executable_path/Frameworks",
643643 "@executable_path/../../Frameworks",
644644 );
645 MARKETING_VERSION = 3.1.0;
645 MARKETING_VERSION = 3.1.1;
646646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
647647 PRODUCT_NAME = "$(TARGET_NAME)";
648648 SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
661661 APPLICATION_EXTENSION_API_ONLY = YES;
662662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
663663 CODE_SIGN_STYLE = Automatic;
664 CURRENT_PROJECT_VERSION = 68;
664 CURRENT_PROJECT_VERSION = 69;
665665 DEVELOPMENT_TEAM = ZCNAX3VL9D;
666666 GENERATE_INFOPLIST_FILE = NO;
667667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
671671 "@executable_path/Frameworks",
672672 "@executable_path/../../Frameworks",
673673 );
674 MARKETING_VERSION = 3.1.0;
674 MARKETING_VERSION = 3.1.1;
675675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676676 PRODUCT_NAME = "$(TARGET_NAME)";
677677 SKIP_INSTALL = YES;
Hutch/Views/Tickets/TicketListViewModel.swift +26 −5
@@ -299,13 +299,34 @@ final class TicketListViewModel {
299299 hasMore = true
300300
301301 do {
302 let page = try await fetchPage(cursor: nil)
303 tickets = page.results
304 cursor = page.cursor
305 hasMore = page.cursor != nil
302 // todo.sr.ht exposes `tickets(cursor:)` only (see Docs/API/todo.json) — no server-side
303 // status filter. The Open tab filters client-side, so we paginate until the cursor is
304 // exhausted; otherwise older open tickets never appear in the first page (25 items).
305 var accumulated: [TicketSummary] = []
306 var nextCursor: String?
307
308 repeat {
309 let page = try await fetchPage(cursor: nextCursor)
310
311 let existingIDs = Set(accumulated.map(\.id))
312 let newTickets = page.results.filter { !existingIDs.contains($0.id) }
313
314 if newTickets.isEmpty && !page.results.isEmpty {
315 break
316 }
317
318 accumulated.append(contentsOf: newTickets)
319 nextCursor = page.cursor
320 } while nextCursor != nil
321
322 tickets = accumulated
323 cursor = nextCursor
324 hasMore = nextCursor != nil
306325 reconcileSelectionWithLoadedTickets()
307326 } catch {
308 self.error = error.userFacingMessage
327 if !Task.isCancelled {
328 self.error = error.userFacingMessage
329 }
309330 }
310331
311332 isLoading = false