ttrpg-tools/src/components/journal/StreamMessage.tsx

99 lines
3.0 KiB
TypeScript
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.

/**
* StreamMessage — single message card in the stream
*/
import { Component, Show, createMemo } from "solid-js";
import type { StreamMessage as StreamMessageType } from "./registry";
import { getMessageType } from "./registry";
import {
revertLatest,
canRevert,
useJournalStream,
} from "../stores/journalStream";
export const StreamMessageCard: Component<{
message: StreamMessageType;
}> = (props) => {
const stream = useJournalStream();
const def = createMemo(() => getMessageType(props.message.type));
const rendered = createMemo(() =>
def()?.render?.(props.message.payload, props.message as any),
);
const time = createMemo(() => {
const d = new Date(props.message.timestamp);
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
});
const isMine = () => props.message.sender === stream.myName;
const latestSeq = () => stream.senderSeq[stream.myName] ?? 0;
const canRevertThis = () =>
isMine() && props.message.seq === latestSeq() && !props.message.reverted;
const senderClass = () =>
props.message.sender === "gm"
? "text-purple-600 font-semibold"
: "text-blue-600 font-semibold";
return (
<Show
when={!props.message.reverted}
fallback={<RevealedCard msg={props.message} />}
>
<div class="bg-white rounded-lg border border-gray-200 shadow-sm overflow-hidden">
{/* Header */}
<div class="flex items-center gap-1.5 px-2.5 py-1 bg-gray-50 border-b border-gray-100">
<span class={`text-xs ${senderClass()}`}>{props.message.sender}</span>
<span class="text-xs text-gray-400">{time()}</span>
<Show when={canRevertThis()}>
<button
onClick={() => revertLatest()}
class="text-xs text-gray-300 hover:text-red-500 ml-auto px-0.5 leading-none"
title="Revert this message"
>
×
</button>
</Show>
<span
class="text-xs text-gray-300 bg-gray-100 px-1 rounded"
classList={{ "ml-auto": !canRevertThis() }}
>
{props.message.type}
</span>
</div>
{/* Body */}
<div class="px-2.5 py-1.5 text-sm text-gray-800">
<Show
when={rendered()}
fallback={<JsonBody payload={props.message.payload} />}
>
{rendered()}
</Show>
</div>
</div>
</Show>
);
};
/** Fallback: render payload as formatted JSON */
const JsonBody: Component<{ payload: unknown }> = (props) => {
const text = () => JSON.stringify(props.payload, null, 2);
return (
<pre class="text-xs text-gray-600 whitespace-pre-wrap font-mono">
{text()}
</pre>
);
};
/** Reverted card: struck-through, grey */
const RevealedCard: Component<{ msg: StreamMessageType }> = (props) => {
return (
<div class="bg-gray-100 rounded-lg border border-gray-200 opacity-50 px-2.5 py-1.5">
<span class="text-xs text-gray-400 line-through">
{props.msg.sender} reverted
</span>
</div>
);
};