Files
ios-gitea-prs/GiteaPR/Inbox/InboxViewModel.swift
T
Hermes 1025634978
Build iOS App / Build .ipa and publish source (push) Failing after 35s
GiteaPR: native iOS app to manage Gitea pull requests
Multi-repo PR inbox, review (approve/request changes/comment), merge with
gating toggle, read-only diff viewer, and Gitea Actions check status with
one-tap re-run. Ships unsigned via the iMac runner to the SideStore source.
2026-08-12 17:07:58 +00:00

122 lines
4.2 KiB
Swift

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 {
let repo: Repository
let pr: PullRequest
let ciStatus: CIStatus
let reviewState: ReviewState
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 filter: InboxFilter = .all {
didSet { recomputeDisplayed() }
}
// Raw rows before filtering, kept so switching filters is instant.
private var allRows: [PRRow] = []
@Published var displayedRows: [PRRow] = []
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
}
}
}
}
func load() async {
guard isConfigured, let client = SettingsStore.shared.client() else {
errorMessage = "Configure your Gitea instance in Settings."
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let repos = try await client.repos()
// Fetch open PRs across all repos concurrently.
var rows: [PRRow] = []
try await withThrowingTaskGroup(of: [PRRow].self) { group in
for repo in repos {
group.addTask {
let prs = try await client.pullRequests(owner: repo.owner.login,
repo: repo.name)
return try await self.enrich(prs, in: repo, client: client)
}
}
for try await batch in group {
rows.append(contentsOf: batch)
}
}
// Sort: most recently updated first.
rows.sort { ($0.pr.updatedAt ?? .distantPast) > ($1.pr.updatedAt ?? .distantPast) }
allRows = rows
recomputeDisplayed()
} catch {
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)
}
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
}
}