From 4b497aaabc5e21afd416ca92794dc80dcebdde3c Mon Sep 17 00:00:00 2001 From: James Magahern Date: Tue, 9 Sep 2025 15:45:50 -0700 Subject: [PATCH] osx: linkify text, enable selection --- .../TranscriptDisplayItemViews.swift | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/osx/kordophone2/Transcript/TranscriptDisplayItemViews.swift b/osx/kordophone2/Transcript/TranscriptDisplayItemViews.swift index 34b471a..981361c 100644 --- a/osx/kordophone2/Transcript/TranscriptDisplayItemViews.swift +++ b/osx/kordophone2/Transcript/TranscriptDisplayItemViews.swift @@ -58,7 +58,7 @@ struct TextBubbleItemView: View let date: Date private var isFromMe: Bool { sender.isMe } - + var body: some View { let bubbleColor: Color = isFromMe ? .blue : Color(NSColor(name: "grayish", dynamicProvider: { appearance in appearance.name == .darkAqua ? .darkGray : NSColor(white: 0.78, alpha: 1.0) @@ -67,7 +67,7 @@ struct TextBubbleItemView: View BubbleView(sender: sender, date: date) { HStack { - Text(text) + Text(text.linkifiedAttributedString()) .foregroundStyle(textColor) .multilineTextAlignment(.leading) } @@ -75,6 +75,7 @@ struct TextBubbleItemView: View .padding(.horizontal, 16.0) .padding(.vertical, 10.0) .background(bubbleColor) + .textSelection(.enabled) } } } @@ -219,14 +220,16 @@ struct SenderAttributionView: View } } -fileprivate extension CGFloat { +fileprivate extension CGFloat +{ static let dominantCornerRadius = 16.0 static let minorCornerRadius = 4.0 static let minimumBubbleHorizontalPadding = 80.0 static let imageMaxWidth = 380.0 } -fileprivate extension CGSize { +fileprivate extension CGSize +{ var aspectRatio: CGFloat { width / height } } @@ -239,3 +242,28 @@ fileprivate func preferredBubbleWidth(forAttachmentSize attachmentSize: CGSize?, return 200.0 // fallback } } + +fileprivate extension String +{ + func linkifiedAttributedString() -> AttributedString { + var attributed = AttributedString(self) + guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return attributed + } + + let nsText = self as NSString + let fullRange = NSRange(location: 0, length: nsText.length) + detector.enumerateMatches(in: self, options: [], range: fullRange) { result, _, _ in + guard let result, let url = result.url, + let swiftRange = Range(result.range, in: self), + let start = AttributedString.Index(swiftRange.lowerBound, within: attributed), + let end = AttributedString.Index(swiftRange.upperBound, within: attributed) else { return } + + attributed[start..