From 7e58cc29faf3596af2b1484dea0600abe570c252 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Mon, 22 Jun 2026 01:24:57 +0200 Subject: [PATCH] feat(telegram): add TopicsDrawer for per-topic fetch toggles Lists a channel's topics with an enable/disable Switch each (optimistic, revert on failure). Empty state explains topics appear after the next scan. Co-Authored-By: Claude Opus 4.8 --- .../telegram/_components/topics-drawer.tsx | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/app/(app)/telegram/_components/topics-drawer.tsx diff --git a/src/app/(app)/telegram/_components/topics-drawer.tsx b/src/app/(app)/telegram/_components/topics-drawer.tsx new file mode 100644 index 0000000..0ccd0b2 --- /dev/null +++ b/src/app/(app)/telegram/_components/topics-drawer.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState, useEffect, useCallback, useTransition } from "react"; +import { toast } from "sonner"; +import { Loader2 } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Switch } from "@/components/ui/switch"; +import { setTopicFetchEnabled } from "../actions"; + +interface TopicRow { + id: string; + topicId: string; + topicName: string | null; + fetchEnabled: boolean; + lastScannedAt: string | null; +} + +interface TopicsDrawerProps { + channelId: string | null; + channelTitle?: string; + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function TopicsDrawer({ + channelId, + channelTitle, + open, + onOpenChange, +}: TopicsDrawerProps) { + const [topics, setTopics] = useState([]); + const [loading, setLoading] = useState(false); + const [, startTransition] = useTransition(); + const [pendingId, setPendingId] = useState(null); + + const fetchTopics = useCallback(async () => { + if (!channelId) return; + setLoading(true); + try { + const res = await fetch(`/api/telegram/channels/${channelId}/topics`); + if (res.ok) setTopics(await res.json()); + else toast.error("Failed to load topics"); + } catch { + toast.error("Failed to load topics"); + } + setLoading(false); + }, [channelId]); + + useEffect(() => { + if (open && channelId) fetchTopics(); + }, [open, channelId, fetchTopics]); + + const handleToggle = (topic: TopicRow, enabled: boolean) => { + // Optimistic update + setTopics((prev) => + prev.map((t) => (t.id === topic.id ? { ...t, fetchEnabled: enabled } : t)) + ); + setPendingId(topic.id); + startTransition(async () => { + const result = await setTopicFetchEnabled(topic.id, enabled); + if (!result.success) { + toast.error(result.error); + // Revert on failure + setTopics((prev) => + prev.map((t) => + t.id === topic.id ? { ...t, fetchEnabled: !enabled } : t + ) + ); + } + setPendingId(null); + }); + }; + + return ( + + + + + Topics{channelTitle ? `: ${channelTitle}` : ""} + + + Enabled topics are scanned and their files fetched. Disable a topic to + stop fetching new files from it — already-fetched files are kept. + + + + +
+ {loading ? ( +
+ + + Loading topics... + +
+ ) : topics.length === 0 ? ( +

+ No topics discovered yet — they'll appear here after the next + scan. +

+ ) : ( + topics.map((topic) => ( +
+
+

+ {topic.topicName ?? `Topic ${topic.topicId}`} +

+

+ {topic.fetchEnabled ? "Fetching enabled" : "Fetching disabled"} + {topic.lastScannedAt + ? ` · last scanned ${new Date( + topic.lastScannedAt + ).toLocaleDateString()}` + : " · not scanned yet"} +

+
+ handleToggle(topic, checked)} + aria-label={`Toggle fetching for ${ + topic.topicName ?? topic.topicId + }`} + /> +
+ )) + )} +
+
+
+
+ ); +}