Shorts (docs/shorts-plan.md): - Add DECISION 6 + architecture/phase items for a NEW portrait 9:16 card design. The existing VideoCard.js is hardcoded 16:9 (ratio-16-9), so shorts need either a "short" variant of VideoCard or a dedicated ShortCard — a first-class Phase 1 deliverable, not an afterthought. Live ingest (docs/live-ingest-plan.md): - Reframe past streams (VOD) and chat from "deferred/maybe" to committed phases: Phase 3 read-only chat (+ optional 3b post), Phase 4 ended/recording VOD playback through the same HLS path. So the plan's phases fully account for live AND past zap.stream streams AND chat. - Add a distinct LiveCard design (LIVE badge, viewer count, ended state). Co-Authored-By: Claude Opus 4.8 <[email protected]>
8.4 KiB
Shorts (short-form vertical video) — Dev Plan
TODO ref: #16b in todo/TODO_2026-06-20_pre-launch.md.
Status: PLANNING — decisions open (see below). Not started.
Short-form vertical video is NIP-71 kind 22 (the short counterpart to kind 21 normal video), plus the addressable short kind 34236. It is a different feature from live streams (#16, NIP-53) — do not conflate them.
The good news: bitvid already ingests kind 22. The inbound NIP-71 adapter
(js/nostr/nip71IngestAdapter.js, NIP71_KINDS = {21, 22, 34235, 34236}) parses
short-form notes today and they already flow into the main feed. So this feature
is not new ingest code — it is (a) a way to distinguish "short" videos,
(b) a dedicated Shorts sidebar tab with a vertical/swipe UI, and (c) a feed
source that lists only shorts. Everything ships behind a config flag that leaves
no trace when off.
Decisions needed
DECISION 1 — Discovery scope. Does Shorts pull from the same whitelisted authors as the main feed (reuse the existing render-time whitelist/WoT filter), or does it have its own discovery scope (e.g. all authors, capped)? Recommendation: same whitelist scope as the main feed for v1 — least surprise, reuses the existing moderation filter, no new relay fan-out.
DECISION 2 — Moderation / NSFW. Inherit the existing filters (blacklist / WoT mute / NSFW / private), or add short-specific handling? Recommendation: inherit — shorts are just kind-22 videos; the render-time
filterVideospath already applies.
DECISION 3 — Do shorts also appear in the main feed, or only under Shorts? Today ingested kind-22 videos land in the main grids. Options: (a) leave them in the main feed AND add a Shorts tab (Shorts = a filtered view); (b) exclude shorts from the main/Recent/For-You feeds and surface them ONLY in the Shorts tab. Recommendation: (a) for v1 — simplest, no changes to existing feeds; revisit exclusion later if the main feed feels diluted.
DECISION 4 — What counts as a "short"? See Field mapping below; confirm the detection rule (kind 22/34236 vs aspect-ratio) once we've eyeballed real events.
DECISION 5 — UI ambition for v1. Minimal (a normal grid that opens shorts in a portrait player) vs full (TikTok-style full-screen vertical swipe/next feed)? Recommendation: ship the grid + portrait player first (small), then layer the swipe feed as a phase 2 — keeps the first release cheap.
DECISION 6 — Card design. Shorts need a new portrait (9:16) card — the existing
js/ui/components/VideoCard.jshardcodes a 16:9 landscape media ratio (ratio-16-9), so it can't render shorts correctly as-is. Two options: (a) a newShortCardcomponent (portrait thumbnail, compact meta, its owndata-testid); or (b) a portrait "mode"/variant ofVideoCarddriven by a flag (e.g.variant: "short"swapping the ratio class + layout). Recommendation: (b) a portrait variant ofVideoCardif the internals are clean enough to parameterize the ratio + layout — it inherits thumbnail binding, moderation badges, engagement controls, and the ⋯ menu for free; fall back to (a) a dedicatedShortCardonly if forcing a variant makesVideoCardunwieldy or pushes it past its size cap. Either way this is a first-class deliverable, not an afterthought — the grid can't ship without it.
Config flag (off = no trace)
Add FEATURE_SHORTS to config/instance-config.js (default false), threaded
through js/config.js → js/constants.js exactly like FEATURE_NIP71_INGEST.
When the flag is off, all of the following must be absent (the "as if it were never added" requirement):
- Sidebar tab — the Shorts link is not rendered in
components/sidebar.html(not merelyhidden). - Feed registration —
FEED_TYPES.SHORTSis not registered injs/app/feedCoordinator.js(no pipeline, no sorter). - Subscription/source — no shorts-specific relay subscription is created.
- Route —
#view=shortsfalls back to the default view (a stale bookmark can't resurrect the tab). - Config surface — the flag is the only footprint.
Field mapping — detecting a "short"
A video object needs a reliable isShort marker so a feed source can filter.
Candidate signals (to confirm against real events in Phase 0):
| Signal | Meaning | Notes |
|---|---|---|
kind === 22 |
NIP-71 regular short | primary signal for ingested foreign shorts |
kind === 34236 |
NIP-71 addressable short | bitvid's own mirror uses 34235/36 (nip71Mirror.js) |
imeta dim height > width |
portrait aspect | fallback when kind is ambiguous (matches the mirror's short = hasDims && height > width heuristic) |
Plan: surface isShort on the bitvid video object in nip71IngestAdapter.js
(it already tracks the source kind) and in the local mirror path, so both foreign
and native shorts are detectable without re-fetching.
Architecture
Reuses the existing feed engine + ingest; new pieces are thin.
- Flag:
FEATURE_SHORTS(instance-config → config → constants). - Feed type: add
SHORTS: "shorts"toFEED_TYPES(js/constants.js). - Feed source/registration: register
FEED_TYPES.SHORTSinjs/app/feedCoordinator.js, sourced from the already-populated active-video cache filtered toisShort(per DECISION 1, scoped to the same authors as the main feed). A dedicated sorter can be chronological or reuse the For-You scorer — TBD, likely chronological for v1. - Sidebar tab: add a Shorts link in
components/sidebar.html, rendered only when the flag is on. - View:
views/shorts.html(a portrait-oriented grid container). - Card design (new): a portrait 9:16 card — either a
variant: "short"mode ofjs/ui/components/VideoCard.js(which is currently 16:9 /ratio-16-9) or a dedicatedShortCard(DECISION 6). Portrait thumbnail, compact title + author, reuse of thumbnail binding, moderation badges, engagement + ⋯ menu. New design tokens if needed (existing--video-card-*tokens live incss/tokens.css). A distinctdata-testidfor e2e selectors. - Player UI: v1 opens a short in the existing player modal with a portrait layout; v2 adds a full-screen vertical swipe/next feed.
- Diversity: apply the existing
spreadAuthorspass (feed sorters) so the Shorts tab doesn't cluster by creator, consistent with For You / Trending.
Phases (each flag-gated from day one)
- Phase 0 — Detection + flag (small). Add
FEATURE_SHORTS(off),isShortderivation in the ingest adapter + mirror, unit tests for detection. No UI. - Phase 1 — Tab + feed + portrait card (medium).
FEED_TYPES.SHORTS, sidebar link,views/shorts.html, feed registration filtered toisShort, route gated by flag, and the new portrait 9:16 card (DECISION 6) so the grid renders shorts correctly. Opens in the standard player for now. - Phase 2 — Vertical UX (medium). Portrait player layout + full-screen swipe/next feed.
- Phase 3 — Polish. Autoplay-on-scroll, mute toggle, per-tab empty states, optional exclusion from the main feed (DECISION 3 revisit).
Moderation, whitelist & NSFW
Per DECISION 2 (recommended: inherit): shorts pass through the same render-time
filterVideos path (blacklist event ids, author blacklist, WoT mute, NSFW gate,
private exclusion) as every other grid. No new moderation surface. The admin
per-event block list (#25) already applies to any event id, shorts included.
Risks / watch-items
- Aspect-ratio detection is soft — not every short carries
imeta dim; kind is the reliable signal, dims are the fallback. Confirm in Phase 0. - Double-listing — if shorts stay in the main feed (DECISION 3a), make sure the cross-ecosystem dedup (import-link + infohash, see nip71 onboarding) still collapses a short that also exists as a 30078/34235 video.
- Empty tab — with a whitelist-scoped source, a small instance may have zero shorts; ship a clear empty state so the tab doesn't look broken.
Sources
- NIP-71 (video events; kinds 21/22 regular, 34235/34236 addressable).
- Existing code:
js/nostr/nip71IngestAdapter.js,js/nostr/nip71Mirror.js,js/app/feedCoordinator.js,js/feedEngine/sorters.js,components/sidebar.html. docs/nip71-migration-plan.md(dual-event model, short=34236 heuristic).