fix(qlpreview): inline share icon instead of fixed position (gitea#7 review)
Android UI Tests / ui-tests (pull_request) Has been cancelled

The previous copy "1. Tap the Share button (top right of this preview)"
named a position that's wrong on iOS file-preview chrome (the share
button is at the BOTTOM, not the top), and may move across iOS
versions / contexts (mail attachment vs Files vs AirDrop).

Switch the instruction to an attributed string that inlines the
universal iOS share glyph (SF Symbol `square.and.arrow.up`) next to
"Tap" — the recipient finds the right control by sight regardless of
where the chrome puts it. New `PreviewViewController.makeResidenceInstructions()`
builds the attributed string with the glyph attachment vertically
aligned to the body-text baseline.

`Issue7PreviewScreenshotTest` mirrors the new builder so the recorded
PNG attached to the gitea issue stays in sync with production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-11 13:46:59 -05:00
parent d26714f043
commit f4c2780e34
3 changed files with 110 additions and 12 deletions
@@ -278,14 +278,13 @@ class PreviewViewController: UIViewController, QLPreviewingController {
titleLabel.text = residence.residenceName titleLabel.text = residence.residenceName
subtitleLabel.text = "honeyDue Residence Invite" subtitleLabel.text = "honeyDue Residence Invite"
// Numbered steps so non-technical recipients know exactly what to // Numbered steps so non-technical recipients know exactly what
// do. Replaces the prior "Tap the share button below" which // to do. The share button's location varies across the iOS
// implied an in-app button and didn't say where the share // QuickLook chrome (bottom on file previews, top in mail
// control lives (top-right of the QuickLook bar). // previews, sometimes hidden behind a "More" menu) so we lean
instructionLabel.text = "How to join:\n" // on the SF Symbol glyph inline instead of describing a
+ "1. Tap the Share button (top right of this preview)\n" // position that may not match what the recipient sees.
+ "2. Choose \"honeyDue\" from the share sheet\n" instructionLabel.attributedText = Self.makeResidenceInstructions()
+ "3. Sign in if prompted — the app finishes the rest"
// Clear existing details // Clear existing details
detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
@@ -362,6 +361,60 @@ class PreviewViewController: UIViewController, QLPreviewingController {
f.timeStyle = .short f.timeStyle = .short
return f 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 // MARK: - Type Discriminator
@@ -228,10 +228,7 @@ private final class MockPreviewViewController: UIViewController {
titleLabel.text = residence.residenceName titleLabel.text = residence.residenceName
subtitleLabel.text = "honeyDue Residence Invite" subtitleLabel.text = "honeyDue Residence Invite"
instructionLabel.text = "How to join:\n" instructionLabel.attributedText = makeResidenceInstructions()
+ "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"
detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } detailsStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
if let sharedBy = residence.sharedBy, !sharedBy.isEmpty { if let sharedBy = residence.sharedBy, !sharedBy.isEmpty {
@@ -268,6 +265,54 @@ private final class MockPreviewViewController: UIViewController {
detailsStackView.addArrangedSubview(row) 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" // Mirrors PreviewViewController.formatExpiresAt with a fixed "now"
// so the rendering is identical regardless of when the test runs. // so the rendering is identical regardless of when the test runs.
private func formatExpiresAt(_ raw: String) -> String { private func formatExpiresAt(_ raw: String) -> String {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 149 KiB