diff --git a/web/src/components/dashboard/ChannelDetail.tsx b/web/src/components/dashboard/ChannelDetail.tsx new file mode 100644 index 0000000..61b097d --- /dev/null +++ b/web/src/components/dashboard/ChannelDetail.tsx @@ -0,0 +1,144 @@ +import React from "react"; +import { useNavigate } from "@tanstack/react-router"; +import { useAppSelector } from "../../hooks"; +import { Separator } from "../Separator"; +import { MessageBubble } from "../messages"; +import { Section } from "../ui/Section"; +import { ArrowLeft, MessageSquare, Users, Wifi } from "lucide-react"; + +interface ChannelDetailProps { + channelId: string; +} + +// Generate a deterministic color based on channel ID +const getChannelColor = (channelId: string) => { + const colors = [ + "bg-green-500", + "bg-blue-500", + "bg-amber-500", + "bg-purple-500", + "bg-pink-500", + "bg-indigo-500", + "bg-red-500", + "bg-teal-500", + ]; + + // Use a hash of the channel ID to pick a consistent color + const hash = channelId + .split("") + .reduce((acc, char) => acc + char.charCodeAt(0), 0); + return colors[hash % colors.length]; +}; + +export const ChannelDetail: React.FC = ({ channelId }) => { + const navigate = useNavigate(); + const { channels, messages, nodes } = useAppSelector( + (state) => state.aggregator + ); + + const channel = channels[channelId]; + // Create the channel key in the same format used by the aggregator + const channelKey = `channel_${channelId}`; + + // Get channel messages and sort by timestamp (oldest to newest) + // This creates a shallow copy of the message array to sort it without modifying the original + const channelMessages = messages[channelKey] + ? [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp) + : []; + + const handleBack = () => { + navigate({ to: "/channels" }); + }; + + if (!channel) { + return ( +
+
+ +

Channel Not Found

+
+

The channel {channelId} was not found or has not been seen yet.

+
+ ); + } + + // Get the channel color based on its ID + const channelColor = getChannelColor(channelId); + + // Determine if the channel is active (has messages in the last 10 minutes) + const isActive = channelMessages.length > 0 && + (Math.floor(Date.now() / 1000) - channelMessages[channelMessages.length - 1].timestamp) < 600; + + return ( +
+ {/* Header with back button and channel info */} +
+ +
+ +
+
+

+ {channelId} +

+
+ + {isActive ? "Active" : "Inactive"} +
+
+
+
+ + {channel.nodes.length} +
+
+ + {channel.textMessageCount} +
+
+
+ + + + {/* Message List - Chat style without title */} +
+ {channelMessages.length === 0 ? ( +
+

No messages in this channel yet.

+
+ ) : ( +
+ {channelMessages.map((message) => { + const nodeName = + nodes[message.from]?.shortName || + nodes[message.from]?.longName; + + return ( + + ); + })} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/web/src/components/dashboard/NodeDetail.tsx b/web/src/components/dashboard/NodeDetail.tsx index 1dc088b..40fbe4d 100644 --- a/web/src/components/dashboard/NodeDetail.tsx +++ b/web/src/components/dashboard/NodeDetail.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from "react"; import { useNavigate, Link } from "@tanstack/react-router"; import { useAppSelector, useAppDispatch } from "../../hooks"; import { selectNode } from "../../store/slices/aggregatorSlice"; -import { +import { ArrowLeft, Radio, Cpu, @@ -17,7 +17,8 @@ import { Earth, TableConfig, Save, - MessageSquare + MessageSquare, + Thermometer, } from "lucide-react"; import { Separator } from "../Separator"; import { KeyValuePair } from "../ui/KeyValuePair"; @@ -31,7 +32,11 @@ import { NodePacketList } from "./NodePacketList"; import { LowBatteryWarning } from "./LowBatteryWarning"; import { UtilizationMetrics } from "./UtilizationMetrics"; import { calculateAccuracyFromPrecisionBits } from "../../lib/mapUtils"; -import { formatUptime, getRegionName, getModemPresetName } from "../../utils/formatters"; +import { + formatUptime, + getRegionName, + getModemPresetName, +} from "../../utils/formatters"; interface NodeDetailProps { nodeId: number; @@ -159,7 +164,7 @@ export const NodeDetail: React.FC = ({ nodeId }) => { return (
{/* Header with back button and basic node info */} -
+
); -}; \ No newline at end of file +}; diff --git a/web/src/components/dashboard/NodePacketList.tsx b/web/src/components/dashboard/NodePacketList.tsx index d540519..3482200 100644 --- a/web/src/components/dashboard/NodePacketList.tsx +++ b/web/src/components/dashboard/NodePacketList.tsx @@ -43,20 +43,6 @@ export const NodePacketList: React.FC = ({ nodeId }) => { return (
-
-

- Showing {nodePackets.length} of{" "} - { - packets.filter( - (p) => p.data.from === nodeId || p.data.to === nodeId - ).length - }{" "} - recent packets -

-
- - -
    {nodePackets.map((packet, index) => (
  • @@ -66,4 +52,4 @@ export const NodePacketList: React.FC = ({ nodeId }) => {
); -}; \ No newline at end of file +}; diff --git a/web/src/components/dashboard/NodePositionData.tsx b/web/src/components/dashboard/NodePositionData.tsx index 7d8fd97..53a0dff 100644 --- a/web/src/components/dashboard/NodePositionData.tsx +++ b/web/src/components/dashboard/NodePositionData.tsx @@ -28,10 +28,10 @@ export const NodePositionData: React.FC = ({ positionAccuracy, precisionBits, satsInView, - groundSpeed + groundSpeed, }) => { return ( -
+
= ({ )}
); -}; \ No newline at end of file +}; diff --git a/web/src/components/dashboard/index.ts b/web/src/components/dashboard/index.ts index 7ace4ba..268ee39 100644 --- a/web/src/components/dashboard/index.ts +++ b/web/src/components/dashboard/index.ts @@ -2,6 +2,7 @@ export * from './NodeList'; export * from './GatewayList'; export * from './MeshCard'; export * from './NodeDetail'; +export * from './ChannelDetail'; export * from './BatteryLevel'; export * from './SignalStrength'; export * from './GoogleMap'; diff --git a/web/src/components/messages/MessageBubble.tsx b/web/src/components/messages/MessageBubble.tsx index 4d66c3e..b37b9cf 100644 --- a/web/src/components/messages/MessageBubble.tsx +++ b/web/src/components/messages/MessageBubble.tsx @@ -40,26 +40,42 @@ const getNodeInitials = (nodeId: number, nodeName?: string) => { return nodeId.toString(16).slice(-4).toUpperCase(); }; +// Get the preferred display name for a node +const getDisplayName = (nodeId: number, nodeName?: string) => { + if (!nodeName) { + return `Node ${nodeId.toString(16)}`; + } + + // Prefer longName if available + return nodeName; +}; + export const MessageBubble: React.FC = ({ message, nodeName }) => { const initials = getNodeInitials(message.from, nodeName); const nodeColor = getNodeColor(message.from); + const displayName = getDisplayName(message.from, nodeName); return ( -
-
- {initials} +
+ {/* Header row with name and timestamp aligned right */} +
+ + {displayName} + + + {formatTimestamp(message.timestamp)} +
-
-
- - {nodeName || `Node ${message.from.toString(16)}`} - - - {formatTimestamp(message.timestamp)} - + + {/* Message row with avatar and text */} +
+
+ {initials}
-
- {message.text} +
+
+ {message.text} +
diff --git a/web/src/routes/channel.$channelId.tsx b/web/src/routes/channel.$channelId.tsx index feab8a6..c66c893 100644 --- a/web/src/routes/channel.$channelId.tsx +++ b/web/src/routes/channel.$channelId.tsx @@ -1,141 +1,33 @@ import { useEffect } from "react"; +import { PageWrapper } from "../components"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { ChannelDetail } from "../components/dashboard"; import { useAppSelector } from "../hooks"; -import { PageWrapper, MessageBubble } from "../components"; -import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; -import { ChevronLeft } from "lucide-react"; export const Route = createFileRoute("/channel/$channelId")({ component: ChannelPage, }); -// Generate a deterministic color based on channel ID -const getChannelColor = (channelId: string) => { - const colors = [ - "bg-green-500", - "bg-blue-500", - "bg-amber-500", - "bg-purple-500", - "bg-pink-500", - "bg-indigo-500", - "bg-red-500", - "bg-teal-500", - ]; - - // Use a hash of the channel ID to pick a consistent color - const hash = channelId - .split("") - .reduce((acc, char) => acc + char.charCodeAt(0), 0); - return colors[hash % colors.length]; -}; - function ChannelPage() { const { channelId } = Route.useParams(); const navigate = useNavigate(); + const { channels } = useAppSelector((state) => state.aggregator); - const { channels, messages, nodes } = useAppSelector( - (state) => state.aggregator - ); - const channel = channels[channelId]; - // Create the channel key in the same format used by the aggregator - const channelKey = `channel_${channelId}`; - - // Get channel messages and sort by timestamp (oldest to newest) - // This creates a shallow copy of the message array to sort it without modifying the original - const channelMessages = messages[channelKey] - ? [...messages[channelKey]].sort((a, b) => a.timestamp - b.timestamp) - : []; - + // Navigation timeout if channel doesn't exist useEffect(() => { - if (!channel) { - console.log(`[Channel] Channel ${channelId} not found`); - // Navigate back to channels page if this channel doesn't exist - setTimeout(() => navigate({ to: "/channels" }), 500); - } else { - console.log( - `[Channel] Displaying ${channelMessages.length} messages for channel ${channelId}` - ); - console.log( - `[Channel] Message count from channel data: ${channel.messageCount}` - ); - console.log(`[Channel] Available messages:`, messages); - console.log(`[Channel] Looking for key:`, channelKey); + if (!channels[channelId]) { + console.log(`[Channel] Channel ${channelId} not found, redirecting...`); + const timeout = setTimeout(() => { + navigate({ to: "/channels" }); + }, 3000); + + return () => clearTimeout(timeout); } - }, [ - channel, - channelId, - channelKey, - channelMessages.length, - navigate, - messages, - ]); - - if (!channel) { - return ( - -
-

Channel not found. Redirecting...

-
-
- ); - } - - // Get the channel color based on its ID - const channelColor = getChannelColor(channelId); + }, [channelId, navigate, channels]); return ( -
- {/* Header */} -
- - - -
-
- {channelId.substring(0, 2).toUpperCase()} -
-
-

- {channelId} -

-

- {channel.nodes.length} nodes ยท {channel.textMessageCount}{" "} - messages -

-
-
-
- - {/* Message List */} -
- {channelMessages.length === 0 ? ( -
-

No messages in this channel yet.

-
- ) : ( -
- {channelMessages.map((message) => { - const nodeName = - nodes[message.from]?.shortName || - nodes[message.from]?.longName; - - return ( - - ); - })} -
- )} -
-
+
); }