fix: share-residence import preview polish (closes #7) #9

Merged
admin merged 5 commits from fix/7-share-residence-import-polish into master 2026-05-11 16:17:16 -05:00
3 changed files with 110 additions and 12 deletions
Showing only changes of commit f4c2780e34 - Show all commits
@@ -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 {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 149 KiB