Build iOS App / Build .ipa and publish source (push) Successful in 34s
- After a merge, the app returns to the landing page but the inbox kept stale PRs. Add an inboxNeedsRefresh flag set on merge success; InboxView reloads on next appearance when it's set. - The PR content (incl. the Checks/workflow tab) is now pull-to-refreshable via .refreshable on the content ScrollView, so workflow status can be updated by pulling down. Bump to 3.4.
129 lines
4.9 KiB
Swift
129 lines
4.9 KiB
Swift
import SwiftUI
|
|
|
|
struct InboxView: View {
|
|
@StateObject private var vm = InboxViewModel()
|
|
@State private var showSettings = false
|
|
// Load the inbox only on first appearance. `.task` fires on every
|
|
// appearance (including popping back from a PR detail), so without this
|
|
// guard the landing page re-runs the full network refresh on each back
|
|
// navigation. After the first load, refresh only via pull-to-refresh.
|
|
@State private var didInitialLoad = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
Group {
|
|
if !vm.isConfigured {
|
|
ContentUnavailableView(
|
|
"Not configured",
|
|
systemImage: "key",
|
|
description: Text("Add your Gitea instance and token in Settings."))
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) { settingsButton }
|
|
}
|
|
} else if vm.displayedRows.isEmpty && !vm.isLoading {
|
|
// Empty state as a refreshable ScrollView so pulling down
|
|
// reloads even when there are no PRs (a ContentUnavailableView
|
|
// alone has no pull-to-refresh).
|
|
ScrollView {
|
|
ContentUnavailableView(
|
|
"No open PRs",
|
|
systemImage: "tray",
|
|
description: Text("Pull down to refresh."))
|
|
.frame(maxWidth: .infinity, minHeight: 400)
|
|
}
|
|
.refreshable { await vm.load() }
|
|
} else {
|
|
List {
|
|
ForEach(vm.displayedRows) { row in
|
|
NavigationLink(value: row) {
|
|
PRRowView(row: row)
|
|
}
|
|
}
|
|
}
|
|
.refreshable { await vm.load() }
|
|
.listStyle(.plain)
|
|
}
|
|
}
|
|
.navigationTitle("Pull Requests")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) { settingsButton }
|
|
}
|
|
.navigationDestination(for: PRRow.self) { row in
|
|
PRDetailView(repo: row.repo, prNumber: row.pr.number)
|
|
}
|
|
.task {
|
|
// Load only on first appearance; subsequent appearances (e.g.
|
|
// popping back from a PR) reuse the cached list and only
|
|
// refresh when the user pulls down.
|
|
guard !didInitialLoad else { return }
|
|
didInitialLoad = true
|
|
await vm.load()
|
|
}
|
|
.onAppear {
|
|
// After a merge, the inbox must reload so the just-merged PR
|
|
// doesn't linger as stale in the list.
|
|
if SettingsStore.shared.inboxNeedsRefresh {
|
|
SettingsStore.shared.inboxNeedsRefresh = false
|
|
Task { await vm.load() }
|
|
}
|
|
}
|
|
.alert("Error", isPresented: .init(
|
|
get: { vm.errorMessage != nil },
|
|
set: { if !$0 { vm.errorMessage = nil } })) {
|
|
Button("OK", role: .cancel) {}
|
|
} message: {
|
|
Text(vm.errorMessage ?? "")
|
|
}
|
|
.sheet(isPresented: $showSettings) {
|
|
SettingsView()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var settingsButton: some View {
|
|
Button { showSettings = true } label: {
|
|
Image(systemName: "gearshape")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PRRowView: View {
|
|
let row: PRRow
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
HStack(spacing: 6) {
|
|
Text("#\(row.pr.number)")
|
|
.font(.subheadline.weight(.bold))
|
|
.foregroundColor(.secondary)
|
|
if row.pr.draft == true {
|
|
Text("Draft")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
Text(row.pr.title)
|
|
.font(.body.weight(.medium))
|
|
.lineLimit(2)
|
|
Text(row.fullName)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
Spacer()
|
|
if let updated = row.pr.updatedAt {
|
|
Text(relative(updated))
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
|
|
private func relative(_ date: Date) -> String {
|
|
let f = RelativeDateTimeFormatter()
|
|
f.unitsStyle = .abbreviated
|
|
return f.localizedString(for: date, relativeTo: Date())
|
|
}
|
|
}
|