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 { useChatAsLegacyMessages } from "@core/hooks/useChatAsLegacyMessages.ts"; import { useNodesAsProto } from "@core/hooks/useNodesAsProto.ts"; import { useToast } from "@core/hooks/useToast.ts"; import { MessageType, useSidebar } from "@core/stores"; import { cn } from "@core/utils/cn.ts"; import { Protobuf, Types } from "@meshtastic/sdk"; import { useActiveClient, useChannels, useNodeErrors, useUnreadByKey, } from "@meshtastic/sdk-react"; 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"; import { getChannelName } from "../components/PageComponents/Channels/Channels.tsx"; type NodeInfoWithUnread = Protobuf.Mesh.NodeInfo & { unreadCount: number }; function SelectMessageChat() { const { t } = useTranslation("messages"); return (
{t("selectChatPrompt.text", { ns: "messages" })}
); } export const MessagesPage = () => { const channels = useChannels(); const unreadByKey = useUnreadByKey(); const meshClient = useActiveClient(); const directUnread = (peer: number): number => unreadByKey.get(`direct:${peer}`) ?? 0; const channelUnread = (idx: number): number => unreadByKey.get(`channel:${idx}`) ?? 0; const markChannelRead = useCallback( (idx: number) => meshClient?.chat.unread.markRead({ kind: "channel", channel: idx as Types.ChannelNumber, }), [meshClient], ); const markDirectRead = useCallback( (peer: number) => meshClient?.chat.unread.markRead({ kind: "direct", peer }), [meshClient], ); const allNodes = useNodesAsProto(); const getNode = (n: number) => allNodes.find((node) => node.num === n); const errors = useNodeErrors(); const errorSet = useMemo(() => new Set(errors.map((e) => e.node)), [errors]); const hasNodeError = useCallback( (num: number) => errorSet.has(num), [errorSet], ); 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 filteredChannels = useMemo( () => channels.filter( (ch) => ch.role !== Protobuf.Channel.Channel_Role.DISABLED, ), [channels], ); 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.find((ch) => ch.index === numericChatId); const otherNode = getNode(numericChatId); const isDirect = chatType === MessageType.Direct; const isBroadcast = chatType === MessageType.Broadcast; const filteredNodes = useCallback((): NodeInfoWithUnread[] => { const lowerCaseSearchTerm = deferredSearch.toLowerCase(); return allNodes .filter((node: Protobuf.Mesh.NodeInfo) => { const longName = node.user?.longName?.toLowerCase() ?? ""; const shortName = node.user?.shortName?.toLowerCase() ?? ""; return ( longName.includes(lowerCaseSearchTerm) || shortName.includes(lowerCaseSearchTerm) ); }) .map((node: Protobuf.Mesh.NodeInfo) => ({ ...node, unreadCount: directUnread(node.num), })) .sort((a: NodeInfoWithUnread, b: NodeInfoWithUnread) => { const diff = b.unreadCount - a.unreadCount; if (diff !== 0) { return diff; } return Number(b.isFavorite) - Number(a.isFavorite); }); }, [deferredSearch, allNodes, directUnread]); const sendText = useCallback( async (message: string) => { if (!meshClient) { console.warn("[MessagesPage] no active mesh client; send dropped"); return; } const destination: Types.Destination = isDirect ? numericChatId : "broadcast"; const channel = isDirect ? Types.ChannelNumber.Primary : numericChatId; const result = await meshClient.chat.send({ text: message, destination, channel, }); if (result.status === "error") { console.error("Failed to send message:", result.error); } // Outbound state (Ack / Failed) is updated by the SDK chat slice when the // routing packet for this message id arrives. }, [meshClient, numericChatId, isDirect], ); const broadcastMessages = useChatAsLegacyMessages({ type: MessageType.Broadcast, channelId: numericChatId, }); const directMessages = useChatAsLegacyMessages({ type: MessageType.Direct, peer: numericChatId, }); 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()); markChannelRead(channel.index); }} > ))} ), [ filteredChannels, numericChatId, chatType, isCollapsed, channelUnread, navigateToChat, markChannelRead, t, ], ); const rightSidebar = (
{filteredNodes()?.map((node) => ( 0 ? node.unreadCount : undefined} active={ numericChatId === node.num && chatType === MessageType.Direct } onClick={() => { navigateToChat(MessageType.Direct, node.num.toString()); markDirectRead(node.num); }} > ))}
); return (
{renderChatContent()}
{isBroadcast || isDirect ? ( ) : (
{t("sendMessage.sendButton", { ns: "messages" })}
)}
); }; export default MessagesPage;