fix(qlpreview): inline share icon instead of fixed position (gitea#7 review)
Android UI Tests / ui-tests (pull_request) Has been cancelled
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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 149 KiB |
Reference in New Issue
Block a user