diff --git a/docs/comment-threading.md b/docs/comment-threading.md new file mode 100644 index 00000000..c697bf0a --- /dev/null +++ b/docs/comment-threading.md @@ -0,0 +1,116 @@ +# Comment threads & discussion counts (Nostr → UI flow) + +This note documents how bitvid turns Nostr comment events into UI comment threads and discussion counts. The primary implementations live in: + +- `js/services/commentThreadService.js` (comment thread hydration + caching) +- `js/services/discussionCountService.js` (NIP-45 count queries + DOM updates) + +## Comment thread flow (`CommentThreadService`) + +### Data sources + +`CommentThreadService` is the single place that fetches and subscribes to comment events for a video. It relies on the injected `nostrClient` APIs: + +- `fetchVideoComments(target, options)` for the initial thread load. +- `subscribeVideoComments(target, options)` to keep the thread up to date in real time. + +The service builds a `target` payload using the current video metadata: + +- `videoEventId` (event id) +- `videoDefinitionAddress` from `buildVideoAddressPointer(video)` +- `videoKind` +- `videoAuthorPubkey` +- `rootIdentifier` / `rootIdentifierRelay` (derived from `video.videoRootId`, pointer identifiers, or tag data) +- optional `parentCommentId` context when fetching a reply thread + +Internally, it also infers parentage from the incoming comment events by looking at `e` tags; the most recent `e` tag that is **not** the video event id becomes the parent comment id for threading. It also watches for the `i` tag to update root identifier and relay details. + +### Caching + +When `FEATURE_IMPROVED_COMMENT_FETCHING` is enabled, comment events are cached in `localStorage`: + +- Key format: `bitvid:comments:` +- TTL: 5 minutes +- Versioned schema: `COMMENT_CACHE_VERSION = 2` + +Cache usage is simple: load from cache during `loadThread`, and write the latest event list after thread updates. Cache diagnostics (`commentCacheDiagnostics.storageUnavailable`) are surfaced to consumers to indicate storage issues. + +### Polling & refresh + +There is **no polling loop** for comments. Refresh happens through: + +1. A one-time fetch via `fetchVideoComments` during `loadThread`. +2. A live subscription via `subscribeVideoComments`, which streams new comments into the thread. + +Profile hydration for comment authors is batched and debounced (default 25ms), with up to three retry attempts and a backoff delay between attempts. + +### UI surfacing + +`VideoModalCommentController` owns the comment UI and subscribes to `CommentThreadService` callbacks: + +- `onThreadReady` → full thread render +- `onCommentsAppended` → incremental updates +- `onError` → status messaging + +The controller translates the thread snapshot into UI state for the modal and uses the comment thread’s profile cache to annotate authors. + +### Telemetry/logging + +All comment-thread telemetry is routed through `logger.dev`/`logger.user` (no direct console logging). The service emits warnings for: + +- Nostr client failures (fetch/subscribe) +- Comment cache read/write errors +- Profile hydration retries and failures + +## Discussion count flow (`DiscussionCountService`) + +### Data sources + +`DiscussionCountService` uses NIP-45 COUNT queries to count comment events across relays. It requires a Nostr client that exposes: + +- `nostrClient.pool` (relay pool) +- `nostrClient.countEventsAcrossRelays(filters)` + +### Filters & event kinds + +The service builds COUNT filters based on the current video metadata. The base event kind is `COMMENT_EVENT_KIND` (video comment kind). Filters include: + +- `#E` (root event id) +- `#A` (address pointer from `buildVideoAddressPointer(video)`) +- `#I` (root identifier / videoRootId) +- `#K` (root kind) +- `#P` (root author) + +It always tries an `#E` filter first, then constructs an uppercase-tag filter that uses the best available pointer information, and finally a lowercase `#A` filter when available. + +### Caching + +Counts are cached in-memory per video id during a session: + +- `videoDiscussionCountCache` stores resolved counts. +- `inFlightDiscussionCounts` deduplicates concurrent requests. + +There is no persistence beyond the runtime session. + +### Polling & refresh + +Discussion counts are not polled on a timer. They refresh when the UI asks for them (typically after feed renders). The application triggers refreshes via `app.refreshVideoDiscussionCounts`, which is wired into video list rendering flows. + +### UI surfacing + +The service updates DOM nodes directly using data attributes: + +- Root node: `[data-discussion-count="