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
|
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 {
|
||||||
|
|||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 149 KiB |
Reference in New Issue
Block a user