diff --git a/backend/app/routes/channels.py b/backend/app/routes/channels.py
index 9578b2dc..d848d6cd 100644
--- a/backend/app/routes/channels.py
+++ b/backend/app/routes/channels.py
@@ -229,6 +229,7 @@ def _channel_detail_dict(
count_live: int,
deep_requested: bool,
priority: int,
+ hidden: bool,
tag_ids: list[int],
) -> dict:
return {
@@ -262,6 +263,9 @@ def _channel_detail_dict(
# This user's per-subscription feed priority (0 when not subscribed) so the channel page can
# offer the same priority stepper as the manager. Editable only while subscribed.
"priority": priority,
+ # This user's per-subscription "hide from feed" flag (False when not subscribed), so the
+ # channel page can offer the same Unhide toggle the manager has. Editable only while subscribed.
+ "hidden": hidden,
"backfill_done": channel.backfill_done,
"from_explore": channel.from_explore,
"details_synced": channel.details_synced_at is not None,
@@ -344,6 +348,7 @@ def channel_detail(
count_live=count_live,
deep_requested=bool(sub.deep_requested) if sub else False,
priority=int(sub.priority) if sub else 0,
+ hidden=bool(sub.hidden) if sub else False,
tag_ids=tag_ids,
)
diff --git a/backend/tests/test_channel_backfill.py b/backend/tests/test_channel_backfill.py
index 91bd4447..d1244641 100644
--- a/backend/tests/test_channel_backfill.py
+++ b/backend/tests/test_channel_backfill.py
@@ -55,6 +55,49 @@ def test_channel_detail_returns_normal_shorts_live_counts(db, monkeypatch):
assert body["count_normal"] == 2
+@pytest.mark.parametrize("hidden", [True, False])
+def test_channel_detail_surfaces_subscription_hidden_flag(db, monkeypatch, hidden):
+ # S6: the detail GET must expose the per-subscription `hidden` flag so the channel page can offer
+ # the same Unhide toggle the manager has (the flag was previously omitted from the detail dict).
+ ch = Channel(id="UC_hid", title="Hidden?", published_at=_PUB)
+ db.add(ch)
+ me = User(email="hid@example.com")
+ db.add(me)
+ db.flush()
+ db.add(Subscription(user_id=me.id, channel_id=ch.id, hidden=hidden))
+ db.commit()
+
+ monkeypatch.setattr(state, "_configured_cache", True)
+ app.dependency_overrides[get_db] = lambda: db
+ app.dependency_overrides[current_user] = lambda: me
+ try:
+ resp = TestClient(app).get(f"/api/channels/{ch.id}")
+ finally:
+ app.dependency_overrides.clear()
+
+ assert resp.status_code == 200, resp.text
+ assert resp.json()["hidden"] is hidden
+
+
+def test_channel_detail_hidden_defaults_false_when_not_subscribed(db, monkeypatch):
+ # No subscription row → the flag can't be True; it must serialize as False, not raise.
+ ch = Channel(id="UC_hid_ns", title="Unsub", published_at=_PUB)
+ me = User(email="hidns@example.com")
+ db.add_all([ch, me])
+ db.commit()
+
+ monkeypatch.setattr(state, "_configured_cache", True)
+ app.dependency_overrides[get_db] = lambda: db
+ app.dependency_overrides[current_user] = lambda: me
+ try:
+ resp = TestClient(app).get(f"/api/channels/{ch.id}")
+ finally:
+ app.dependency_overrides.clear()
+
+ assert resp.status_code == 200, resp.text
+ assert resp.json()["hidden"] is False
+
+
def _setup_channel_and_sub(db, *, email, is_demo=False):
ch = Channel(
id="UC_wire",
diff --git a/frontend/src/components/ChannelPage.tsx b/frontend/src/components/ChannelPage.tsx
index 0595c20b..80ebce67 100644
--- a/frontend/src/components/ChannelPage.tsx
+++ b/frontend/src/components/ChannelPage.tsx
@@ -12,6 +12,8 @@ import {
Ban,
Check,
ExternalLink,
+ Eye,
+ EyeOff,
History,
Loader2,
Plus,
@@ -137,6 +139,17 @@ export default function ChannelPage({
// a caller toast here would just double it.
});
+ // "Hide from feed": the same per-subscription flag the manager toggles (updateChannel {hidden}).
+ // Local DB op — keeps the subscription, only drops the channel's videos from the feed/explore.
+ const toggleHidden = useMutation({
+ mutationFn: () => api.updateChannel(channelId, { hidden: !ch?.hidden }),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: qk.channel(channelId) });
+ qc.invalidateQueries({ queryKey: qk.feed() });
+ qc.invalidateQueries({ queryKey: qk.channels() });
+ },
+ });
+
const subscribe = useMutation({
mutationFn: () => api.subscribeChannel(channelId),
onSuccess: () => {
@@ -470,6 +483,24 @@ export default function ChannelPage({