krz/hutch

an ios client for sourcehut

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

9e48e4681487b1c69577c0e9e5f83d8d8225e494

verified · cmc

author: Christian Cleberg <hello@cleberg.net> · 2026-04-12T16:49:45Z

feat: add prioritization of polling hutch-stats for logged in user
 Hutch.xcodeproj/project.pbxproj            | 12 ++++++------
 Hutch/Networking/HutchStatsService.swift   | 19 ++++++++++++++-----
 Hutch/Views/Lookup/UserProfileView.swift   |  5 ++++-
 Hutch/Views/More/ProfileView.swift         |  5 ++++-
 HutchTests/ContributionCalendarTests.swift | 24 ++++++++++++++++++++++++
 5 files changed, 52 insertions(+), 13 deletions(-)

diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 1fe01ce..7e0ee15 100644
--- a/Hutch.xcodeproj/project.pbxproj
+++ b/Hutch.xcodeproj/project.pbxproj
@@ -532,7 +532,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.15.1;
+				MARKETING_VERSION = 2.15.2;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -569,7 +569,7 @@
 					"$(inherited)",
 					"@executable_path/Frameworks",
 				);
-				MARKETING_VERSION = 2.15.1;
+				MARKETING_VERSION = 2.15.2;
 				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 = 33;
+				CURRENT_PROJECT_VERSION = 34;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.15.1;
+				MARKETING_VERSION = 2.15.2;
 				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 = 33;
+				CURRENT_PROJECT_VERSION = 34;
 				DEVELOPMENT_TEAM = ZCNAX3VL9D;
 				GENERATE_INFOPLIST_FILE = NO;
 				INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
 					"@executable_path/Frameworks",
 					"@executable_path/../../Frameworks",
 				);
-				MARKETING_VERSION = 2.15.1;
+				MARKETING_VERSION = 2.15.2;
 				PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				SKIP_INSTALL = YES;
diff --git a/Hutch/Networking/HutchStatsService.swift b/Hutch/Networking/HutchStatsService.swift
index 69a2a20..52d86b9 100644
--- a/Hutch/Networking/HutchStatsService.swift
+++ b/Hutch/Networking/HutchStatsService.swift
@@ -9,20 +9,23 @@ struct HutchStatsService: ContributionCalendarServing {
     private let session: URLSession
     private let decoder: JSONDecoder
     private let baseURL: URL
+    private let currentActor: String?
 
     init(
         session: URLSession = .shared,
-        configuration: AppConfiguration
+        configuration: AppConfiguration,
+        currentActor: String? = nil
     ) {
         self.session = session
         self.baseURL = configuration.hutchStatsBaseURL
         self.decoder = JSONDecoder()
+        self.currentActor = currentActor
     }
 
     func fetchContributionCalendar(actor: String, endingOn endDate: Date) async throws -> ContributionCalendarResponse {
         return try await fetch(
             path: "api/contributions/\(actor)",
-            queryItems: trailingYearQueryItems(endingOn: endDate),
+            queryItems: contributionQueryItems(actor: actor, endingOn: endDate),
             responseType: ContributionCalendarResponse.self
         )
     }
@@ -30,7 +33,7 @@ struct HutchStatsService: ContributionCalendarServing {
     func fetchContributionStats(actor: String, endingOn endDate: Date) async throws -> ContributionStatsResponse {
         return try await fetch(
             path: "api/contributions/\(actor)/stats",
-            queryItems: trailingYearQueryItems(endingOn: endDate),
+            queryItems: contributionQueryItems(actor: actor, endingOn: endDate),
             responseType: ContributionStatsResponse.self
         )
     }
@@ -86,15 +89,21 @@ struct HutchStatsService: ContributionCalendarServing {
         return normalizedStartDate...normalizedEndDate
     }
 
-    private func trailingYearQueryItems(endingOn endDate: Date) -> [URLQueryItem] {
+    func contributionQueryItems(actor: String, endingOn endDate: Date) -> [URLQueryItem] {
         let range = trailingRange(endingOn: endDate)
         let start = Self.rangeFormatter.string(from: range.lowerBound)
         let end = Self.rangeFormatter.string(from: range.upperBound)
 
-        return [
+        var queryItems = [
             URLQueryItem(name: "from", value: start),
             URLQueryItem(name: "to", value: end)
         ]
+
+        if actor == currentActor {
+            queryItems.append(URLQueryItem(name: "prioritize", value: "self"))
+        }
+
+        return queryItems
     }
 
     private static let rangeFormatter: DateFormatter = {
diff --git a/Hutch/Views/Lookup/UserProfileView.swift b/Hutch/Views/Lookup/UserProfileView.swift
index bc04b08..b467c63 100644
--- a/Hutch/Views/Lookup/UserProfileView.swift
+++ b/Hutch/Views/Lookup/UserProfileView.swift
@@ -175,7 +175,10 @@ struct UserProfileView: View {
                     ownerUsername: owner,
                     actor: actor,
                     client: appState.client,
-                    statsService: HutchStatsService(configuration: appState.configuration)
+                    statsService: HutchStatsService(
+                        configuration: appState.configuration,
+                        currentActor: appState.currentUser?.canonicalName
+                    )
                 )
                 profileViewModel = newViewModel
                 vm = newViewModel
diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift
index a7172e4..53c5ce3 100644
--- a/Hutch/Views/More/ProfileView.swift
+++ b/Hutch/Views/More/ProfileView.swift
@@ -46,7 +46,10 @@ struct ProfileView: View {
                     ownerUsername: owner,
                     actor: actor,
                     client: appState.client,
-                    statsService: HutchStatsService(configuration: appState.configuration)
+                    statsService: HutchStatsService(
+                        configuration: appState.configuration,
+                        currentActor: appState.currentUser?.canonicalName
+                    )
                 )
                 contributionViewModel = newViewModel
                 vm = newViewModel
diff --git a/HutchTests/ContributionCalendarTests.swift b/HutchTests/ContributionCalendarTests.swift
index 4ce2f1f..9e76e42 100644
--- a/HutchTests/ContributionCalendarTests.swift
+++ b/HutchTests/ContributionCalendarTests.swift
@@ -93,6 +93,30 @@ struct ContributionCalendarTests {
         #expect(range.upperBound == endDate)
     }
 
+    @Test
+    func selfContributionRequestsIncludeExplicitPrioritySignal() {
+        let service = HutchStatsService(
+            configuration: AppConfiguration(),
+            currentActor: "~alice"
+        )
+        let endDate = ContributionDateParser.parse("2026-04-11")!
+        let queryItems = service.contributionQueryItems(actor: "~alice", endingOn: endDate)
+
+        #expect(queryItems.contains(URLQueryItem(name: "prioritize", value: "self")))
+    }
+
+    @Test
+    func otherContributionRequestsDoNotIncludePrioritySignal() {
+        let service = HutchStatsService(
+            configuration: AppConfiguration(),
+            currentActor: "~alice"
+        )
+        let endDate = ContributionDateParser.parse("2026-04-11")!
+        let queryItems = service.contributionQueryItems(actor: "~bob", endingOn: endDate)
+
+        #expect(queryItems.contains(URLQueryItem(name: "prioritize", value: "self")) == false)
+    }
+
     @Test
     func contributionIntensityBucketsMatchProductRules() {
         #expect(ContributionIntensity(count: 0) == .empty)