2026-07-06 15:06:19 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 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";
|
2026-07-06 17:26:37 +08:00
|
|
|
|
import {
|
|
|
|
|
|
revertLatest,
|
|
|
|
|
|
canRevert,
|
|
|
|
|
|
useJournalStream,
|
|
|
|
|
|
} from "../stores/journalStream";
|
2026-07-06 15:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
export const StreamMessageCard: Component<{
|
|
|
|
|
|
message: StreamMessageType;
|
|
|
|
|
|
}> = (props) => {
|
2026-07-06 17:26:37 +08:00
|
|
|
|
const stream = useJournalStream();
|
2026-07-06 15:06:19 +08:00
|
|
|
|
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" });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-06 17:26:37 +08:00
|
|
|
|
const isMine = () => props.message.sender === stream.myName;
|
|
|
|
|
|
const latestSeq = () => stream.senderSeq[stream.myName] ?? 0;
|
|
|
|
|
|
const canRevertThis = () =>
|
|
|
|
|
|
isMine() && props.message.seq === latestSeq() && !props.message.reverted;
|
|
|
|
|
|
|
2026-07-06 15:06:19 +08:00
|
|
|
|
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>
|
2026-07-06 17:26:37 +08:00
|
|
|
|
<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() }}
|
|
|
|
|
|
>
|
2026-07-06 15:06:19 +08:00
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|