Separate chat messages with match dividers

This commit is contained in:
2026-09-20 22:19:19 -04:00
parent 5c5e75526d
commit 50c81456c4
3 changed files with 118 additions and 20 deletions
+25 -11
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
import { parseGameChat, type ChatMessage } from '$lib/game-chat.js';
import { parseGameChatTimeline, type ChatEntry } from '$lib/game-chat.js';
import { pollVisible } from '$ui/poll.js';
import Button from './primitives/Button.svelte';
let { server, game }: { server: string; game: string | null } = $props();
let messages = $state<ChatMessage[]>([]);
let entries = $state<ChatEntry[]>([]);
let hasMessages = $derived(entries.some(entry => entry.type === 'message'));
let loaded = $state(false);
let loading = $state(false);
let error = $state('');
@@ -25,7 +26,7 @@
const data = await response.json();
if (!response.ok || !data.ok) throw new Error(data.reason ?? 'Could not refresh chat.');
if (request.signal.aborted) return;
messages = parseGameChat(data.lines ?? []);
entries = parseGameChatTimeline(data.lines ?? []);
missing = !!data.missing;
loaded = true;
error = '';
@@ -61,17 +62,26 @@
{#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 && !messages.length}
{: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>
{:else if messages.length}
{: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">
<ol>
{#each messages as message}
<li>
<span class="time" title="Elapsed game-log time">{message.time}</span>
<div><strong>{message.player}</strong><p>{message.message}</p></div>
</li>
{#each entries as entry}
{#if entry.type === 'boundary'}
<li class="match-divider" class:match-end={entry.event === 'end'}>
<span class="match-label">
{entry.event === 'start' ? 'Match started' : entry.event === 'end' ? 'Match ended' : 'New match · timer reset'}
{#if entry.time}<span class="time" title="Elapsed game-log time">{entry.time}</span>{/if}
</span>
</li>
{:else}
<li class="message">
<span class="time" title="Elapsed game-log time">{entry.message.time}</span>
<div><strong>{entry.message.player}</strong><p>{entry.message.message}</p></div>
</li>
{/if}
{/each}
</ol>
</div>
@@ -85,7 +95,11 @@
.chat-head p, .note, .empty { color: var(--text-secondary); font-size: var(--text-sm); }
.messages { max-height: 60vh; overflow: auto; margin-top: 16px; }
ol { list-style: none; padding: 0; margin: 0; }
li { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-subtle); }
.message { display: grid; grid-template-columns: 72px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--border-subtle); }
.match-divider { display: flex; align-items: center; gap: 12px; padding: 20px 0; color: var(--text-primary); font-size: var(--text-sm); }
.match-divider::before, .match-divider::after { content: ''; height: 1px; flex: 1; background: var(--border-subtle); }
.match-label { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 8px 12px; padding: 6px 12px; border: 1px solid var(--border-subtle); border-radius: 999px; font-weight: 600; }
.match-end { color: var(--text-secondary); }
.time { font-family: var(--font-mono); color: var(--text-tertiary); font-size: var(--text-sm); }
strong { color: var(--text-primary); overflow-wrap: anywhere; }
li p { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; }