Speed up landing screen: drop per-PR enrichment + add inbox cache; bump to 2.1
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.
This commit is contained in:
Hermes committed 2026-08-13 06:27:32 +00:00
1 parent 53b3f69d0f
commit 2b929e65e9
4 files changed
+76 -79

No files matched your search

+25 -70
View File
@@ -1,21 +1,13 @@
import Foundation
import SwiftUI
enum InboxFilter: String, CaseIterable, Identifiable {
case all = "All"
case assigned = "Assigned"
case created = "By me"
case review = "Review"
var id: String { rawValue }
}
/// A PR row enriched with its repo and aggregate status.
struct PRRow: Identifiable, Hashable {
/// 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
let ciStatus: CIStatus
let reviewState: ReviewState
var id: String { "\(repo.fullName)#\(pr.number)" }
var fullName: String { repo.fullName }
@@ -26,44 +18,29 @@ final class InboxViewModel: ObservableObject {
@Published var rows: [PRRow] = []
@Published var isLoading = false
@Published var errorMessage: String?
@Published var filter: InboxFilter = .all {
didSet { recomputeDisplayed() }
}
// Raw rows before filtering, kept so switching filters is instant.
private var allRows: [PRRow] = []
@Published var displayedRows: [PRRow] = []
private let cache = InboxCache()
var isConfigured: Bool { SettingsStore.shared.isConfigured }
private func recomputeDisplayed() {
switch filter {
case .all:
displayedRows = allRows
case .assigned:
displayedRows = allRows.filter {
($0.pr.assignees ?? []).contains {
$0.login == AppSession.currentLogin
}
}
case .created:
displayedRows = allRows.filter {
$0.pr.user?.login == AppSession.currentLogin
}
case .review:
displayedRows = allRows.filter {
switch $0.reviewState {
case .needsReview, .unknown: return true
default: return false
}
}
}
}
/// 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 }
@@ -82,7 +59,7 @@ final class InboxViewModel: ObservableObject {
do {
let prs = try await client.pullRequests(owner: repo.owner.login,
repo: repo.name)
return .success(try await self.enrich(prs, in: repo, client: client))
return .success(prs.map { PRRow(repo: repo, pr: $0) })
} catch {
return .failure(error)
}
@@ -102,40 +79,18 @@ final class InboxViewModel: ObservableObject {
}
// Sort: most recently updated first.
rows.sort { ($0.pr.updatedAt ?? .distantPast) > ($1.pr.updatedAt ?? .distantPast) }
allRows = rows
recomputeDisplayed()
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
allRows = []
recomputeDisplayed()
}
}
/// Enrich each PR with CI + review status. Runs sequentially per repo to
/// avoid hammering the API from one inbox refresh.
private func enrich(_ prs: [PullRequest], in repo: Repository,
client: GiteaClient) async throws -> [PRRow] {
var out: [PRRow] = []
for pr in prs {
let sha = pr.head?.sha
var ci: CIStatus = .unknown
var review: ReviewState = .unknown
if let sha, !sha.isEmpty {
let runs = (try? await client.runs(owner: repo.owner.login,
repo: repo.name, sha: sha)) ?? []
ci = CIStatus.from(runs: runs)
// Keep the cached list on a network failure rather than blanking it.
if displayedRows.isEmpty {
displayedRows = cache.load()
}
let reviews = (try? await client.reviews(owner: repo.owner.login,
repo: repo.name,
number: pr.number)) ?? []
review = ReviewState.from(reviews: reviews)
out.append(PRRow(repo: repo, pr: pr,
ciStatus: ci, reviewState: review))
}
return out
}
}