diff --git a/src/app/messages/page.tsx b/src/app/messages/page.tsx index dfe1a6a..e5699d7 100644 --- a/src/app/messages/page.tsx +++ b/src/app/messages/page.tsx @@ -1,144 +1,22 @@ "use client"; -import { useEffect, useState, useMemo, useRef } from "react"; import { useConfig } from "@/components/ConfigContext"; -import { decryptMeshcoreGroupMessage } from "@/lib/meshcore_decrypt"; -import { getChannelIdFromKey } from "@/lib/meshcore"; -import ChatMessageItem, { ChatMessage } from "@/components/ChatMessageItem"; -import RefreshButton from "@/components/RefreshButton"; +import ChatBox from "@/components/ChatBox"; -// Messages page: displays all chat messages from all channels with infinite scrolling. If a message cannot be decrypted, shows a row explaining the reason. - -const PAGE_SIZE = 40; - -function formatLocalTime(utcString: string): string { - const utcDate = new Date(utcString + (utcString.endsWith('Z') ? '' : 'Z')); - return utcDate.toLocaleString(); -} +// Messages page: displays all chat messages from all channels using the ChatBox component with tabs export default function MessagesPage() { const { config } = useConfig(); - const meshcoreKeys = useMemo(() => [ - "izOH6cXN6mrJ5e26oRXNcg==", // Public key always included - ...(config?.meshcoreKeys?.map((k: any) => k.privateKey) || []) - ], [config]); - - const [messages, setMessages] = useState([]); - const [loading, setLoading] = useState(false); - const [hasMore, setHasMore] = useState(true); - const [lastBefore, setLastBefore] = useState(undefined); - const [autoRefreshing, setAutoRefreshing] = useState(false); - const autoRefreshTimeout = useRef(null); - - // Infinite scroll - useEffect(() => { - const onScroll = () => { - if ( - window.innerHeight + window.scrollY >= document.body.offsetHeight - 300 && - !loading && hasMore - ) { - fetchMessages(lastBefore); - } - }; - window.addEventListener("scroll", onScroll); - return () => window.removeEventListener("scroll", onScroll); - // eslint-disable-next-line - }, [loading, hasMore, lastBefore]); - - useEffect(() => { - setMessages([]); - setHasMore(true); - setLastBefore(undefined); - fetchMessages(undefined, true); - // eslint-disable-next-line - }, [meshcoreKeys.join(",")]); - - // Poll for new messages every 10s - useEffect(() => { - const interval = setInterval(async () => { - if (messages.length === 0) return; - const latest = messages[0]?.ingest_timestamp; - if (!latest) return; - try { - setAutoRefreshing(true); - let url = `/api/chat?limit=${PAGE_SIZE}&after=${encodeURIComponent(latest)}`; - const res = await fetch(url); - const data = await res.json(); - if (Array.isArray(data) && data.length > 0) { - // Only prepend messages that are not already present - const existingKeys = new Set(messages.map((m) => m.ingest_timestamp + (m.origins?.join(',') ?? ''))); - const newMessages = data.filter((msg: any) => !existingKeys.has(msg.ingest_timestamp + (msg.origins?.join(',') ?? ''))); - if (newMessages.length > 0) { - setMessages((prev) => [...newMessages, ...prev]); - } - } - } catch {} - // Show auto-refresh spinner for 1s - if (autoRefreshTimeout.current) clearTimeout(autoRefreshTimeout.current); - autoRefreshTimeout.current = setTimeout(() => setAutoRefreshing(false), 1000); - }, 10000); - return () => { - clearInterval(interval); - if (autoRefreshTimeout.current) clearTimeout(autoRefreshTimeout.current); - }; - }, [messages]); - - const fetchMessages = async (before?: string, replace = false) => { - setLoading(true); - try { - let url = `/api/chat?limit=${PAGE_SIZE}`; - if (before) url += `&before=${encodeURIComponent(before)}`; - // No channel_id: fetch all channels - const res = await fetch(url); - const data = await res.json(); - if (Array.isArray(data)) { - setMessages((prev) => replace ? data : [...prev, ...data]); - setHasMore(data.length === PAGE_SIZE); - if (data.length > 0) { - setLastBefore(data[data.length - 1].ingest_timestamp); - } - } else { - setHasMore(false); - } - } catch { - setHasMore(false); - } finally { - setLoading(false); - } - }; return ( -
- {/* Header row with title and refresh button */} +
+ {/* Header row with title */}

MeshCore Messages

- fetchMessages(undefined, true)} - loading={loading} - autoRefreshing={autoRefreshing} - title={autoRefreshing ? "Auto-refreshing..." : "Refresh messages"} - ariaLabel="Refresh messages" - small - />
- {/* Add keyframes for spin animation */} - -
- {messages.length === 0 && !loading && ( -
No chat messages found.
- )} - {messages.map((msg, i) => ( - - ))} - {loading && ( -
Loading...
- )} - {!hasMore && messages.length > 0 && ( -
No more messages.
- )} + + {/* ChatBox component with all messages tab enabled and expanded behavior */} +
+
); diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index 8840c8b..2f8e10e 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -19,24 +19,42 @@ function formatLocalTime(utcString: string): string { return utcDate.toLocaleString(); } -export default function ChatBox() { +interface ChatBoxProps { + showAllMessagesTab?: boolean; + className?: string; + expanded?: boolean; // New prop to control expanded behavior +} + +interface TabItem { + channelName: string; + privateKey: string; + isAllMessages?: boolean; +} + +export default function ChatBox({ showAllMessagesTab = false, className = "", expanded = false }: ChatBoxProps) { const { config } = useConfig(); - const meshcoreKeys = [ + const meshcoreKeys: TabItem[] = [ { channelName: "Public", privateKey: "izOH6cXN6mrJ5e26oRXNcg==" }, ...(config?.meshcoreKeys || []) ]; + + // Add "All Messages" tab if requested + const allTabs: TabItem[] = showAllMessagesTab + ? [{ channelName: "All Messages", privateKey: "", isAllMessages: true }, ...meshcoreKeys] + : meshcoreKeys; + const [selectedTab, setSelectedTab] = useState(0); - const [minimized, setMinimized] = useState(true); + const [minimized, setMinimized] = useState(!expanded); const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [lastBefore, setLastBefore] = useState(undefined); - const selectedKey = meshcoreKeys[selectedTab]; - const channelId = getChannelIdFromKey(selectedKey.privateKey).toUpperCase(); + const selectedKey = allTabs[selectedTab]; + const channelId = selectedKey.isAllMessages ? undefined : getChannelIdFromKey(selectedKey.privateKey).toUpperCase(); - // Only show tabs if more than one channel (public + at least one custom key) - const showTabs = meshcoreKeys.length > 1; + // Only show tabs if more than one channel (or if we have all messages tab) + const showTabs = allTabs.length > 1; useEffect(() => { if (!minimized) { @@ -45,13 +63,13 @@ export default function ChatBox() { setLastBefore(undefined); fetchMessages(undefined, true); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, [minimized, selectedTab]); const fetchMessages = async (before?: string, replace = false) => { setLoading(true); try { - let url = `/api/chat?limit=${PAGE_SIZE}&channel_id=${channelId}`; + let url = `/api/chat?limit=${PAGE_SIZE}`; + if (channelId) url += `&channel_id=${channelId}`; if (before) url += `&before=${encodeURIComponent(before)}`; const res = await fetch(url); const data = await res.json(); @@ -79,29 +97,34 @@ export default function ChatBox() { return (
-
+
MeshCore Chat - + {!expanded && ( + + )}
- {!minimized && ( + + {(!minimized || expanded) && ( <> {showTabs && ( -
- {meshcoreKeys.map((key, idx) => ( +
+ {allTabs.map((key, idx) => (