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") .navigationBarTitleDisplayMode(.large) .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() } .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()) } }