summaryrefslogtreecommitdiff
path: root/Hutch/App
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-03-19 01:17:49 -0500
committerChristian Cleberg <[email protected]>2026-03-19 01:17:49 -0500
commit441b69afae30ee6b662be38004fd7b5de1e47302 (patch)
tree610827d41c7e7157290727b69b320f8dce95f08f /Hutch/App
parentffc0ef0988b5e7d01951c23af85a1cf026b3da9f (diff)
downloadhutch-441b69afae30ee6b662be38004fd7b5de1e47302.tar.gz
hutch-441b69afae30ee6b662be38004fd7b5de1e47302.tar.bz2
hutch-441b69afae30ee6b662be38004fd7b5de1e47302.zip
feat: add support for reading inbox messages (emails) and replying in-app
Diffstat (limited to 'Hutch/App')
-rw-r--r--Hutch/App/AppState.swift14
-rw-r--r--Hutch/App/RootView.swift42
2 files changed, 36 insertions, 20 deletions
diff --git a/Hutch/App/AppState.swift b/Hutch/App/AppState.swift
index 624c78d..7492fa6 100644
--- a/Hutch/App/AppState.swift
+++ b/Hutch/App/AppState.swift
@@ -7,6 +7,15 @@ import WebKit
@MainActor
final class AppState {
+ enum Tab: Hashable {
+ case home
+ case inbox
+ case repositories
+ case builds
+ case tickets
+ case settings
+ }
+
enum AuthPhase {
/// App just launched, checking for an existing token.
case launching
@@ -25,6 +34,8 @@ final class AppState {
authPhase == .authenticated && currentUser != nil
}
+ var selectedTab: Tab = .home
+
// MARK: - Current user (populated after successful validation)
private(set) var currentUser: User?
@@ -94,6 +105,7 @@ final class AppState {
await clearWebData()
clearWebContentRenderCaches()
authPhase = .unauthenticated
+ selectedTab = .home
}
func resetAppData() async {
@@ -108,6 +120,7 @@ final class AppState {
clearWebContentRenderCaches()
authPhase = .unauthenticated
+ selectedTab = .home
}
// MARK: - Deep link resolution
@@ -208,6 +221,7 @@ final class AppState {
client.responseCache.clear()
currentUser = nil
pendingDeepLink = nil
+ selectedTab = .home
}
private func clearWebData() async {
diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift
index 79411ed..36cbe5c 100644
--- a/Hutch/App/RootView.swift
+++ b/Hutch/App/RootView.swift
@@ -4,17 +4,8 @@ import SwiftUI
/// full-screen sheet for token entry on first launch.
struct RootView: View {
@Environment(AppState.self) private var appState
-
- enum Tab: Hashable {
- case home
- case repositories
- case builds
- case tickets
- case settings
- }
-
- @State private var selectedTab: Tab = .home
@State private var homePath = NavigationPath()
+ @State private var inboxPath = NavigationPath()
@State private var repoPath = NavigationPath()
@State private var buildsPath = NavigationPath()
@State private var ticketsPath = NavigationPath()
@@ -48,19 +39,29 @@ struct RootView: View {
// MARK: - Tab View
private var tabContent: some View {
- TabView(selection: $selectedTab) {
+ @Bindable var appState = appState
+
+ return TabView(selection: $appState.selectedTab) {
NavigationStack(path: $homePath) {
HomeView()
}
- .tag(Tab.home)
+ .tag(AppState.Tab.home)
.tabItem {
Label("Home", systemImage: "house")
}
+ NavigationStack(path: $inboxPath) {
+ InboxView()
+ }
+ .tag(AppState.Tab.inbox)
+ .tabItem {
+ Label("Inbox", systemImage: "tray")
+ }
+
NavigationStack(path: $repoPath) {
RepositoryListView()
}
- .tag(Tab.repositories)
+ .tag(AppState.Tab.repositories)
.tabItem {
Label("Repositories", systemImage: "book.closed")
}
@@ -73,7 +74,7 @@ struct RootView: View {
BuildDetailView(jobId: jobId)
}
}
- .tag(Tab.builds)
+ .tag(AppState.Tab.builds)
.tabItem {
Label("Builds", systemImage: "hammer")
}
@@ -85,13 +86,13 @@ struct RootView: View {
TicketDetailView(ownerUsername: target.ownerUsername, trackerName: target.trackerName, trackerId: target.trackerId, trackerRid: target.trackerRid, ticketId: target.ticketId)
}
}
- .tag(Tab.tickets)
+ .tag(AppState.Tab.tickets)
.tabItem {
Label("Tickets", systemImage: "ticket")
}
SettingsView()
- .tag(Tab.settings)
+ .tag(AppState.Tab.settings)
.tabItem {
Label("Settings", systemImage: "gear")
}
@@ -117,10 +118,11 @@ struct RootView: View {
break
case .unauthenticated:
homePath = NavigationPath()
+ inboxPath = NavigationPath()
repoPath = NavigationPath()
buildsPath = NavigationPath()
ticketsPath = NavigationPath()
- selectedTab = .home
+ appState.selectedTab = .home
isResolvingDeepLink = false
case .authenticated:
consumePendingDeepLinkIfPossible(appState.pendingDeepLink)
@@ -143,7 +145,7 @@ struct RootView: View {
case .build(let jobId):
// Reset the builds navigation and push the detail
buildsPath = NavigationPath()
- selectedTab = .builds
+ appState.selectedTab = .builds
// Defer the push slightly so the tab switch takes effect
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(100))
@@ -162,7 +164,7 @@ struct RootView: View {
do {
let summary = try await appState.resolveRepository(owner: owner, name: repo)
repoPath = NavigationPath()
- selectedTab = .repositories
+ appState.selectedTab = .repositories
try? await Task.sleep(for: .milliseconds(100))
repoPath.append(summary)
} catch {
@@ -178,7 +180,7 @@ struct RootView: View {
do {
let trackerSummary = try await appState.resolveTracker(owner: owner, name: tracker)
ticketsPath = NavigationPath()
- selectedTab = .tickets
+ appState.selectedTab = .tickets
try? await Task.sleep(for: .milliseconds(100))
ticketsPath.append(trackerSummary)
try? await Task.sleep(for: .milliseconds(100))