Add native iOS notifications (#64)
This commit is contained in:
179
ios/Sources/NotificationsScreen.swift
Normal file
179
ios/Sources/NotificationsScreen.swift
Normal file
@@ -0,0 +1,179 @@
|
||||
import UIKit
|
||||
|
||||
final class NotificationCell: UITableViewCell {
|
||||
private let icon = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private let detailLabel = UILabel()
|
||||
private let metaLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .headline)
|
||||
icon.setContentHuggingPriority(.required, for: .horizontal)
|
||||
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||
titleLabel.numberOfLines = 2
|
||||
detailLabel.font = .preferredFont(forTextStyle: .subheadline)
|
||||
detailLabel.textColor = .secondaryLabel
|
||||
detailLabel.numberOfLines = 2
|
||||
metaLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||
metaLabel.textColor = .tertiaryLabel
|
||||
[titleLabel, detailLabel, metaLabel].forEach {
|
||||
$0.adjustsFontForContentSizeCategory = true
|
||||
}
|
||||
let labels = UIStackView(arrangedSubviews: [titleLabel, detailLabel, metaLabel])
|
||||
labels.axis = .vertical
|
||||
labels.spacing = 4
|
||||
let stack = UIStackView(arrangedSubviews: [icon, labels])
|
||||
stack.alignment = .top
|
||||
stack.spacing = 12
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(stack)
|
||||
NSLayoutConstraint.activate([
|
||||
stack.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16),
|
||||
stack.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8),
|
||||
stack.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10),
|
||||
stack.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10),
|
||||
icon.widthAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
func configure(_ row: NotificationRow) {
|
||||
icon.image = UIImage(systemName: symbolName(for: row.target))
|
||||
icon.tintColor = row.unread ? .tintColor : .secondaryLabel
|
||||
titleLabel.text = row.title
|
||||
detailLabel.text = row.detail
|
||||
metaLabel.text = row.meta
|
||||
accessoryType = row.target == .none ? .none : .disclosureIndicator
|
||||
selectionStyle = row.target == .none ? .none : .default
|
||||
accessibilityValue = row.unread ? "Open" : "Closed"
|
||||
}
|
||||
|
||||
private func symbolName(for target: ActivityTargetKind) -> String {
|
||||
switch target {
|
||||
case .repository: return "books.vertical"
|
||||
case .issue: return "exclamationmark.circle"
|
||||
case .pullRequest: return "arrow.triangle.pull"
|
||||
case .commit: return "point.topleft.down.to.point.bottomright.curvepath"
|
||||
case .none: return "bell"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class NotificationsViewController: RefreshingTableViewController {
|
||||
private let context: AppContext
|
||||
private let statusControl = UISegmentedControl(items: ["Open", "Closed"])
|
||||
private var rows: [NotificationRow] = []
|
||||
private var currentPage: UInt32 = 0
|
||||
|
||||
init(context: AppContext) {
|
||||
self.context = context
|
||||
super.init()
|
||||
title = "Notifications"
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
tableView.register(NotificationCell.self, forCellReuseIdentifier: "notification")
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 92
|
||||
statusControl.selectedSegmentIndex = 0
|
||||
statusControl.addTarget(self, action: #selector(statusChanged), for: .valueChanged)
|
||||
statusControl.accessibilityLabel = "Notification status"
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: statusControl)
|
||||
loadContent(refreshing: false)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
guard currentPage > 0 else { return }
|
||||
loadNotifications(page: 1, refreshing: false)
|
||||
}
|
||||
|
||||
override func loadContent(refreshing: Bool) {
|
||||
loadNotifications(page: 1, refreshing: refreshing)
|
||||
}
|
||||
|
||||
override func loadMoreContent() {
|
||||
loadNotifications(page: currentPage + 1, refreshing: false)
|
||||
}
|
||||
|
||||
private func loadNotifications(page: UInt32, refreshing: Bool) {
|
||||
if page == 1 {
|
||||
resetPagination()
|
||||
beginLoading(refreshing: refreshing)
|
||||
}
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
let result = try await context.core.notifications(
|
||||
status: statusControl.selectedSegmentIndex == 0 ? .open : .closed,
|
||||
page: page
|
||||
)
|
||||
if page == 1 { rows = result.rows } else { rows.append(contentsOf: result.rows) }
|
||||
currentPage = page
|
||||
finishPagination(hasMore: result.hasMore)
|
||||
tableView.reloadData()
|
||||
let status = statusControl.selectedSegmentIndex == 0 ? "open" : "closed"
|
||||
tableView.backgroundView = rows.isEmpty
|
||||
? EmptyBackgroundView(
|
||||
title: "No \(status) notifications",
|
||||
detail: "This server has no \(status) notifications."
|
||||
)
|
||||
: nil
|
||||
} catch {
|
||||
if !Task.isCancelled {
|
||||
show(error: error)
|
||||
failPagination()
|
||||
}
|
||||
}
|
||||
if page == 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: "notification",
|
||||
for: indexPath
|
||||
) as! NotificationCell
|
||||
cell.configure(rows[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
let row = rows[indexPath.row]
|
||||
guard row.target != .none else { return }
|
||||
guard row.unread else {
|
||||
context.route(row)
|
||||
return
|
||||
}
|
||||
loadingTask?.cancel()
|
||||
loadingTask = Task {
|
||||
do {
|
||||
try await context.core.markNotificationRead(serverId: row.serverId, id: row.id)
|
||||
guard !Task.isCancelled else { return }
|
||||
context.route(row)
|
||||
} catch {
|
||||
if !Task.isCancelled { show(error: error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func statusChanged() {
|
||||
loadNotifications(page: 1, refreshing: false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user