Files
ios-gitea-prs/GiteaPR/Views/InboxView.swift
T
Hermes 75f5f3bc64
Build iOS App / Build .ipa and publish source (push) Successful in 22s
Load inbox only on first appearance; refresh via pull-to-refresh; bump to 2.2
.task fires on every appearance, including popping back from a PR detail, so
the landing page re-ran the full network refresh on each back navigation.
Guard the initial load with didInitialLoad so the list loads once and then
only refreshes when the user pulls down. Bump to 2.2.
2026-08-13 06:35:20 +00:00

121 lines
4.4 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 {
ContentUnavailableView(
"No open PRs",
systemImage: "tray",
description: Text("Nothing to review."))
} else {
List {
ForEach(vm.displayedRows) { row in
NavigationLink(value: row) {
PRRowView(row: row)
}
}
}
.refreshable { await vm.load() }
.listStyle(.plain)
}
}
.navigationTitle("Pull Requests")
.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()
}
.overlay {
// Only show a spinner when there's nothing cached to display
// yet; with a cache the list renders instantly and refreshes
// in the background.
if vm.isLoading && vm.displayedRows.isEmpty {
ProgressView()
}
}
.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())
}
}