diff options
| author | Christian Cleberg <[email protected]> | 2026-04-11 19:47:40 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-11 19:47:40 -0500 |
| commit | 61164b6dd8343c8699d091dd5fccc55beb0b90b9 (patch) | |
| tree | 0a2befeb986966d42062802e2a29d795be907fee /Hutch/App | |
| parent | 27bfcbbed6acde158ec40c5bb46e45e76c81a319 (diff) | |
| download | hutch-2.11.0.tar.gz hutch-2.11.0.tar.bz2 hutch-2.11.0.zip | |
feat: add contribution calendar to user profilesv2.11.0
Add a trailing 365-day contribution heatmap to user profile views,
backed by HutchStatsService. Includes calendar model, view model,
SwiftUI calendar view, app configuration, and unit tests.
Diffstat (limited to 'Hutch/App')
| -rw-r--r-- | Hutch/App/AppConfiguration.swift | 42 | ||||
| -rw-r--r-- | Hutch/App/AppState.swift | 7 | ||||
| -rw-r--r-- | Hutch/App/AppStorageKeys.swift | 2 | ||||
| -rw-r--r-- | Hutch/App/RootView.swift | 3 |
4 files changed, 54 insertions, 0 deletions
diff --git a/Hutch/App/AppConfiguration.swift b/Hutch/App/AppConfiguration.swift new file mode 100644 index 0000000..7824371 --- /dev/null +++ b/Hutch/App/AppConfiguration.swift @@ -0,0 +1,42 @@ +import Foundation + +struct AppConfiguration: Sendable { + static let defaultHutchStatsBaseURL = URL(string: "https://hutch-stats.zerolabs.sh")! + static let hutchStatsBaseURLEnvironmentKey = "HUTCH_STATS_BASE_URL" + + let hutchStatsBaseURL: URL + + init( + userDefaults: UserDefaults = .standard, + processInfo: ProcessInfo = .processInfo, + environment: [String: String]? = nil + ) { + let resolvedEnvironment = environment ?? processInfo.environment + + if + let rawValue = resolvedEnvironment[Self.hutchStatsBaseURLEnvironmentKey], + let url = Self.normalizedURL(from: rawValue) + { + self.hutchStatsBaseURL = url + } else if + let rawValue = userDefaults.string(forKey: AppStorageKeys.hutchStatsBaseURL), + let url = Self.normalizedURL(from: rawValue) + { + self.hutchStatsBaseURL = url + } else { + self.hutchStatsBaseURL = Self.defaultHutchStatsBaseURL + } + } + + private static func normalizedURL(from rawValue: String) -> URL? { + guard var components = URLComponents(string: rawValue.trimmingCharacters(in: .whitespacesAndNewlines)) else { + return nil + } + + if components.path.isEmpty { + components.path = "/" + } + + return components.url + } +} diff --git a/Hutch/App/AppState.swift b/Hutch/App/AppState.swift index 8613dc8..c519adc 100644 --- a/Hutch/App/AppState.swift +++ b/Hutch/App/AppState.swift @@ -56,6 +56,7 @@ final class AppState { // MARK: - Networking let client: SRHTClient + let configuration: AppConfiguration // MARK: - Deep link pending navigation @@ -67,6 +68,7 @@ final class AppState { // MARK: - Init init() { + self.configuration = AppConfiguration() let token = KeychainHelper.loadToken() self.client = SRHTClient(token: token) } @@ -107,11 +109,13 @@ final class AppState { accounts = storedAccounts activeAccountID = target.id currentUser = user + ContributionWidgetContextStore.saveActor(user.canonicalName) authPhase = .authenticated await refreshNeedsAttentionSnapshot() } catch { client.setToken(nil) currentUser = nil + ContributionWidgetContextStore.clear() authPhase = .unauthenticated NeedsAttentionSnapshotStore.clear() } @@ -131,6 +135,7 @@ final class AppState { UserDefaults.standard.set(entry.id, forKey: AppStorageKeys.activeAccountID) try KeychainHelper.saveAccounts(accounts) currentUser = user + ContributionWidgetContextStore.saveActor(user.canonicalName) authPhase = .authenticated await refreshNeedsAttentionSnapshot() } catch { @@ -168,6 +173,7 @@ final class AppState { let user = try await fetchMe() currentUser = user + ContributionWidgetContextStore.saveActor(user.canonicalName) authPhase = .authenticated await refreshNeedsAttentionSnapshot() } @@ -355,6 +361,7 @@ final class AppState { activeAccountID = "" UserDefaults.standard.removeObject(forKey: AppStorageKeys.activeAccountID) currentUser = nil + ContributionWidgetContextStore.clear() pendingDeepLink = nil pendingTabNavigation = nil deepLinkError = nil diff --git a/Hutch/App/AppStorageKeys.swift b/Hutch/App/AppStorageKeys.swift index 6b8c804..dcf38ba 100644 --- a/Hutch/App/AppStorageKeys.swift +++ b/Hutch/App/AppStorageKeys.swift @@ -1,7 +1,9 @@ // Shared UserDefaults key constants enum AppStorageKeys { static let swipeActionsEnabled = "swipeActionsEnabled" + static let contributionGraphsEnabled = "contributionGraphsEnabled" static let activeAccountID = "activeAccountID" static let wrapRepositoryFileLines = "wrapRepositoryFileLines" static let lookupHistory = "lookupHistory" + static let hutchStatsBaseURL = "hutchStatsBaseURL" } diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift index 96c17a8..f3e7159 100644 --- a/Hutch/App/RootView.swift +++ b/Hutch/App/RootView.swift @@ -266,6 +266,7 @@ enum MoreRoute: Hashable { case lookup case lists case pastes + case profile case settings case mailingList(InboxMailingListReference) case thread(InboxThreadSummary) @@ -284,6 +285,8 @@ private struct MoreNavigationRoot: View { MailingListListView() case .pastes: PasteListView() + case .profile: + ProfileView() case .settings: SettingsView() case .mailingList(let mailingList): |
