feat(channels): tag editing on the channel detail page (R8 S1 Part 2)

Bring the same tag control to the channel page, next to Subscribe:
- backend: GET /api/channels/{id} now returns this user's `tag_ids` for the channel
  (added to _channel_detail_dict + the ChannelDetail interface), matching the manager
  list shape;
- ChannelPage renders the shared ChannelTagsControl, wired to attach/detach/create with
  an optimistic update on the channel-detail cache (reverting on failure) and a manager
  list invalidation so a tag added here shows there too.

Reuses the R8 popover platform + ChannelTagsControl — no new tag UI.
This commit is contained in:
2026-07-30 23:33:50 +02:00
parent 2b70ba7a1f
commit 56f95fa626
3 changed files with 56 additions and 1 deletions
+12
View File
@@ -230,6 +230,7 @@ def _channel_detail_dict(
blocked: bool,
stored: int,
deep_requested: bool,
tag_ids: list[int],
) -> dict:
return {
"id": channel.id,
@@ -257,6 +258,9 @@ def _channel_detail_dict(
"backfill_done": channel.backfill_done,
"from_explore": channel.from_explore,
"details_synced": channel.details_synced_at is not None,
# This user's personal tag links for the channel (same shape as the manager list), so the
# channel page can offer the same tag editor next to Subscribe.
"tag_ids": tag_ids,
}
@@ -297,6 +301,13 @@ def channel_detail(
).first() is not None
blocked = _is_blocked(db, user, channel_id)
stored = db.scalar(select(func.count(Video.id)).where(Video.channel_id == channel_id)) or 0
tag_ids = list(
db.scalars(
select(ChannelTag.tag_id).where(
ChannelTag.user_id == user.id, ChannelTag.channel_id == channel_id
)
).all()
)
return _channel_detail_dict(
channel,
subscribed=subscribed,
@@ -304,6 +315,7 @@ def channel_detail(
blocked=blocked,
stored=int(stored),
deep_requested=bool(sub.deep_requested) if sub else False,
tag_ids=tag_ids,
)