your personal website on atproto - mirror blento.app
at button 157 lines 4.5 kB view raw
1import type { PostData, PostEmbed } from '../post'; 2import type { PostView } from '@atcute/bluesky/types/app/feed/defs'; 3import { segmentize, type Facet, type RichtextSegment } from '@atcute/bluesky-richtext-segmenter'; 4 5function blueskyEmbedTypeToEmbedType(type: string) { 6 switch (type) { 7 case 'app.bsky.embed.external#view': 8 case 'app.bsky.embed.external': 9 return 'external'; 10 case 'app.bsky.embed.images#view': 11 case 'app.bsky.embed.images': 12 return 'images'; 13 case 'app.bsky.embed.video#view': 14 case 'app.bsky.embed.video': 15 return 'video'; 16 default: 17 return 'unknown'; 18 } 19} 20 21export function blueskyPostToPostData( 22 data: PostView, 23 baseUrl: string = 'https://bsky.app' 24): PostData { 25 const post = data; 26 // const reason = data.reason; 27 // const reply = data.reply?.parent; 28 // const replyId = reply?.uri?.split('/').pop(); 29 console.log(JSON.parse(JSON.stringify(data))); 30 31 const id = post.uri.split('/').pop(); 32 33 return { 34 id, 35 href: `${baseUrl}/profile/${post.author.handle}/post/${id}`, 36 // reposted: 37 // reason && reason.$type === 'app.bsky.feed.defs#reasonRepost' 38 // ? { 39 // handle: reason.by.handle, 40 // href: `${baseUrl}/profile/${reason.by.handle}` 41 // } 42 // : undefined, 43 44 // replyTo: 45 // reply && replyId 46 // ? { 47 // handle: reply.author.handle, 48 // href: `${baseUrl}/profile/${reply.author.handle}/post/${replyId}` 49 // } 50 // : undefined, 51 author: { 52 displayName: post.author.displayName || '', 53 handle: post.author.handle, 54 avatar: post.author.avatar, 55 href: `${baseUrl}/profile/${post.author.did}` 56 }, 57 replyCount: post.replyCount ?? 0, 58 repostCount: post.repostCount ?? 0, 59 likeCount: post.likeCount ?? 0, 60 createdAt: post.record.createdAt as string, 61 62 embed: post.embed 63 ? ({ 64 type: blueskyEmbedTypeToEmbedType(post.embed?.$type), 65 // Cast to any to handle union type - properties are conditionally accessed 66 images: (post.embed as any)?.images?.map((image: any) => ({ 67 alt: image.alt, 68 thumb: image.thumb, 69 aspectRatio: image.aspectRatio, 70 fullsize: image.fullsize 71 })), 72 external: (post.embed as any)?.external 73 ? { 74 href: (post.embed as any).external.uri, 75 title: (post.embed as any).external.title, 76 description: (post.embed as any).external.description, 77 thumb: (post.embed as any).external.thumb 78 } 79 : undefined, 80 video: (post.embed as any)?.playlist 81 ? { 82 playlist: (post.embed as any).playlist, 83 thumb: (post.embed as any).thumbnail, 84 alt: (post.embed as any).alt, 85 aspectRatio: (post.embed as any).aspectRatio 86 } 87 : undefined 88 } as PostEmbed) 89 : undefined, 90 91 htmlContent: blueskyPostToHTML(post, baseUrl), 92 labels: post.labels ? post.labels.map((label) => label.val) : undefined 93 }; 94} 95 96interface MentionFeature { 97 $type: 'app.bsky.richtext.facet#mention'; 98 did: string; 99} 100 101interface LinkFeature { 102 $type: 'app.bsky.richtext.facet#link'; 103 uri: string; 104} 105 106interface TagFeature { 107 $type: 'app.bsky.richtext.facet#tag'; 108 tag: string; 109} 110 111type Feature = MentionFeature | LinkFeature | TagFeature; 112 113const renderSegment = (segment: RichtextSegment, baseUrl: string) => { 114 const { text, features } = segment; 115 116 if (!features) { 117 return `<span>${text}</span>`; 118 } 119 120 // segments can have multiple features, use the first one 121 const feature = features[0] as Feature; 122 123 const createLink = (href: string, text: string) => { 124 return `<a target="_blank" rel="noopener noreferrer nofollow" href="${encodeURI(href)}">${text}</a>`; 125 }; 126 127 switch (feature.$type) { 128 case 'app.bsky.richtext.facet#mention': 129 return createLink(`${baseUrl}/profile/${feature.did}`, segment.text); 130 case 'app.bsky.richtext.facet#link': 131 return createLink(feature.uri, segment.text); 132 case 'app.bsky.richtext.facet#tag': 133 return createLink(`${baseUrl}/hashtag/${feature.tag}`, segment.text); 134 default: 135 return `<span>${text}</span>`; 136 } 137}; 138 139const RichText = ({ text, facets }: { text: string; facets?: Facet[] }, baseUrl: string) => { 140 const segments = segmentize(text, facets); 141 return segments.map((v) => renderSegment(v, baseUrl)).join(''); 142}; 143 144export function blueskyPostToHTML(post: PostView, baseUrl: string = 'https://bsky.app') { 145 if (!post?.record) { 146 return ''; 147 } 148 149 const html = RichText( 150 { text: post.record.text as string, facets: post.record.facets as Facet[] }, 151 baseUrl 152 ); 153 154 return html.replace(/\n/g, '<br>'); 155} 156 157export { default as BlueskyPost } from './BlueskyPost.svelte';