diff --git a/iosApp/HoneyDueQLPreview/PreviewViewController.swift b/iosApp/HoneyDueQLPreview/PreviewViewController.swift index 12053c6..41ed41f 100644 --- a/iosApp/HoneyDueQLPreview/PreviewViewController.swift +++ b/iosApp/HoneyDueQLPreview/PreviewViewController.swift @@ -278,14 +278,13 @@ class PreviewViewController: UIViewController, QLPreviewingController { titleLabel.text = residence.residenceName subtitleLabel.text = "honeyDue Residence Invite" - // Numbered steps so non-technical recipients know exactly what to - // do. Replaces the prior "Tap the share button below…" which - // implied an in-app button and didn't say where the share - // control lives (top-right of the QuickLook bar). - instructionLabel.text = "How to join:\n" - + "1. Tap the Share button (top right of this preview)\n" - + "2. Choose \"honeyDue\" from the share sheet\n" - + "3. Sign in if prompted — the app finishes the rest" + // Numbered steps so non-technical recipients know exactly what + // to do. The share button's location varies across the iOS + // QuickLook chrome (bottom on file previews, top in mail + // previews, sometimes hidden behind a "More" menu) so we lean + // on the SF Symbol glyph inline instead of describing a + // position that may not match what the recipient sees. + instructionLabel.attributedText = Self.makeResidenceInstructions() // Clear existing details detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } @@ -362,6 +361,60 @@ class PreviewViewController: UIViewController, QLPreviewingController { f.timeStyle = .short return f }() + + /// Builds the "How to join" instruction copy as an attributed + /// string with the iOS share-icon glyph (square + up-arrow) inlined + /// next to "Tap [icon]". The glyph is the universal share symbol + /// across iOS, so the recipient finds the right control whether + /// it's at the top, bottom, or behind a More menu — instead of us + /// claiming a fixed position the chrome can move (gitea#7 review + /// feedback). + private static func makeResidenceInstructions() -> NSAttributedString { + let bodyFont = UIFont.systemFont(ofSize: 15, weight: .medium) + let tint = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 1) + let paragraph = NSMutableParagraphStyle() + paragraph.lineSpacing = 2 + paragraph.alignment = .left + + let result = NSMutableAttributedString() + + func appendText(_ s: String) { + result.append(NSAttributedString( + string: s, + attributes: [ + .font: bodyFont, + .foregroundColor: tint, + .paragraphStyle: paragraph, + ] + )) + } + + appendText("How to join:\n1. Tap ") + + let shareImage = UIImage( + systemName: "square.and.arrow.up", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold) + )?.withTintColor(tint, renderingMode: .alwaysOriginal) + if let shareImage { + let attachment = NSTextAttachment() + attachment.image = shareImage + // Align the glyph baseline with the surrounding text by + // nudging the bounds down a few points; the SF Symbol's + // natural bounds sit a hair above the cap height. + attachment.bounds = CGRect( + x: 0, + y: -3, + width: shareImage.size.width, + height: shareImage.size.height + ) + result.append(NSAttributedString(attachment: attachment)) + } + + appendText("\n2. Choose \"honeyDue\" from the share sheet") + appendText("\n3. Sign in if prompted — the app finishes the rest") + + return result + } } // MARK: - Type Discriminator diff --git a/iosApp/HoneyDueTests/Issue7PreviewScreenshotTest.swift b/iosApp/HoneyDueTests/Issue7PreviewScreenshotTest.swift index 8bc0aee..3100b0b 100644 --- a/iosApp/HoneyDueTests/Issue7PreviewScreenshotTest.swift +++ b/iosApp/HoneyDueTests/Issue7PreviewScreenshotTest.swift @@ -228,10 +228,7 @@ private final class MockPreviewViewController: UIViewController { titleLabel.text = residence.residenceName subtitleLabel.text = "honeyDue Residence Invite" - instructionLabel.text = "How to join:\n" - + "1. Tap the Share button (top right of this preview)\n" - + "2. Choose \"honeyDue\" from the share sheet\n" - + "3. Sign in if prompted — the app finishes the rest" + instructionLabel.attributedText = makeResidenceInstructions() detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } if let sharedBy = residence.sharedBy, !sharedBy.isEmpty { @@ -268,6 +265,54 @@ private final class MockPreviewViewController: UIViewController { detailsStackView.addArrangedSubview(row) } + /// Mirrors `PreviewViewController.makeResidenceInstructions()` — see + /// the rationale comment there. Inlined here because the QL + /// extension target can't be `@testable import`ed without + /// project-file surgery. + private func makeResidenceInstructions() -> NSAttributedString { + let bodyFont = UIFont.systemFont(ofSize: 15, weight: .medium) + let tint = UIColor(red: 7/255, green: 160/255, blue: 195/255, alpha: 1) + let paragraph = NSMutableParagraphStyle() + paragraph.lineSpacing = 2 + paragraph.alignment = .left + + let result = NSMutableAttributedString() + + func appendText(_ s: String) { + result.append(NSAttributedString( + string: s, + attributes: [ + .font: bodyFont, + .foregroundColor: tint, + .paragraphStyle: paragraph, + ] + )) + } + + appendText("How to join:\n1. Tap ") + + let shareImage = UIImage( + systemName: "square.and.arrow.up", + withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .semibold) + )?.withTintColor(tint, renderingMode: .alwaysOriginal) + if let shareImage { + let attachment = NSTextAttachment() + attachment.image = shareImage + attachment.bounds = CGRect( + x: 0, + y: -3, + width: shareImage.size.width, + height: shareImage.size.height + ) + result.append(NSAttributedString(attachment: attachment)) + } + + appendText("\n2. Choose \"honeyDue\" from the share sheet") + appendText("\n3. Sign in if prompted — the app finishes the rest") + + return result + } + // Mirrors PreviewViewController.formatExpiresAt with a fixed "now" // so the rendering is identical regardless of when the test runs. private func formatExpiresAt(_ raw: String) -> String { diff --git a/iosApp/HoneyDueTests/__Snapshots__/Issue7PreviewScreenshotTest/test_residence_invite_preview_after_issue7_fix.issue7_residence_invite_preview_dark.png b/iosApp/HoneyDueTests/__Snapshots__/Issue7PreviewScreenshotTest/test_residence_invite_preview_after_issue7_fix.issue7_residence_invite_preview_dark.png index 191eb66..cae69c4 100644 Binary files a/iosApp/HoneyDueTests/__Snapshots__/Issue7PreviewScreenshotTest/test_residence_invite_preview_after_issue7_fix.issue7_residence_invite_preview_dark.png and b/iosApp/HoneyDueTests/__Snapshots__/Issue7PreviewScreenshotTest/test_residence_invite_preview_after_issue7_fix.issue7_residence_invite_preview_dark.png differ