Files
beavergram/frontend/src/lib/components/feed/FeedBulk.svelte
T

152 lines
3.3 KiB
Svelte

<script lang="ts">
import { onMount } from "svelte";
import { ripple } from "#lib/actions/ripple.js";
import { listFeedMessages } from "#lib/api/endpoints.js";
import type { FeedItem, MessageView } from "#lib/api/types.js";
import FeedText from "#lib/components/feed/FeedText.svelte";
import Spinner from "#lib/components/ui/Spinner.svelte";
import { formatDayTime, formatFull } from "#lib/format/datetime.js";
import { t } from "#lib/i18n/locale.svelte.js";
interface Props {
item: FeedItem;
onopen: (messageId: number) => void;
senderOf: (message: MessageView) => string;
}
let { item, onopen, senderOf }: Props = $props();
const PAGE = 50;
let messages = $state<MessageView[]>([]);
let cursor = $state<string | null>(null);
let loading = $state(false);
let failed = $state(false);
async function load(next: string | null) {
loading = true;
failed = false;
try {
const page = await listFeedMessages(item.id, {
cursor: next ?? undefined,
limit: PAGE,
});
messages = [...messages, ...page.messages];
cursor = page.next_cursor;
} catch {
failed = true;
} finally {
loading = false;
}
}
onMount(() => {
load(null);
});
</script>
<div class="bulk">
{#each messages as message (message.message_id)}
<button
class="member"
onclick={() => onopen(message.message_id)}
type="button"
use:ripple
>
<FeedText deleted {message} prefix={senderOf(message)} />
<time
class="date"
datetime={message.date}
title={formatFull(message.date)}
>
{formatDayTime(message.date)}
</time>
</button>
{/each}
{#if loading}
<div class="state"><Spinner /></div>
{:else if failed}
<button class="more" onclick={() => load(cursor)} type="button">
{t("feed.retry")}
</button>
{:else if cursor !== null}
<button class="more" onclick={() => load(cursor)} type="button">
{t("feed.more")}
</button>
{/if}
</div>
<style lang="scss">
.bulk {
display: flex;
flex-direction: column;
gap: 0.125rem;
margin: 0 0 0.375rem 4.125rem;
padding-inline-start: 0.625rem;
border-inline-start: 2px solid var(--color-borders);
}
.member {
cursor: pointer;
position: relative;
overflow: hidden;
display: flex;
gap: 0.5rem;
align-items: baseline;
width: 100%;
padding: 0.375rem 0.5rem;
border: 0;
border-radius: var(--border-radius-default-small);
font: inherit;
text-align: start;
color: var(--color-text);
background: none;
--ripple-color: var(--color-interactive-element-hover);
:global(.feed-text) {
flex: 1;
min-width: 0;
font-size: 0.875rem;
}
@media (hover: hover) {
&:hover {
background-color: var(--color-chat-hover);
}
}
}
.date {
flex-shrink: 0;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
color: var(--color-text-secondary);
}
.state {
display: flex;
justify-content: center;
padding: 0.5rem 0;
}
.more {
cursor: pointer;
margin-top: 0.25rem;
padding: 0.5rem;
border: 0;
border-radius: 0.5rem;
font: inherit;
font-size: 0.875rem;
color: var(--color-primary);
background-color: var(--color-background-secondary);
}
</style>