a native ios client for gitbay

client ios swift

https://gitbay.org

commits: show the patch in place; move the profile to a tab !22

merged cmc wants to merge krz/gitbay-ios:commit-diff-and-profile-tab into main

9 files changed, +39 −88

gitbay/ContentView.swift +8 −2
@@ -31,6 +31,14 @@ struct ContentView: View {
3131 .navigationDestinations(client: client)
3232 }
3333 }
34 Tab("My Profile", systemImage: "person.crop.circle") {
35 NavigationStack {
36 ProfileView(client: client, name: account.username,
37 title: "My Profile")
38 .toolbar { AccountMenu() }
39 .navigationDestinations(client: client)
40 }
41 }
3442 }
3543 .id(account.id) // fresh screens on account switch
3644 } else {
@@ -96,8 +104,6 @@ private struct RouteDestinations: ViewModifier {
96104 WikiPageView(client: client, repo: repo, page: page)
97105 case .commit(let repo, let sha):
98106 CommitView(client: client, repo: repo, sha: sha)
99 case .commitDiff(let repo, let sha):
100 CommitDiffView(client: client, repo: repo, sha: sha)
101107 case .profile(let name):
102108 ProfileView(client: client, name: name)
103109 case .account:
gitbay/Views/Dashboard/DashboardView.swift −1
@@ -52,7 +52,6 @@ struct DashboardView: View {
5252 }
5353 .accessibilityIdentifier("dashboard-create-button")
5454 }
55 AccountMenu()
5655 }
5756 .sheet(isPresented: $composing) {
5857 RepoCreateSheet(
gitbay/Views/Discovery/ExploreView.swift −1
@@ -61,7 +61,6 @@ struct ExploreView: View {
6161 }
6262 .overlay { LoadStateOverlay(state: list.state) }
6363 .navigationTitle("Explore")
64 .toolbar { AccountMenu() }
6564 .task {
6665 if list.state.value == nil { await list.reload() }
6766 }
gitbay/Views/Discovery/FeedView.swift −1
@@ -22,7 +22,6 @@ struct FeedView: View {
2222 }
2323 .overlay { LoadStateOverlay(state: list.state) }
2424 .navigationTitle("Feed")
25 .toolbar { AccountMenu() }
2625 .task {
2726 if list.state.value == nil {
2827 await list.reload()
gitbay/Views/Discovery/ProfileView.swift +7 −2
@@ -6,8 +6,13 @@ struct ProfileView: View {
66
77 @State private var model: ProfileViewModel
88
9 init(client: GitbayClient, name: String) {
9 /// Someone else's profile is titled with their name. Your own is
10 /// reached from a tab and titled to match it.
11 private let title: String?
12
13 init(client: GitbayClient, name: String, title: String? = nil) {
1014 _model = State(initialValue: ProfileViewModel(client: client, name: name))
15 self.title = title
1116 }
1217
1318 var body: some View {
@@ -27,7 +32,7 @@ struct ProfileView: View {
2732 }
2833 }
2934 .overlay { LoadStateOverlay(state: model.state) }
30 .navigationTitle(model.name)
35 .navigationTitle(title ?? model.name)
3136 .navigationBarTitleDisplayMode(.inline)
3237 .task { await model.load() }
3338 .refreshable { await model.load() }
gitbay/Views/Repos/CommitView.swift +5 −59
@@ -64,73 +64,19 @@ struct CommitView: View {
6464 }
6565
6666 if let diff = model.diff, !diff.files.isEmpty {
67 Section("Changes +\(diff.additions)\(diff.deletions)") {
68 ForEach(diff.files) { file in
69 NavigationLink(value: RepoRoute.file(
70 repo: model.repoPath, path: file.displayPath, ref: commit.sha
71 )) {
72 HStack {
73 Text(file.displayPath)
74 .font(.gbMono(.caption))
75 .lineLimit(1)
76 .truncationMode(.head)
77 Spacer()
78 Text("+\(file.additions)").foregroundStyle(Color.gbOK)
79 Text("\(file.deletions)").foregroundStyle(Color.gbBad)
80 }
81 .font(.gbSans(.caption2))
82 }
83 }
84 }
85 }
86 }
87 }
88 .overlay { LoadStateOverlay(state: model.state) }
89 .navigationTitle(String(model.sha.prefix(10)))
90 .navigationBarTitleDisplayMode(.inline)
91 .toolbar {
92 ToolbarItem(placement: .topBarTrailing) {
93 if model.diff?.files.isEmpty == false {
94 NavigationLink(value: RepoRoute.commitDiff(
95 repo: model.repoPath, sha: model.sha
96 )) {
97 Image(systemName: "plus.forwardslash.minus")
98 }
99 .accessibilityIdentifier("commit-diff-button")
100 }
101 }
102 }
103 .task { await model.load() }
104 .refreshable { await model.load() }
105 }
106}
107
108/// The commit's patch, rendered by the same view the MR diff uses.
109struct CommitDiffView: View {
110
111 @State private var model: CommitDetailViewModel
112
113 init(client: GitbayClient, repo: String, sha: String) {
114 _model = State(initialValue: CommitDetailViewModel(
115 client: client, repoPath: repo, sha: sha
116 ))
117 }
118
119 var body: some View {
120 ZStack {
121 Color.clear
122 if let diff = model.diff, !diff.files.isEmpty {
123 List {
67 // The patch, in place. The web's commit page is the
68 // diff a file header expands its hunks rather than
69 // linking away to the whole file.
12470 ForEach(diff.files) { file in
12571 DiffFileSection(file: file)
12672 }
12773 }
128 .listStyle(.plain)
12974 }
13075 }
13176 .overlay { LoadStateOverlay(state: model.state) }
132 .navigationTitle("Diff")
77 .navigationTitle(String(model.sha.prefix(10)))
13378 .navigationBarTitleDisplayMode(.inline)
13479 .task { await model.load() }
80 .refreshable { await model.load() }
13581 }
13682 }
gitbay/Views/Repos/RepoListView.swift +2 −5
@@ -35,7 +35,6 @@ struct RepoListView: View {
3535 }
3636 .accessibilityIdentifier("repo-create-button")
3737 }
38 AccountMenu()
3938 }
4039 .sheet(isPresented: $composing) {
4140 RepoCreateSheet(
@@ -148,7 +147,8 @@ private struct RepoRow: View {
148147 }
149148 }
150149
151/// The account switcher and sign-out, on every top-level screen.
150/// The account switcher and sign-out. It lives on the My Profile tab
151/// only: the other tabs are about the instance, not about who you are.
152152 struct AccountMenu: ToolbarContent {
153153
154154 @Environment(SessionStore.self) private var session
@@ -158,9 +158,6 @@ struct AccountMenu: ToolbarContent {
158158 Menu {
159159 if let current = session.current {
160160 Section(current.label) {
161 NavigationLink(value: RepoRoute.profile(current.username)) {
162 Label("My Profile", systemImage: "person.crop.circle")
163 }
164161 NavigationLink(value: RepoRoute.account) {
165162 Label("Keys & Email", systemImage: "key")
166163 }
gitbay/Views/Repos/RepoRoute.swift −1
@@ -15,7 +15,6 @@ nonisolated enum RepoRoute: Hashable {
1515 case wiki(repo: String)
1616 case wikiPage(repo: String, page: String)
1717 case commit(repo: String, sha: String)
18 case commitDiff(repo: String, sha: String)
1918 case profile(String)
2019 case account
2120 case addAccount
gitbayUITests/LiveSmokeUITests.swift +17 −16
@@ -182,9 +182,10 @@ final class LiveSmokeUITests: XCTestCase {
182182 app.navigationBars.buttons.firstMatch.tap()
183183 }
184184
185 /// Keys, PGP and email live behind the profile menu now, not their
186 /// own toolbar button.
185 /// Keys, PGP and email live behind the account menu, which is on the
186 /// My Profile tab and nowhere else.
187187 func openAccountScreen(file: StaticString = #filePath, line: UInt = #line) {
188 selectTab("My Profile")
188189 let menu = app.descendants(matching: .any)
189190 .matching(identifier: "account-menu").firstMatch
190191 XCTAssertTrue(menu.waitForExistence(timeout: 15),
@@ -512,7 +513,9 @@ extension LiveSmokeUITests {
512513 // A bogus verification code is refused, not swallowed.
513514 let code = app.descendants(matching: .any)
514515 .matching(identifier: "email-code").firstMatch
515 XCTAssertTrue(code.waitForExistence(timeout: 5))
516 // Email sits below the keys on a List, which does not build rows
517 // it has not shown.
518 XCTAssertTrue(scrollTo(code), "email section not reachable")
516519 focusAndType(code, "000000")
517520 let verify = app.descendants(matching: .any)
518521 .matching(identifier: "email-verify").firstMatch
@@ -713,16 +716,16 @@ extension LiveSmokeUITests {
713716 "dashboard + did not open the create sheet")
714717 app.buttons["Cancel"].firstMatch.tap()
715718
716 // --- identity is one menu: profile and keys together ---
719 // --- identity is its own tab; the account menu rides with it ---
720 selectTab("My Profile")
717721 let menu = app.descendants(matching: .any)
718722 .matching(identifier: "account-menu").firstMatch
719 XCTAssertTrue(menu.waitForExistence(timeout: 10))
723 XCTAssertTrue(menu.waitForExistence(timeout: 10),
724 "account menu missing from My Profile")
720725 menu.tap()
721726 XCTAssertTrue(app.buttons["Keys & Email"].firstMatch.waitForExistence(timeout: 5),
722727 "keys not in the account menu")
723 let myProfile = app.buttons["My Profile"].firstMatch
724 XCTAssertTrue(myProfile.exists, "profile not in the account menu")
725 myProfile.tap()
728 app.tap() // dismiss the menu; the profile is already on screen
726729
727730 // --- a profile is a profile: description, links, orgs, graph, repos ---
728731 XCTAssertTrue(app.staticTexts
@@ -746,14 +749,12 @@ extension LiveSmokeUITests {
746749 let firstCommit = app.cells.firstMatch
747750 XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty")
748751 firstCommit.tap()
749 // The commit screen shows its changed files and can open the patch.
750 XCTAssertTrue(app.descendants(matching: .any)
751 .matching(identifier: "commit-diff-button").firstMatch
752 .waitForExistence(timeout: 20), "commit did not open")
753 app.descendants(matching: .any).matching(identifier: "commit-diff-button")
754 .firstMatch.tap()
755 XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20),
756 "commit diff rendered nothing")
752 // The commit screen IS the patch: no navigating away to find it.
753 // A hunk header (@@) only appears in a rendered diff.
754 XCTAssertTrue(app.staticTexts
755 .containing(NSPredicate(format: "label BEGINSWITH '@@'")).firstMatch
756 .waitForExistence(timeout: 20),
757 "commit screen did not render the patch inline")
757758 }
758759 }
759760