blob: c37be3890b8584deed93ab298eebb0dad80b5417 (
plain) (
blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
|
# feat: User resource browsing from profile
## Goal
Extend `UserProfileView` so that after looking up a user, their public
repositories and trackers are shown as browsable sections below the existing
profile metadata. This mirrors the sr.ht `~username` page.
---
## Context
**Entry point:** `Hutch/Views/Lookup/LookupView.swift`
Looking up a user opens `UserProfileView` in a sheet. Currently the view only
shows static metadata fields from the `User` model.
**Owner identifier:** `user.canonicalName` (e.g. `~username`). Strip the leading
`~` when passing to GraphQL `username` parameters — see the existing pattern in
`AppState.resolveRepository(owner:name:)`.
**Existing row views to reuse:**
- `RepositoryRowView` in `Hutch/Views/Repositories/RepositoryRowView.swift`
- Tracker row style from `TrackerListView` private `TrackerRowView`
**API pattern for user-scoped queries** (from `AppState.swift`):
```graphql
query repoLookup($owner: String!, $name: String!) {
user(username: $owner) {
repository(name: $name) { ... }
}
}
```
The same `user(username:)` root field supports `repositories` and `trackers`
paginated collections on git.sr.ht and todo.sr.ht respectively.
---
## New File: `Hutch/Views/Lookup/UserProfileViewModel.swift`
Create an `@Observable @MainActor` view model following the same pattern as
`RepositoryListViewModel` and `TrackerListViewModel`.
```swift
@Observable
@MainActor
final class UserProfileViewModel {
private(set) var repositories: [RepositorySummary] = []
private(set) var trackers: [TrackerSummary] = []
private(set) var isLoadingRepositories = false
private(set) var isLoadingTrackers = false
var repositoriesError: String?
var trackersError: String?
private let client: SRHTClient
let ownerUsername: String // without leading ~
init(ownerUsername: String, client: SRHTClient) { ... }
func loadRepositories() async { ... }
func loadTrackers() async { ... }
}
```
**GraphQL queries:**
Repositories (execute against `.git` service):
```graphql
query userRepositories($owner: String!) {
user(username: $owner) {
repositories {
results {
id rid name description visibility updated
owner { canonicalName }
HEAD { name target }
}
cursor
}
}
}
```
Trackers (execute against `.todo` service):
```graphql
query userTrackers($owner: String!) {
user(username: $owner) {
trackers {
results {
id rid name description visibility updated
owner { canonicalName }
}
cursor
}
}
}
```
Decode using private response structs identical to those in
`RepositoryListViewModel` and `TrackerListViewModel`. Map results to
`RepositorySummary` and `TrackerSummary` exactly as those view models do.
---
## Modified File: `Hutch/Views/Lookup/UserProfileView.swift`
### View model instantiation
Add `@State private var profileViewModel: UserProfileViewModel?` and initialise
it in `.task` using `user.canonicalName` with the leading `~` stripped:
```swift
.task {
let owner = user.canonicalName.hasPrefix("~")
? String(user.canonicalName.dropFirst())
: user.canonicalName
let vm = UserProfileViewModel(ownerUsername: owner, client: appState.client)
profileViewModel = vm
async let repos: () = vm.loadRepositories()
async let trackers: () = vm.loadTrackers()
_ = await (repos, trackers)
}
```
Restore `@Environment(AppState.self) private var appState` (it was removed in a
recent commit but is needed for `client` access).
### Repositories section
Add after the existing metadata sections:
```swift
Section {
if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty {
ProgressView()
} else if viewModel.repositories.isEmpty {
Text("No public repositories.")
.foregroundStyle(.secondary)
} else {
ForEach(viewModel.repositories.prefix(4)) { repo in
NavigationLink {
RepositoryDetailView(repository: repo)
} label: {
RepositoryRowView(repository: repo, buildStatus: .none)
}
}
if viewModel.repositories.count > 4 {
NavigationLink("See All") {
UserRepositoriesView(viewModel: viewModel)
}
}
}
} header: {
Text("Repositories")
}
```
### Trackers section
Immediately after the Repositories section:
```swift
Section {
if viewModel.isLoadingTrackers && viewModel.trackers.isEmpty {
ProgressView()
} else if viewModel.trackers.isEmpty {
Text("No public trackers.")
.foregroundStyle(.secondary)
} else {
ForEach(viewModel.trackers.prefix(4)) { tracker in
NavigationLink {
TicketListView(tracker: tracker)
} label: {
TrackerRowView(tracker: tracker)
}
}
if viewModel.trackers.count > 4 {
NavigationLink("See All") {
UserTrackersView(viewModel: viewModel)
}
}
}
} header: {
Text("Trackers")
}
```
`TrackerRowView` — use the same VStack layout as the private `TrackerRowView`
in `TrackerListView.swift`. Define it as a private struct in
`UserProfileView.swift` rather than duplicating from `TrackerListView`.
---
## New File: `Hutch/Views/Lookup/UserRepositoriesView.swift`
A simple full-list view for "See All" repositories:
```swift
struct UserRepositoriesView: View {
let viewModel: UserProfileViewModel
var body: some View {
List {
ForEach(viewModel.repositories) { repo in
NavigationLink {
RepositoryDetailView(repository: repo)
} label: {
RepositoryRowView(repository: repo, buildStatus: .none)
}
}
}
.listStyle(.plain)
.navigationTitle("Repositories")
.navigationBarTitleDisplayMode(.inline)
.overlay {
if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty {
SRHTLoadingStateView(message: "Loading repositories…")
}
}
.refreshable {
await viewModel.loadRepositories()
}
}
}
```
## New File: `Hutch/Views/Lookup/UserTrackersView.swift`
Same pattern as `UserRepositoriesView` but for trackers, navigating to
`TicketListView(tracker:)`.
---
## Navigation context
`UserProfileView` is always presented inside a `NavigationStack` via the sheet
in `LookupView`. `NavigationLink` destinations push correctly within that stack.
No changes to `LookupView` or `MoreRoute` are needed.
---
## Verification
1. Look up a user with public repositories and trackers. Confirm both sections
appear below the profile metadata.
2. Tap a repository row — confirm `RepositoryDetailView` pushes correctly.
3. Tap a tracker row — confirm `TicketListView` pushes correctly.
4. For users with more than 4 items, confirm "See All" pushes the full list.
5. Look up a user with no public resources — confirm the empty-state text
renders in each section rather than crashing.
6. Build with no warnings.
|