a native ios client for gitbay

client ios swift

https://gitbay.org

Profile repo rows carry topics, license and last commit !32

merged cmc wants to merge krz/gitbay-ios:profile-repo-metadata into main

3 files changed, +43 −1

gitbay/Discovery/ProfileViewModel.swift +19
@@ -37,11 +37,30 @@ final class ProfileViewModel {
3737 let visibility: String
3838 let description: String?
3939 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 }
4049
4150 var id: String { path }
4251 var isPrivate: Bool { visibility == "private" }
4352 var isArchived: Bool { archived ?? false }
4453 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 }
4564 }
4665
4766 /// One day's contributions. Days with none are omitted, so the
gitbay/Views/Discovery/ProfileView.swift +15
@@ -134,6 +134,21 @@ struct ProfileView: View {
134134 .foregroundStyle(.secondary)
135135 .lineLimit(2)
136136 }
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 }
137152 }
138153 }
139154 }
gitbayTests/ProfileCommitTests.swift +9 −1
@@ -16,7 +16,8 @@ private let userProfileJSON = """
1616 {"protocol_version":1,"data":{"name":"cmc","kind":"user",\
1717 "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\
1818 "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"},\
2021 {"path":"cmc/cv","visibility":"public"}],\
2122 "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\
2223 "activity_total":15},"exit_code":0}
@@ -42,6 +43,13 @@ struct ProfileAggregateTests {
4243 #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"])
4344 #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"])
4445 #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)
4553 #expect(profile.activityTotal == 15)
4654 // One read builds the whole page.
4755 #expect(stub.seen.count == 1)