Build iOS App / Build .ipa and publish source (push) Successful in 23s
The inbox was slow because it enriched every PR with CI runs + reviews (2 extra API calls per PR, sequential). Drop that from the list — CI/review status now shows only in the PR detail screen. Add a disk cache so the landing screen renders instantly from the last load, then refreshes in the background and keeps the cached list on network failure. Bump to 2.1.
97 lines
3.8 KiB
Swift
97 lines
3.8 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
/// A PR row in the inbox. Deliberately lightweight: just the repo + PR, with
|
|
/// NO per-PR CI/review enrichment. Enriching every PR with runs+reviews was
|
|
/// the main cause of the slow landing screen (2 extra API calls per PR).
|
|
/// CI/review status is shown in the PR detail screen instead.
|
|
struct PRRow: Identifiable, Hashable, Codable {
|
|
let repo: Repository
|
|
let pr: PullRequest
|
|
|
|
var id: String { "\(repo.fullName)#\(pr.number)" }
|
|
var fullName: String { repo.fullName }
|
|
}
|
|
|
|
@MainActor
|
|
final class InboxViewModel: ObservableObject {
|
|
@Published var rows: [PRRow] = []
|
|
@Published var isLoading = false
|
|
@Published var errorMessage: String?
|
|
@Published var displayedRows: [PRRow] = []
|
|
|
|
private let cache = InboxCache()
|
|
|
|
var isConfigured: Bool { SettingsStore.shared.isConfigured }
|
|
|
|
/// Load the inbox. Shows the cached list instantly, then refreshes from
|
|
/// the network in the background and updates when done.
|
|
func load() async {
|
|
guard isConfigured, let client = SettingsStore.shared.client() else {
|
|
errorMessage = "Configure your Gitea instance in Settings."
|
|
return
|
|
}
|
|
|
|
// 1. Render the cached list immediately (no network wait).
|
|
if displayedRows.isEmpty {
|
|
let cached = cache.load()
|
|
if !cached.isEmpty {
|
|
displayedRows = cached
|
|
}
|
|
}
|
|
|
|
// 2. Refresh from the network in the background.
|
|
isLoading = true
|
|
errorMessage = nil
|
|
defer { isLoading = false }
|
|
|
|
do {
|
|
let repos = try await client.repos()
|
|
LogStore.shared.log(.info, "Inbox: \(repos.count) repos")
|
|
// Fetch open PRs across all repos concurrently. A single repo that
|
|
// fails (e.g. 404) must NOT wipe the whole inbox — catch per-repo
|
|
// errors, log them, and keep whatever succeeded.
|
|
var rows: [PRRow] = []
|
|
var failures: [String] = []
|
|
await withTaskGroup(of: Result<[PRRow], Error>.self) { group in
|
|
for repo in repos {
|
|
group.addTask {
|
|
do {
|
|
let prs = try await client.pullRequests(owner: repo.owner.login,
|
|
repo: repo.name)
|
|
return .success(prs.map { PRRow(repo: repo, pr: $0) })
|
|
} catch {
|
|
return .failure(error)
|
|
}
|
|
}
|
|
}
|
|
for await result in group {
|
|
switch result {
|
|
case .success(let batch):
|
|
rows.append(contentsOf: batch)
|
|
case .failure(let error):
|
|
failures.append(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
if !failures.isEmpty {
|
|
LogStore.shared.log(.warn, "Inbox: \(failures.count) repo(s) failed: \(failures.joined(separator: "; "))")
|
|
}
|
|
// Sort: most recently updated first.
|
|
rows.sort { ($0.pr.updatedAt ?? .distantPast) > ($1.pr.updatedAt ?? .distantPast) }
|
|
displayedRows = rows
|
|
cache.save(rows)
|
|
if rows.isEmpty && !failures.isEmpty {
|
|
errorMessage = "Couldn't load any PRs (\(failures.count) repo(s) failed). Check the Logs screen."
|
|
}
|
|
} catch {
|
|
LogStore.shared.log(.error, "Inbox load failed: \(error.localizedDescription)")
|
|
errorMessage = error.localizedDescription
|
|
// Keep the cached list on a network failure rather than blanking it.
|
|
if displayedRows.isEmpty {
|
|
displayedRows = cache.load()
|
|
}
|
|
}
|
|
}
|
|
}
|