data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: let puppy reply to guestbook entries and show them

dusk 44525e92 cec59f95

+99 -33
+78 -29
src/components/note.svelte
··· 5 5 name: string; 6 6 link: string; 7 7 } 8 + 8 9 export interface NoteData { 9 10 content: string; 10 11 published: number; 11 12 hasMedia: boolean; 12 13 hasQuote: boolean; 13 14 outgoingLinks?: OutgoingLink[]; 15 + purposeAction?: string; 16 + children?: NoteData[]; 17 + depth?: number; 14 18 } 15 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 + 16 31 export const noteFromBskyPost = (post: Post): NoteData => { 17 32 return { 18 33 content: post.text, ··· 31 46 import { renderDate, renderRelativeDate } from '$lib/dateFmt'; 32 47 33 48 interface Props { 34 - note: NoteData; 49 + rootNote: NoteData; 35 50 isHighlighted?: boolean; 36 51 onlyContent?: boolean; 37 52 showOutgoing?: boolean; 53 + mapOutgoingNames?: Record<string, string>; 38 54 } 39 55 40 - let { note, isHighlighted = false, onlyContent = false, showOutgoing = true }: Props = $props(); 56 + let { 57 + rootNote, 58 + isHighlighted = false, 59 + onlyContent = false, 60 + showOutgoing = true, 61 + mapOutgoingNames = {} 62 + }: Props = $props(); 41 63 42 - const getOutgoingLink = (name: string, link: string) => { 43 - if (name === 'bsky') { 44 - return `https://bsky.app/profile/did:plc:dfl62fgb7wtjj3fcbb72naae/post/${link.split('/').pop()}`; 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 + } 45 78 } 46 - return link; 79 + return [name, link]; 47 80 }; 48 81 // this is ASS this should be a tailwind class 49 82 const getTextShadowStyle = (color: string) => { 50 83 return `text-shadow: 0 0 1px theme(colors.ralsei.black), 0 0 5px ${color};`; 51 84 }; 52 85 const outgoingLinkColors: Record<string, string> = { 53 - bsky: 'rgb(0, 133, 255)' 86 + bsky: 'rgb(0, 133, 255)', 87 + reply: 'rgb(0, 133, 255)' 54 88 }; 55 89 </script> 56 90 57 - <p class="m-0 max-w-[70ch] text-wrap break-words leading-tight align-middle"> 58 - {#if !onlyContent}<Token 59 - title={renderDate(note.published)} 60 - v={renderRelativeDate(note.published)} 61 - small={!isHighlighted} 62 - />{/if} 63 - <Token v={note.content} str /> 64 - {#if note.hasMedia}<Token v="-contains media-" keywd small />{/if} 65 - {#if note.hasQuote}<Token v="-contains quote-" keywd small />{/if} 66 - {#if showOutgoing} 67 - {#each note.outgoingLinks ?? [] as { name, link }} 68 - {@const color = outgoingLinkColors[name]} 69 - <span class="text-sm" 70 - ><Token v="(" punct /><a 71 - class="hover:motion-safe:animate-squiggle hover:underline" 72 - style="color: {color};{getTextShadowStyle(color)}" 73 - href={getOutgoingLink(name, link)}>{name}</a 74 - ><Token v=")" punct /></span 75 - > 76 - {/each} 77 - {/if} 78 - </p> 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>&gt; 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}
+1 -1
src/routes/+page.svelte
··· 95 95 published {renderRelativeDate(data.lastNote.published)}! 96 96 </p> 97 97 <div class="mt-0 p-1.5 border-4 border-double bg-ralsei-black min-w-full max-w-[60ch]"> 98 - <Note note={data.lastNote} onlyContent /> 98 + <Note rootNote={data.lastNote} onlyContent /> 99 99 </div> 100 100 </div> 101 101 {/if}
+14 -1
src/routes/guestbook/+page.server.ts
··· 109 109 // actually get posts 110 110 try { 111 111 const { posts } = await getUserPosts('did:web:guestbook.gaze.systems', 16); 112 - data.entries = posts.map(noteFromBskyPost); 112 + for (const post of posts) { 113 + const note = noteFromBskyPost(post); 114 + // the only reply can be mine 115 + if (post.replyCount ?? 0 > 0) { 116 + const replies = await post.fetchChildren({ depth: 1, force: true }); 117 + note.children = replies.map((reply) => { 118 + const note = noteFromBskyPost(reply); 119 + note.purposeAction = 'reply'; 120 + note.outgoingLinks = [{ name: 'bsky-reply', link: reply.uri }]; 121 + return note; 122 + }); 123 + } 124 + data.entries.push(note); 125 + } 113 126 // eslint-disable-next-line @typescript-eslint/no-explicit-any 114 127 } catch (err: any) { 115 128 data.getError = err.toString();
+5 -1
src/routes/guestbook/+page.svelte
··· 125 125 <br /> 126 126 <br /> 127 127 {#each data.entries as note, index} 128 - <Note showOutgoing={false} {note} /> 128 + <Note 129 + mapOutgoingNames={{ bsky: '', reply: 'src' }} 130 + showOutgoing={true} 131 + rootNote={note} 132 + /> 129 133 {#if index < data.entries.length - 1} 130 134 <div class="mt-3"></div> 131 135 {/if}
+1 -1
src/routes/log/+page.svelte
··· 53 53 <br /> 54 54 <br /> 55 55 {#each data.feedPosts as note, index} 56 - <Note {note} /> 56 + <Note rootNote={note} /> 57 57 {#if index < data.feedPosts.length - 1} 58 58 <div class="mt-3"></div> 59 59 {/if}