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 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';