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
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:
1 parent
53b3f69d0f
commit
2b929e65e9
4 files changed
+76
-79
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user