Files
ios-gitea-prs/GiteaPR/Views/InboxView.swift
T
Hermes 880cff9e68
Build iOS App / Build .ipa and publish source (push) Successful in 22s
Remove in-list spinner + add monitored-repos setting; bump to 2.3
- Remove the in-list loading wheel overlay; the landing page just shows the
  cached list or the 'No open PRs' screen.
- Add a 'Monitored repos' setting (Settings > Inbox) that limits the inbox to
  specific repos, cutting the number of API calls dramatically. Empty =
  monitor all accessible repos. Bump to 2.3.
2026-08-13 06:43:46 +00:00

113 lines
4.1 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()
}
.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())
}
}