Build iOS App / Build .ipa and publish source (push) Successful in 22s
- 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.
104 lines
4.2 KiB
Swift
104 lines
4.2 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()
|
|
// If the user has configured monitored repos, only fetch PRs for
|
|
// those — this cuts the number of API calls dramatically (the
|
|
// inbox was fetching every accessible repo).
|
|
let monitored = SettingsStore.shared.monitoredRepos
|
|
let targets = monitored.isEmpty
|
|
? repos
|
|
: repos.filter { monitored.contains($0.fullName) }
|
|
LogStore.shared.log(.info, "Inbox: \(targets.count)/\(repos.count) repos")
|
|
// Fetch open PRs across the target 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 targets {
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
}
|