Commit 5fc8e80dbe

5fc8e80dbe926c2f5a6d38a3d4763be542f8d2a4

parent: 4f441dbc04

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 23:07 UTC

push: route a tap to the issue, merge request or build

A tap for another account switches to it first; the rebuild that
causes is what delivers the route, since the pending target outlives
the subtree.

Ref krz/gitbay#89

Layout: unified · split

gitbay/ContentView.swift +44 −7
@@ -3,35 +3,41 @@ import SwiftUI
3struct ContentView: View { 3struct ContentView: View {
4 4
5 @Environment(SessionStore.self) private var session 5 @Environment(SessionStore.self) private var session
6 @Environment(PushRouter.self) private var router
7
8 enum Tabs { case dashboard, feed, repositories, explore, profile }
9
10 @State private var selectedTab = Tabs.dashboard
11 @State private var dashboardPath = NavigationPath()
6 12
7 var body: some View { 13 var body: some View {
8 if let client = session.client, let account = session.current { 14 if let client = session.client, let account = session.current {
9 TabView { 15 TabView(selection: $selectedTab) {
10 Tab("Dashboard", systemImage: "square.grid.2x2") { 16 Tab("Dashboard", systemImage: "square.grid.2x2", value: Tabs.dashboard) {
11 NavigationStack { 17 NavigationStack(path: $dashboardPath) {
12 DashboardView(client: client) 18 DashboardView(client: client)
13 .navigationDestinations(client: client) 19 .navigationDestinations(client: client)
14 } 20 }
15 } 21 }
16 Tab("Feed", systemImage: "bolt") { 22 Tab("Feed", systemImage: "bolt", value: Tabs.feed) {
17 NavigationStack { 23 NavigationStack {
18 FeedView(client: client) 24 FeedView(client: client)
19 .navigationDestinations(client: client) 25 .navigationDestinations(client: client)
20 } 26 }
21 } 27 }
22 Tab("Repositories", systemImage: "books.vertical") { 28 Tab("Repositories", systemImage: "books.vertical", value: Tabs.repositories) {
23 NavigationStack { 29 NavigationStack {
24 RepoListView(client: client) 30 RepoListView(client: client)
25 .navigationDestinations(client: client) 31 .navigationDestinations(client: client)
26 } 32 }
27 } 33 }
28 Tab("Explore", systemImage: "safari") { 34 Tab("Explore", systemImage: "safari", value: Tabs.explore) {
29 NavigationStack { 35 NavigationStack {
30 ExploreView(client: client) 36 ExploreView(client: client)
31 .navigationDestinations(client: client) 37 .navigationDestinations(client: client)
32 } 38 }
33 } 39 }
34 Tab("My Profile", systemImage: "person.crop.circle") { 40 Tab("My Profile", systemImage: "person.crop.circle", value: Tabs.profile) {
35 NavigationStack { 41 NavigationStack {
36 ProfileView(client: client, name: account.username, 42 ProfileView(client: client, name: account.username,
37 title: "My Profile") 43 title: "My Profile")
@@ -41,6 +47,7 @@ struct ContentView: View {
41 } 47 }
42 } 48 }
43 .id(account.id) // fresh screens on account switch 49 .id(account.id) // fresh screens on account switch
50 .task(id: router.pending) { await routePendingNotification() }
44 } else { 51 } else {
45 NavigationStack { 52 NavigationStack {
46 SignInView() 53 SignInView()
@@ -48,6 +55,36 @@ struct ContentView: View {
48 } 55 }
49 } 56 }
50 57
58 /// Routes a tapped notification, switching accounts first when it
59 /// belongs to another one.
60 ///
61 /// A switch changes `account.id`, which rebuilds this whole view.
62 /// That is why the target is left pending across it: the rebuilt
63 /// tree runs this again and finds the account already active, so the
64 /// second pass does the navigating. The drain is the mechanism, not
65 /// a workaround for it.
66 private func routePendingNotification() async {
67 guard let target = router.pending else { return }
68 if session.current?.id != target.accountID {
69 guard let account = session.accounts.first(where: { $0.id == target.accountID }) else {
70 router.pending = nil // signed out of it since the tap
71 return
72 }
73 _ = session.activate(account)
74 return // the rebuilt tree drains it
75 }
76 selectedTab = .dashboard
77 switch target.destination {
78 case .issue(let repo, let number):
79 dashboardPath.append(IssueRoute.issue(repo: repo, number: number))
80 case .mr(let repo, let number):
81 dashboardPath.append(MRRoute.mr(repo: repo, number: number))
82 case .build(let repo, let number):
83 dashboardPath.append(BuildRoute.detail(repo: repo, number: number))
84 }
85 router.pending = nil
86 }
87
51} 88}
52 89
53/// Every value-routed screen, attachable to any stack root. 90/// Every value-routed screen, attachable to any stack root.