mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-07-22 23:12:02 +00:00
fix(telegram): make topics drawer scrollable + add topic title filter
The Radix ScrollArea viewport did not get a bounded height inside the flex-column, max-h, vertically-centred dialog, so a long topic list overflowed the dialog instead of scrolling (last topics unreachable). Switch to a native flex-1 min-h-0 overflow-y-auto container, which is the canonical touch-friendly flex-scroll pattern. Also add a "Filter topics..." input (matches by topic title) so any topic can be reached regardless of list length; reset the filter on close; and add a toast fallback message on toggle failure. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useTransition } from "react";
|
import { useState, useEffect, useCallback, useMemo, useTransition } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2, Search } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { setTopicFetchEnabled } from "../actions";
|
import { setTopicFetchEnabled } from "../actions";
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@ interface TopicsDrawerProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function topicLabel(t: TopicRow): string {
|
||||||
|
return t.topicName ?? `Topic ${t.topicId}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function TopicsDrawer({
|
export function TopicsDrawer({
|
||||||
channelId,
|
channelId,
|
||||||
channelTitle,
|
channelTitle,
|
||||||
@@ -37,6 +41,7 @@ export function TopicsDrawer({
|
|||||||
}: TopicsDrawerProps) {
|
}: TopicsDrawerProps) {
|
||||||
const [topics, setTopics] = useState<TopicRow[]>([]);
|
const [topics, setTopics] = useState<TopicRow[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
const [, startTransition] = useTransition();
|
const [, startTransition] = useTransition();
|
||||||
const [pendingId, setPendingId] = useState<string | null>(null);
|
const [pendingId, setPendingId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -57,6 +62,13 @@ export function TopicsDrawer({
|
|||||||
if (open && channelId) fetchTopics();
|
if (open && channelId) fetchTopics();
|
||||||
}, [open, channelId, fetchTopics]);
|
}, [open, channelId, fetchTopics]);
|
||||||
|
|
||||||
|
const handleOpenChange = (next: boolean) => {
|
||||||
|
// Reset the filter on close so the next open starts clean (the drawer is
|
||||||
|
// opened per-channel, so close-then-open is the normal channel switch).
|
||||||
|
if (!next) setFilter("");
|
||||||
|
onOpenChange(next);
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggle = (topic: TopicRow, enabled: boolean) => {
|
const handleToggle = (topic: TopicRow, enabled: boolean) => {
|
||||||
// Optimistic update
|
// Optimistic update
|
||||||
setTopics((prev) =>
|
setTopics((prev) =>
|
||||||
@@ -66,7 +78,7 @@ export function TopicsDrawer({
|
|||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
const result = await setTopicFetchEnabled(topic.id, enabled);
|
const result = await setTopicFetchEnabled(topic.id, enabled);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
toast.error(result.error);
|
toast.error(result.error ?? "Failed to update topic");
|
||||||
// Revert on failure
|
// Revert on failure
|
||||||
setTopics((prev) =>
|
setTopics((prev) =>
|
||||||
prev.map((t) =>
|
prev.map((t) =>
|
||||||
@@ -78,20 +90,43 @@ export function TopicsDrawer({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const q = filter.trim().toLowerCase();
|
||||||
|
if (!q) return topics;
|
||||||
|
return topics.filter((t) => topicLabel(t).toLowerCase().includes(q));
|
||||||
|
}, [topics, filter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="sm:max-w-lg max-h-[80vh] flex flex-col gap-0 p-0">
|
<DialogContent className="sm:max-w-lg max-h-[80vh] flex flex-col gap-0 p-0">
|
||||||
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-1">
|
<DialogHeader className="px-6 pt-6 pb-4 border-b border-border space-y-3">
|
||||||
<DialogTitle className="truncate pr-8">
|
<div className="space-y-1">
|
||||||
Topics{channelTitle ? `: ${channelTitle}` : ""}
|
<DialogTitle className="truncate pr-8">
|
||||||
</DialogTitle>
|
Topics{channelTitle ? `: ${channelTitle}` : ""}
|
||||||
<DialogDescription>
|
</DialogTitle>
|
||||||
Enabled topics are scanned and their files fetched. Disable a topic to
|
<DialogDescription>
|
||||||
stop fetching new files from it — already-fetched files are kept.
|
Enabled topics are scanned and their files fetched. Disable a topic
|
||||||
</DialogDescription>
|
to stop fetching new files from it — already-fetched files are kept.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
{topics.length > 0 && (
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Filter topics..."
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
className="pl-9 h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
{/* Native overflow scroll: the Radix ScrollArea viewport does not get a
|
||||||
|
bounded height inside this flex-column, max-h, vertically-centred
|
||||||
|
dialog, so the list overflowed instead of scrolling. flex-1 +
|
||||||
|
min-h-0 + overflow-y-auto is the canonical, touch-friendly fix. */}
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
<div className="px-6 py-4 space-y-2">
|
<div className="px-6 py-4 space-y-2">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center gap-2 py-12">
|
<div className="flex items-center justify-center gap-2 py-12">
|
||||||
@@ -105,18 +140,24 @@ export function TopicsDrawer({
|
|||||||
No topics discovered yet — they'll appear here after the next
|
No topics discovered yet — they'll appear here after the next
|
||||||
scan.
|
scan.
|
||||||
</p>
|
</p>
|
||||||
|
) : filtered.length === 0 ? (
|
||||||
|
<p className="py-12 text-center text-sm text-muted-foreground">
|
||||||
|
No topics match "{filter}".
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
topics.map((topic) => (
|
filtered.map((topic) => (
|
||||||
<div
|
<div
|
||||||
key={topic.id}
|
key={topic.id}
|
||||||
className="flex items-center justify-between gap-3 rounded-md border p-3"
|
className="flex items-center justify-between gap-3 rounded-md border p-3"
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm font-medium">
|
<p className="truncate text-sm font-medium">
|
||||||
{topic.topicName ?? `Topic ${topic.topicId}`}
|
{topicLabel(topic)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{topic.fetchEnabled ? "Fetching enabled" : "Fetching disabled"}
|
{topic.fetchEnabled
|
||||||
|
? "Fetching enabled"
|
||||||
|
: "Fetching disabled"}
|
||||||
{topic.lastScannedAt
|
{topic.lastScannedAt
|
||||||
? ` · last scanned ${new Date(
|
? ` · last scanned ${new Date(
|
||||||
topic.lastScannedAt
|
topic.lastScannedAt
|
||||||
@@ -128,15 +169,13 @@ export function TopicsDrawer({
|
|||||||
checked={topic.fetchEnabled}
|
checked={topic.fetchEnabled}
|
||||||
disabled={pendingId === topic.id}
|
disabled={pendingId === topic.id}
|
||||||
onCheckedChange={(checked) => handleToggle(topic, checked)}
|
onCheckedChange={(checked) => handleToggle(topic, checked)}
|
||||||
aria-label={`Toggle fetching for ${
|
aria-label={`Toggle fetching for ${topicLabel(topic)}`}
|
||||||
topic.topicName ?? topic.topicId
|
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user