Commit 4328d08603
Verified · cmc
gitbay/ContentView.swift +8 −2
| @@ -31,6 +31,14 @@ struct ContentView: View { | ||
| 31 | 31 | .navigationDestinations(client: client) |
| 32 | 32 | } |
| 33 | 33 | } |
| 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 | } | |
| 34 | 42 | } |
| 35 | 43 | .id(account.id) // fresh screens on account switch |
| 36 | 44 | } else { |
| @@ -96,8 +104,6 @@ private struct RouteDestinations: ViewModifier { | ||
| 96 | 104 | WikiPageView(client: client, repo: repo, page: page) |
| 97 | 105 | case .commit(let repo, let sha): |
| 98 | 106 | CommitView(client: client, repo: repo, sha: sha) |
| 99 | case .commitDiff(let repo, let sha): | |
| 100 | CommitDiffView(client: client, repo: repo, sha: sha) | |
| 101 | 107 | case .profile(let name): |
| 102 | 108 | ProfileView(client: client, name: name) |
| 103 | 109 | case .account: |
gitbay/Views/Dashboard/DashboardView.swift −1
| @@ -52,7 +52,6 @@ struct DashboardView: View { | ||
| 52 | 52 | } |
| 53 | 53 | .accessibilityIdentifier("dashboard-create-button") |
| 54 | 54 | } |
| 55 | AccountMenu() | |
| 56 | 55 | } |
| 57 | 56 | .sheet(isPresented: $composing) { |
| 58 | 57 | RepoCreateSheet( |
gitbay/Views/Discovery/ExploreView.swift −1
| @@ -61,7 +61,6 @@ struct ExploreView: View { | ||
| 61 | 61 | } |
| 62 | 62 | .overlay { LoadStateOverlay(state: list.state) } |
| 63 | 63 | .navigationTitle("Explore") |
| 64 | .toolbar { AccountMenu() } | |
| 65 | 64 | .task { |
| 66 | 65 | if list.state.value == nil { await list.reload() } |
| 67 | 66 | } |
gitbay/Views/Discovery/FeedView.swift −1
| @@ -22,7 +22,6 @@ struct FeedView: View { | ||
| 22 | 22 | } |
| 23 | 23 | .overlay { LoadStateOverlay(state: list.state) } |
| 24 | 24 | .navigationTitle("Feed") |
| 25 | .toolbar { AccountMenu() } | |
| 26 | 25 | .task { |
| 27 | 26 | if list.state.value == nil { |
| 28 | 27 | await list.reload() |
gitbay/Views/Discovery/ProfileView.swift +7 −2
| @@ -6,8 +6,13 @@ struct ProfileView: View { | ||
| 6 | 6 | |
| 7 | 7 | @State private var model: ProfileViewModel |
| 8 | 8 | |
| 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) { | |
| 10 | 14 | _model = State(initialValue: ProfileViewModel(client: client, name: name)) |
| 15 | self.title = title | |
| 11 | 16 | } |
| 12 | 17 | |
| 13 | 18 | var body: some View { |
| @@ -27,7 +32,7 @@ struct ProfileView: View { | ||
| 27 | 32 | } |
| 28 | 33 | } |
| 29 | 34 | .overlay { LoadStateOverlay(state: model.state) } |
| 30 | .navigationTitle(model.name) | |
| 35 | .navigationTitle(title ?? model.name) | |
| 31 | 36 | .navigationBarTitleDisplayMode(.inline) |
| 32 | 37 | .task { await model.load() } |
| 33 | 38 | .refreshable { await model.load() } |
gitbay/Views/Repos/CommitView.swift +5 −59
| @@ -64,73 +64,19 @@ struct CommitView: View { | ||
| 64 | 64 | } |
| 65 | 65 | |
| 66 | 66 | 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. | |
| 109 | struct 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. | |
| 124 | 70 | ForEach(diff.files) { file in |
| 125 | 71 | DiffFileSection(file: file) |
| 126 | 72 | } |
| 127 | 73 | } |
| 128 | .listStyle(.plain) | |
| 129 | 74 | } |
| 130 | 75 | } |
| 131 | 76 | .overlay { LoadStateOverlay(state: model.state) } |
| 132 | .navigationTitle("Diff") | |
| 77 | .navigationTitle(String(model.sha.prefix(10))) | |
| 133 | 78 | .navigationBarTitleDisplayMode(.inline) |
| 134 | 79 | .task { await model.load() } |
| 80 | .refreshable { await model.load() } | |
| 135 | 81 | } |
| 136 | 82 | } |
gitbay/Views/Repos/RepoListView.swift +2 −5
| @@ -35,7 +35,6 @@ struct RepoListView: View { | ||
| 35 | 35 | } |
| 36 | 36 | .accessibilityIdentifier("repo-create-button") |
| 37 | 37 | } |
| 38 | AccountMenu() | |
| 39 | 38 | } |
| 40 | 39 | .sheet(isPresented: $composing) { |
| 41 | 40 | RepoCreateSheet( |
| @@ -148,7 +147,8 @@ private struct RepoRow: View { | ||
| 148 | 147 | } |
| 149 | 148 | } |
| 150 | 149 | |
| 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. | |
| 152 | 152 | struct AccountMenu: ToolbarContent { |
| 153 | 153 | |
| 154 | 154 | @Environment(SessionStore.self) private var session |
| @@ -158,9 +158,6 @@ struct AccountMenu: ToolbarContent { | ||
| 158 | 158 | Menu { |
| 159 | 159 | if let current = session.current { |
| 160 | 160 | Section(current.label) { |
| 161 | NavigationLink(value: RepoRoute.profile(current.username)) { | |
| 162 | Label("My Profile", systemImage: "person.crop.circle") | |
| 163 | } | |
| 164 | 161 | NavigationLink(value: RepoRoute.account) { |
| 165 | 162 | Label("Keys & Email", systemImage: "key") |
| 166 | 163 | } |
gitbay/Views/Repos/RepoRoute.swift −1
| @@ -15,7 +15,6 @@ nonisolated enum RepoRoute: Hashable { | ||
| 15 | 15 | case wiki(repo: String) |
| 16 | 16 | case wikiPage(repo: String, page: String) |
| 17 | 17 | case commit(repo: String, sha: String) |
| 18 | case commitDiff(repo: String, sha: String) | |
| 19 | 18 | case profile(String) |
| 20 | 19 | case account |
| 21 | 20 | case addAccount |
gitbayUITests/LiveSmokeUITests.swift +17 −16
| @@ -182,9 +182,10 @@ final class LiveSmokeUITests: XCTestCase { | ||
| 182 | 182 | app.navigationBars.buttons.firstMatch.tap() |
| 183 | 183 | } |
| 184 | 184 | |
| 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. | |
| 187 | 187 | func openAccountScreen(file: StaticString = #filePath, line: UInt = #line) { |
| 188 | selectTab("My Profile") | |
| 188 | 189 | let menu = app.descendants(matching: .any) |
| 189 | 190 | .matching(identifier: "account-menu").firstMatch |
| 190 | 191 | XCTAssertTrue(menu.waitForExistence(timeout: 15), |
| @@ -512,7 +513,9 @@ extension LiveSmokeUITests { | ||
| 512 | 513 | // A bogus verification code is refused, not swallowed. |
| 513 | 514 | let code = app.descendants(matching: .any) |
| 514 | 515 | .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") | |
| 516 | 519 | focusAndType(code, "000000") |
| 517 | 520 | let verify = app.descendants(matching: .any) |
| 518 | 521 | .matching(identifier: "email-verify").firstMatch |
| @@ -713,16 +716,16 @@ extension LiveSmokeUITests { | ||
| 713 | 716 | "dashboard + did not open the create sheet") |
| 714 | 717 | app.buttons["Cancel"].firstMatch.tap() |
| 715 | 718 | |
| 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") | |
| 717 | 721 | let menu = app.descendants(matching: .any) |
| 718 | 722 | .matching(identifier: "account-menu").firstMatch |
| 719 | XCTAssertTrue(menu.waitForExistence(timeout: 10)) | |
| 723 | XCTAssertTrue(menu.waitForExistence(timeout: 10), | |
| 724 | "account menu missing from My Profile") | |
| 720 | 725 | menu.tap() |
| 721 | 726 | XCTAssertTrue(app.buttons["Keys & Email"].firstMatch.waitForExistence(timeout: 5), |
| 722 | 727 | "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 | |
| 726 | 729 | |
| 727 | 730 | // --- a profile is a profile: description, links, orgs, graph, repos --- |
| 728 | 731 | XCTAssertTrue(app.staticTexts |
| @@ -746,14 +749,12 @@ extension LiveSmokeUITests { | ||
| 746 | 749 | let firstCommit = app.cells.firstMatch |
| 747 | 750 | XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") |
| 748 | 751 | 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") | |
| 757 | 758 | } |
| 758 | 759 | } |
| 759 | 760 | |