Something went wrong. Try again.
A slopfork of Sam Henri Gold's Twitter client, reskinned like Tweetie 2 / Twitter for iPhone circa 2012. Personal use.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102import PostTextimport Routingimport SwiftUI
extension Text { /// The typeset text, with each entity's run (a mention, hashtag, cashtag or link) tagged by an ``EntityRun`` /// attribute so its glyphs can be found in the text's layout. Text without entities is a single attributed `Text`. init(entitiesTaggedIn typeset: TypesetPostText) { guard typeset.hasEntities else { self.init(typeset.attributedString) return } // A custom attribute belongs to a whole `Text`, so the string is joined from one `Text` per segment. The // format is nothing but placeholders ("%@%@…"), so the lookup finds no translation and adds no characters. var interpolation = LocalizedStringKey.StringInterpolation( literalCapacity: 0, interpolationCount: typeset.segments.count) for (index, segment) in typeset.segments.enumerated() { let text = Text(segment.text) interpolation.appendInterpolation( segment.entity == nil ? text : text.customAttribute(EntityRun(index: index))) } self.init(LocalizedStringKey(stringInterpolation: interpolation)) }}
/// Marks the glyph runs of one entity in a laid-out `Text`; `index` is its segment in ``TypesetPostText/segments``.struct EntityRun: TextAttribute { let index: Int}
extension EnvironmentValues { /// Off where text isn't interactive, such as inside a preview or a card that is one big button. @Entry package var postTextPreviewsEnabled = true}
/// Long-pressing a mention, hashtag, cashtag or link in post text lifts it with a preview and menu, as Twitter's/// status body does for its active ranges (`bodyTextView:previewConfigurationForActiveTextRange:`).////// SwiftUI can't attach a context menu to part of a `Text`, so the entities' glyph rects are read from the text's/// layout (`Text.LayoutKey`, matched through ``EntityRun``) and a transparent UIKit layer over the text claims/// touches only inside them. Elsewhere touches fall through to the row underneath, including the row's own context/// menu. Text without entities adds nothing.struct PostTextPreviews: ViewModifier { let typeset: TypesetPostText @Environment(\.postTextPreviewsEnabled) private var isEnabled
func body(content: Content) -> some View { if typeset.hasEntities, isEnabled { content.overlayPreferenceValue(Text.LayoutKey.self) { layouts in GeometryReader { proxy in EntityInteractionLayer(targets: Self.targets(in: typeset, layouts: layouts, proxy: proxy)) } } } else { content } }
/// Each entity's rects, one per line it lands on, in the overlay's coordinates. @MainActor static func targets( in typeset: TypesetPostText, layouts: Text.LayoutKey.Value, proxy: GeometryProxy ) -> [EntityTarget] { var rects: [Int: [CGRect]] = [:] for anchored in layouts { let origin = proxy[anchored.origin] for line in anchored.layout { var lineRects: [Int: CGRect] = [:] for run in line { guard let index = run[EntityRun.self]?.index else { continue } let rect = run.typographicBounds.rect.offsetBy(dx: origin.x, dy: origin.y) lineRects[index] = lineRects[index]?.union(rect) ?? rect } for (index, rect) in lineRects { rects[index, default: []].append(rect) } } } return rects.sorted { $0.key < $1.key }.compactMap { index, rects in typeset.segments[index].entity.map { EntityTarget(index: index, entity: $0, rects: rects) } } }}
private struct EntityInteractionLayer: UIViewRepresentable { let targets: [EntityTarget] @Environment(\.openURL) private var openURL @Environment(\.openWindow) private var openWindow @Environment(\.supportsMultipleWindows) private var supportsMultipleWindows
func makeUIView(context: Context) -> EntityInteractionView { EntityInteractionView() }
func updateUIView(_ view: EntityInteractionView, context: Context) { view.targets = targets let openURL = openURL view.open = { openURL($0) } let openWindow = openWindow view.openWindow = supportsMultipleWindows ? { openWindow(value: $0.window) } : nil }}