your personal website on atproto - mirror
blento.app
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';