193 lines
5 KiB
Svelte
193 lines
5 KiB
Svelte
<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>
|