<svelte:options runes={true} /> <script lang="ts"> import { onMount } from 'svelte'; import sanitizeHtml from 'sanitize-html'; import { Skeleton } from '$lib/components/ui/skeleton/index.js'; import { DoubleArrowUp } from 'radix-icons-svelte'; let { account, maxToots, accountId, excludeReplies }: { account: string; maxToots?: number; accountId?: string; excludeReplies: boolean } = $props(); let toots: Toot[] = $state([]); let loading = $state(false); onMount(() => { loading = true; loadToots(account, accountId, maxToots, excludeReplies); }); interface Toot { created_at: string; in_reply_to_id: string | null; content: string; url: string; account: { username: string; display_name: string; avatar: string; url: string; }; reblog?: Toot; media_attachments: { type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'; url: string; preview_url: string; description: string; blurhash: string; }[]; } export async function getToots( userURL: string, limit: number, excludeReplies: boolean, accountId?: string ): Promise<Toot[]> { const url = new URL(userURL); // Either use the account id specified or look it up based on the username // in the link. const userId: string = accountId ?? (await (async () => { // Extract username from URL. const parts = /@(\w+)$/.exec(url.pathname); if (!parts) { throw 'not a Mastodon user URL'; } const username = parts[1]; // Look up user ID from username. const lookupURL = Object.assign(new URL(url), { pathname: '/api/v1/accounts/lookup', search: `?acct=${username}` }); return (await (await fetch(lookupURL)).json())['id']; })()); // Fetch toots. const tootURL = Object.assign(new URL(url), { pathname: `/api/v1/accounts/${userId}/statuses`, search: `?limit=${limit ?? 5}&exclude_replies=${!!excludeReplies}` }); return await (await fetch(tootURL)).json(); } function loadToots() { getToots(account, maxToots ?? 5, excludeReplies === true, accountId).then((data) => { toots = data; loading = false; }); } </script> {#snippet avatar(toot)} <a class="flex flex-row gap-2" href={toot.account.url}> <img class="rounded-md" width="48px" height="48px" src={toot.account.avatar} alt="{toot.account.username} avatar" /> <div class="flex flex-col items-start"> <span class="h-6 font-bold hover:underline">{toot.account.display_name}</span> <span class="h-4 text-sm text-muted">@{toot.account.username}</span> </div> </a> {/snippet} {#snippet body(toot)} <div> <div class="[&>p>span>a]:hover:underline"> {@html sanitizeHtml(toot.content)} </div> {#each toot.media_attachments.filter((att) => att.type === 'image') as image} <a class="block aspect-16/9 w-full overflow-hidden rounded-md" href={image.url} target="_blank" rel="noopener noreferrer" > <img class="h-full w-full object-cover" src={image.preview_url} alt={image.description} /> </a> {/each} </div> {/snippet} <ol class="w-full"> {#if loading} {#each Array(maxToots ?? 5) as placeholder} <li class="flex flex-col gap-3 px-4 py-3"> <div class="flex flex-row justify-between"> <div class="flex flex-row gap-2"> <Skeleton class="h-12 w-12 rounded-md" /> <div class="flex flex-col items-start gap-1"> <Skeleton class="h-6 w-24"></Skeleton> <Skeleton class="h-4 w-20"></Skeleton> </div> </div> <Skeleton class="h-10 w-16" /> </div> <Skeleton class="h-36 w-full"></Skeleton> </li> {/each} {:else} {#each toots as toot} <li class="flex flex-col gap-3 px-4 py-3"> {#if toot.reblog} <div class="flex flex-row justify-between"> <div class="flex flex-col gap-1"> <a class="flex flex-row items-center gap-1" href={toot.account.url}> <DoubleArrowUp /> <img class="rounded-md" width="23px" height="23px" src={toot.account.avatar} alt="{toot.account.username} avatar" /> <span class="h-6 font-bold hover:underline">{toot.account.display_name}</span> </a> {@render avatar(toot.reblog)} </div> <a class="flex flex-col items-center text-sm text-muted hover:underline" href={toot.url} > <time datetime={toot.created_at}> {new Date(toot.created_at).toLocaleDateString()} </time> <time datetime={toot.created_at}> {new Date(toot.created_at).toLocaleTimeString()} </time> </a> </div> {@render body(toot.reblog)} {:else} <div class="flex flex-row justify-between"> {@render avatar(toot)} <a class="flex flex-col items-center text-sm text-muted hover:underline" href={toot.url} > <time datetime={toot.created_at}> {new Date(toot.created_at).toLocaleDateString()} </time> <time datetime={toot.created_at}> {new Date(toot.created_at).toLocaleTimeString()} </time> </a> </div> {@render body(toot)} {/if} </li> {/each} {/if} </ol>