import { Tooltip, TooltipArrow, TooltipContent, TooltipProvider, TooltipTrigger, } from "@components/UI/Tooltip.tsx"; import { useDeviceStore } from "@core/stores/deviceStore.ts"; import { cn } from "@core/utils/cn.ts"; import { Avatar } from "@components/UI/Avatar.tsx"; import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react"; import type { LucideIcon } from "lucide-react"; import { ReactNode, useMemo } from "react"; import { Message, MessageState, useMessageStore } from "@core/stores/messageStore.ts"; import { Protobuf } from "@meshtastic/js"; interface MessageProps { lastMsgSameUser: boolean; message: Message; } interface MessageStatus { state: MessageState; displayText: string; icon: LucideIcon; } const MESSAGE_STATUS: Record = { [MessageState.Ack]: { state: MessageState.Ack, displayText: "Message delivered", icon: CheckCircle2 }, [MessageState.Waiting]: { state: MessageState.Waiting, displayText: "Waiting for delivery", icon: CircleEllipsis }, [MessageState.Failed]: { state: MessageState.Failed, displayText: "Delivery failed", icon: AlertCircle }, }; const getMessageStatus = (state: MessageState): MessageStatus => MESSAGE_STATUS[state] ?? { state: MessageState.Failed, displayText: "Unknown state", icon: AlertCircle }; const StatusTooltip = ({ status, children }: { status: MessageStatus; children: ReactNode }) => ( {children} {status.displayText} ); const StatusIcon = ({ status, className, ...otherProps }: { status: MessageStatus; className?: string }) => { const isFailed = status.state === MessageState.Failed; const iconClass = cn("w-4 h-4 shrink-0", className); const Icon = status.icon; return ( ); }; const getMessageTextStyles = (status: MessageState, isDeviceUser: boolean) => { const isFailed = status === MessageState.Failed; return cn( "break-words overflow-hidden whitespace-pre-wrap flex items-center gap-1.5", isFailed && (isDeviceUser ? "text-red-500" : "text-red-600 dark:text-red-500") ); }; const TimeDisplay = ({ date, className }: { date: number; className?: string }) => { const _date = new Date(date); const locale = 'en-US'; // TODO: this should be dynamic based on user settings return (
{_date?.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12: true })} {/* TODO: Conditionally show date for older messages? */}
); }; export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => { const myNodeNum = useMessageStore((state) => state.nodeNum); const { getDevices } = useDeviceStore(); const isDeviceUser = message.from === myNodeNum; const messageUser: Protobuf.Mesh.NodeInfo | null = useMemo(() => { if (message?.from === null || message?.from === undefined) return null; for (const device of getDevices()) { if (device.nodes.has(message.from)) { return device.nodes.get(message.from) ?? null; } } return null; }, [getDevices, message.from]); const fallbackName = `${message.from}`; const longName = messageUser?.user?.longName; const shortName = messageUser?.user?.shortName ?? fallbackName.slice(0, 2).toUpperCase(); const displayName = isDeviceUser ? "You" : (longName || fallbackName); const messageContainerClass = cn( "flex flex-col w-full px-4 justify-start", !lastMsgSameUser ? "pt-3" : "pt-0.5" ); const alignmentClass = cn( "flex flex-col flex-wrap w-full", isDeviceUser ? "items-end" : "items-start" ); const bubbleBaseStyle = "flex flex-col max-w-[75%] rounded-lg px-3 py-1.5 text-sm shadow-md"; const sentBubbleStyle = "bg-gradient-to-br from-blue-600 to-blue-700 dark:from-blue-500 dark:to-blue-600 text-white"; const receivedBubbleStyle = "bg-slate-200 dark:bg-slate-500 text-slate-900 dark:text-white"; const timeStatusColor = isDeviceUser ? "text-blue-100 dark:text-blue-200" : "text-slate-500 dark:text-slate-300"; const messageStatus = getMessageStatus(message.state); return (
{/* Show only if not consecutive message AND not sent by self */} {!lastMsgSameUser && (
{displayName}
)}
{message.message || Empty message} {isDeviceUser && }
); };