117 lines
2.9 KiB
TypeScript
117 lines
2.9 KiB
TypeScript
|
|
import React, { useEffect, useRef } from "react";
|
||
|
|
import type { ChatMessage } from "../../../shared/types";
|
||
|
|
import type { ChatThread } from "./types";
|
||
|
|
import { useI18n } from "../i18n";
|
||
|
|
|
||
|
|
function formatTime(value: number) {
|
||
|
|
return new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ChatThread({
|
||
|
|
thread,
|
||
|
|
messages,
|
||
|
|
meId,
|
||
|
|
onSend,
|
||
|
|
readOnly,
|
||
|
|
nameById,
|
||
|
|
}: {
|
||
|
|
thread: ChatThread;
|
||
|
|
messages: ChatMessage[];
|
||
|
|
meId: string | null;
|
||
|
|
onSend?: (body: string) => void;
|
||
|
|
readOnly?: boolean;
|
||
|
|
nameById: Record<string, string>;
|
||
|
|
}) {
|
||
|
|
const { t } = useI18n();
|
||
|
|
const listRef = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (listRef.current) {
|
||
|
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||
|
|
}
|
||
|
|
}, [messages]);
|
||
|
|
|
||
|
|
const showSender = thread.kind === "group" || thread.kind === "global";
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="chat-thread">
|
||
|
|
<div className="chat-thread-body" ref={listRef}>
|
||
|
|
{messages.length === 0 && (
|
||
|
|
<div className="chat-empty">
|
||
|
|
<p>{t("chat.noMessages")}</p>
|
||
|
|
<span>{t("chat.startConversation")}</span>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{messages.map((message) => {
|
||
|
|
const isMe = message.fromId === meId;
|
||
|
|
return (
|
||
|
|
<div key={message.id} className={`chat-message-row ${isMe ? "me" : ""}`}>
|
||
|
|
<div className={`chat-bubble ${isMe ? "me" : ""}`}>
|
||
|
|
{showSender && !isMe && (
|
||
|
|
<span className="chat-sender">
|
||
|
|
{nameById[message.fromId] ?? t("common.player")}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
<p>{message.body}</p>
|
||
|
|
<span className="chat-time">{formatTime(message.createdAt)}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{!readOnly && (
|
||
|
|
<ChatComposer
|
||
|
|
placeholder={t("chat.messagePlaceholder")}
|
||
|
|
sendLabel={t("common.send")}
|
||
|
|
onSend={(body) => {
|
||
|
|
if (!body.trim()) return;
|
||
|
|
onSend?.(body);
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function ChatComposer({
|
||
|
|
onSend,
|
||
|
|
placeholder,
|
||
|
|
sendLabel,
|
||
|
|
}: {
|
||
|
|
onSend: (body: string) => void;
|
||
|
|
placeholder: string;
|
||
|
|
sendLabel: string;
|
||
|
|
}) {
|
||
|
|
const [value, setValue] = React.useState("");
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="chat-composer">
|
||
|
|
<input
|
||
|
|
value={value}
|
||
|
|
onChange={(event) => setValue(event.target.value)}
|
||
|
|
placeholder={placeholder}
|
||
|
|
onKeyDown={(event) => {
|
||
|
|
if (event.key === "Enter") {
|
||
|
|
event.preventDefault();
|
||
|
|
if (!value.trim()) return;
|
||
|
|
onSend(value);
|
||
|
|
setValue("");
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
<button
|
||
|
|
className="chat-send"
|
||
|
|
type="button"
|
||
|
|
onClick={() => {
|
||
|
|
if (!value.trim()) return;
|
||
|
|
onSend(value);
|
||
|
|
setValue("");
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{sendLabel}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|