import * as React from "react"; import type { RawMessage } from "../values"; import Text from "@tensamin/markdown/text"; import { AlertTriangle, Check, Ellipse, Forward, Laugh, RefreshCw, Reply, } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import type { User } from "@tensamin/user/context"; import { Avatar, AvatarFallback, AvatarImage, Button, Card, cn, Separator, Skeleton, } from "@tensamin/ui"; import MessageContextMenu from "./messageContextMenu"; import Media from "./media"; import { useStorage } from "@tensamin/storage/context"; import { useMTP } from "@tensamin/mtp"; import Input from "@tensamin/markdown/input"; import { useChat } from "../context"; import { encryptChatText } from "@tensamin/crypto/chatSecret"; import { log, toast } from "@tensamin/shared/log"; function MessageComponent({ grouped, message, user, }: { grouped: boolean; message: RawMessage & { failed?: boolean; decryptionFailed?: boolean; }; user: User | null; }) { const actuallyFailed = (message.failed && message.MessageState === "awaiting") || message.decryptionFailed; // Fade-in const [hasFadedIn, setHasFadedIn] = useState(false); const opacityClass = !hasFadedIn ? "opacity-0" : message.MessageState === "awaiting" ? "opacity-50" : "opacity-100"; useEffect(() => { const timeout = window.setTimeout(() => { setHasFadedIn(true); }, 100); return () => { window.clearTimeout(timeout); }; }, []); // Message states const { load } = useStorage(); const { send } = useMTP(); const [ownId, setOwnId] = useState(0); useEffect(() => { load("user_id").then(setOwnId); }, [load]); const messageStateReadUpdate = useCallback(async () => { const readConfirmations = await load("settings.read_confirmations"); if (readConfirmations) { if (!user?.UserId) return; await send("MessageState", { ChatPartnerId: user?.UserId, SendTime: message.SendTime, MessageState: "read", }); } else { await send("MessageState", { ChatPartnerId: user?.UserId, SendTime: message.SendTime, MessageState: "received", }); } }, [load, message.SendTime, user?.UserId, send]); useEffect(() => { if (message.SenderId === ownId || ownId === 0) return; let cancelled = false; async function run() { const receiveConfirmations = await load("settings.receive_confirmations"); if (cancelled) return; switch (message.MessageState) { case "sent": if (receiveConfirmations) { messageStateReadUpdate(); } break; case "received": messageStateReadUpdate(); break; default: break; } } run(); return () => { cancelled = true; }; }, [ message.MessageState, load, messageStateReadUpdate, message.SenderId, ownId, ]); // Check if message is a url const [isValidURL, setIsValidURL] = useState(false); useEffect(() => { try { if (message.Content.split(" ").length > 1) throw new Error(); new URL(message.Content); setIsValidURL(true); } catch { setIsValidURL(false); } }, [message.Content]); // Message editing const { chatSecret, editMessageContent, userId } = useChat(); const [editing, setEditing] = useState(false); const [editDraft, setEditDraft] = useState(message.Content); useEffect(() => { if (!editing) { setEditDraft(message.Content); } }, [editing, message.Content]); const editMessage = useCallback( async (newContent: string) => { if (!chatSecret) return; const encryptedContent = await encryptChatText( chatSecret, newContent, ).catch((err) => { toast("error", "Failed to encrypt edit", String(err)); }); if (!encryptedContent) return; const previousContent = message.Content; editMessageContent(message.SendTime, newContent); try { const response = await send("MessageEdit", { ChatPartnerId: userId, Content: encryptedContent, SendTime: message.SendTime, }); if (response.type.startsWith("Error")) { throw new Error(response.type); } } catch (err) { editMessageContent( message.SendTime, previousContent, message.Edited ?? false, ); log(1, "chat", "red", "Failed to edit message", err); toast("error", "Failed to edit message", String(err)); } }, [ chatSecret, editMessageContent, userId, message.Content, message.Edited, message.SendTime, send, ], ); return (
{user && message.Content ? (
<> {grouped ? (

{new Date(message.SendTime).toLocaleString([], { hour: "2-digit", minute: "2-digit", })}

) : ( {user.Display.slice(0, 2).toUpperCase()} )}
{[0, 1, 2].map((item) => ( ))} {!grouped && (

{user.Display}

{new Date(message.SendTime).toLocaleString([], { hour: "2-digit", minute: "2-digit", })}

{message.MessageState === "read" ? ( ) : message.MessageState === "received" ? ( ) : message.MessageState === "sent" ? ( ) : message.MessageState === "sending" ? ( ) : null} {actuallyFailed && ( )}
)} {editing ? ( { editMessage(editDraft); setEditing(false); }} /> ) : isValidURL ? ( ) : ( )}
) : ( )}
); } export default React.memo(MessageComponent, (prev, next) => { return ( prev.message.SendTime === next.message.SendTime && prev.message.Content === next.message.Content && prev.message.SenderId === next.message.SenderId && prev.message.MessageState === next.message.MessageState && prev.message.failed === next.message.failed && prev.message.decryptionFailed === next.message.decryptionFailed && prev.grouped === next.grouped && prev.user === next.user ); });