Commit d56d8c6c96
Unsigned
Layout: unified · split
gitbay/Discovery/ProfileViewModel.swift +19
| @@ -37,11 +37,30 @@ final class ProfileViewModel { | |||
| 37 | let visibility: String | 37 | let visibility: String |
| 38 | let description: String? | 38 | let description: String? |
| 39 | let archived: Bool? | 39 | let archived: Bool? |
| 40 | let topics: [String]? | ||
| 41 | let license: String? | ||
| 42 | let defaultBranch: String? | ||
| 43 | let updated: String? | ||
| 44 | |||
| 45 | enum CodingKeys: String, CodingKey { | ||
| 46 | case path, visibility, description, archived, topics, license, updated | ||
| 47 | case defaultBranch = "default_branch" | ||
| 48 | } | ||
| 40 | 49 | ||
| 41 | var id: String { path } | 50 | var id: String { path } |
| 42 | var isPrivate: Bool { visibility == "private" } | 51 | var isPrivate: Bool { visibility == "private" } |
| 43 | var isArchived: Bool { archived ?? false } | 52 | var isArchived: Bool { archived ?? false } |
| 44 | var name: String { String(path.split(separator: "/").last ?? "") } | 53 | var name: String { String(path.split(separator: "/").last ?? "") } |
| 54 | |||
| 55 | /// "main · MIT · updated 2026-08-30", the line the web's repo | ||
| 56 | /// rows carry. Parts the server did not report are left out. | ||
| 57 | var meta: String { | ||
| 58 | var parts: [String] = [] | ||
| 59 | if let defaultBranch, !defaultBranch.isEmpty { parts.append(defaultBranch) } | ||
| 60 | if let license, !license.isEmpty { parts.append(license) } | ||
| 61 | if let updated, !updated.isEmpty { parts.append("updated \(updated)") } | ||
| 62 | return parts.joined(separator: " · ") | ||
| 63 | } | ||
| 45 | } | 64 | } |
| 46 | 65 | ||
| 47 | /// One day's contributions. Days with none are omitted, so the | 66 | /// One day's contributions. Days with none are omitted, so the |
gitbay/Views/Discovery/ProfileView.swift +15
| @@ -134,6 +134,21 @@ struct ProfileView: View { | |||
| 134 | .foregroundStyle(.secondary) | 134 | .foregroundStyle(.secondary) |
| 135 | .lineLimit(2) | 135 | .lineLimit(2) |
| 136 | } | 136 | } |
| 137 | if let topics = repo.topics, !topics.isEmpty { | ||
| 138 | ScrollView(.horizontal, showsIndicators: false) { | ||
| 139 | HStack(spacing: 6) { | ||
| 140 | ForEach(topics, id: \.self) { topic in | ||
| 141 | GBChip(topic, .gbAccent) | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | if !repo.meta.isEmpty { | ||
| 147 | Text(repo.meta) | ||
| 148 | .font(.gbSans(.caption2)) | ||
| 149 | .foregroundStyle(.secondary) | ||
| 150 | .lineLimit(1) | ||
| 151 | } | ||
| 137 | } | 152 | } |
| 138 | } | 153 | } |
| 139 | } | 154 | } |
gitbayTests/ProfileCommitTests.swift +9 −1
| @@ -16,7 +16,8 @@ private let userProfileJSON = """ | |||
| 16 | {"protocol_version":1,"data":{"name":"cmc","kind":"user",\ | 16 | {"protocol_version":1,"data":{"name":"cmc","kind":"user",\ |
| 17 | "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\ | 17 | "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\ |
| 18 | "orgs":[{"name":"krz","role":"admin"},{"name":"audit-labs","role":"admin"}],\ | 18 | "orgs":[{"name":"krz","role":"admin"},{"name":"audit-labs","role":"admin"}],\ |
| 19 | "repos":[{"path":"cmc/notes","visibility":"private","description":"notes"},\ | 19 | "repos":[{"path":"cmc/notes","visibility":"private","description":"notes",\ |
| 20 | "topics":["org","writing"],"license":"MIT","default_branch":"main","updated":"2026-08-28"},\ | ||
| 20 | {"path":"cmc/cv","visibility":"public"}],\ | 21 | {"path":"cmc/cv","visibility":"public"}],\ |
| 21 | "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\ | 22 | "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\ |
| 22 | "activity_total":15},"exit_code":0} | 23 | "activity_total":15},"exit_code":0} |
| @@ -42,6 +43,13 @@ struct ProfileAggregateTests { | |||
| 42 | #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"]) | 43 | #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"]) |
| 43 | #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"]) | 44 | #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"]) |
| 44 | #expect(profile.repos[0].isPrivate) | 45 | #expect(profile.repos[0].isPrivate) |
| 46 | // Repo rows carry the listing metadata, so a profile lists | ||
| 47 | // repositories the way explore does. | ||
| 48 | #expect(profile.repos[0].topics == ["org", "writing"]) | ||
| 49 | #expect(profile.repos[0].meta == "main · MIT · updated 2026-08-28") | ||
| 50 | // A row the server reported without any of it still renders. | ||
| 51 | #expect(profile.repos[1].topics == nil) | ||
| 52 | #expect(profile.repos[1].meta.isEmpty) | ||
| 45 | #expect(profile.activityTotal == 15) | 53 | #expect(profile.activityTotal == 15) |
| 46 | // One read builds the whole page. | 54 | // One read builds the whole page. |
| 47 | #expect(stub.seen.count == 1) | 55 | #expect(stub.seen.count == 1) |