Files
ios-gitea-prs/GiteaPR/Views/InboxView.swift
T
Hermes ed22fbc351
Build iOS App / Build .ipa and publish source (push) Successful in 22s
Empty state pull-to-refresh + inline title aligned with gear; bump to 2.8
- Empty state ('No open PRs') was a bare ContentUnavailableView with no
  pull-to-refresh, so you couldn't reload when the list was empty. Wrap it in
  a refreshable ScrollView so pulling down reloads even with no PRs.
- Switch the landing title from .large to .inline so 'Pull Requests' sits on
  the same row as the settings gear (better space use). Bump to 2.8.
2026-08-13 15:48:13 +00:00

121 lines
4.5 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()
}
.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())
}
}