A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
at main 3.1 kB view raw
1import React from "react"; 2import type { AppBskyRichtextFacet } from "@atcute/bluesky"; 3import { createTextSegments, type TextSegment } from "../utils/richtext"; 4import { useAtProto } from "../providers/AtProtoProvider"; 5 6export interface RichTextProps { 7 text: string; 8 facets?: AppBskyRichtextFacet.Main[]; 9 style?: React.CSSProperties; 10} 11 12/** 13 * RichText component that renders text with facets (mentions, links, hashtags). 14 * Properly handles byte offsets and multi-byte characters. 15 */ 16export const RichText: React.FC<RichTextProps> = ({ text, facets, style }) => { 17 const { blueskyAppBaseUrl } = useAtProto(); 18 const segments = createTextSegments(text, facets); 19 20 return ( 21 <span style={style}> 22 {segments.map((segment, idx) => ( 23 <RichTextSegment key={idx} segment={segment} blueskyAppBaseUrl={blueskyAppBaseUrl} /> 24 ))} 25 </span> 26 ); 27}; 28 29interface RichTextSegmentProps { 30 segment: TextSegment; 31 blueskyAppBaseUrl: string; 32} 33 34const RichTextSegment: React.FC<RichTextSegmentProps> = ({ segment, blueskyAppBaseUrl }) => { 35 if (!segment.facet) { 36 return <>{segment.text}</>; 37 } 38 39 // Find the first feature in the facet 40 const feature = segment.facet.features?.[0]; 41 if (!feature) { 42 return <>{segment.text}</>; 43 } 44 45 const featureType = (feature as { $type?: string }).$type; 46 47 // Render based on feature type 48 switch (featureType) { 49 case "app.bsky.richtext.facet#link": { 50 const linkFeature = feature as AppBskyRichtextFacet.Link; 51 return ( 52 <a 53 href={linkFeature.uri} 54 target="_blank" 55 rel="noopener noreferrer" 56 style={{ 57 color: "var(--atproto-color-link)", 58 textDecoration: "none", 59 }} 60 onMouseEnter={(e) => { 61 e.currentTarget.style.textDecoration = "underline"; 62 }} 63 onMouseLeave={(e) => { 64 e.currentTarget.style.textDecoration = "none"; 65 }} 66 > 67 {segment.text} 68 </a> 69 ); 70 } 71 72 case "app.bsky.richtext.facet#mention": { 73 const mentionFeature = feature as AppBskyRichtextFacet.Mention; 74 const profileUrl = `${blueskyAppBaseUrl}/profile/${mentionFeature.did}`; 75 return ( 76 <a 77 href={profileUrl} 78 target="_blank" 79 rel="noopener noreferrer" 80 style={{ 81 color: "var(--atproto-color-link)", 82 textDecoration: "none", 83 }} 84 onMouseEnter={(e) => { 85 e.currentTarget.style.textDecoration = "underline"; 86 }} 87 onMouseLeave={(e) => { 88 e.currentTarget.style.textDecoration = "none"; 89 }} 90 > 91 {segment.text} 92 </a> 93 ); 94 } 95 96 case "app.bsky.richtext.facet#tag": { 97 const tagFeature = feature as AppBskyRichtextFacet.Tag; 98 const tagUrl = `${blueskyAppBaseUrl}/hashtag/${encodeURIComponent(tagFeature.tag)}`; 99 return ( 100 <a 101 href={tagUrl} 102 target="_blank" 103 rel="noopener noreferrer" 104 style={{ 105 color: "var(--atproto-color-link)", 106 textDecoration: "none", 107 }} 108 onMouseEnter={(e) => { 109 e.currentTarget.style.textDecoration = "underline"; 110 }} 111 onMouseLeave={(e) => { 112 e.currentTarget.style.textDecoration = "none"; 113 }} 114 > 115 {segment.text} 116 </a> 117 ); 118 } 119 120 default: 121 return <>{segment.text}</>; 122 } 123}; 124 125export default RichText;