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