Files
electric-cherry/src/lib/ui/components/moderation/EvidenceMessages.svelte
T
2026-10-18 16:20:29 -04:00

169 lines
5.7 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>