gitbay/Views/Discovery/FeedView.swift
90 lines · 2946 bytes
1import SwiftUI
2
3/// Activity on everything you can reach — the `feed` command, paginated.
4struct FeedView: View {
5
6 @State private var list: PagedListModel<FeedEvent>
7
8 init(client: GitbayClient) {
9 _list = State(initialValue: PagedListModel(
10 client: client,
11 argv: ["feed"],
12 emptyMessage: "Nothing has happened yet on repositories you can reach."
13 ))
14 }
15
16 var body: some View {
17 List {
18 ForEach(list.state.value ?? []) { event in
19 link(for: event)
20 }
21 PageFooter(list: list)
22 }
23 .overlay { LoadStateOverlay(state: list.state) }
24 .navigationTitle("Feed")
25 .toolbar { AccountMenu() }
26 .task {
27 if list.state.value == nil {
28 await list.reload()
29 }
30 }
31 .refreshable { await list.reload() }
32 }
33
34 @ViewBuilder
35 private func link(for event: FeedEvent) -> some View {
36 switch event.destination {
37 case .repo(let repo):
38 NavigationLink(value: RepoRoute.repo(repo)) { FeedRow(event: event) }
39 case .mr(let repo, let number):
40 NavigationLink(value: MRRoute.mr(repo: repo, number: number)) { FeedRow(event: event) }
41 case .issue(let repo, let number):
42 NavigationLink(value: IssueRoute.issue(repo: repo, number: number)) { FeedRow(event: event) }
43 case .build(let repo, let number):
44 NavigationLink(value: BuildRoute.log(repo: repo, number: number)) { FeedRow(event: event) }
45 case .release(let repo, let tag):
46 NavigationLink(value: ReleaseRoute.release(repo: repo, tag: tag)) { FeedRow(event: event) }
47 case nil:
48 FeedRow(event: event)
49 }
50 }
51}
52
53private struct FeedRow: View {
54 let event: FeedEvent
55
56 var body: some View {
57 HStack(spacing: 10) {
58 Image(systemName: event.icon.name)
59 .foregroundStyle(color)
60 .frame(width: 22)
61 VStack(alignment: .leading, spacing: 2) {
62 Text(event.repo)
63 .font(.caption)
64 .foregroundStyle(.secondary)
65 HStack(spacing: 4) {
66 if let actor = event.actor, !actor.isEmpty {
67 Text(actor)
68 .font(.subheadline.weight(.semibold))
69 }
70 Text(event.phrase)
71 .font(.subheadline)
72 .lineLimit(1)
73 }
74 }
75 Spacer()
76 Text(event.createdAt, format: .relative(presentation: .named))
77 .font(.caption)
78 .foregroundStyle(.tertiary)
79 }
80 .padding(.vertical, 2)
81 }
82
83 private var color: Color {
84 switch event.icon.isPositive {
85 case true: .green
86 case false: .red
87 default: .secondary
88 }
89 }
90}