import { messagesWithParamsRoute } from "@app/routes.tsx"; import { ChannelChat } from "@components/PageComponents/Messages/ChannelChat.tsx"; import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx"; import { PageLayout } from "@components/PageLayout.tsx"; import { Sidebar } from "@components/Sidebar.tsx"; import { Avatar } from "@components/UI/Avatar.tsx"; import { Input } from "@components/UI/Input.tsx"; import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx"; import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; import { useToast } from "@core/hooks/useToast.ts"; import { MessageState, MessageType, useDevice, useMessages, useNodeDB, useSidebar, } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import { randId } from "@core/utils/randId.ts"; import { Protobuf, Types } from "@meshtastic/core"; import { getChannelName } from "@pages/Config/ChannelConfig.tsx"; import { useNavigate, useParams } from "@tanstack/react-router"; import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react"; import { useCallback, useDeferredValue, useEffect, useMemo, useState, } from "react"; import { useTranslation } from "react-i18next"; type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number }; function SelectMessageChat() { const { t } = useTranslation("messages"); return (
{t("selectChatPrompt.text", { ns: "messages" })}
); } export const MessagesPage = () => { const { channels, getUnreadCount, resetUnread, connection } = useDevice(); const { getNodes, getNode, getMyNode, hasNodeError } = useNodeDB(); const { getMessages, setMessageState } = useMessages(); const { type, chatId } = useParams({ from: messagesWithParamsRoute.id }); const navigate = useNavigate(); const { toast } = useToast(); const { isCollapsed } = useSidebar(); const [searchTerm, setSearchTerm] = useState(""); const { t } = useTranslation(["messages", "channels", "ui"]); const deferredSearch = useDeferredValue(searchTerm); const navigateToChat = useCallback( (type: MessageType, id: string) => { const typeParam = type === MessageType.Direct ? "direct" : "broadcast"; navigate({ to: `/messages/${typeParam}/${id}` }); }, [navigate], ); const chatType = type === "direct" ? MessageType.Direct : MessageType.Broadcast; const numericChatId = Number(chatId); const allChannels = Array.from(channels.values()); const filteredChannels = allChannels.filter( (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, ); useEffect(() => { if (!type && !chatId && filteredChannels.length > 0) { const defaultChannel = filteredChannels[0]; navigateToChat( MessageType.Broadcast, defaultChannel?.index.toString() ?? "0", ); } }, [type, chatId, filteredChannels, navigateToChat]); const currentChannel = channels.get(numericChatId); const otherNode = getNode(numericChatId); const isDirect = chatType === MessageType.Direct; const isBroadcast = chatType === MessageType.Broadcast; const filteredNodes = useCallback((): NodeInfoWithUnread[] => { const lowerCaseSearchTerm = deferredSearch.toLowerCase(); return getNodes((node: Protobuf.Mesh.NodeInfo) => { const longName = node.user?.longName?.toLowerCase() ?? ""; const shortName = node.user?.shortName?.toLowerCase() ?? ""; return ( longName.includes(lowerCaseSearchTerm) || shortName.includes(lowerCaseSearchTerm) ); }, true) .map((node: Protobuf.Mesh.NodeInfo) => ({ ...node, unreadCount: getUnreadCount(node.num) ?? 0, })) .sort((a: NodeInfoWithUnread, b: NodeInfoWithUnread) => { const diff = b.unreadCount - a.unreadCount; if (diff !== 0) { return diff; } return Number(b.isFavorite) - Number(a.isFavorite); }); }, [deferredSearch, getNodes, getUnreadCount]); const sendText = useCallback( async (message: string) => { const toValue = isDirect ? numericChatId : MessageType.Broadcast; const channelValue = isDirect ? Types.ChannelNumber.Primary : numericChatId; let messageId: number | undefined; try { messageId = await connection?.sendText( message, toValue, true, channelValue, ); if (messageId !== undefined) { if (chatType === MessageType.Broadcast) { setMessageState({ type: MessageType.Broadcast, channelId: channelValue, messageId, newState: MessageState.Ack, }); } else { setMessageState({ type: MessageType.Direct, nodeA: getMyNode().num, nodeB: numericChatId, messageId, newState: MessageState.Ack, }); } } else { console.warn("sendText completed but messageId is undefined"); } } catch (e: unknown) { console.error("Failed to send message:", e); const failedId = messageId ?? randId(); if (chatType === MessageType.Broadcast) { setMessageState({ type: MessageType.Broadcast, channelId: channelValue, messageId: failedId, newState: MessageState.Failed, }); } else { setMessageState({ type: MessageType.Direct, nodeA: getMyNode().num, nodeB: numericChatId, messageId: failedId, newState: MessageState.Failed, }); } } }, [numericChatId, chatType, connection, getMyNode, setMessageState, isDirect], ); const renderChatContent = () => { switch (chatType) { case MessageType.Broadcast: return ( ); case MessageType.Direct: return ( ); default: return ; } }; const leftSidebar = useMemo( () => ( {filteredChannels?.map((channel) => ( { navigateToChat(MessageType.Broadcast, channel.index.toString()); resetUnread(channel.index); }} > ))} ), [ filteredChannels, numericChatId, chatType, isCollapsed, getUnreadCount, navigateToChat, resetUnread, t, ], ); const rightSidebar = useMemo( () => (
{filteredNodes()?.map((node) => ( 0 ? node.unreadCount : undefined} active={ numericChatId === node.num && chatType === MessageType.Direct } onClick={() => { navigateToChat(MessageType.Direct, node.num.toString()); resetUnread(node.num); }} > ))}
), [ filteredNodes, searchTerm, numericChatId, chatType, navigateToChat, resetUnread, hasNodeError, t, ], ); return (
{renderChatContent()}
{isBroadcast || isDirect ? ( ) : (
{t("sendMessage.sendButton", { ns: "messages" })}
)}
); }; export default MessagesPage;