diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts
index 0f692fd..983879e 100644
--- a/src/app/api/chat/route.ts
+++ b/src/app/api/chat/route.ts
@@ -9,7 +9,8 @@ export async function GET(req: Request) {
const before = searchParams.get("before") || undefined;
const after = searchParams.get("after") || undefined;
const channelId = searchParams.get("channel_id") || undefined;
- const messages = await getLatestChatMessages({ limit, before, after, channelId } as { limit?: number, before?: string, after?: string, channelId?: string });
+ const region = searchParams.get("region") || undefined;
+ const messages = await getLatestChatMessages({ limit, before, after, channelId, region } as { limit?: number, before?: string, after?: string, channelId?: string, region?: string });
return NextResponse.json(messages);
} catch (error) {
return NextResponse.json({ error: "Failed to fetch chat messages" }, { status: 500 });
diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx
index ff9853c..d6ee23b 100644
--- a/src/components/ChatBox.tsx
+++ b/src/components/ChatBox.tsx
@@ -7,6 +7,8 @@ import { getChannelIdFromKey } from "../lib/meshcore";
import ChatMessageItem, { ChatMessage } from "./ChatMessageItem";
import RefreshButton from "./RefreshButton";
import { buildApiUrl } from "../lib/api";
+import RegionSelector from "./RegionSelector";
+import { getRegionConfig } from "../lib/regions";
const PAGE_SIZE = 20;
@@ -59,9 +61,11 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
const showTabs = allTabs.length > 1;
const fetchMessages = useCallback(async (before?: string, replace = false, after?: string) => {
+ if (!config?.selectedRegion) return;
+
setLoading(true);
try {
- let url = `/api/chat?limit=${PAGE_SIZE}`;
+ let url = `/api/chat?limit=${PAGE_SIZE}®ion=${encodeURIComponent(config.selectedRegion!)}`;
if (channelId) url += `&channel_id=${channelId}`;
if (after) {
@@ -94,18 +98,11 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
}
}
} catch (error) {
- // Only set hasMore to false if we don't have a lastBefore value (can't load more)
- if (!lastBefore) {
- setHasMore(false);
- }
- if (after) {
- // Silently fail for auto-refresh
- console.error('Auto-refresh failed:', error);
- }
+ console.error('Load failed:', error);
} finally {
setLoading(false);
}
- }, [channelId]);
+ }, [channelId, config.selectedRegion]);
useEffect(() => {
if (!minimized) {
@@ -114,7 +111,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
setLastBefore(undefined);
fetchMessages(undefined, true);
}
- }, [minimized, selectedTab]);
+ }, [minimized, selectedTab, config?.selectedRegion, fetchMessages]);
// Auto-refresh effect
useEffect(() => {
@@ -128,7 +125,7 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
return () => clearInterval(interval);
}
- }, [minimized, channelId, messages]);
+ }, [minimized, channelId, messages, fetchMessages]);
const handleLoadMore = () => {
if (lastBefore) {
@@ -144,15 +141,59 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st
};
const LoadMoreButton = () => (
-
+
);
+ // If no region is selected, show the region selector
+ if (!config?.selectedRegion) {
+ return (
+
+
+
MeshCore Chat
+ {!startExpanded && (
+
+ )}
+
+
+ {!minimized && (
+
+ {
+ setMessages([]);
+ setHasMore(true);
+ setLastBefore(undefined);
+ fetchMessages(undefined, true);
+ }}
+ className="w-full"
+ />
+
+ )}
+
+ );
+ }
+
return (
-
MeshCore Chat
+
+ MeshCore Chat
+
+ {getRegionConfig(config.selectedRegion!)?.friendlyName || config.selectedRegion}
+
+
{(!minimized) && (
+
+
Chat Region
+
+
+ Select a region to filter chat messages by broker and topic
+
+
);
}
diff --git a/src/components/RegionSelector.tsx b/src/components/RegionSelector.tsx
new file mode 100644
index 0000000..29efb30
--- /dev/null
+++ b/src/components/RegionSelector.tsx
@@ -0,0 +1,59 @@
+"use client";
+import { useConfig } from "./ConfigContext";
+import { getRegionFriendlyNames } from "../lib/regions";
+
+interface RegionSelectorProps {
+ onRegionSelected?: () => void;
+ className?: string;
+}
+
+export default function RegionSelector({ onRegionSelected, className = "" }: RegionSelectorProps) {
+ const { config, setConfig } = useConfig();
+ const regions = getRegionFriendlyNames();
+
+ const handleRegionSelect = (regionName: string) => {
+ setConfig({ ...config, selectedRegion: regionName });
+ if (onRegionSelected) {
+ onRegionSelected();
+ }
+ };
+
+ return (
+
+
+
+ Select a Chat Region
+
+
+ Choose a region to filter chat messages
+
+
+
+
+ {regions.map(({ name, friendlyName }) => (
+
+ ))}
+
+
+
+
+ You can change this selection later in the Settings menu
+
+
+
+ );
+}
+
diff --git a/src/lib/clickhouse/actions.ts b/src/lib/clickhouse/actions.ts
index e0dab4c..c160cf0 100644
--- a/src/lib/clickhouse/actions.ts
+++ b/src/lib/clickhouse/actions.ts
@@ -50,10 +50,11 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
}
}
-export async function getLatestChatMessages({ limit = 20, before, after, channelId }: { limit?: number, before?: string, after?: string, channelId?: string } = {}) {
+export async function getLatestChatMessages({ limit = 20, before, after, channelId, region }: { limit?: number, before?: string, after?: string, channelId?: string, region?: string } = {}) {
try {
let where = [];
const params: Record
= { limit };
+
if (before) {
where.push('ingest_timestamp < {before:DateTime64}');
params.before = before;
@@ -66,6 +67,18 @@ export async function getLatestChatMessages({ limit = 20, before, after, channel
where.push('channel_hash = {channelId:String}');
params.channelId = channelId;
}
+
+ // Add region filtering if specified
+ if (region) {
+ if (region === 'seattle') {
+ where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND (x.2 = 'meshcore' OR x.2 = 'meshcore/salish'), topic_broker_array)");
+ } else if (region === 'portland') {
+ where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/pdx', topic_broker_array)");
+ } else if (region === 'boston') {
+ where.push("arrayExists(x -> x.1 = 'tcp://mqtt.davekeogh.com:1883' AND x.2 = 'meshcore/bos', topic_broker_array)");
+ }
+ }
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
const query = `SELECT ingest_timestamp, mesh_timestamp, channel_hash, mac, hex(encrypted_message) AS encrypted_message, message_count, origin_path_array FROM meshcore_public_channel_messages ${whereClause} ORDER BY ingest_timestamp DESC LIMIT {limit:UInt32}`;
const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' });
diff --git a/src/lib/regions.ts b/src/lib/regions.ts
new file mode 100644
index 0000000..c353f7c
--- /dev/null
+++ b/src/lib/regions.ts
@@ -0,0 +1,40 @@
+export interface RegionConfig {
+ name: string;
+ friendlyName: string;
+ broker: string;
+ topics: string[];
+}
+
+export const REGIONS: RegionConfig[] = [
+ {
+ name: "seattle",
+ friendlyName: "Seattle (PugetMesh, SalishMesh)",
+ broker: "tcp://mqtt.davekeogh.com:1883",
+ topics: ["meshcore", "meshcore/salish"]
+ },
+ {
+ name: "portland",
+ friendlyName: "Portland",
+ broker: "tcp://mqtt.davekeogh.com:1883",
+ topics: ["meshcore/pdx"]
+ },
+ {
+ name: "boston",
+ friendlyName: "Boston",
+ broker: "tcp://mqtt.davekeogh.com:1883",
+ topics: ["meshcore/bos"]
+ }
+];
+
+export function getRegionConfig(regionName: string): RegionConfig | undefined {
+ return REGIONS.find(region => region.name === regionName);
+}
+
+export function getRegionNames(): string[] {
+ return REGIONS.map(region => region.name);
+}
+
+export function getRegionFriendlyNames(): { name: string; friendlyName: string }[] {
+ return REGIONS.map(region => ({ name: region.name, friendlyName: region.friendlyName }));
+}
+