Commit 972217316c
Unsigned
Layout: unified · split
Hutch.xcodeproj/project.pbxproj +6 −6
| @@ -614,7 +614,7 @@ | |||
| 614 | "$(inherited)", | 614 | "$(inherited)", |
| 615 | "@executable_path/Frameworks", | 615 | "@executable_path/Frameworks", |
| 616 | ); | 616 | ); |
| 617 | MARKETING_VERSION = 3.8.1; | 617 | MARKETING_VERSION = 3.8.2; |
| 618 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; | 618 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 619 | PRODUCT_NAME = "$(TARGET_NAME)"; | 619 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 620 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 620 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -651,7 +651,7 @@ | |||
| 651 | "$(inherited)", | 651 | "$(inherited)", |
| 652 | "@executable_path/Frameworks", | 652 | "@executable_path/Frameworks", |
| 653 | ); | 653 | ); |
| 654 | MARKETING_VERSION = 3.8.1; | 654 | MARKETING_VERSION = 3.8.2; |
| 655 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; | 655 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 656 | PRODUCT_NAME = "$(TARGET_NAME)"; | 656 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 657 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 657 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -724,7 +724,7 @@ | |||
| 724 | "@executable_path/Frameworks", | 724 | "@executable_path/Frameworks", |
| 725 | "@executable_path/../../Frameworks", | 725 | "@executable_path/../../Frameworks", |
| 726 | ); | 726 | ); |
| 727 | MARKETING_VERSION = 3.8.1; | 727 | MARKETING_VERSION = 3.8.2; |
| 728 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; | 728 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 729 | PRODUCT_NAME = "$(TARGET_NAME)"; | 729 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 730 | SKIP_INSTALL = YES; | 730 | SKIP_INSTALL = YES; |
| @@ -753,7 +753,7 @@ | |||
| 753 | "@executable_path/Frameworks", | 753 | "@executable_path/Frameworks", |
| 754 | "@executable_path/../../Frameworks", | 754 | "@executable_path/../../Frameworks", |
| 755 | ); | 755 | ); |
| 756 | MARKETING_VERSION = 3.8.1; | 756 | MARKETING_VERSION = 3.8.2; |
| 757 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; | 757 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 758 | PRODUCT_NAME = "$(TARGET_NAME)"; | 758 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 759 | SKIP_INSTALL = YES; | 759 | SKIP_INSTALL = YES; |
| @@ -782,7 +782,7 @@ | |||
| 782 | "@executable_path/Frameworks", | 782 | "@executable_path/Frameworks", |
| 783 | "@executable_path/../../Frameworks", | 783 | "@executable_path/../../Frameworks", |
| 784 | ); | 784 | ); |
| 785 | MARKETING_VERSION = 3.8.1; | 785 | MARKETING_VERSION = 3.8.2; |
| 786 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; | 786 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; |
| 787 | PRODUCT_NAME = "$(TARGET_NAME)"; | 787 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 788 | SKIP_INSTALL = YES; | 788 | SKIP_INSTALL = YES; |
| @@ -811,7 +811,7 @@ | |||
| 811 | "@executable_path/Frameworks", | 811 | "@executable_path/Frameworks", |
| 812 | "@executable_path/../../Frameworks", | 812 | "@executable_path/../../Frameworks", |
| 813 | ); | 813 | ); |
| 814 | MARKETING_VERSION = 3.8.1; | 814 | MARKETING_VERSION = 3.8.2; |
| 815 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; | 815 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchSafariExtension; |
| 816 | PRODUCT_NAME = "$(TARGET_NAME)"; | 816 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 817 | SKIP_INSTALL = YES; | 817 | SKIP_INSTALL = YES; |
Hutch/Views/Home/HomeView.swift +52 −21
| @@ -10,6 +10,7 @@ struct HomeView: View { | |||
| 10 | @State private var isOpeningRecentItem = false | 10 | @State private var isOpeningRecentItem = false |
| 11 | @State private var selectedPinnedProject: Project? | 11 | @State private var selectedPinnedProject: Project? |
| 12 | @State private var selectedPinnedUser: User? | 12 | @State private var selectedPinnedUser: User? |
| 13 | @State private var isShowingSystemStatus = false | ||
| 13 | 14 | ||
| 14 | var body: some View { | 15 | var body: some View { |
| 15 | Group { | 16 | Group { |
| @@ -20,6 +21,19 @@ struct HomeView: View { | |||
| 20 | } | 21 | } |
| 21 | } | 22 | } |
| 22 | .navigationTitle("Home") | 23 | .navigationTitle("Home") |
| 24 | .toolbar { | ||
| 25 | ToolbarItem(placement: .topBarTrailing) { | ||
| 26 | Button { | ||
| 27 | isShowingSystemStatus = true | ||
| 28 | } label: { | ||
| 29 | HomeSystemStatusIndicator( | ||
| 30 | snapshot: viewModel?.systemStatusSnapshot, | ||
| 31 | isLoading: viewModel?.isLoadingSystemStatus ?? false | ||
| 32 | ) | ||
| 33 | } | ||
| 34 | .accessibilityLabel("System status") | ||
| 35 | } | ||
| 36 | } | ||
| 23 | .navigationDestination(isPresented: Binding( | 37 | .navigationDestination(isPresented: Binding( |
| 24 | get: { selectedPinnedProject != nil }, | 38 | get: { selectedPinnedProject != nil }, |
| 25 | set: { isPresented in | 39 | set: { isPresented in |
| @@ -44,6 +58,9 @@ struct HomeView: View { | |||
| 44 | UserProfileView(user: selectedPinnedUser) | 58 | UserProfileView(user: selectedPinnedUser) |
| 45 | } | 59 | } |
| 46 | } | 60 | } |
| 61 | .navigationDestination(isPresented: $isShowingSystemStatus) { | ||
| 62 | SystemStatusView() | ||
| 63 | } | ||
| 47 | .task { | 64 | .task { |
| 48 | guard let currentUser = appState.currentUser else { return } | 65 | guard let currentUser = appState.currentUser else { return } |
| 49 | await ensureViewModel(currentUser: currentUser).loadDashboard() | 66 | await ensureViewModel(currentUser: currentUser).loadDashboard() |
| @@ -64,7 +81,6 @@ struct HomeView: View { | |||
| 64 | @ViewBuilder | 81 | @ViewBuilder |
| 65 | private func content(_ viewModel: HomeViewModel) -> some View { | 82 | private func content(_ viewModel: HomeViewModel) -> some View { |
| 66 | List { | 83 | List { |
| 67 | systemStatusSection(viewModel) | ||
| 68 | workSection(viewModel) | 84 | workSection(viewModel) |
| 69 | recentSection | 85 | recentSection |
| 70 | buildsSection(viewModel) | 86 | buildsSection(viewModel) |
| @@ -84,26 +100,6 @@ struct HomeView: View { | |||
| 84 | } | 100 | } |
| 85 | } | 101 | } |
| 86 | 102 | ||
| 87 | @ViewBuilder | ||
| 88 | private func systemStatusSection(_ viewModel: HomeViewModel) -> some View { | ||
| 89 | if viewModel.systemStatusSnapshot?.hasDisruption == true { | ||
| 90 | Section { | ||
| 91 | NavigationLink { | ||
| 92 | SystemStatusView() | ||
| 93 | } label: { | ||
| 94 | SystemStatusSummaryRow( | ||
| 95 | snapshot: viewModel.systemStatusSnapshot, | ||
| 96 | isLoading: viewModel.isLoadingSystemStatus, | ||
| 97 | errorMessage: viewModel.systemStatusErrorMessage, | ||
| 98 | isShowingStaleData: viewModel.isShowingStaleSystemStatus | ||
| 99 | ) | ||
| 100 | } | ||
| 101 | .buttonStyle(.plain) | ||
| 102 | .themedRow() | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | |||
| 107 | private func workSection(_ viewModel: HomeViewModel) -> some View { | 103 | private func workSection(_ viewModel: HomeViewModel) -> some View { |
| 108 | Section("Work") { | 104 | Section("Work") { |
| 109 | NavigationLink(value: HomeRoute.work(scope: .all)) { | 105 | NavigationLink(value: HomeRoute.work(scope: .all)) { |
| @@ -490,6 +486,41 @@ private struct HomeSummaryRow: View { | |||
| 490 | } | 486 | } |
| 491 | } | 487 | } |
| 492 | 488 | ||
| 489 | private struct HomeSystemStatusIndicator: View { | ||
| 490 | let snapshot: SystemStatusSnapshot? | ||
| 491 | let isLoading: Bool | ||
| 492 | |||
| 493 | var body: some View { | ||
| 494 | if isLoading && snapshot == nil { | ||
| 495 | ProgressView() | ||
| 496 | .controlSize(.small) | ||
| 497 | } else { | ||
| 498 | Circle() | ||
| 499 | .fill(color) | ||
| 500 | .frame(width: 22, height: 22) | ||
| 501 | .overlay { | ||
| 502 | Image(systemName: glyph) | ||
| 503 | .font(.system(size: 11, weight: .bold)) | ||
| 504 | .foregroundStyle(.white) | ||
| 505 | } | ||
| 506 | } | ||
| 507 | } | ||
| 508 | |||
| 509 | private var color: Color { | ||
| 510 | if let snapshot { | ||
| 511 | return snapshot.hasDisruption ? .orange : .green | ||
| 512 | } | ||
| 513 | return .secondary | ||
| 514 | } | ||
| 515 | |||
| 516 | private var glyph: String { | ||
| 517 | if let snapshot { | ||
| 518 | return snapshot.hasDisruption ? "exclamationmark" : "checkmark" | ||
| 519 | } | ||
| 520 | return "questionmark" | ||
| 521 | } | ||
| 522 | } | ||
| 523 | |||
| 493 | private struct HomeRecentRow: View { | 524 | private struct HomeRecentRow: View { |
| 494 | let item: RecentActivityEntry | 525 | let item: RecentActivityEntry |
| 495 | 526 | ||
Hutch/Views/Home/HomeViewModel.swift +13 −10
| @@ -365,6 +365,19 @@ final class HomeViewModel { | |||
| 365 | async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh) | 365 | async let inboxUnreadTask = loadInboxUnreadSnapshot(forceRefresh: forceRefresh) |
| 366 | async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh) | 366 | async let systemStatusTask = loadSystemStatusSnapshot(forceRefresh: forceRefresh) |
| 367 | 367 | ||
| 368 | // Resolve system status first so the Home title-bar status badge can | ||
| 369 | // settle from cache without waiting on the slower list data below. | ||
| 370 | let systemStatusResult = await systemStatusTask | ||
| 371 | switch systemStatusResult { | ||
| 372 | case .success(let result): | ||
| 373 | systemStatusSnapshot = result.value | ||
| 374 | isShowingStaleSystemStatus = result.isStale | ||
| 375 | systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil | ||
| 376 | case .failure(let error): | ||
| 377 | systemStatusErrorMessage = error.userFacingMessage | ||
| 378 | } | ||
| 379 | isLoadingSystemStatus = false | ||
| 380 | |||
| 368 | let projectsResult = await projectsTask | 381 | let projectsResult = await projectsTask |
| 369 | switch projectsResult { | 382 | switch projectsResult { |
| 370 | case .success(let projects): | 383 | case .success(let projects): |
| @@ -404,16 +417,6 @@ final class HomeViewModel { | |||
| 404 | unreadInboxThreadCount = inboxUnreadSnapshot?.unreadCount | 417 | unreadInboxThreadCount = inboxUnreadSnapshot?.unreadCount |
| 405 | unreadInboxThreads = inboxUnreadSnapshot?.threads ?? [] | 418 | unreadInboxThreads = inboxUnreadSnapshot?.threads ?? [] |
| 406 | hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0 | 419 | hasUnreadInboxThreads = (unreadInboxThreadCount ?? 0) > 0 |
| 407 | let systemStatusResult = await systemStatusTask | ||
| 408 | switch systemStatusResult { | ||
| 409 | case .success(let result): | ||
| 410 | systemStatusSnapshot = result.value | ||
| 411 | isShowingStaleSystemStatus = result.isStale | ||
| 412 | systemStatusErrorMessage = result.isStale ? result.refreshErrorMessage : nil | ||
| 413 | case .failure(let error): | ||
| 414 | systemStatusErrorMessage = error.userFacingMessage | ||
| 415 | } | ||
| 416 | isLoadingSystemStatus = false | ||
| 417 | lastRefreshed = Date() | 420 | lastRefreshed = Date() |
| 418 | persistNeedsAttentionSnapshot() | 421 | persistNeedsAttentionSnapshot() |
| 419 | persistSystemStatusWidgetSnapshot() | 422 | persistSystemStatusWidgetSnapshot() |
ROADMAP.md +1
| @@ -142,6 +142,7 @@ so "breaking change" does not apply. These buckets track *user-visible scale*. | |||
| 142 | | Version | Contents | Why here | | 142 | | Version | Contents | Why here | |
| 143 | | --- | --- | --- | | 143 | | --- | --- | --- | |
| 144 | | v3.8.1 | SonarCloud triage; housekeeping | No behaviour change at all | | 144 | | v3.8.1 | SonarCloud triage; housekeeping | No behaviour change at all | |
| 145 | | v3.8.2 | Home system status moved to a title-bar status badge | Small UI relocation, no new surface | | ||
| 145 | | v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map | | 146 | | v3.9.0 | "What's cooking" ingest; doc truth-up; deploy keys | Ships one feature, corrects the map | |
| 146 | | v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds | | 147 | | v3.10.0 | hub.sr.ht writes: projects, discovery, `mailingListSubscribe` | Provisional — gated on what v3.9.0 finds | |
| 147 | | v3.11.0 | Accessibility | Independent, device-verified | | 148 | | v3.11.0 | Accessibility | Independent, device-verified | |