diff --git a/backend/app/routes/channels.py b/backend/app/routes/channels.py index ed14a89..4c47b71 100644 --- a/backend/app/routes/channels.py +++ b/backend/app/routes/channels.py @@ -209,7 +209,7 @@ def discover_channels( def _channel_detail_dict( - channel: Channel, *, subscribed: bool, explored: bool, stored: int + channel: Channel, *, subscribed: bool, explored: bool, blocked: bool, stored: int ) -> dict: return { "id": channel.id, @@ -226,6 +226,7 @@ def _channel_detail_dict( "country": channel.country, "subscribed": subscribed, "explored": explored, + "blocked": blocked, "stored_videos": stored, "from_explore": channel.from_explore, "details_synced": channel.details_synced_at is not None, @@ -266,9 +267,14 @@ def channel_detail( ExploredChannel.user_id == user.id, ExploredChannel.channel_id == channel_id ) ).first() is not None + blocked = db.execute( + select(BlockedChannel.id).where( + BlockedChannel.user_id == user.id, BlockedChannel.channel_id == channel_id + ) + ).first() is not None stored = db.scalar(select(func.count(Video.id)).where(Video.channel_id == channel_id)) or 0 return _channel_detail_dict( - channel, subscribed=subscribed, explored=explored, stored=int(stored) + channel, subscribed=subscribed, explored=explored, blocked=blocked, stored=int(stored) ) diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx index fc856f7..2c81407 100644 --- a/frontend/src/components/ChannelPage.tsx +++ b/frontend/src/components/ChannelPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowLeft, Check, ExternalLink, Loader2, Plus, RefreshCw } from "lucide-react"; +import { ArrowLeft, Ban, Check, ExternalLink, Loader2, Plus, RefreshCw } from "lucide-react"; import Avatar from "./Avatar"; import Feed from "./Feed"; import { useConfirm } from "./ConfirmProvider"; @@ -68,12 +68,21 @@ export default function ChannelPage({ useEffect(() => { if (!ch || me.is_demo) return; if (autoTried.current === channelId) return; - if (ch.subscribed) return; + if (ch.subscribed || ch.blocked) return; if (ch.explored && ch.stored_videos > 0) return; autoTried.current = channelId; runExplore().catch(() => {}); }, [ch, channelId, me.is_demo, runExplore]); + const block = useMutation({ + mutationFn: () => (ch?.blocked ? api.unblockChannel(channelId) : api.blockChannel(channelId)), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["channel", channelId] }); + qc.invalidateQueries({ queryKey: ["feed"] }); + qc.invalidateQueries({ queryKey: ["blocked-channels"] }); + }, + }); + const subscribe = useMutation({ mutationFn: () => api.subscribeChannel(channelId), onSuccess: () => { @@ -178,10 +187,17 @@ export default function ChannelPage({

{name}

- {ch?.explored && !ch?.subscribed && ( - - {t("channel.exploringBadge")} + {ch?.blocked ? ( + + {t("channel.blockedBadge")} + ) : ( + ch?.explored && + !ch?.subscribed && ( + + {t("channel.exploringBadge")} + + ) )}
@@ -204,7 +220,23 @@ export default function ChannelPage({ > + {!me.is_demo && ( + + )} {!me.is_demo && + !ch?.blocked && (ch?.subscribed ? ( + + ))} +
+

{t("channels.blocked.hint")}

+
+ )} ); diff --git a/frontend/src/components/Feed.tsx b/frontend/src/components/Feed.tsx index 6c66450..64ec348 100644 --- a/frontend/src/components/Feed.tsx +++ b/frontend/src/components/Feed.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query"; -import { ArrowDown, ArrowUp, ArrowLeft, RefreshCw, Youtube } from "lucide-react"; +import { ArrowDown, ArrowUp, ArrowLeft, Info, RefreshCw, Trash2, Youtube } from "lucide-react"; import { api, HttpError, type FeedFilters, type Video } from "../lib/api"; import i18n from "../i18n"; import { notify, resolveVideo } from "../lib/notifications"; @@ -118,6 +118,9 @@ export default function Feed({ ); const ytActive = !!ytSearch; + // How many live-search results to gather (the count selector replaces a manual "load more"; + // the free scrape source pages through continuations until this many are collected). + const [ytCount, setYtCount] = useState(20); // Debounce only the search term feeding the queries: the input still updates instantly (it // owns filters.q), but the feed/count keys settle after a pause, so typing doesn't refetch — @@ -139,8 +142,8 @@ export default function Feed({ // retry:false so a 429 (quota/limit) surfaces at once for inline display. staleTime keeps a // revisited search from re-spending. const ytQuery = useInfiniteQuery({ - queryKey: ["yt-search", ytSearch], - queryFn: ({ pageParam }) => api.searchYoutube(ytSearch as string, pageParam as string | null), + queryKey: ["yt-search", ytSearch, ytCount], + queryFn: ({ pageParam }) => api.searchYoutube(ytSearch as string, pageParam as string | null, ytCount), initialPageParam: null as string | null, getNextPageParam: (last) => last.next_cursor ?? undefined, enabled: ytActive, @@ -316,33 +319,74 @@ export default function Feed({ return (
-
- -