Show labels on issue details

This commit is contained in:
Georg Bauer
2026-07-31 18:27:57 +02:00
parent 0a38dbe9f2
commit acf2c839ad
6 changed files with 101 additions and 13 deletions

View File

@@ -183,8 +183,10 @@ xcrun simctl launch booted de.rfc1437.gotcha
issue list/detail remains usable with Dynamic Type and VoiceOver labels.
- [ ] Open both an open and a closed issue. A green open or purple closed icon
appears beside the title and VoiceOver announces the state; author
metadata, Markdown body, and comments remain correct. Links and
selectable text use normal iOS interaction.
metadata, every colored label, Markdown body, and comments remain correct.
Multiple labels wrap without clipping at large Dynamic Type sizes and
VoiceOver announces each label. Links and selectable text use normal iOS
interaction.
- [ ] Tap the **Add comment** icon on an issue. Enter headings, emphasis, a
list, link, and fenced code block; **Write** syntax-highlights the
Markdown, **Preview** renders it, and switching modes preserves the exact

View File

@@ -77,6 +77,7 @@ pub struct IssuePage {
pub state: String,
pub meta: String,
pub milestone: String,
pub labels: Vec<LabelRow>,
pub body: String,
pub comments: Vec<CommentRow>,
pub has_more: bool,
@@ -554,6 +555,14 @@ pub fn issue_page(details: IssueDetails) -> IssuePage {
.unwrap_or_else(|| "unknown".into()),
meta: issue_meta(&details.issue),
milestone: issue_milestone(&details.issue),
labels: details
.issue
.labels
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(label_row)
.collect(),
body: details
.issue
.body
@@ -1269,6 +1278,11 @@ mod tests {
let page = issue_page(IssueDetails {
issue: models::Issue {
state: Some("closed".into()),
labels: Some(vec![models::Label {
name: Some("bug".into()),
color: Some("ff0000".into()),
..Default::default()
}]),
..Default::default()
},
comments: vec![
@@ -1297,6 +1311,7 @@ mod tests {
});
assert_eq!(page.state, "closed");
assert_eq!(page.labels[0].name, "bug");
assert_eq!(page.comments[0].id, 7);
assert!(page.comments[0].can_edit);
assert!(!page.comments[1].can_edit);

View File

@@ -2236,17 +2236,19 @@ public struct IssuePage: Equatable, Hashable {
public var state: String
public var meta: String
public var milestone: String
public var labels: [LabelRow]
public var body: String
public var comments: [CommentRow]
public var hasMore: Bool
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow], hasMore: Bool) {
public init(title: String, state: String, meta: String, milestone: String, labels: [LabelRow], body: String, comments: [CommentRow], hasMore: Bool) {
self.title = title
self.state = state
self.meta = meta
self.milestone = milestone
self.labels = labels
self.body = body
self.comments = comments
self.hasMore = hasMore
@@ -2272,6 +2274,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
state: FfiConverterString.read(from: &buf),
meta: FfiConverterString.read(from: &buf),
milestone: FfiConverterString.read(from: &buf),
labels: FfiConverterSequenceTypeLabelRow.read(from: &buf),
body: FfiConverterString.read(from: &buf),
comments: FfiConverterSequenceTypeCommentRow.read(from: &buf),
hasMore: FfiConverterBool.read(from: &buf)
@@ -2283,6 +2286,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer {
FfiConverterString.write(value.state, into: &buf)
FfiConverterString.write(value.meta, into: &buf)
FfiConverterString.write(value.milestone, into: &buf)
FfiConverterSequenceTypeLabelRow.write(value.labels, into: &buf)
FfiConverterString.write(value.body, into: &buf)
FfiConverterSequenceTypeCommentRow.write(value.comments, into: &buf)
FfiConverterBool.write(value.hasMore, into: &buf)

View File

@@ -370,14 +370,7 @@ final class IssueCell: UITableViewCell {
labels.arrangedSubviews.forEach { $0.removeFromSuperview() }
labels.isHidden = row.labels.isEmpty
for label in row.labels.prefix(3) {
let view = UILabel()
view.text = " \(label.name) "
view.font = .preferredFont(forTextStyle: .caption2)
view.textColor = label.light ? .black : .white
view.backgroundColor = UIColor(hex: label.color)
view.layer.cornerRadius = 9
view.clipsToBounds = true
labels.addArrangedSubview(view)
labels.addArrangedSubview(issueLabelView(label))
}
labels.addArrangedSubview(UIView())
}

View File

@@ -232,7 +232,8 @@ final class IssueViewController: MarkdownPageViewController {
body: page.body,
comments: page.comments,
editComment: { [weak self] comment in self?.editComment(comment) },
milestone: page.milestone
milestone: page.milestone,
labels: page.labels
))
finishPagination(hasMore: page.hasMore)
}
@@ -1142,7 +1143,8 @@ private func detailViews(
body: String,
comments: [CommentRow],
editComment: ((CommentRow) -> Void)? = nil,
milestone: String = ""
milestone: String = "",
labels: [LabelRow] = []
) -> [UIView] {
let titleLabel = UILabel()
titleLabel.text = title
@@ -1166,6 +1168,7 @@ private func detailViews(
milestoneLabel.accessibilityLabel = "Milestone \(milestone)"
views.append(milestoneLabel)
}
if !labels.isEmpty { views.append(IssueLabelsView(labels)) }
return views + [separator(), bodyView] + commentViews(comments, editComment: editComment)
}

View File

@@ -287,3 +287,74 @@ func configureIssueStateIcon(
icon.isAccessibilityElement = true
icon.accessibilityLabel = accessibilityLabel
}
func issueLabelView(_ label: LabelRow) -> UILabel {
let view = UILabel()
view.text = " \(label.name) "
view.font = .preferredFont(forTextStyle: .caption2)
view.adjustsFontForContentSizeCategory = true
view.textColor = label.light ? .black : .white
view.backgroundColor = UIColor(hex: label.color)
view.layer.cornerRadius = 9
view.clipsToBounds = true
view.accessibilityLabel = "Label \(label.name)"
return view
}
final class IssueLabelsView: UIView {
private let labels: [UILabel]
private var contentHeight: CGFloat
private let spacing: CGFloat = 6
init(_ rows: [LabelRow]) {
labels = rows.map(issueLabelView)
contentHeight = labels.first?.intrinsicContentSize.height ?? 0
super.init(frame: .zero)
labels.forEach(addSubview)
NotificationCenter.default.addObserver(
self,
selector: #selector(contentSizeCategoryDidChange),
name: UIContentSizeCategory.didChangeNotification,
object: nil
)
}
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override var intrinsicContentSize: CGSize {
CGSize(width: UIView.noIntrinsicMetric, height: contentHeight)
}
override func layoutSubviews() {
super.layoutSubviews()
guard bounds.width > 0 else { return }
var origin = CGPoint.zero
var rowHeight: CGFloat = 0
for label in labels {
let size = label.sizeThatFits(
CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
)
if origin.x > 0, origin.x + size.width > bounds.width {
origin.x = 0
origin.y += rowHeight + spacing
rowHeight = 0
}
label.frame = CGRect(origin: origin, size: size)
origin.x += size.width + spacing
rowHeight = max(rowHeight, size.height)
}
let height = origin.y + rowHeight
if contentHeight != height {
contentHeight = height
invalidateIntrinsicContentSize()
}
}
@objc private func contentSizeCategoryDidChange() {
labels.forEach { $0.font = .preferredFont(forTextStyle: .caption2) }
contentHeight = labels.first?.intrinsicContentSize.height ?? 0
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}