Commit 8bc7411fd3
Unsigned
Layout: unified · split
gitbay/ContentView.swift +8 −2
| @@ -31,6 +31,14 @@ struct ContentView: View { | |||
| 31 | .navigationDestinations(client: client) | 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 | .id(account.id) // fresh screens on account switch | 43 | .id(account.id) // fresh screens on account switch |
| 36 | } else { | 44 | } else { |
| @@ -96,8 +104,6 @@ private struct RouteDestinations: ViewModifier { | |||
| 96 | WikiPageView(client: client, repo: repo, page: page) | 104 | WikiPageView(client: client, repo: repo, page: page) |
| 97 | case .commit(let repo, let sha): | 105 | case .commit(let repo, let sha): |
| 98 | CommitView(client: client, repo: repo, sha: sha) | 106 | CommitView(client: client, repo: repo, sha: sha) |
| 99 | case .commitDiff(let repo, let sha): | ||
| 100 | CommitDiffView(client: client, repo: repo, sha: sha) | ||
| 101 | case .profile(let name): | 107 | case .profile(let name): |
| 102 | ProfileView(client: client, name: name) | 108 | ProfileView(client: client, name: name) |
| 103 | case .account: | 109 | case .account: |
gitbay/Views/Dashboard/DashboardView.swift −1
| @@ -52,7 +52,6 @@ struct DashboardView: View { | |||
| 52 | } | 52 | } |
| 53 | .accessibilityIdentifier("dashboard-create-button") | 53 | .accessibilityIdentifier("dashboard-create-button") |
| 54 | } | 54 | } |
| 55 | AccountMenu() | ||
| 56 | } | 55 | } |
| 57 | .sheet(isPresented: $composing) { | 56 | .sheet(isPresented: $composing) { |
| 58 | RepoCreateSheet( | 57 | RepoCreateSheet( |
gitbay/Views/Discovery/ExploreView.swift −1
| @@ -61,7 +61,6 @@ struct ExploreView: View { | |||
| 61 | } | 61 | } |
| 62 | .overlay { LoadStateOverlay(state: list.state) } | 62 | .overlay { LoadStateOverlay(state: list.state) } |
| 63 | .navigationTitle("Explore") | 63 | .navigationTitle("Explore") |
| 64 | .toolbar { AccountMenu() } | ||
| 65 | .task { | 64 | .task { |
| 66 | if list.state.value == nil { await list.reload() } | 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 | .overlay { LoadStateOverlay(state: list.state) } | 23 | .overlay { LoadStateOverlay(state: list.state) } |
| 24 | .navigationTitle("Feed") | 24 | .navigationTitle("Feed") |
| 25 | .toolbar { AccountMenu() } | ||
| 26 | .task { | 25 | .task { |
| 27 | if list.state.value == nil { | 26 | if list.state.value == nil { |
| 28 | await list.reload() | 27 | await list.reload() |
gitbay/Views/Discovery/ProfileView.swift +7 −2
| @@ -6,8 +6,13 @@ struct ProfileView: View { | |||
| 6 | 6 | ||
| 7 | @State private var model: ProfileViewModel | 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 | _model = State(initialValue: ProfileViewModel(client: client, name: name)) | 14 | _model = State(initialValue: ProfileViewModel(client: client, name: name)) |
| 15 | self.title = title | ||
| 11 | } | 16 | } |
| 12 | 17 | ||
| 13 | var body: some View { | 18 | var body: some View { |
| @@ -27,7 +32,7 @@ struct ProfileView: View { | |||
| 27 | } | 32 | } |
| 28 | } | 33 | } |
| 29 | .overlay { LoadStateOverlay(state: model.state) } | 34 | .overlay { LoadStateOverlay(state: model.state) } |
| 30 | .navigationTitle(model.name) | 35 | .navigationTitle(title ?? model.name) |
| 31 | .navigationBarTitleDisplayMode(.inline) | 36 | .navigationBarTitleDisplayMode(.inline) |
| 32 | .task { await model.load() } | 37 | .task { await model.load() } |
| 33 | .refreshable { await model.load() } | 38 | .refreshable { await model.load() } |
gitbay/Views/Repos/CommitView.swift +5 −59
| @@ -64,73 +64,19 @@ struct CommitView: View { | |||
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | if let diff = model.diff, !diff.files.isEmpty { | 66 | if let diff = model.diff, !diff.files.isEmpty { |
| 67 | Section("Changes +\(diff.additions) −\(diff.deletions)") { | 67 | // The patch, in place. The web's commit page is the |
| 68 | ForEach(diff.files) { file in | 68 | // diff — a file header expands its hunks rather than |
| 69 | NavigationLink(value: RepoRoute.file( | 69 | // linking away to the whole 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 { | ||
| 124 | ForEach(diff.files) { file in | 70 | ForEach(diff.files) { file in |
| 125 | DiffFileSection(file: file) | 71 | DiffFileSection(file: file) |
| 126 | } | 72 | } |
| 127 | } | 73 | } |
| 128 | .listStyle(.plain) | ||
| 129 | } | 74 | } |
| 130 | } | 75 | } |
| 131 | .overlay { LoadStateOverlay(state: model.state) } | 76 | .overlay { LoadStateOverlay(state: model.state) } |
| 132 | .navigationTitle("Diff") | 77 | .navigationTitle(String(model.sha.prefix(10))) |
| 133 | .navigationBarTitleDisplayMode(.inline) | 78 | .navigationBarTitleDisplayMode(.inline) |
| 134 | .task { await model.load() } | 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 | .accessibilityIdentifier("repo-create-button") | 36 | .accessibilityIdentifier("repo-create-button") |
| 37 | } | 37 | } |
| 38 | AccountMenu() | ||
| 39 | } | 38 | } |
| 40 | .sheet(isPresented: $composing) { | 39 | .sheet(isPresented: $composing) { |
| 41 | RepoCreateSheet( | 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 | struct AccountMenu: ToolbarContent { | 152 | struct AccountMenu: ToolbarContent { |
| 153 | 153 | ||
| 154 | @Environment(SessionStore.self) private var session | 154 | @Environment(SessionStore.self) private var session |
| @@ -158,9 +158,6 @@ struct AccountMenu: ToolbarContent { | |||
| 158 | Menu { | 158 | Menu { |
| 159 | if let current = session.current { | 159 | if let current = session.current { |
| 160 | Section(current.label) { | 160 | Section(current.label) { |
| 161 | NavigationLink(value: RepoRoute.profile(current.username)) { | ||
| 162 | Label("My Profile", systemImage: "person.crop.circle") | ||
| 163 | } | ||
| 164 | NavigationLink(value: RepoRoute.account) { | 161 | NavigationLink(value: RepoRoute.account) { |
| 165 | Label("Keys & Email", systemImage: "key") | 162 | Label("Keys & Email", systemImage: "key") |
| 166 | } | 163 | } |
gitbay/Views/Repos/RepoRoute.swift −1
| @@ -15,7 +15,6 @@ nonisolated enum RepoRoute: Hashable { | |||
| 15 | case wiki(repo: String) | 15 | case wiki(repo: String) |
| 16 | case wikiPage(repo: String, page: String) | 16 | case wikiPage(repo: String, page: String) |
| 17 | case commit(repo: String, sha: String) | 17 | case commit(repo: String, sha: String) |
| 18 | case commitDiff(repo: String, sha: String) | ||
| 19 | case profile(String) | 18 | case profile(String) |
| 20 | case account | 19 | case account |
| 21 | case addAccount | 20 | case addAccount |
gitbayUITests/LiveSmokeUITests.swift +17 −16
| @@ -182,9 +182,10 @@ final class LiveSmokeUITests: XCTestCase { | |||
| 182 | app.navigationBars.buttons.firstMatch.tap() | 182 | app.navigationBars.buttons.firstMatch.tap() |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | /// Keys, PGP and email live behind the profile menu now, not their | 185 | /// Keys, PGP and email live behind the account menu, which is on the |
| 186 | /// own toolbar button. | 186 | /// My Profile tab and nowhere else. |
| 187 | func openAccountScreen(file: StaticString = #filePath, line: UInt = #line) { | 187 | func openAccountScreen(file: StaticString = #filePath, line: UInt = #line) { |
| 188 | selectTab("My Profile") | ||
| 188 | let menu = app.descendants(matching: .any) | 189 | let menu = app.descendants(matching: .any) |
| 189 | .matching(identifier: "account-menu").firstMatch | 190 | .matching(identifier: "account-menu").firstMatch |
| 190 | XCTAssertTrue(menu.waitForExistence(timeout: 15), | 191 | XCTAssertTrue(menu.waitForExistence(timeout: 15), |
| @@ -512,7 +513,9 @@ extension LiveSmokeUITests { | |||
| 512 | // A bogus verification code is refused, not swallowed. | 513 | // A bogus verification code is refused, not swallowed. |
| 513 | let code = app.descendants(matching: .any) | 514 | let code = app.descendants(matching: .any) |
| 514 | .matching(identifier: "email-code").firstMatch | 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 | focusAndType(code, "000000") | 519 | focusAndType(code, "000000") |
| 517 | let verify = app.descendants(matching: .any) | 520 | let verify = app.descendants(matching: .any) |
| 518 | .matching(identifier: "email-verify").firstMatch | 521 | .matching(identifier: "email-verify").firstMatch |
| @@ -713,16 +716,16 @@ extension LiveSmokeUITests { | |||
| 713 | "dashboard + did not open the create sheet") | 716 | "dashboard + did not open the create sheet") |
| 714 | app.buttons["Cancel"].firstMatch.tap() | 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 | let menu = app.descendants(matching: .any) | 721 | let menu = app.descendants(matching: .any) |
| 718 | .matching(identifier: "account-menu").firstMatch | 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 | menu.tap() | 725 | menu.tap() |
| 721 | XCTAssertTrue(app.buttons["Keys & Email"].firstMatch.waitForExistence(timeout: 5), | 726 | XCTAssertTrue(app.buttons["Keys & Email"].firstMatch.waitForExistence(timeout: 5), |
| 722 | "keys not in the account menu") | 727 | "keys not in the account menu") |
| 723 | let myProfile = app.buttons["My Profile"].firstMatch | 728 | app.tap() // dismiss the menu; the profile is already on screen |
| 724 | XCTAssertTrue(myProfile.exists, "profile not in the account menu") | ||
| 725 | myProfile.tap() | ||
| 726 | 729 | ||
| 727 | // --- a profile is a profile: description, links, orgs, graph, repos --- | 730 | // --- a profile is a profile: description, links, orgs, graph, repos --- |
| 728 | XCTAssertTrue(app.staticTexts | 731 | XCTAssertTrue(app.staticTexts |
| @@ -746,14 +749,12 @@ extension LiveSmokeUITests { | |||
| 746 | let firstCommit = app.cells.firstMatch | 749 | let firstCommit = app.cells.firstMatch |
| 747 | XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") | 750 | XCTAssertTrue(firstCommit.waitForExistence(timeout: 20), "history is empty") |
| 748 | firstCommit.tap() | 751 | firstCommit.tap() |
| 749 | // The commit screen shows its changed files and can open the patch. | 752 | // The commit screen IS the patch: no navigating away to find it. |
| 750 | XCTAssertTrue(app.descendants(matching: .any) | 753 | // A hunk header (@@) only appears in a rendered diff. |
| 751 | .matching(identifier: "commit-diff-button").firstMatch | 754 | XCTAssertTrue(app.staticTexts |
| 752 | .waitForExistence(timeout: 20), "commit did not open") | 755 | .containing(NSPredicate(format: "label BEGINSWITH '@@'")).firstMatch |
| 753 | app.descendants(matching: .any).matching(identifier: "commit-diff-button") | 756 | .waitForExistence(timeout: 20), |
| 754 | .firstMatch.tap() | 757 | "commit screen did not render the patch inline") |
| 755 | XCTAssertTrue(app.cells.firstMatch.waitForExistence(timeout: 20), | ||
| 756 | "commit diff rendered nothing") | ||
| 757 | } | 758 | } |
| 758 | } | 759 | } |
| 759 | 760 | ||