1<script module lang="ts">
2 import type { Post } from '@skyware/bot';
3
4 export interface OutgoingLink {
5 name: string;
6 link: string;
7 }
8
9 export interface NoteData {
10 content: string;
11 published: number;
12 hasMedia: boolean;
13 hasQuote: boolean;
14 outgoingLinks?: OutgoingLink[];
15 purposeAction?: string;
16 children?: NoteData[];
17 depth?: number;
18 }
19
20 export const flattenNotes = (note: NoteData, currentDepth: number = 0): NoteData[] => {
21 note.depth = currentDepth;
22 const flattened = [note];
23 if (note.children) {
24 note.children.forEach((child) => {
25 flattened.push(...flattenNotes(child, currentDepth + 1));
26 });
27 }
28 return flattened;
29 };
30
31 export const noteFromBskyPost = (post: Post): NoteData => {
32 return {
33 content: post.text,
34 published: post.createdAt.getTime(),
35 outgoingLinks: [{ name: 'bsky', link: post.uri }],
36 hasMedia:
37 (post.embed?.isImages() || post.embed?.isVideo() || post.embed?.isRecordWithMedia()) ??
38 false,
39 hasQuote: (post.embed?.isRecord() || post.embed?.isRecordWithMedia()) ?? false
40 };
41 };
42</script>
43
44<script lang="ts">
45 import Token from './token.svelte';
46 import { renderDate, renderRelativeDate } from '$lib/dateFmt';
47
48 interface Props {
49 rootNote: NoteData;
50 isHighlighted?: boolean;
51 onlyContent?: boolean;
52 showOutgoing?: boolean;
53 mapOutgoingNames?: Record<string, string>;
54 }
55
56 let {
57 rootNote,
58 isHighlighted = false,
59 onlyContent = false,
60 showOutgoing = true,
61 mapOutgoingNames = {}
62 }: Props = $props();
63
64 const getOutgoingLink = ({ name, link }: { name: string; link: string }) => {
65 if (name.startsWith('bsky')) {
66 // Parse the atproto URI to extract DID and rkey
67 const match = link.match(/at:\/\/(did:[^/]+)\/[^/]+\/([^/]+)/);
68 if (match && match.length >= 3) {
69 // eslint-disable-next-line @typescript-eslint/no-unused-vars
70 const [_, did, rkey] = match;
71 link = `https://bsky.app/profile/${did}/post/${rkey}`;
72 }
73 if (name === 'bsky-reply') {
74 return ['reply', link];
75 } else {
76 return [name, link];
77 }
78 }
79 return [name, link];
80 };
81 // this is ASS this should be a tailwind class
82 const getTextShadowStyle = (color: string) => {
83 return `text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px ${color};`;
84 };
85 const outgoingLinkColors: Record<string, string> = {
86 bsky: 'rgb(0, 133, 255)',
87 reply: 'rgb(0, 133, 255)'
88 };
89</script>
90
91{#each flattenNotes(rootNote) as note}
92 <p class="m-0 max-w-[70ch] text-wrap break-words leading-tight align-middle">
93 {#if note.depth ?? 0 > 0}
94 <span class="inline-block">|{'=='.repeat(note.depth ?? 0)}</span>>
95 {/if}
96 {#if !onlyContent}
97 {#if (note.purposeAction ?? '').length > 0}
98 <Token v="({note.purposeAction!})" small={!isHighlighted} funct />
99 {/if}
100 {#if note.purposeAction !== 'reply'}
101 <Token
102 title={renderDate(note.published)}
103 v={renderRelativeDate(note.published)}
104 small={!isHighlighted}
105 />
106 {/if}
107 {/if}
108 <Token v={note.content} str />
109 {#if note.hasMedia}<Token v="-contains media-" keywd small />{/if}
110 {#if note.hasQuote}<Token v="-contains quote-" keywd small />{/if}
111 {#if showOutgoing}
112 {#each (note.outgoingLinks ?? []).map(getOutgoingLink) as [name, link]}
113 {@const color = outgoingLinkColors[name]}
114 {@const viewName = mapOutgoingNames[name] ?? name}
115 {#if viewName.length > 0}
116 <span class="text-sm"
117 ><Token v="(" punct /><a
118 class="hover:motion-safe:animate-squiggle hover:underline"
119 style="color: {color};{getTextShadowStyle(color)}"
120 href={link}>{viewName}</a
121 ><Token v=")" punct /></span
122 >
123 {/if}
124 {/each}
125 {/if}
126 </p>
127{/each}