169 lines
5.7 KiB
Svelte
169 lines
5.7 KiB
Svelte
<script lang="ts">
|
||
import type { ContextualChatEntry } from '$lib/game-chat.js';
|
||
import { timestampISO } from '$lib/time-zone.js';
|
||
import { serverIdentity } from '$ui/server-labels.js';
|
||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||
import Badge from '../primitives/Badge.svelte';
|
||
|
||
let {
|
||
entries,
|
||
selectedMessageIds = [],
|
||
highlightedMessageIds = [],
|
||
matchId,
|
||
server,
|
||
startedAt
|
||
}: {
|
||
entries: ContextualChatEntry[];
|
||
selectedMessageIds?: string[];
|
||
highlightedMessageIds?: string[];
|
||
matchId?: number | null;
|
||
server?: string | null;
|
||
startedAt?: string | null;
|
||
} = $props();
|
||
const messages = $derived(entries.filter((entry) => entry.type === 'message'));
|
||
const context = $derived.by(() => {
|
||
const times = messages
|
||
.map((entry) => timestampISO(entry.context.realTime))
|
||
.filter((time): time is string => !!time)
|
||
.sort();
|
||
const dates = [...new Set(times.map((time) => $timeFormat.date(time)))];
|
||
const rounds = messages
|
||
.map((entry) => entry.context.round)
|
||
.filter((round): round is number => Number.isInteger(round));
|
||
const firstRound = Math.min(...rounds);
|
||
const lastRound = Math.max(...rounds);
|
||
return [
|
||
...(server ? [serverIdentity(server).map] : []),
|
||
...(matchId ? [`Match #${matchId}`] : []),
|
||
dates.length
|
||
? dates.length === 1
|
||
? dates[0]
|
||
: `${dates[0]} – ${dates[dates.length - 1]}`
|
||
: timestampISO(startedAt)
|
||
? `Match started ${$timeFormat.date(startedAt)}`
|
||
: 'Date not recorded',
|
||
rounds.length
|
||
? `${firstRound === lastRound ? `Round ${firstRound}` : `Rounds ${firstRound}–${lastRound}`}${rounds.length < messages.length ? ' (some unknown)' : ''}`
|
||
: 'Round not recorded',
|
||
'Times are estimates.'
|
||
].join(' · ');
|
||
});
|
||
</script>
|
||
|
||
<div class="excerpt-context">
|
||
<p>{context}</p>
|
||
<p>
|
||
Highlighted lines are subject messages. Messages between separate selections may be omitted.
|
||
</p>
|
||
</div>
|
||
<ol class="excerpt" aria-label="Preserved conversation excerpt">
|
||
{#each messages as entry}
|
||
{@const highlighted = !!entry.messageId && highlightedMessageIds.includes(entry.messageId)}
|
||
{@const selected = !!entry.messageId && selectedMessageIds.includes(entry.messageId)}
|
||
{@const realTime = timestampISO(entry.context.realTime)}
|
||
<li class:highlighted>
|
||
<span class="message-time">
|
||
{#if realTime}<time datetime={realTime} title={$timeFormat.dateTime(realTime)}
|
||
>{$timeFormat.clockTime(realTime)}</time
|
||
>{:else}<span title="Message time unknown">Unknown time</span>{/if}
|
||
</span>
|
||
<strong class="author">{entry.message.player}</strong>
|
||
<p class="message">
|
||
<span class="sr-only"
|
||
>{highlighted
|
||
? 'Subject message: '
|
||
: selected
|
||
? 'Selected context: '
|
||
: 'Surrounding context: '}</span
|
||
>{#if entry.message.channel === 'team'}<Badge>Team chat</Badge>{' '}{/if}{entry
|
||
.message.message}
|
||
</p>
|
||
</li>
|
||
{/each}
|
||
</ol>
|
||
<details class="message-details">
|
||
<summary>Message details</summary>
|
||
<ol aria-label="Preserved message details">
|
||
{#each messages as entry}
|
||
<li>
|
||
<strong>{entry.message.player}</strong>
|
||
<span>GUID <code>{entry.message.guid}</code></span>
|
||
<span>Original log time · {entry.message.time}</span>
|
||
{#if Number.isInteger(entry.context.round)}<span>Round {entry.context.round}</span
|
||
>{/if}
|
||
{#if entry.messageId}<span>Message identifier <code>{entry.messageId}</code></span
|
||
>{/if}
|
||
</li>
|
||
{/each}
|
||
</ol>
|
||
</details>
|
||
|
||
<style>
|
||
.excerpt {
|
||
display: grid;
|
||
gap: var(--space-2);
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
.excerpt > li {
|
||
display: grid;
|
||
grid-template-columns: 7.5em minmax(6em, 1fr) minmax(0, 3fr);
|
||
align-items: baseline;
|
||
gap: var(--space-3);
|
||
padding: var(--space-2) var(--space-3);
|
||
border-left: 3px solid transparent;
|
||
background: var(--surface-sunken);
|
||
min-width: 0;
|
||
font-size: var(--text-sm);
|
||
line-height: var(--leading-body);
|
||
}
|
||
.excerpt > li.highlighted {
|
||
border-left-color: var(--state-warning);
|
||
background: var(--surface-raised);
|
||
}
|
||
.author,
|
||
.message {
|
||
min-width: 0;
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.message {
|
||
white-space: pre-wrap;
|
||
}
|
||
.message-time {
|
||
color: var(--text-secondary);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
p {
|
||
margin: 0;
|
||
}
|
||
.excerpt-context,
|
||
.message-details {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-secondary);
|
||
line-height: var(--leading-body);
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.message-details ol {
|
||
padding-left: var(--space-5);
|
||
}
|
||
.message-details li {
|
||
padding: var(--space-2) 0;
|
||
}
|
||
.message-details li > span {
|
||
display: block;
|
||
}
|
||
summary {
|
||
cursor: pointer;
|
||
}
|
||
@media (max-width: 640px) {
|
||
.excerpt > li {
|
||
grid-template-columns: 7.5em minmax(0, 1fr);
|
||
gap: var(--space-1) var(--space-3);
|
||
}
|
||
.message {
|
||
grid-column: 1 / -1;
|
||
}
|
||
}
|
||
</style>
|