a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Dashboard/DashboardView.swift

166 lines · 5758 bytes

  1import SwiftUI
  2
  3/// The native representation of `gitbay dashboard`, ordered and phrased
  4/// like the logged-in web dashboard.
  5struct DashboardView: View {
  6
  7    @Environment(SessionStore.self) private var session
  8    @State private var model: DashboardViewModel
  9    @State private var createModel: RepoCreateViewModel
 10    @State private var composing = false
 11
 12    init(client: GitbayClient) {
 13        _model = State(initialValue: DashboardViewModel(client: client))
 14        _createModel = State(initialValue: RepoCreateViewModel(client: client))
 15    }
 16
 17    var body: some View {
 18        List {
 19            if let data = model.state.value {
 20                itemSection(
 21                    "Waiting on your review", items: data.reviewQueue,
 22                    empty: "Nothing waiting on you", marker: "!"
 23                ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
 24
 25                itemSection(
 26                    "Assigned to you", items: data.assignedIssues,
 27                    empty: "Nothing assigned to you", marker: "#"
 28                ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
 29
 30                itemSection(
 31                    "Open merge requests", items: data.openMRs,
 32                    empty: "No open merge requests", marker: "!"
 33                ) { MRRoute.mr(repo: $0.repo, number: $0.number) }
 34
 35                itemSection(
 36                    "Open issues", items: data.openIssues,
 37                    empty: "No open issues", marker: "#"
 38                ) { IssueRoute.issue(repo: $0.repo, number: $0.number) }
 39
 40                pinnedSection(data.pinned)
 41                activitySection(data.recentActivity)
 42            }
 43        }
 44        .overlay { LoadStateOverlay(state: model.state) }
 45        .navigationTitle("Dashboard")
 46        .toolbar {
 47            ToolbarItem(placement: .topBarTrailing) {
 48                Button {
 49                    composing = true
 50                } label: {
 51                    Image(systemName: "plus")
 52                }
 53                .accessibilityIdentifier("dashboard-create-button")
 54            }
 55        }
 56        .sheet(isPresented: $composing) {
 57            RepoCreateSheet(
 58                model: createModel,
 59                ownerPrefix: session.current?.username ?? ""
 60            ) {
 61                composing = false
 62                Task { await model.load() }
 63            }
 64        }
 65        .task { await model.load() }
 66        .refreshable { await model.load() }
 67    }
 68
 69    private func itemSection(
 70        _ title: String,
 71        items: [DashboardItem],
 72        empty: String,
 73        marker: String,
 74        route: @escaping (DashboardItem) -> some Hashable
 75    ) -> some View {
 76        Section {
 77            if items.isEmpty {
 78                Text(empty)
 79                    .font(.gbSans(.subheadline))
 80                    .foregroundStyle(.secondary)
 81            } else {
 82                ForEach(items) { item in
 83                    NavigationLink(value: route(item)) {
 84                        dashboardItem(item, marker: marker)
 85                    }
 86                }
 87            }
 88        } header: {
 89            sectionHeader(title, count: items.count)
 90        }
 91    }
 92
 93    private func dashboardItem(_ item: DashboardItem, marker: String) -> some View {
 94        VStack(alignment: .leading, spacing: 3) {
 95            Text(item.title)
 96                .font(.gbSans(.subheadline).weight(.semibold))
 97                .lineLimit(2)
 98            HStack(spacing: 4) {
 99                Text(item.repo + marker + String(item.number))
100                    .font(.gbSans(.caption))
101                Text("·")
102                Text(item.author)
103                    .foregroundStyle(Color.gbAccent)
104                Text("·")
105                Text(item.updatedAt, format: .relative(presentation: .named))
106                if item.state == "source_gone" {
107                    Text("·")
108                    GBChip("source gone", .secondary)
109                }
110            }
111            .font(.gbSans(.caption))
112            .foregroundStyle(.secondary)
113            .lineLimit(1)
114        }
115        .padding(.vertical, 2)
116    }
117
118    private func pinnedSection(_ pinned: [RepoSummary]) -> some View {
119        Section {
120            if pinned.isEmpty {
121                Text("Pin a repository to keep it here")
122                    .font(.gbSans(.subheadline))
123                    .foregroundStyle(.secondary)
124            } else {
125                ForEach(pinned) { repo in
126                    NavigationLink(value: RepoRoute.repo(repo.path)) {
127                        HStack(spacing: 6) {
128                            Text(repo.path)
129                                .font(.gbSans(.subheadline).weight(.medium))
130                                .foregroundStyle(Color.gbAccent)
131                            if repo.isPrivate {
132                                GBChip("Private", .secondary)
133                            }
134                        }
135                    }
136                }
137            }
138        } header: {
139            Text("Pinned")
140        }
141    }
142
143    private func activitySection(_ events: [FeedEvent]) -> some View {
144        Section {
145            if events.isEmpty {
146                Text("No activity yet")
147                    .font(.gbSans(.subheadline))
148                    .foregroundStyle(.secondary)
149            } else {
150                ForEach(events) { event in
151                    FeedLink(event: event)
152                }
153            }
154        } header: {
155            Text("Recent activity")
156        }
157    }
158
159    private func sectionHeader(_ title: String, count: Int) -> some View {
160        HStack(alignment: .firstTextBaseline, spacing: 4) {
161            Text(title)
162            Text(String(count))
163                .foregroundStyle(.secondary)
164        }
165    }
166}