Add full-server activity to iOS home (#65)

This commit is contained in:
Georg Bauer
2026-08-15 17:25:25 +02:00
parent 6d271e7402
commit f84ab774e6
15 changed files with 492 additions and 15 deletions

View File

@@ -119,6 +119,13 @@ final class HomeViewController: RefreshingTableViewController {
NotificationsViewController(context: self.context),
animated: true
)
},
onServerActivity: { [weak self] in
guard let self else { return }
self.navigationController?.pushViewController(
ServerActivityViewController(context: self.context),
animated: true
)
}
)
} }
@@ -148,7 +155,7 @@ final class HomeViewController: RefreshingTableViewController {
cell,
title: row.title,
detail: "\(row.detail)\n\(row.meta)",
image: context.symbol(symbolName(for: row.icon))
image: context.symbol(activitySymbolName(for: row.icon))
)
cell.accessoryType = row.target == .none ? .none : .disclosureIndicator
return cell
@@ -173,16 +180,17 @@ final class HomeViewController: RefreshingTableViewController {
: nil
}
private func symbolName(for icon: ActivityIcon) -> String {
switch icon {
case .pullRequest: return "arrow.triangle.pull"
case .issue: return "exclamationmark.circle"
case .branch: return "arrow.triangle.branch"
case .tag: return "tag"
case .push: return "arrow.up.circle"
case .release: return "shippingbox"
case .repository: return "books.vertical"
}
}
func activitySymbolName(for icon: ActivityIcon) -> String {
switch icon {
case .pullRequest: return "arrow.triangle.pull"
case .issue: return "exclamationmark.circle"
case .branch: return "arrow.triangle.branch"
case .tag: return "tag"
case .push: return "arrow.up.circle"
case .release: return "shippingbox"
case .repository: return "books.vertical"
}
}
@@ -194,7 +202,8 @@ final class HeatmapView: UIView {
page: HomePage,
selectedFilter: Int,
onFilter: @escaping (Int) -> Void,
onNotifications: @escaping () -> Void
onNotifications: @escaping () -> Void,
onServerActivity: @escaping () -> Void
) {
cells = page.heatCells
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 180))
@@ -241,6 +250,15 @@ final class HeatmapView: UIView {
button.widthAnchor.constraint(equalToConstant: 44).isActive = true
filters.addArrangedSubview(button)
}
let serverActivity = UIButton(
type: .custom,
primaryAction: UIAction { _ in onServerActivity() }
)
serverActivity.setImage(UIImage(systemName: "person.3"), for: .normal)
serverActivity.tintColor = .secondaryLabel
serverActivity.accessibilityLabel = "Server Activity"
serverActivity.widthAnchor.constraint(equalToConstant: 44).isActive = true
filters.addArrangedSubview(serverActivity)
let notifications = UIButton(
type: .custom,
primaryAction: UIAction { _ in onNotifications() }

View File

@@ -0,0 +1,104 @@
import UIKit
@MainActor
final class ServerActivityViewController: RefreshingTableViewController {
private let context: AppContext
private var rows: [ActivityRow] = []
private var nextPage: UInt32?
private var loaded = false
init(context: AppContext) {
self.context = context
super.init()
title = "Server Activity"
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard !loaded else { return }
loadContent(refreshing: false)
}
override func loadContent(refreshing: Bool) {
loadPage(1, refreshing: refreshing)
}
override func loadMoreContent() {
guard let nextPage else { return }
loadPage(nextPage, refreshing: false)
}
private func loadPage(_ requestedPage: UInt32, refreshing: Bool) {
if requestedPage == 1 {
resetPagination()
beginLoading(refreshing: refreshing)
}
loadingTask?.cancel()
loadingTask = Task {
do {
let page = try await context.core.serverActivity(page: requestedPage)
if requestedPage == 1 {
rows = page.rows
loaded = true
} else {
rows.append(contentsOf: page.rows)
}
nextPage = page.hasMore ? requestedPage + 1 : nil
finishPagination(hasMore: page.hasMore)
updateRows()
} catch {
if !Task.isCancelled {
show(error: error)
failPagination()
}
}
if requestedPage == 1 { endLoading() }
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
rows.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "server-activity")
?? UITableViewCell(style: .subtitle, reuseIdentifier: "server-activity")
let row = rows[indexPath.row]
configureTextCell(
cell,
title: row.title,
detail: "\(row.detail)\n\(row.meta)",
image: context.symbol(activitySymbolName(for: row.icon))
)
cell.accessoryType = row.target == .none ? .none : .disclosureIndicator
cell.selectionStyle = row.target == .none ? .none : .default
return cell
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
92
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let row = rows[indexPath.row]
guard row.target != .none else { return }
tableView.deselectRow(at: indexPath, animated: true)
context.route(row)
}
private func updateRows() {
tableView.reloadData()
tableView.backgroundView = rows.isEmpty
? EmptyBackgroundView(
title: "No server activity",
detail: "No activity is visible to this server account."
)
: nil
}
}