diff --git a/TESTING.md b/TESTING.md index 941a83e..0473e64 100644 --- a/TESTING.md +++ b/TESTING.md @@ -133,8 +133,10 @@ xcrun simctl launch booted de.rfc1437.gotcha - [ ] Open a repository; the issue list defaults to the saved Open/Closed filter. - [ ] Change the native filter menu between Open and Closed; the checkmark, rows, and persisted selection update. -- [ ] Open an issue and verify title, state/author metadata, Markdown body, and - comments. Links and selectable text use normal iOS interaction. +- [ ] 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. ## Repositories and commits diff --git a/crates/app/src/presentation.rs b/crates/app/src/presentation.rs index f9b9aab..506364e 100644 --- a/crates/app/src/presentation.rs +++ b/crates/app/src/presentation.rs @@ -59,6 +59,7 @@ pub struct CommentRow { #[derive(Clone, uniffi::Record)] pub struct IssuePage { pub title: String, + pub state: String, pub meta: String, pub milestone: String, pub body: String, @@ -355,6 +356,11 @@ pub fn issue_page(details: IssueDetails) -> IssuePage { .title .clone() .unwrap_or_else(|| "Untitled issue".into()), + state: details + .issue + .state + .clone() + .unwrap_or_else(|| "unknown".into()), meta: issue_meta(&details.issue), milestone: issue_milestone(&details.issue), body: details @@ -860,4 +866,17 @@ mod tests { }; assert_eq!(issue_rows(&[issue])[0].milestone, "Version 1"); } + + #[test] + fn issue_page_exposes_state() { + let page = issue_page(IssueDetails { + issue: models::Issue { + state: Some("closed".into()), + ..Default::default() + }, + comments: Vec::new(), + }); + + assert_eq!(page.state, "closed"); + } } diff --git a/ios/Generated/gotcha_core.swift b/ios/Generated/gotcha_core.swift index bb41ab5..28146f0 100644 --- a/ios/Generated/gotcha_core.swift +++ b/ios/Generated/gotcha_core.swift @@ -1659,6 +1659,7 @@ public func FfiConverterTypeHomePage_lower(_ value: HomePage) -> RustBuffer { public struct IssuePage: Equatable, Hashable { public var title: String + public var state: String public var meta: String public var milestone: String public var body: String @@ -1666,8 +1667,9 @@ public struct IssuePage: Equatable, Hashable { // Default memberwise initializers are never public by default, so we // declare one manually. - public init(title: String, meta: String, milestone: String, body: String, comments: [CommentRow]) { + public init(title: String, state: String, meta: String, milestone: String, body: String, comments: [CommentRow]) { self.title = title + self.state = state self.meta = meta self.milestone = milestone self.body = body @@ -1691,6 +1693,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer { return try IssuePage( title: FfiConverterString.read(from: &buf), + state: FfiConverterString.read(from: &buf), meta: FfiConverterString.read(from: &buf), milestone: FfiConverterString.read(from: &buf), body: FfiConverterString.read(from: &buf), @@ -1700,6 +1703,7 @@ public struct FfiConverterTypeIssuePage: FfiConverterRustBuffer { public static func write(_ value: IssuePage, into buf: inout [UInt8]) { FfiConverterString.write(value.title, into: &buf) + FfiConverterString.write(value.state, into: &buf) FfiConverterString.write(value.meta, into: &buf) FfiConverterString.write(value.milestone, into: &buf) FfiConverterString.write(value.body, into: &buf) diff --git a/ios/Sources/DetailScreens.swift b/ios/Sources/DetailScreens.swift index fb617c6..e3b2b10 100644 --- a/ios/Sources/DetailScreens.swift +++ b/ios/Sources/DetailScreens.swift @@ -103,6 +103,7 @@ final class IssueViewController: MarkdownPageViewController { ) replaceContent(detailViews( title: page.title, + issueState: page.state, meta: page.meta, body: page.body, comments: page.comments, @@ -665,6 +666,7 @@ final class DiffViewController: UIViewController { private func detailViews( title: String, + issueState: String? = nil, meta: String, body: String, comments: [CommentRow], @@ -680,7 +682,7 @@ private func detailViews( metaLabel.textColor = .secondaryLabel metaLabel.numberOfLines = 0 let bodyView = markdownView(body) - var views: [UIView] = [titleLabel, metaLabel] + var views: [UIView] = [issueState.map { issueTitle(titleLabel, state: $0) } ?? titleLabel, metaLabel] if !milestone.isEmpty { let milestoneLabel = UILabel() milestoneLabel.attributedText = symbolText( @@ -695,6 +697,31 @@ private func detailViews( return views + [separator(), bodyView] + commentViews(comments) } +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 stack = UIStackView(arrangedSubviews: [icon, titleLabel]) + stack.axis = .horizontal + stack.alignment = .firstBaseline + stack.spacing = 8 + return stack +} + private func commentViews(_ comments: [CommentRow]) -> [UIView] { guard !comments.isEmpty else { return [] } var views: [UIView] = [sectionLabel("Comments")]