152 lines
3.3 KiB
Svelte
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>
|