Scope live chat to the current match and save match history

This commit is contained in:
2026-10-07 00:40:20 -04:00
parent 8fcabe27fc
commit 2b8e102b86
14 changed files with 266 additions and 30 deletions
+18 -12
View File
@@ -7,11 +7,13 @@
import { timeFormat } from '$ui/stores/time-zone.js';
import Button from './primitives/Button.svelte';
let { server }: { server: string } = $props();
let entries = $state<ContextualChatEntry[]>([]);
let { server, history }: { server?: string; history?: ContextualChatEntry[] } = $props();
let liveEntries = $state<ContextualChatEntry[]>([]);
const entries = $derived(history ?? liveEntries);
let timeline = $derived(groupChatTimeline(entries));
let hasMessages = $derived(entries.some(entry => entry.type === 'message'));
let loaded = $state(false);
let liveLoaded = $state(false);
const loaded = $derived(history !== undefined || liveLoaded);
let loading = $state(false);
let manualRefreshing = $state(false);
let error = $state('');
@@ -22,6 +24,7 @@
let controller: AbortController | null = null;
async function refresh(manual = false) {
if (!server) return;
if (loading) {
if (manual) manualRefreshing = true;
return;
@@ -36,9 +39,9 @@
if (!response.ok || !data?.ok) throw new Error('Could not refresh chat. Try again shortly.');
if (request.signal.aborted) return;
if (paused && !manual) return;
entries = data.entries ?? [];
liveEntries = data.entries ?? [];
missing = !!data.missing;
loaded = true;
liveLoaded = true;
error = '';
await tick();
if (follow && body) body.scrollTop = body.scrollHeight;
@@ -56,6 +59,7 @@
if (body) body.scrollTop = body.scrollHeight;
}
onMount(() => {
if (!server) return;
const stop = pollVisible(() => paused ? Promise.resolve() : refresh(), 5000);
return () => { stop(); controller?.abort(); };
});
@@ -89,18 +93,20 @@
<section aria-label="Player chat">
<div class="chat-head">
<div class="chat-title"><h2>Chat</h2><span class="update-cadence">{paused ? 'Paused' : 'Live · 5s'}</span></div>
<div class="actions">
{#if hasMessages}<Button size="sm" variant="ghost" onclick={() => paused = !paused}>{paused ? 'Resume' : 'Pause'}</Button>{/if}
{#if !follow}<Button size="sm" variant="ghost" onclick={latest}>Latest messages</Button>{/if}
<Button size="sm" onclick={() => refresh(true)} loading={manualRefreshing} ariaLabel={manualRefreshing ? 'Refreshing chat' : 'Refresh chat'}>Refresh</Button>
</div>
<div class="chat-title"><h2>Chat</h2><span class="update-cadence">{server ? paused ? 'Paused' : 'Live · 5s' : 'Match history'}</span></div>
{#if server}
<div class="actions">
{#if hasMessages}<Button size="sm" variant="ghost" onclick={() => paused = !paused}>{paused ? 'Resume' : 'Pause'}</Button>{/if}
{#if !follow}<Button size="sm" variant="ghost" onclick={latest}>Latest messages</Button>{/if}
<Button size="sm" onclick={() => refresh(true)} loading={manualRefreshing} ariaLabel={manualRefreshing ? 'Refreshing chat' : 'Refresh chat'}>Refresh</Button>
</div>
{/if}
</div>
{#if error}<p class="ui-error" role="alert">{error} {loaded ? 'Showing the last loaded messages.' : ''}</p>{/if}
{#if !loaded && !error}
<p class="empty" role="status">Loading chat…</p>
{:else if loaded && !hasMessages}
<p class="empty">{missing ? 'Waiting for the game log. Player messages will appear here.' : 'No player messages in the recent game log.'}</p>
<p class="empty">{history !== undefined ? 'No chat messages were saved for this match.' : missing ? 'Waiting for the game log. Player messages will appear here.' : 'No player messages in this match yet.'}</p>
{:else if hasMessages}
<!-- svelte-ignore a11y_no_noninteractive_tabindex (The scrollable chat region must support keyboard scrolling.) -->
<div class="messages" bind:this={body} onscroll={onScroll} tabindex="0" role="region" aria-label="Chat messages">