gitbay/Views/Discovery/FeedView.swift
94 lines · 3030 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 FeedLink(event: event)
20 }
21 PageFooter(list: list)
22 }
23 .overlay { LoadStateOverlay(state: list.state) }
24 .navigationTitle("Feed")
25 .task {
26 if list.state.value == nil {
27 await list.reload()
28 }
29 }
30 .refreshable { await list.reload() }
31 }
32
33}
34
35/// One feed event linked to the same native destination everywhere it is
36/// represented, including the dashboard's recent-activity section.
37struct FeedLink: View {
38 let event: FeedEvent
39
40 @ViewBuilder
41 var body: some View {
42 switch event.destination {
43 case .repo(let repo):
44 NavigationLink(value: RepoRoute.repo(repo)) { FeedRow(event: event) }
45 case .mr(let repo, let number):
46 NavigationLink(value: MRRoute.mr(repo: repo, number: number)) { FeedRow(event: event) }
47 case .issue(let repo, let number):
48 NavigationLink(value: IssueRoute.issue(repo: repo, number: number)) { FeedRow(event: event) }
49 case .build(let repo, let number):
50 NavigationLink(value: BuildRoute.detail(repo: repo, number: number)) { FeedRow(event: event) }
51 case .release(let repo, let tag):
52 NavigationLink(value: ReleaseRoute.release(repo: repo, tag: tag)) { FeedRow(event: event) }
53 case nil:
54 FeedRow(event: event)
55 }
56 }
57}
58
59struct FeedRow: View {
60 let event: FeedEvent
61
62 var body: some View {
63 HStack(spacing: 10) {
64 Image(systemName: event.icon.name)
65 .foregroundStyle(color)
66 .frame(width: 22)
67 VStack(alignment: .leading, spacing: 2) {
68 Text(event.repo)
69 .font(.gbSans(.caption))
70 .foregroundStyle(.secondary)
71 HStack(spacing: 4) {
72 Text(event.displayActor)
73 .font(.gbSans(.subheadline).weight(.semibold))
74 Text(event.phrase)
75 .font(.gbSans(.subheadline))
76 .lineLimit(1)
77 }
78 }
79 Spacer()
80 Text(event.createdAt, format: .relative(presentation: .named))
81 .font(.gbSans(.caption))
82 .foregroundStyle(.secondary)
83 }
84 .padding(.vertical, 2)
85 }
86
87 private var color: Color {
88 switch event.icon.isPositive {
89 case true: .gbOK
90 case false: .gbBad
91 default: .secondary
92 }
93 }
94}