import clsx from "clsx"; import { sub } from "date-fns"; import { SendHorizontal } from "lucide-react"; import { QRCodeSVG } from "qrcode.react"; import * as React from "react"; import { Button, ListBox, ListBoxItem, ListLayout, Virtualizer, } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { Avatar } from "../../../components/Avatar"; import { SendouButton } from "../../../components/elements/Button"; import { SubmitButton } from "../../../components/SubmitButton"; import { useDateTimeFormat } from "../../../hooks/intl/useDateTimeFormat"; import { findRoomLinks, MESSAGE_MAX_LENGTH } from "../chat-constants"; import { useChatAutoScroll } from "../chat-hooks"; import type { ChatMessage, ChatProps, ChatUser } from "../chat-types"; import styles from "./Chat.module.css"; const MESSAGE_GAP = 8; const ESTIMATED_MESSAGE_HEIGHT = 44; const VIRTUALIZER_LAYOUT_OPTIONS = { gap: MESSAGE_GAP, estimatedRowSize: ESTIMATED_MESSAGE_HEIGHT, }; export interface ChatAdapter { messages: ChatMessage[]; send: (contents: string) => void; currentRoom: string | undefined; setCurrentRoom: (room: string) => void; readyState: "CONNECTING" | "CONNECTED" | "CLOSED"; unseenMessages: Map; } export function Chat({ users, rooms, className, messagesContainerClassName, hidden = false, chat, onMount, onUnmount, disabled, missingUserName, }: Omit & { chat: ChatAdapter; }) { const { t } = useTranslation(["common"]); const messagesContainerRef = React.useRef(null); const inputRef = React.useRef(null); const { send, messages, currentRoom, setCurrentRoom, readyState, unseenMessages, } = chat; const handleSubmit = React.useCallback( (e: React.FormEvent) => { e.preventDefault(); // can't send empty messages if (inputRef.current!.value.trim().length === 0) { return; } send(inputRef.current!.value); inputRef.current!.value = ""; }, [send], ); const { unseenMessagesInTheRoom, scrollToBottom, resetScroller } = useChatAutoScroll(messages, messagesContainerRef); React.useEffect(() => { onMount?.(); return () => { onUnmount?.(); }; }, [onMount, onUnmount]); const sendingMessagesDisabled = disabled || readyState !== "CONNECTED"; const systemMessageText = (msg: ChatMessage) => { const name = () => { if (!msg.context) return ""; return msg.context.name; }; switch (msg.type) { case "SCORE_REPORTED": { return t("common:chat.systemMsg.scoreReported", { name: name() }); } case "SCORE_CONFIRMED": { return t("common:chat.systemMsg.scoreConfirmed", { name: name() }); } case "CANCEL_REPORTED": { return t("common:chat.systemMsg.cancelReported", { name: name() }); } case "CANCEL_CONFIRMED": { return t("common:chat.systemMsg.cancelConfirmed", { name: name() }); } case "CANCEL_REFUSED": { return t("common:chat.systemMsg.cancelRefused", { name: name() }); } case "USER_LEFT": { return t("common:chat.systemMsg.userLeft", { name: name() }); } case "MAP_REPLAYED": { return t("common:chat.systemMsg.mapReplayed", { name: name() }); } case "MAP_PICKED": { return t("common:chat.systemMsg.mapPicked", { name: name() }); } default: { return null; } } }; const renderableMessages = messages.filter((msg) => { if (systemMessageText(msg)) return true; const user = msg.userId ? users[msg.userId] : null; return Boolean(user) || Boolean(missingUserName); }); return ( ); } function Message({ user, message, missingUserName, }: { user?: ChatUser | null; message: ChatMessage; missingUserName?: string; }) { return ( {user ? (
{user.title ? ( {user.title} ) : null}
) : null}
{user?.username ?? missingUserName}
{user?.pronouns ? ( {user.pronouns.subject}/{user.pronouns.object} ) : null} {!message.pending ? ( ) : null}
{message.contents ? ( ) : null}
); } function SystemMessage({ message, text, }: { message: ChatMessage; text: string; }) { return (
{text}
); } function MessageContents({ text }: { text: string }) { const matches = findRoomLinks(text); if (matches.length === 0) return <>{text}; const parts: React.ReactNode[] = []; let lastIndex = 0; for (const [i, match] of matches.entries()) { if (match.index > lastIndex) { parts.push(text.slice(lastIndex, match.index)); } parts.push( {match.url} , ); lastIndex = match.index + match.url.length; } if (lastIndex < text.length) { parts.push(text.slice(lastIndex)); } return <>{parts}; } function MessageTimestamp({ timestamp }: { timestamp: number }) { const { formatter: dateTimeFormatter } = useDateTimeFormat({ day: "numeric", month: "numeric", hour: "numeric", minute: "numeric", }); const { formatter: timeFormatter } = useDateTimeFormat({ hour: "numeric", minute: "numeric", }); const moreThanDayAgo = sub(new Date(), { days: 1 }) > new Date(timestamp); return ( ); }