import type { PostData, PostEmbed } from '../post'; import type { PostView } from '@atcute/bluesky/types/app/feed/defs'; import { segmentize, type Facet, type RichtextSegment } from '@atcute/bluesky-richtext-segmenter'; function blueskyEmbedTypeToEmbedType(type: string) { switch (type) { case 'app.bsky.embed.external#view': case 'app.bsky.embed.external': return 'external'; case 'app.bsky.embed.images#view': case 'app.bsky.embed.images': return 'images'; case 'app.bsky.embed.video#view': case 'app.bsky.embed.video': return 'video'; default: return 'unknown'; } } export function blueskyPostToPostData( data: PostView, baseUrl: string = 'https://bsky.app' ): PostData { const post = data; // const reason = data.reason; // const reply = data.reply?.parent; // const replyId = reply?.uri?.split('/').pop(); console.log(JSON.parse(JSON.stringify(data))); const id = post.uri.split('/').pop(); return { id, href: `${baseUrl}/profile/${post.author.handle}/post/${id}`, // reposted: // reason && reason.$type === 'app.bsky.feed.defs#reasonRepost' // ? { // handle: reason.by.handle, // href: `${baseUrl}/profile/${reason.by.handle}` // } // : undefined, // replyTo: // reply && replyId // ? { // handle: reply.author.handle, // href: `${baseUrl}/profile/${reply.author.handle}/post/${replyId}` // } // : undefined, author: { displayName: post.author.displayName || '', handle: post.author.handle, avatar: post.author.avatar, href: `${baseUrl}/profile/${post.author.did}` }, replyCount: post.replyCount ?? 0, repostCount: post.repostCount ?? 0, likeCount: post.likeCount ?? 0, createdAt: post.record.createdAt as string, embed: post.embed ? ({ type: blueskyEmbedTypeToEmbedType(post.embed?.$type), // Cast to any to handle union type - properties are conditionally accessed images: (post.embed as any)?.images?.map((image: any) => ({ alt: image.alt, thumb: image.thumb, aspectRatio: image.aspectRatio, fullsize: image.fullsize })), external: (post.embed as any)?.external ? { href: (post.embed as any).external.uri, title: (post.embed as any).external.title, description: (post.embed as any).external.description, thumb: (post.embed as any).external.thumb } : undefined, video: (post.embed as any)?.playlist ? { playlist: (post.embed as any).playlist, thumb: (post.embed as any).thumbnail, alt: (post.embed as any).alt, aspectRatio: (post.embed as any).aspectRatio } : undefined } as PostEmbed) : undefined, htmlContent: blueskyPostToHTML(post, baseUrl), labels: post.labels ? post.labels.map((label) => label.val) : undefined }; } interface MentionFeature { $type: 'app.bsky.richtext.facet#mention'; did: string; } interface LinkFeature { $type: 'app.bsky.richtext.facet#link'; uri: string; } interface TagFeature { $type: 'app.bsky.richtext.facet#tag'; tag: string; } type Feature = MentionFeature | LinkFeature | TagFeature; const renderSegment = (segment: RichtextSegment, baseUrl: string) => { const { text, features } = segment; if (!features) { return `${text}`; } // segments can have multiple features, use the first one const feature = features[0] as Feature; const createLink = (href: string, text: string) => { return `${text}`; }; switch (feature.$type) { case 'app.bsky.richtext.facet#mention': return createLink(`${baseUrl}/profile/${feature.did}`, segment.text); case 'app.bsky.richtext.facet#link': return createLink(feature.uri, segment.text); case 'app.bsky.richtext.facet#tag': return createLink(`${baseUrl}/hashtag/${feature.tag}`, segment.text); default: return `${text}`; } }; const RichText = ({ text, facets }: { text: string; facets?: Facet[] }, baseUrl: string) => { const segments = segmentize(text, facets); return segments.map((v) => renderSegment(v, baseUrl)).join(''); }; export function blueskyPostToHTML(post: PostView, baseUrl: string = 'https://bsky.app') { if (!post?.record) { return ''; } const html = RichText( { text: post.record.text as string, facets: post.record.facets as Facet[] }, baseUrl ); return html.replace(/\n/g, '
'); } export { default as BlueskyPost } from './BlueskyPost.svelte';