diff --git a/TESTING.md b/TESTING.md index e6aaeed..82a7d10 100644 --- a/TESTING.md +++ b/TESTING.md @@ -145,6 +145,9 @@ xcrun simctl launch booted de.rfc1437.gotcha date, and current favorite state. - [ ] Toggle a favorite and confirm it remains after refresh and relaunch. - [ ] Open a repository; the issue list defaults to the saved Open/Closed filter. +- [ ] Repository issue rows and issue rows inside a milestone show a green open + or purple closed icon beside the title; mixed milestone results use the + correct icon per row and VoiceOver announces each state. - [ ] Change the native filter menu between Open and Closed; the checkmark, rows, and persisted selection update. - [ ] Select one and then multiple labels in the issue filter. Checkmarks and diff --git a/crates/app/src/presentation.rs b/crates/app/src/presentation.rs index 2ba0b5e..91ce7eb 100644 --- a/crates/app/src/presentation.rs +++ b/crates/app/src/presentation.rs @@ -38,6 +38,7 @@ pub struct LabelRow { #[derive(Clone, uniffi::Record)] pub struct IssueRow { pub number: i64, + pub state: String, pub title: String, pub summary: String, pub meta: String, @@ -288,6 +289,7 @@ pub fn issue_rows(issues: &[models::Issue]) -> Vec { .iter() .map(|issue| IssueRow { number: issue.number.unwrap_or_default(), + state: issue.state.clone().unwrap_or_else(|| "unknown".into()), title: issue .title .clone() @@ -1251,10 +1253,13 @@ mod tests { assert_eq!(milestone_row.progress, 0.4); assert_eq!(milestone_row.progress_accessibility, "2 closed, 3 open"); let issue = models::Issue { + state: Some("closed".into()), milestone: Some(Box::new(milestone)), ..Default::default() }; - assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1"); + let row = &issue_rows(&[issue])[0]; + assert_eq!(row.state, "closed"); + assert_eq!(row.milestone, "Version 1"); } #[test] diff --git a/ios/Generated/gotcha_core.swift b/ios/Generated/gotcha_core.swift index d69fdbf..e949656 100644 --- a/ios/Generated/gotcha_core.swift +++ b/ios/Generated/gotcha_core.swift @@ -2288,6 +2288,7 @@ public func FfiConverterTypeIssuePage_lower(_ value: IssuePage) -> RustBuffer { public struct IssueRow: Equatable, Hashable { public var number: Int64 + public var state: String public var title: String public var summary: String public var meta: String @@ -2296,8 +2297,9 @@ public struct IssueRow: Equatable, Hashable { // Default memberwise initializers are never public by default, so we // declare one manually. - public init(number: Int64, title: String, summary: String, meta: String, milestone: String, labels: [LabelRow]) { + public init(number: Int64, state: String, title: String, summary: String, meta: String, milestone: String, labels: [LabelRow]) { self.number = number + self.state = state self.title = title self.summary = summary self.meta = meta @@ -2322,6 +2324,7 @@ public struct FfiConverterTypeIssueRow: FfiConverterRustBuffer { return try IssueRow( number: FfiConverterInt64.read(from: &buf), + state: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf), summary: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf), @@ -2332,6 +2335,7 @@ public struct FfiConverterTypeIssueRow: FfiConverterRustBuffer { public static func write(_ value: IssueRow, into buf: inout [UInt8]) { FfiConverterInt64.write(value.number, into: &buf) + FfiConverterString.write(value.state, into: &buf) FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.summary, into: &buf) FfiConverterString.write(value.meta, into: &buf) @@ -4061,4 +4065,4 @@ public func uniffiEnsureGotchaCoreInitialized() { } } -// swiftlint:enable all \ No newline at end of file +// swiftlint:enable all diff --git a/ios/Sources/ContentScreens.swift b/ios/Sources/ContentScreens.swift index 9c25d09..26bfa49 100644 --- a/ios/Sources/ContentScreens.swift +++ b/ios/Sources/ContentScreens.swift @@ -282,6 +282,7 @@ final class IssuesViewController: RefreshingTableViewController { } final class IssueCell: UITableViewCell { + private let stateIcon = UIImageView() private let titleLabel = UILabel() private let summaryLabel = UILabel() private let labels = UIStackView() @@ -293,6 +294,9 @@ final class IssueCell: UITableViewCell { accessoryType = .disclosureIndicator titleLabel.font = .preferredFont(forTextStyle: .headline) titleLabel.numberOfLines = 2 + let titleStack = UIStackView(arrangedSubviews: [stateIcon, titleLabel]) + titleStack.alignment = .firstBaseline + titleStack.spacing = 8 summaryLabel.font = .preferredFont(forTextStyle: .subheadline) summaryLabel.textColor = .secondaryLabel summaryLabel.numberOfLines = 2 @@ -304,7 +308,7 @@ final class IssueCell: UITableViewCell { milestoneLabel.font = .preferredFont(forTextStyle: .caption1) milestoneLabel.adjustsFontForContentSizeCategory = true let stack = UIStackView( - arrangedSubviews: [titleLabel, summaryLabel, labels, metaLabel, milestoneLabel] + arrangedSubviews: [titleStack, summaryLabel, labels, metaLabel, milestoneLabel] ) stack.axis = .vertical stack.spacing = 6 @@ -322,6 +326,7 @@ final class IssueCell: UITableViewCell { required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") } func configure(_ row: IssueRow) { + configureIssueStateIcon(stateIcon, state: row.state, textStyle: .headline) titleLabel.text = row.title summaryLabel.text = row.summary metaLabel.text = row.meta diff --git a/ios/Sources/DetailScreens.swift b/ios/Sources/DetailScreens.swift index 37db998..6fc62cc 100644 --- a/ios/Sources/DetailScreens.swift +++ b/ios/Sources/DetailScreens.swift @@ -1130,23 +1130,8 @@ private func detailViews( } private func issueTitle(_ titleLabel: UILabel, state: String) -> UIView { - let symbol: String - let color: UIColor - let accessibilityLabel: String - switch state { - case "open": - (symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue") - case "closed": - (symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue") - default: - (symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state") - } - let icon = UIImageView(image: UIImage(systemName: symbol)) - icon.tintColor = color - icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .title1) - icon.setContentHuggingPriority(.required, for: .horizontal) - icon.isAccessibilityElement = true - icon.accessibilityLabel = accessibilityLabel + let icon = UIImageView() + configureIssueStateIcon(icon, state: state, textStyle: .title1) let stack = UIStackView(arrangedSubviews: [icon, titleLabel]) stack.axis = .horizontal stack.alignment = .firstBaseline diff --git a/ios/Sources/Support.swift b/ios/Sources/Support.swift index 6828476..2bc6900 100644 --- a/ios/Sources/Support.swift +++ b/ios/Sources/Support.swift @@ -255,3 +255,28 @@ func symbolText(_ symbol: String, text: String, font: UIFont, color: UIColor) -> ])) return output } + +func configureIssueStateIcon( + _ icon: UIImageView, + state: String, + textStyle: UIFont.TextStyle +) { + let symbol: String + let color: UIColor + let accessibilityLabel: String + switch state { + case "open": + (symbol, color, accessibilityLabel) = ("exclamationmark.circle.fill", .systemGreen, "Open issue") + case "closed": + (symbol, color, accessibilityLabel) = ("checkmark.circle.fill", .systemPurple, "Closed issue") + default: + (symbol, color, accessibilityLabel) = ("questionmark.circle.fill", .systemGray, "Unknown issue state") + } + icon.image = UIImage(systemName: symbol) + icon.tintColor = color + icon.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: textStyle) + icon.setContentHuggingPriority(.required, for: .horizontal) + icon.setContentCompressionResistancePriority(.required, for: .horizontal) + icon.isAccessibilityElement = true + icon.accessibilityLabel = accessibilityLabel +}