krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

6ec9754ce34f331641b000eddef3d44bca2631a5

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-14T00:13:09Z

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
 Hutch.xcodeproj/project.pbxproj               | 16 +++++++-------
 Hutch/Views/Tickets/TicketListViewModel.swift | 31 ++++++++++++++++++++++-----
 2 files changed, 34 insertions(+), 13 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index befab2b..8f98e1b 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -515,7 +515,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 68;
+				CURRENT_PROJECT_VERSION = 69;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 3.1.0;
+				MARKETING_VERSION = 3.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 68;
+				CURRENT_PROJECT_VERSION = 69;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 3.1.0;
+				MARKETING_VERSION = 3.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 68;
+				CURRENT_PROJECT_VERSION = 69;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 3.1.0;
+				MARKETING_VERSION = 3.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
 				APPLICATION_EXTENSION_API_ONLY = YES;
 				CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
 				CODE_SIGN_STYLE = Automatic;
-				CURRENT_PROJECT_VERSION = 68;
+				CURRENT_PROJECT_VERSION = 69;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 3.1.0;
+				MARKETING_VERSION = 3.1.1;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
diff --git a/Hutch/Views/Tickets/TicketListViewModel.swift b/Hutch/Views/Tickets/TicketListViewModel.swift
index 30e90a7..18a0a5b 100644
--- a/Hutch/Views/Tickets/TicketListViewModel.swift
+++ b/Hutch/Views/Tickets/TicketListViewModel.swift
@@ -299,13 +299,34 @@ final class TicketListViewModel {
         hasMore = true
 
         do {
-            let page = try await fetchPage(cursor: nil)
-            tickets = page.results
-            cursor = page.cursor
-            hasMore = page.cursor != nil
+            // todo.sr.ht exposes `tickets(cursor:)` only (see Docs/API/todo.json) — no server-side
+            // status filter. The Open tab filters client-side, so we paginate until the cursor is
+            // exhausted; otherwise older open tickets never appear in the first page (25 items).
+            var accumulated: [TicketSummary] = []
+            var nextCursor: String?
+
+            repeat {
+                let page = try await fetchPage(cursor: nextCursor)
+
+                let existingIDs = Set(accumulated.map(\.id))
+                let newTickets = page.results.filter { !existingIDs.contains($0.id) }
+
+                if newTickets.isEmpty && !page.results.isEmpty {
+                    break
+                }
+
+                accumulated.append(contentsOf: newTickets)
+                nextCursor = page.cursor
+            } while nextCursor != nil
+
+            tickets = accumulated
+            cursor = nextCursor
+            hasMore = nextCursor != nil
             reconcileSelectionWithLoadedTickets()
         } catch {
-            self.error = error.userFacingMessage
+            if !Task.isCancelled {
+                self.error = error.userFacingMessage
+            }
         }
 
         isLoading = false