mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2025-09-08 06:58:43 +00:00
added new roadmap documentation
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
# **bitvid: Enhanced Multi-View Architecture Migration Plan**
|
||||
|
||||
This plan describes how to transform your current `index.html` file so that different sections of content are loaded as separate views. It keeps the header and footer consistent on each view, while the center portion of the page switches among “most recent videos,” user profiles, trending feeds, and more. Future features like personalized feeds or channel pages can be added following the same approach.
|
||||
|
||||
---
|
||||
|
||||
## **1. Goals**
|
||||
|
||||
1. **Preserve Navigation Bar & Footer**
|
||||
Keep the top navigation (logo, login/logout, “add video” controls) and the bottom footer in `index.html` at all times.
|
||||
|
||||
2. **Separate the Content Grid**
|
||||
Move the existing video grid or “recent videos” listing into its own file, for example `views/most-recent-videos.html`. You will load this content into a main container within `index.html`.
|
||||
|
||||
3. **Handle Additional Views**
|
||||
Prepare to load other views (profiles, trending, personalized feeds) in the same container. Each view can be its own HTML snippet or partial, stored separately (e.g. `views/profile-view.html`, `views/trending.html`, etc.).
|
||||
|
||||
4. **Single-Page Navigation**
|
||||
Use JavaScript to switch or load the correct view based on the URL. This keeps the user on a single page, but updates what they see in the main section.
|
||||
|
||||
5. **Maintain Existing Modal**
|
||||
The video-modal (`video-modal.html`) will remain a separate file, loaded into the DOM as is. This ensures consistent playback.
|
||||
|
||||
---
|
||||
|
||||
## **2. Proposed File Structure**
|
||||
|
||||
Below is an example layout. You do not need to follow it exactly, but it helps you see where each piece goes.
|
||||
|
||||
```
|
||||
project/
|
||||
├─ index.html
|
||||
├─ components/
|
||||
│ └─ video-modal.html
|
||||
├─ views/
|
||||
│ ├─ most-recent-videos.html
|
||||
│ ├─ profile-view.html
|
||||
│ ├─ trending.html
|
||||
│ └─ ...
|
||||
├─ js/
|
||||
│ ├─ app.js
|
||||
│ ├─ nostr.js
|
||||
│ ├─ webtorrent.js
|
||||
│ ├─ ...
|
||||
│ └─ viewManager.js <-- new file for handling view loading
|
||||
├─ css/
|
||||
│ └─ style.css
|
||||
└─ ...
|
||||
```
|
||||
|
||||
1. **`index.html`**
|
||||
- Contains the header, top nav, login/logout, plus the footer.
|
||||
- Has a single `<div>` where content from your partial views will be loaded.
|
||||
|
||||
2. **`views/most-recent-videos.html`**
|
||||
- Contains only the HTML (and minimal inline scripts) for the grid of most recent videos.
|
||||
- No header or footer.
|
||||
- No scripts for Nostr or WebTorrent—those remain in your main JS files.
|
||||
|
||||
3. **Other Views** (optional)
|
||||
- Similar structure to `most-recent-videos.html`.
|
||||
- Example: `profile-view.html`, `trending.html`, etc.
|
||||
|
||||
4. **`video-modal.html`**
|
||||
- Remains a separate component file for the modal.
|
||||
- Inserted into the DOM in `index.html` or on demand, as you already do.
|
||||
|
||||
5. **`viewManager.js`** (new optional file)
|
||||
- Manages the logic of fetching these partial view files and inserting them into the page container.
|
||||
- Handles route changes to decide which view to load.
|
||||
|
||||
---
|
||||
|
||||
## **3. Modifying `index.html`**
|
||||
|
||||
Below is a suggested strategy for `index.html`:
|
||||
|
||||
1. **Keep the current `<header>`**
|
||||
It has your logo and login/logout UI.
|
||||
|
||||
2. **Keep the current `<footer>`**
|
||||
It has the links to GitHub, Nostr, blog, and so on.
|
||||
|
||||
3. **Replace the big video listing area with a single container**
|
||||
For example:
|
||||
```html
|
||||
<div id="viewContainer" class="flex-grow">
|
||||
<!-- Dynamically loaded view content goes here -->
|
||||
</div>
|
||||
```
|
||||
|
||||
4. **Move the “most recent videos” grid**
|
||||
- Copy that section (including the `<div id="videoList">...</div>`) into `views/most-recent-videos.html`.
|
||||
- You can remove it from the main `index.html`, leaving only your `<header>`, login controls, disclaimers, and `<footer>`.
|
||||
|
||||
5. **Load the newly created partial**
|
||||
- In your JavaScript, on page load, fetch `views/most-recent-videos.html` via `fetch()` and place it inside `#viewContainer`.
|
||||
- Example:
|
||||
```js
|
||||
async function loadMostRecentVideosView() {
|
||||
const res = await fetch('views/most-recent-videos.html');
|
||||
const html = await res.text();
|
||||
document.getElementById('viewContainer').innerHTML = html;
|
||||
// Then re-initialize anything needed (e.g. event listeners, etc.)
|
||||
}
|
||||
```
|
||||
- This keeps the same content, but it’s now in a separate file.
|
||||
|
||||
6. **Keep the existing disclaimers and disclaimers modal**
|
||||
- The disclaimer modal can stay in `index.html` since it is site-wide.
|
||||
- The same applies to the video player modal. You can keep a `<div id="modalContainer"></div>` to insert `video-modal.html`, or load it separately.
|
||||
|
||||
---
|
||||
|
||||
## **4. JavaScript for View Switching**
|
||||
|
||||
### **4.1 Single-File Approach**
|
||||
|
||||
You could place view-loading code in `app.js`. For example:
|
||||
|
||||
```js
|
||||
// app.js
|
||||
async function showView(viewName) {
|
||||
let viewUrl = '';
|
||||
switch (viewName) {
|
||||
case 'recent':
|
||||
viewUrl = 'views/most-recent-videos.html';
|
||||
break;
|
||||
case 'profile':
|
||||
viewUrl = 'views/profile-view.html';
|
||||
break;
|
||||
// etc.
|
||||
default:
|
||||
viewUrl = 'views/most-recent-videos.html';
|
||||
break;
|
||||
}
|
||||
|
||||
const res = await fetch(viewUrl);
|
||||
const html = await res.text();
|
||||
document.getElementById('viewContainer').innerHTML = html;
|
||||
|
||||
// Re-initialize any needed scripts for the new view
|
||||
}
|
||||
```
|
||||
|
||||
Then when the page loads, you do `showView('recent');`
|
||||
|
||||
### **4.2 Dedicated `viewManager.js`**
|
||||
|
||||
Alternatively, create a separate file (`viewManager.js`) with functions like:
|
||||
|
||||
```js
|
||||
export async function loadView(viewUrl, containerId = 'viewContainer') {
|
||||
const res = await fetch(viewUrl);
|
||||
const html = await res.text();
|
||||
document.getElementById(containerId).innerHTML = html;
|
||||
}
|
||||
```
|
||||
|
||||
Then in your main app code, call `loadView('views/most-recent-videos.html');`.
|
||||
|
||||
---
|
||||
|
||||
## **5. Preserving Existing Functionality**
|
||||
|
||||
1. **Form Submission**
|
||||
Your “Share Video” form can remain in `index.html` or be placed within a dedicated “upload video” view. If you do move it, you’ll just need to ensure the form’s JS event listeners are attached once the view loads.
|
||||
|
||||
2. **Login/Logout**
|
||||
The login button and user status references can remain in the header. This code continues to be managed by `app.js` without changes, as it is global.
|
||||
|
||||
3. **Video List**
|
||||
Since the “most recent videos” grid moves to a partial, the original `<div id="videoList">...</div>` is replaced by a container in `most-recent-videos.html`. After loading that partial, your existing script logic for populating the video list (like `app.renderVideoList()`) will still work. You just need to ensure the new partial has the same IDs.
|
||||
|
||||
4. **Modal**
|
||||
The `video-modal.html` can stay a separate component. You already fetch and inject it into the DOM. Nothing changes there, aside from making sure the container is in `index.html`, so the modal can appear on top of whichever view the user is in.
|
||||
|
||||
5. **Disclaimer Modal**
|
||||
Similar approach. It can stay in `index.html` or be a partial if you prefer. Keep using the same logic to display it on first load.
|
||||
|
||||
---
|
||||
|
||||
## **6. Routing for Future Features**
|
||||
|
||||
### **6.1 Hash Routing**
|
||||
|
||||
As your platform grows, you may want a URL like `/#/profile/npub123` for user profiles. In that case:
|
||||
|
||||
1. **Listen for `hashchange`**:
|
||||
```js
|
||||
window.addEventListener('hashchange', handleRouting);
|
||||
```
|
||||
2. **Parse the hash**:
|
||||
```js
|
||||
function handleRouting() {
|
||||
const hash = window.location.hash; // e.g. "#/profile/npub123"
|
||||
if (hash.startsWith('#/profile/')) {
|
||||
const parts = hash.split('/');
|
||||
const npub = parts[2];
|
||||
loadProfileView(npub);
|
||||
} else {
|
||||
// default
|
||||
showView('recent');
|
||||
}
|
||||
}
|
||||
```
|
||||
3. **Load partial** (e.g. `profile-view.html`), then run logic to fetch user’s videos.
|
||||
|
||||
### **6.2 Future Sections**
|
||||
|
||||
- **Trending:** `/#/trending`
|
||||
- **Personalized:** `/#/for-you`
|
||||
- **Channel:** `/#/channel/someid`
|
||||
|
||||
Each route calls the correct partial load function, then re-initializes the data fetch and rendering.
|
||||
|
||||
---
|
||||
|
||||
## **7. Example of New `index.html` Structure**
|
||||
|
||||
A simplified version (in concept):
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- meta, styles, etc. -->
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Container for the entire site -->
|
||||
<div id="app" class="container mx-auto px-4 py-8 min-h-screen flex flex-col">
|
||||
|
||||
<!-- Header / Nav -->
|
||||
<header class="mb-8">
|
||||
<div class="flex items-start">
|
||||
<!-- Logo -->
|
||||
<img
|
||||
src="assets/svg/bitvid-logo-light-mode.svg"
|
||||
alt="BitVid Logo"
|
||||
class="h-16"
|
||||
/>
|
||||
</div>
|
||||
<!-- Buttons: login, logout, etc. -->
|
||||
</header>
|
||||
|
||||
<!-- Error and success containers, disclaimers, etc. -->
|
||||
<!-- ... -->
|
||||
|
||||
<!-- Main content container for dynamic views -->
|
||||
<main id="viewContainer" class="flex-grow">
|
||||
<!-- This is where we load something like most-recent-videos.html -->
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto pb-8 text-center">
|
||||
<!-- Footer links, contact info, IPNS, etc. -->
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Modal for video player -->
|
||||
<div id="modalContainer"></div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<!-- Example:
|
||||
<script type="module" src="js/viewManager.js"></script>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **8. Step-by-Step Migration**
|
||||
|
||||
1. **Create `views/most-recent-videos.html`**
|
||||
- Copy the entire grid section from your current `index.html` into this file.
|
||||
- Keep the same IDs (`videoList`, etc.) so the existing code that populates it still works.
|
||||
|
||||
2. **Replace the Original Grid in `index.html`**
|
||||
- Remove that big chunk of HTML, leaving just `<div id="viewContainer"></div>`.
|
||||
|
||||
3. **Load the Partial**
|
||||
- On your page initialization in `app.js` (or a new `viewManager.js`), run a function to fetch `most-recent-videos.html` and inject it into `viewContainer`.
|
||||
```js
|
||||
import { loadView } from './viewManager.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadView('views/most-recent-videos.html');
|
||||
// Then call your existing code to load videos, etc.
|
||||
});
|
||||
```
|
||||
|
||||
4. **Re-link Any JS Event Listeners**
|
||||
- After loading the partial, you might need to re-run any code that attaches event listeners to elements like `#videoList`. If your existing code is triggered on DOMContentLoaded, it should be aware that some elements appear later.
|
||||
|
||||
5. **Check Modal**
|
||||
- Make sure that your code for injecting `video-modal.html` or referencing it still points to the correct container (`modalContainer`). That part likely won’t change.
|
||||
|
||||
6. **Future Views**
|
||||
- Create additional partial files (e.g., `profile-view.html`, `trending.html`) using the same approach.
|
||||
- Expand your router logic to load different views based on the URL.
|
||||
|
||||
---
|
||||
|
||||
## **9. Potential Enhancements**
|
||||
|
||||
1. **Deep Linking**
|
||||
- Use URLs to direct users to specific videos, profiles, or sections.
|
||||
- For example, a link like `bitvid.com/#/profile/npub1234` opens that user’s channel.
|
||||
|
||||
2. **Templating Libraries**
|
||||
- If your views become complex, you might adopt a minimal client-side templating approach or even a lightweight framework.
|
||||
- For now, simple partial HTML with `fetch()` is enough.
|
||||
|
||||
3. **Reusable Components**
|
||||
- Create a folder for shared components (like your disclaimers, video card layout, or new sidebars).
|
||||
- Load them when needed, or store them as `<template>` elements that can be cloned into the DOM.
|
||||
|
||||
4. **Animation / Transition**
|
||||
- Add simple fade-in or slide-in effects when swapping views to make the UI more polished.
|
||||
|
||||
---
|
||||
|
||||
## **10. Summary**
|
||||
|
||||
By isolating the main grid into `most-recent-videos.html` (and doing the same for future content sections), you’ll keep `index.html` focused on the site-wide header, footer, and scripts. This approach makes it easy to add new views (trending, user profiles, etc.) without cluttering the main file. Your existing functionality—like the video modal, login system, and disclaimers—remains intact and available across all views. Over time, you can add routing for advanced sections such as personalized feeds or advanced channel pages, all while loading them into the same container.
|
||||
|
||||
In short, this plan preserves the existing UI elements that should remain global (header, footer) and relocates the content grid into a dedicated partial. It positions you to grow the platform with more views and a clean single-page architecture.
|
@@ -0,0 +1,261 @@
|
||||
# **bitvid: Enhanced Migration of Note Spec Logic**
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Currently, logic related to the **video note specification** and how data is structured (version, `isPrivate`, encryption placeholders, and so on) is scattered across three files:
|
||||
|
||||
1. **app.js**: Contains UI handling, form submissions, and some note structure (like `version`, `title`, `magnet`).
|
||||
2. **nostr.js**: Builds and edits Nostr events (`publishVideo`, `editVideo`, `deleteVideo`). It also holds methods for “fake encryption” and “fake decryption,” among other utilities.
|
||||
3. **webtorrent.js**: Mostly focuses on torrent streaming but does not handle note logic directly. It rarely touches the note data, so it may not need major restructuring.
|
||||
|
||||
To isolate note-spec-related operations, you can create a new file (for example, `bitvidNoteSpec.js`). This file will have all the code that deals with creating or parsing your event content fields (version, magnet link encryption, etc.). Then `app.js` and `nostr.js` can import those functions.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
- **Centralize the note specification**: Keep details like `version`, `deleted`, `isPrivate`, encryption, and decryption in one place.
|
||||
- **Simplify `app.js`**: Move form building/parsing to new spec-related functions. That way, `app.js` only handles UI and user actions.
|
||||
- **Streamline `nostr.js`**: Shift event creation logic into a function from the new note spec file. Nostr code then just calls that function, signs it, and publishes it.
|
||||
|
||||
---
|
||||
|
||||
## Proposed File: `bitvidNoteSpec.js`
|
||||
|
||||
This new file could export:
|
||||
|
||||
1. **Constants / Defaults** (for example, default `kind=30078`, default `version=2`, etc.).
|
||||
2. **Helper Functions**:
|
||||
- `buildNewNote(data, pubkey)`: Takes basic form data and returns a fully structured Nostr note (an object) ready to be signed.
|
||||
- `buildEditNote(originalEvent, updatedData)`: Merges old note content with new fields.
|
||||
- `softDeleteNote(originalEvent)`: Constructs a note with `deleted = true`.
|
||||
- `encryptMagnet(magnet)`, `decryptMagnet(magnet)`: Real or placeholder encryption functions.
|
||||
- `validateNoteContent(content)`: Ensures essential fields (title, magnet, mode, etc.) are present and valid.
|
||||
|
||||
Because you’re not implementing Version 3 yet, keep your existing version logic. If you do plan to adopt Version 3 later, the new file is where you’d add or change fields without scattering edits across multiple files.
|
||||
|
||||
---
|
||||
|
||||
### 1. Extracting Logic from `app.js`
|
||||
|
||||
In `app.js`, you have code in the `handleSubmit` method that constructs a `formData` object with fields like `version`, `title`, `magnet`, and so on. You can:
|
||||
|
||||
- Remove direct references to `version` or encryption from `handleSubmit`.
|
||||
- Instead, pass the raw form input to a function in `bitvidNoteSpec.js` named, for instance, `prepareNewNote(formInput, pubkey)`.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
// bitvidNoteSpec.js (simplified example)
|
||||
export function prepareNewNote(formInput, pubkey) {
|
||||
// Combine user inputs with defaults
|
||||
const isPrivate = formInput.isPrivate === true;
|
||||
const finalMagnet = isPrivate
|
||||
? encryptMagnet(formInput.magnet)
|
||||
: formInput.magnet;
|
||||
|
||||
return {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", generateUniqueDTag()]
|
||||
],
|
||||
content: JSON.stringify({
|
||||
version: formInput.version ?? 2,
|
||||
deleted: false,
|
||||
isPrivate,
|
||||
title: formInput.title,
|
||||
magnet: finalMagnet,
|
||||
thumbnail: formInput.thumbnail,
|
||||
description: formInput.description,
|
||||
mode: formInput.mode
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// app.js (handleSubmit excerpt)
|
||||
import { prepareNewNote } from "./bitvidNoteSpec.js";
|
||||
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.pubkey) {
|
||||
this.showError("Please login to post a video.");
|
||||
return;
|
||||
}
|
||||
|
||||
const formInput = {
|
||||
version: 2,
|
||||
title: document.getElementById("title")?.value.trim() || "",
|
||||
magnet: document.getElementById("magnet")?.value.trim() || "",
|
||||
thumbnail: document.getElementById("thumbnail")?.value.trim() || "",
|
||||
description: document.getElementById("description")?.value.trim() || "",
|
||||
mode: isDevMode ? "dev" : "live",
|
||||
isPrivate: this.isPrivateCheckbox.checked
|
||||
};
|
||||
|
||||
try {
|
||||
const eventToPublish = prepareNewNote(formInput, this.pubkey);
|
||||
await nostrClient.publishNote(eventToPublish);
|
||||
this.submitForm.reset();
|
||||
...
|
||||
} catch (err) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now `handleSubmit` is only handling the UI, while actual note-building moves to `bitvidNoteSpec.js`.
|
||||
|
||||
---
|
||||
|
||||
### 2. Extracting Logic from `nostr.js`
|
||||
|
||||
In `nostr.js`, you have methods like `publishVideo`, `editVideo`, and `deleteVideo`. They build the note content, sign it, and publish it. You can simplify them:
|
||||
|
||||
- **Rename** them to something more generic (e.g., `publishNote`, `updateNote`, `deleteNote`) if that aligns better with Nostr usage.
|
||||
- **Import** helper functions from `bitvidNoteSpec.js` to build or edit the actual content. That way, `nostr.js` doesn’t need to know about `version`, `deleted`, or encryption details.
|
||||
|
||||
For example, you might do this:
|
||||
|
||||
```js
|
||||
// nostr.js
|
||||
import { buildEditNote, buildDeleteNote } from "./bitvidNoteSpec.js";
|
||||
|
||||
class NostrClient {
|
||||
...
|
||||
async editVideo(originalEvent, updatedData, pubkey) {
|
||||
// 1) Build the note object using shared function
|
||||
const eventToPublish = buildEditNote(originalEvent, updatedData);
|
||||
|
||||
// 2) Sign and publish
|
||||
const signedEvent = await window.nostr.signEvent(eventToPublish);
|
||||
...
|
||||
}
|
||||
|
||||
async deleteVideo(originalEvent, pubkey) {
|
||||
const eventToPublish = buildDeleteNote(originalEvent);
|
||||
const signedEvent = await window.nostr.signEvent(eventToPublish);
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By delegating the actual note-building to `buildEditNote` and `buildDeleteNote`, you keep `nostr.js` focused on signing and relaying events.
|
||||
|
||||
---
|
||||
|
||||
### 3. Minimal Impact on `webtorrent.js`
|
||||
|
||||
`webtorrent.js` deals mostly with streaming and service workers. It does not appear to handle note building or encryption. You likely do not need to change anything there for this refactor, unless you want to move `fakeDecrypt` references. If you do, just import the spec’s encryption/decryption functions where needed.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Migration Plan
|
||||
|
||||
1. **Create `bitvidNoteSpec.js`:**
|
||||
- Place all code that deals with constructing, editing, or deleting your media note events.
|
||||
- Include minimal encryption/decryption functions if they are purely for magnet links.
|
||||
|
||||
2. **Update `app.js`:**
|
||||
- Remove direct references to building the final note object in `handleSubmit`.
|
||||
- Instead, gather user inputs, call a helper function from `bitvidNoteSpec.js` to produce the final note object, then pass that object to `nostrClient.publishNote` (or a similar method).
|
||||
|
||||
3. **Update `nostr.js`:**
|
||||
- Rename or refactor `publishVideo`, `editVideo`, and `deleteVideo` to call your new helper methods from `bitvidNoteSpec.js`.
|
||||
- Keep the Nostr signing and publishing logic inside `nostr.js`.
|
||||
|
||||
4. **Verify Data Flow:**
|
||||
- Confirm that after form submission, the final event object is built in `bitvidNoteSpec.js`, returned to `app.js`, and forwarded to `nostrClient`.
|
||||
- Ensure you can still subscribe to events and parse them without issues.
|
||||
|
||||
5. **Remove Redundant Code:**
|
||||
- Delete any leftover duplication in `app.js` or `nostr.js` relating to magnet encryption or note structuring.
|
||||
|
||||
6. **Test Thoroughly:**
|
||||
- Create, edit, and delete events to ensure everything behaves the same.
|
||||
- Confirm that private videos are still encrypted as expected.
|
||||
|
||||
---
|
||||
|
||||
## Example File Outline for `bitvidNoteSpec.js`
|
||||
|
||||
Below is a small outline showing how you might organize the new file. The actual details will depend on your existing code and future needs:
|
||||
|
||||
```js
|
||||
// bitvidNoteSpec.js
|
||||
|
||||
// A placeholder or real encryption method
|
||||
export function encryptMagnet(magnet) {
|
||||
return magnet.split("").reverse().join("");
|
||||
}
|
||||
|
||||
export function decryptMagnet(magnet) {
|
||||
return magnet.split("").reverse().join("");
|
||||
}
|
||||
|
||||
function generateUniqueDTag() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
// Build a brand-new note
|
||||
export function prepareNewNote(formInput, pubkey) {
|
||||
const isPrivate = formInput.isPrivate === true;
|
||||
const finalMagnet = isPrivate
|
||||
? encryptMagnet(formInput.magnet)
|
||||
: formInput.magnet;
|
||||
|
||||
return {
|
||||
kind: 30078,
|
||||
pubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", generateUniqueDTag()]
|
||||
],
|
||||
content: JSON.stringify({
|
||||
version: formInput.version ?? 2,
|
||||
deleted: false,
|
||||
isPrivate,
|
||||
title: formInput.title,
|
||||
magnet: finalMagnet,
|
||||
thumbnail: formInput.thumbnail,
|
||||
description: formInput.description,
|
||||
mode: formInput.mode ?? "live"
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// Build an edited note using original event data
|
||||
export function buildEditNote(originalEvent, updatedData) {
|
||||
// parse old content
|
||||
// combine with new fields
|
||||
// handle old vs new encryption
|
||||
// return the final event object
|
||||
}
|
||||
|
||||
// Build a deleted note
|
||||
export function buildDeleteNote(originalEvent) {
|
||||
// parse old content
|
||||
// set deleted=true, remove magnet, etc.
|
||||
// return the final event object
|
||||
}
|
||||
|
||||
// Validate content structure
|
||||
export function isValidNoteContent(content) {
|
||||
// check for required fields
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
By keeping these details in a single file, you won’t have to search through `app.js` or `nostr.js` whenever you need to tweak the note structure.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
Shifting all note spec logic into a dedicated file will make your codebase cleaner and set you up for easier upgrades down the road. You can proceed with the steps above, ensuring you keep each file (UI, Nostr communication, torrent streaming) focused on its primary role. Once done, you’ll be able to implement higher-level changes (like Version 3 or additional content fields) in one place without sifting through unrelated code.
|
@@ -0,0 +1,330 @@
|
||||
# **bitvid: Enhanced Nostr Video/Audio Note Specification: Version 3**
|
||||
|
||||
This document updates the existing Version 2 specification by adding optional fields for adult content, multiple categories, extended metadata, audio/podcast features, and more. These changes remain backward-compatible so Version 2 clients can continue to display basic information.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Nostr posts use **kind = 30078**, with a JSON structure stored in the `content` field. Version 3 retains all fields from Version 2 while adding new ones for richer functionality, easier filtering, and support for audio content.
|
||||
|
||||
---
|
||||
|
||||
## General Format
|
||||
|
||||
A typical note follows this format:
|
||||
|
||||
| **Field** | **Type** | **Description** |
|
||||
|---------------|----------------|--------------------------------------------------------------------|
|
||||
| `kind` | Integer | Fixed as `30078` for media-sharing events (video, music, etc.). |
|
||||
| `pubkey` | String | Public key of the note creator. |
|
||||
| `created_at` | Integer | Unix timestamp (seconds). |
|
||||
| `tags` | Array of Arrays| Includes metadata such as `["t", "video"]` or `["t", "music"]` and `["d", "<id>"]`. |
|
||||
| `content` | JSON String | JSON object containing the post data (detailed below). |
|
||||
|
||||
---
|
||||
|
||||
## Post Content for Version 3
|
||||
|
||||
| **Field** | **Type** | **Description** |
|
||||
|-------------------|------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| `version` | Integer | Now set to `3`. |
|
||||
| `deleted` | Boolean | Indicates soft deletion. |
|
||||
| `isPrivate` | Boolean | `true` if content is private (magnet and certain optional fields may be encrypted). |
|
||||
| `title` | String | Title of the media (video title, track title, podcast episode, etc.). |
|
||||
| `magnet` | String | Magnet link for the primary media file (encrypted if `isPrivate = true`). |
|
||||
| `extraMagnets` | Array (optional) | Additional magnet links for multiple resolutions/versions (commonly used for video but can be used for audio too). |
|
||||
| `thumbnail` | String (optional) | URL or magnet link to a thumbnail image. |
|
||||
| `description` | String (optional) | Description of the media. |
|
||||
| `mode` | String | Indicates `live` or `dev` mode for streaming or test scenarios. |
|
||||
| `adult` | Boolean (optional) | `true` if content is adult-only. Default is `false` or omitted. |
|
||||
| `categories` | Array (optional) | A list of categories or tags, e.g., `["comedy", "music"]`. |
|
||||
| `language` | String (optional) | Language code (e.g., `en`, `es`). |
|
||||
| `payment` | String (optional) | Monetization field, such as a Lightning address. |
|
||||
| `i18n` | Object (optional) | Holds internationalized fields (e.g., `{"title_en": "Hello", "title_es": "Hola"}`). |
|
||||
| `encryptedMeta` | Boolean (optional) | If `true`, indicates fields like `description` or `thumbnail` may be encrypted. |
|
||||
| **Audio/Podcast-Specific Fields** | | |
|
||||
| `contentType` | String (optional) | Type of media, e.g., `"video"`, `"music"`, `"podcast"`, `"audiobook"`. |
|
||||
| `albumName` | String (optional) | Name of the album (if part of a music album). |
|
||||
| `trackNumber` | Integer (optional) | Track number in an album. |
|
||||
| `trackTitle` | String (optional) | Track title if different from `title`. |
|
||||
| `podcastSeries` | String (optional) | Name of the podcast series. |
|
||||
| `seasonNumber` | Integer (optional) | Season number for a podcast. |
|
||||
| `episodeNumber` | Integer (optional) | Episode number for a podcast series. |
|
||||
| `duration` | Integer (optional) | Duration in seconds (useful for audio or video players). |
|
||||
| `artistName` | String (optional) | Main artist or presenter. |
|
||||
| `contributors` | Array (optional) | List of additional contributors, e.g., `[{"name": "John", "role": "Producer"}]`. |
|
||||
| `audioQuality` | Array (optional) | Multiple magnet links with different audio formats or bitrates. Example: `[{"quality": "lossless", "magnet": "..."}]`.|
|
||||
| `playlist` | Array (optional) | Array of magnet links or references forming a playlist (useful for albums or sets). |
|
||||
|
||||
> **Note**: All fields except `version`, `deleted`, `isPrivate`, `title`, `magnet`, and `mode` are optional. You can omit fields that are not relevant to your post.
|
||||
|
||||
---
|
||||
|
||||
## Tagging
|
||||
|
||||
### `tags`
|
||||
- Purpose: Quick lookups for content type, unique IDs, or user-defined categories.
|
||||
- Examples:
|
||||
- `["t", "video"]` — Type indicator for videos.
|
||||
- `["t", "music"]` — Type indicator for audio/music.
|
||||
- `["d", "unique-identifier"]` — Unique ID for direct references.
|
||||
- `["adult", "true"]` — Optional. Some clients may store adult flags here rather than in `content`.
|
||||
|
||||
No changes are required for Version 3 tagging, but more detailed tags (e.g., `"music"`, `"podcast"`, or `"audiobook"`) can be used to help clients sort or filter specific media.
|
||||
|
||||
---
|
||||
|
||||
## Behavior Based on Privacy
|
||||
|
||||
### Public Posts
|
||||
- `isPrivate = false`.
|
||||
- Magnet and other fields are in plaintext.
|
||||
- Visible to all users.
|
||||
|
||||
### Private Posts
|
||||
- `isPrivate = true`.
|
||||
- Main magnet link (and optional fields like `description` or `thumbnail`) are encrypted with the preferred encryption method.
|
||||
- `extraMagnets` may also be encrypted if the user chooses.
|
||||
|
||||
---
|
||||
|
||||
## New or Expanded Features in Version 3
|
||||
|
||||
1. **Adult Content Flag**
|
||||
- `adult: true` marks content as adult-only.
|
||||
- Clients can filter this by default.
|
||||
|
||||
2. **Multiple Categories**
|
||||
- `categories` can hold several entries, e.g. `["comedy", "music"]`.
|
||||
- Advanced filtering is possible.
|
||||
|
||||
3. **Extended Metadata**
|
||||
- Fields like `language`, `payment`, and custom data let creators provide more details.
|
||||
- Optional, so older clients ignore what they do not recognize.
|
||||
|
||||
4. **Multi-Resolution or Multi-Format Links**
|
||||
- `extraMagnets` for video variants.
|
||||
- `audioQuality` for different audio formats or bitrates.
|
||||
|
||||
5. **Internationalization (`i18n`)**
|
||||
- Include translated or localized titles, descriptions, and more.
|
||||
|
||||
6. **Encrypted Metadata**
|
||||
- `encryptedMeta: true` to signal that certain fields (e.g., `description`, `thumbnail`) may be encrypted.
|
||||
|
||||
7. **Audio, Podcast, Audiobook Support**
|
||||
- New optional fields: `contentType`, `albumName`, `trackNumber`, `podcastSeries`, etc.
|
||||
- `playlist` can reference multiple tracks under one post.
|
||||
|
||||
---
|
||||
|
||||
Below is a section you can drop into your Version 3 specification. It expands on filtering logic for adult content and outlines a basic submission flow to help implementers. Feel free to adjust headings or formatting as needed.
|
||||
|
||||
---
|
||||
|
||||
## Filtering Logic and Submission Flow
|
||||
|
||||
### Filtering Logic
|
||||
|
||||
1. **Adult Content Detection**
|
||||
- To mark posts as adult-only, set `adult = true` inside the content object (e.g., `"adult": true`) or include `["adult", "true"]` in the `tags` array.
|
||||
- Clients should exclude these posts by default unless users explicitly enable adult content in their settings or preferences.
|
||||
|
||||
2. **Category-Based Filtering**
|
||||
- If `categories` is present (e.g., `["comedy", "music"]`), clients can allow users to search or filter based on these entries.
|
||||
- Alternatively, if tags such as `["t", "video"]` or `["category", "comedy"]` are used, handle them in the same way by grouping or filtering.
|
||||
|
||||
3. **Default UI Behavior**
|
||||
- Provide a toggle or checkbox for “Show Adult Content.” If unchecked, do not display posts marked as adult content.
|
||||
- Offer a dropdown or checkbox list for category filters. Only display posts that match selected categories.
|
||||
|
||||
4. **Edge Cases**
|
||||
- If both `adult = true` and one or more categories are set, the post should remain hidden unless the user opts into adult content, even if it matches other selected categories.
|
||||
- When a post omits `adult`, assume it is not adult content unless the user or platform policy states otherwise.
|
||||
|
||||
### Submission Flow Example
|
||||
|
||||
Below is a simple illustration of how a client could guide users when creating or editing a post:
|
||||
|
||||
1. **User Fills Out the Form**
|
||||
- Title, magnet link, description, and any other relevant fields.
|
||||
|
||||
2. **Set the Version**
|
||||
- When adding new Version 3 features (e.g., multiple categories, adult flag), ensure `version` is set to `3`.
|
||||
- If editing a Version 2 post to add adult or category data, update `version` to `3`.
|
||||
|
||||
3. **Adult Content Checkbox**
|
||||
- If the user marks the post as adult-only, set `"adult": true` in `content` or include `["adult", "true"]` in `tags`.
|
||||
- If not marked, leave the field out or set it to `false`.
|
||||
|
||||
4. **Category Selection**
|
||||
- Let users select categories (e.g., “comedy,” “music,” “gaming”). Store them in `categories` or as tags (`["category", "comedy"]`).
|
||||
|
||||
5. **Publish**
|
||||
- Create the Nostr event with `kind = 30078`.
|
||||
- In the `content` field (as JSON), include the user-provided data plus the new fields (e.g., `adult`, `categories`).
|
||||
- In the `tags` array, ensure `["t", "video"]` or any other relevant tags. Add `["adult", "true"]` if needed.
|
||||
|
||||
6. **Post-Submission**
|
||||
- When the post is published, clients or relays can immediately filter or categorize it according to the adult flag and categories.
|
||||
- Users who opted in to adult content see the post, while others do not.
|
||||
|
||||
This process helps developers maintain a consistent approach to adult content handling, category-based filtering, and integration of new Version 3 features.
|
||||
|
||||
---
|
||||
|
||||
## Editing and Deleting Posts
|
||||
|
||||
### Editing
|
||||
- Retain the same `["d", "<unique-id>"]` tag.
|
||||
- Update or add new fields (e.g., adding `extraMagnets` or switching `contentType` to `"music"`).
|
||||
- When moving from `version=2` to `version=3`, older clients ignore new fields but still see basic fields like `title` and `magnet`.
|
||||
|
||||
### Deletion
|
||||
- Mark `deleted: true` in the `content`.
|
||||
- Remove or encrypt sensitive fields (e.g., `magnet`, `description`) so they are not visible to others.
|
||||
|
||||
---
|
||||
|
||||
## Example Post: Version 3 (Video)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "npub1...",
|
||||
"created_at": 1700000000,
|
||||
"tags": [
|
||||
["t", "video"],
|
||||
["d", "unique-identifier"]
|
||||
// ["adult", "true"] // optional if storing adult info in tags
|
||||
],
|
||||
"content": "{
|
||||
\"version\": 3,
|
||||
\"deleted\": false,
|
||||
\"isPrivate\": false,
|
||||
\"adult\": true,
|
||||
\"categories\": [\"comedy\", \"pranks\"],
|
||||
\"title\": \"Funny Prank Video\",
|
||||
\"magnet\": \"magnet:?xt=urn:btih:examplehash\",
|
||||
\"extraMagnets\": [
|
||||
\"magnet:?xt=urn:btih:examplehashHD\",
|
||||
\"magnet:?xt=urn:btih:examplehashMobile\"
|
||||
],
|
||||
\"thumbnail\": \"https://example.com/thumb.jpg\",
|
||||
\"description\": \"An adult-oriented prank video.\",
|
||||
\"mode\": \"live\",
|
||||
\"language\": \"en\",
|
||||
\"payment\": \"alice@getalby.com\",
|
||||
\"i18n\": {
|
||||
\"title_es\": \"Video de broma chistosa\",
|
||||
\"description_es\": \"Un video de bromas para adultos.\"
|
||||
}
|
||||
}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Post: Version 3 (Music Track)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "npub1...",
|
||||
"created_at": 1700000000,
|
||||
"tags": [
|
||||
["t", "music"],
|
||||
["d", "unique-id-for-track"]
|
||||
],
|
||||
"content": "{
|
||||
\"version\": 3,
|
||||
\"deleted\": false,
|
||||
\"isPrivate\": false,
|
||||
\"contentType\": \"music\",
|
||||
\"title\": \"Amazing Track\",
|
||||
\"trackTitle\": \"Amazing Track\",
|
||||
\"albumName\": \"Great Album\",
|
||||
\"trackNumber\": 1,
|
||||
\"artistName\": \"Awesome Artist\",
|
||||
\"duration\": 300,
|
||||
\"magnet\": \"magnet:?xt=urn:btih:examplehash\",
|
||||
\"audioQuality\": [
|
||||
{
|
||||
\"quality\": \"lossless\",
|
||||
\"magnet\": \"magnet:?xt=urn:btih:losslessHash\"
|
||||
},
|
||||
{
|
||||
\"quality\": \"mp3\",
|
||||
\"magnet\": \"magnet:?xt=urn:btih:mp3Hash\"
|
||||
}
|
||||
],
|
||||
\"description\": \"This is an amazing track from the Great Album.\",
|
||||
\"categories\": [\"music\", \"pop\"],
|
||||
\"contributors\": [
|
||||
{ \"name\": \"John Doe\", \"role\": \"Producer\" },
|
||||
{ \"name\": \"Jane Smith\", \"role\": \"Writer\" }
|
||||
]
|
||||
}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Post: Version 3 (Playlist or Album)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "npub1...",
|
||||
"created_at": 1700000000,
|
||||
"tags": [
|
||||
["t", "playlist"],
|
||||
["d", "unique-id-for-playlist"]
|
||||
],
|
||||
"content": "{
|
||||
\"version\": 3,
|
||||
\"deleted\": false,
|
||||
\"isPrivate\": false,
|
||||
\"title\": \"Chill Vibes Playlist\",
|
||||
\"contentType\": \"music\",
|
||||
\"playlist\": [
|
||||
\"magnet:?xt=urn:btih:hashTrack1\",
|
||||
\"magnet:?xt=urn:btih:hashTrack2\",
|
||||
\"magnet:?xt=urn:btih:hashTrack3\"
|
||||
],
|
||||
\"description\": \"Curated set of relaxing tracks.\",
|
||||
\"categories\": [\"music\", \"chill\"]
|
||||
}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Transition Plan from Version 2 to 3
|
||||
|
||||
1. **Backward Compatibility**
|
||||
- All new fields are optional.
|
||||
- Version 2 clients still see basic fields like `title` and `magnet`.
|
||||
|
||||
2. **Gradual Roll-Out**
|
||||
- Existing posts remain at `version=2`.
|
||||
- Users can adopt `version=3` when editing or creating new posts with extra features.
|
||||
|
||||
3. **Client Handling**
|
||||
- If a client detects `version=3`, it can display or parse additional fields.
|
||||
- Otherwise, it treats posts with older logic.
|
||||
|
||||
4. **Relay Compatibility**
|
||||
- The same `kind=30078` is used.
|
||||
- Relays generally store the data as-is.
|
||||
|
||||
5. **Potential Breaking Changes**
|
||||
- If certain new fields are mandatory in your app, older clients may not parse them.
|
||||
- Keep new features optional where possible.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Version 3 extends the specification to include adult flags, multi-category tagging, multi-resolution magnet links, internationalization, and now audio-specific fields like `contentType = "music"`, `podcastSeries`, and `playlist`. Older clients can keep working without error, while new clients can take advantage of these extra fields for better organization and search.
|
@@ -0,0 +1,149 @@
|
||||
# **bitvid: Enhanced Profile/Channel Views Specification**
|
||||
|
||||
## **Overview**
|
||||
We aim to integrate a multi-view system within `index.html`, allowing smooth navigation between views like the home grid and user profiles. This will leverage JavaScript for dynamic DOM manipulation, maintaining a consistent layout (header, footer, future sidebar) across views.
|
||||
|
||||
---
|
||||
|
||||
## **Structure and Navigation**
|
||||
### **Navigation Logic**
|
||||
1. **Default View (Home Grid):**
|
||||
- A grid showcasing all videos.
|
||||
- Acts as the primary landing page.
|
||||
|
||||
2. **Profile View:**
|
||||
- A user's profile page containing:
|
||||
- Profile banner and information.
|
||||
- Action buttons (Subscribe, Share, Block).
|
||||
- Videos grid (only videos posted by the user).
|
||||
|
||||
3. **Templating System:**
|
||||
- Use dynamic DOM manipulation to switch views without reloading the page.
|
||||
- Structure each "view" as a reusable container rendered based on URL or state.
|
||||
|
||||
---
|
||||
|
||||
## **Dynamic Routing**
|
||||
### **Route Handling**
|
||||
- Use the `hashchange` or `pushState` method to detect and handle navigation.
|
||||
- Route format:
|
||||
- `/#home`: Default home grid view.
|
||||
- `/#profile/{npub}`: Profile view of a specific user, determined by `{npub}`.
|
||||
|
||||
### **Implementation Plan**
|
||||
- Use JavaScript to parse the `window.location.hash` or `window.location.pathname` and determine which view to render.
|
||||
- Example:
|
||||
```javascript
|
||||
const renderView = () => {
|
||||
const hash = window.location.hash;
|
||||
if (hash.startsWith("#profile")) {
|
||||
const npub = hash.split("/")[1];
|
||||
loadProfileView(npub);
|
||||
} else {
|
||||
loadHomeGrid();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("hashchange", renderView);
|
||||
window.addEventListener("load", renderView);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **View Templates**
|
||||
### **Template System**
|
||||
1. **Home Grid Template:**
|
||||
- Container: `#homeGrid`.
|
||||
- Dynamically populate the grid with all videos fetched from Nostr relays.
|
||||
|
||||
2. **Profile Template:**
|
||||
- Container: `#profileView`.
|
||||
- Display user details and their videos based on the `npub`.
|
||||
|
||||
3. **Shared Components:**
|
||||
- Header, footer, and optional sidebar remain static.
|
||||
- Use `display: none` and `block` to toggle view visibility.
|
||||
|
||||
### **HTML Structure**
|
||||
Add placeholders for different views in `index.html`:
|
||||
```html
|
||||
<div id="homeGrid" class="view hidden">
|
||||
<!-- Home grid content -->
|
||||
</div>
|
||||
|
||||
<div id="profileView" class="view hidden">
|
||||
<header class="profile-header">
|
||||
<img id="profileBanner" src="" alt="Banner" />
|
||||
<img id="profileImage" src="" alt="Profile" />
|
||||
<h1 id="profileName"></h1>
|
||||
<p id="profileBio"></p>
|
||||
<button id="subscribeBtn">Subscribe</button>
|
||||
<button id="shareProfileBtn">Share</button>
|
||||
<button id="blockProfileBtn">Block</button>
|
||||
</header>
|
||||
<div id="profileVideos" class="videos-grid">
|
||||
<!-- User's videos -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Functionality**
|
||||
### **Profile Fetching**
|
||||
- Use Nostr protocol (`kind 0`) to fetch profile details.
|
||||
- Display:
|
||||
- Profile picture, name, bio, and website link.
|
||||
- Action buttons for Subscribe, Share, and Block.
|
||||
|
||||
### **Videos Fetching**
|
||||
- Fetch videos (`kind 30078`) filtered by the user's `npub`.
|
||||
|
||||
### **Subscriptions**
|
||||
- Use `kind 30002` to manage the subscription list:
|
||||
- Subscribe: Add user to the list.
|
||||
- Unsubscribe: Remove user.
|
||||
|
||||
### **Implementation Example**
|
||||
```javascript
|
||||
const loadProfileView = async (npub) => {
|
||||
// Fetch profile details
|
||||
const profileEvent = await nostrClient.fetchProfile(npub);
|
||||
const { name, picture, about } = profileEvent.content;
|
||||
|
||||
// Update profile view
|
||||
document.getElementById("profileImage").src = picture;
|
||||
document.getElementById("profileName").textContent = name;
|
||||
document.getElementById("profileBio").textContent = about;
|
||||
|
||||
// Fetch and display user videos
|
||||
const userVideos = await nostrClient.fetchVideosByNpub(npub);
|
||||
renderVideos(userVideos, "profileVideos");
|
||||
|
||||
// Show profile view
|
||||
showView("profileView");
|
||||
};
|
||||
|
||||
const showView = (viewId) => {
|
||||
document.querySelectorAll(".view").forEach((view) => {
|
||||
view.classList.toggle("hidden", view.id !== viewId);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Unique Profile URLs**
|
||||
- Format: `https://bitvid.network/#profile/{npub}`.
|
||||
- Navigation to this URL will directly load the profile view.
|
||||
- Use `history.pushState` or `location.hash` to set the URL.
|
||||
|
||||
---
|
||||
|
||||
## **Next Steps**
|
||||
- **Integrate Dynamic Routing**: Update `app.js` with route handling for views.
|
||||
- **Refactor HTML**: Add placeholders for views in `index.html`.
|
||||
- **Build Profile Fetching Logic**: Use Nostr client to fetch and display user details dynamically.
|
||||
- **Enhance UX**: Smooth transitions between views with CSS animations.
|
||||
|
||||
This setup achieves a modular SPA-like architecture while keeping development lightweight and aligned with your project’s goals.
|
@@ -0,0 +1,287 @@
|
||||
# **bitvid: Enhanced Block, Subscription, and Reporting Specification**
|
||||
|
||||
This document describes how to implement **Block Lists**, **Subscription Lists**, and **Reporting** (NIP-56) for a Nostr-based video platform such as bitvid. It covers how users can manage their own moderation tools and how the platform can apply additional checks.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Subscription List Specification](#subscription-list-specification)
|
||||
1.1 [Purpose](#purpose)
|
||||
1.2 [Event Kind](#event-kind)
|
||||
1.3 [JSON Structure](#json-structure)
|
||||
1.4 [Example](#example)
|
||||
1.5 [Features](#features)
|
||||
|
||||
2. [Block List Specification](#block-list-specification)
|
||||
2.1 [Purpose](#purpose-1)
|
||||
2.2 [Event Kind](#event-kind-1)
|
||||
2.3 [JSON Structure](#json-structure-1)
|
||||
2.4 [Examples](#examples)
|
||||
2.5 [Features](#features-1)
|
||||
|
||||
3. [Reporting with NIP-56](#reporting-with-nip-56)
|
||||
3.1 [Overview](#overview)
|
||||
3.2 [Report Types](#report-types)
|
||||
3.3 [Example Events](#example-events)
|
||||
3.4 [Client and Relay Behavior](#client-and-relay-behavior)
|
||||
|
||||
4. [Implementation Details](#implementation-details)
|
||||
4.1 [Replaceable Events](#replaceable-events)
|
||||
4.2 [Encryption](#encryption)
|
||||
4.3 [Fetching User Lists](#fetching-user-lists)
|
||||
4.4 [Pushing Updates to Relays](#pushing-updates-to-relays)
|
||||
|
||||
5. [UI Integration](#ui-integration)
|
||||
5.1 [Subscription Management](#subscription-management)
|
||||
5.2 [Block Management](#block-management)
|
||||
5.3 [Report Management](#report-management)
|
||||
|
||||
6. [Future Considerations](#future-considerations)
|
||||
|
||||
---
|
||||
|
||||
## Subscription List Specification
|
||||
|
||||
### Purpose
|
||||
A **Subscription List** lets users follow video creators independently of their main “following” list on Nostr. It supports categorization and can be made private via encryption.
|
||||
|
||||
### Event Kind
|
||||
- **Kind**: `30002`
|
||||
- **Description**: “Video Subscription List” (inspired by NIP-51 but for custom lists)
|
||||
|
||||
### JSON Structure
|
||||
**Public Tags**
|
||||
- `["p", <pubkey>]`: Public keys of creators to follow
|
||||
- `["t", <category>]`: Optional categories (e.g., “comedy,” “music”)
|
||||
|
||||
**Private Tags**
|
||||
- Encrypted list of subscriptions using NIP-04
|
||||
|
||||
**Metadata**
|
||||
- Additional information like category names, custom labels, etc.
|
||||
|
||||
### Example
|
||||
```json
|
||||
{
|
||||
"kind": 30002,
|
||||
"tags": [
|
||||
["d", "favorite-creators"],
|
||||
["p", "npub1creator1pubkey"],
|
||||
["p", "npub1creator2pubkey"],
|
||||
["t", "comedy"],
|
||||
["t", "science"]
|
||||
],
|
||||
"content": "Encrypted list content for private subscriptions",
|
||||
"created_at": 1735689600,
|
||||
"pubkey": "your-public-key",
|
||||
"id": "event-id"
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
1. **Categorization**
|
||||
Users can group subscribed creators by genres or topics.
|
||||
2. **Privacy Options**
|
||||
Private subscriptions can be hidden by encrypting tags.
|
||||
3. **Replaceable Event**
|
||||
Users can update their list by publishing a new event with the same `d` tag (e.g., `["d","favorite-creators"]`) and a later `created_at`.
|
||||
|
||||
---
|
||||
|
||||
## Block List Specification
|
||||
|
||||
### Purpose
|
||||
A **Block List** gives users the ability to mute or block specific creators or users. It supports both public reasons (tags) and private reasons (encrypted content).
|
||||
|
||||
### Event Kind
|
||||
- **Kind**: `10001`
|
||||
- **Description**: “Block or Mute List” (per NIP-51)
|
||||
|
||||
### JSON Structure
|
||||
**Public Tags**
|
||||
- `["p", <pubkey>]`: Public keys of blocked users
|
||||
- `["r", <reason>]`: Optional reasons (spam, harassment, etc.)
|
||||
|
||||
**Private Tags**
|
||||
- Encrypted details for blocking, using NIP-04
|
||||
|
||||
### Examples
|
||||
|
||||
#### Public Block List
|
||||
```json
|
||||
{
|
||||
"kind": 10001,
|
||||
"tags": [
|
||||
["p", "npub1blockeduser1pubkey", "reason", "spam"],
|
||||
["p", "npub1blockeduser2pubkey", "reason", "harassment"]
|
||||
],
|
||||
"content": "",
|
||||
"created_at": 1735689600,
|
||||
"pubkey": "your-public-key",
|
||||
"id": "event-id"
|
||||
}
|
||||
```
|
||||
|
||||
#### Private Block List
|
||||
```json
|
||||
{
|
||||
"kind": 10001,
|
||||
"tags": [
|
||||
["p", "npub1blockeduser1pubkey"],
|
||||
["p", "npub1blockeduser2pubkey"]
|
||||
],
|
||||
"content": "Encrypted reasons for blocking (e.g., personal dispute info)",
|
||||
"created_at": 1735689600,
|
||||
"pubkey": "your-public-key",
|
||||
"id": "event-id"
|
||||
}
|
||||
```
|
||||
|
||||
### Features
|
||||
1. **Integration**
|
||||
- Offer a “Block/Unblock” button in the UI.
|
||||
- Provide a page to manage blocks.
|
||||
2. **Filtering**
|
||||
- Automatically exclude blocked users from feed results.
|
||||
3. **Categorization**
|
||||
- Tag reasons for blocking, such as spam or harassment.
|
||||
4. **Privacy Options**
|
||||
- Keep certain block reasons encrypted if needed.
|
||||
|
||||
---
|
||||
|
||||
## Reporting with NIP-56
|
||||
|
||||
### Overview
|
||||
NIP-56 introduces a **kind `1984`** event that flags content or profiles as objectionable. It’s a flexible way to let users or relays see reports and decide on any actions.
|
||||
|
||||
### Report Types
|
||||
The `p` tag references a pubkey, and the `e` tag references a note. The third element of the tag can be:
|
||||
- `nudity`
|
||||
- `malware`
|
||||
- `profanity`
|
||||
- `illegal`
|
||||
- `spam`
|
||||
- `impersonation`
|
||||
- `other`
|
||||
|
||||
### Example Events
|
||||
|
||||
```jsonc
|
||||
// Reporting a user for nudity
|
||||
{
|
||||
"kind": 1984,
|
||||
"tags": [
|
||||
["p", "<pubkey>", "nudity"]
|
||||
],
|
||||
"content": "Optional comment or additional info.",
|
||||
"created_at": 1735689600,
|
||||
"pubkey": "your-public-key",
|
||||
"id": "report-event-id"
|
||||
}
|
||||
|
||||
// Reporting a note as illegal
|
||||
{
|
||||
"kind": 1984,
|
||||
"tags": [
|
||||
["e", "<eventId>", "illegal"],
|
||||
["p", "<pubkey>"]
|
||||
],
|
||||
"content": "User is breaking local laws.",
|
||||
"created_at": 1735689600,
|
||||
"pubkey": "your-public-key",
|
||||
"id": "report-event-id"
|
||||
}
|
||||
```
|
||||
|
||||
### Client and Relay Behavior
|
||||
- **Clients**
|
||||
- May choose to highlight or hide reported notes if enough trusted users report them.
|
||||
- Could display “flagged content” warnings based on the user’s web-of-trust.
|
||||
- **Relays**
|
||||
- Not mandated to do anything automatically.
|
||||
- An admin could manually block content if a trusted source files many valid reports.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Replaceable Events
|
||||
- **Subscription Lists** (`30002`) and **Block Lists** (`10001`) can be implemented as replaceable events by using a deterministic `d` tag.
|
||||
- For example, `["d","my-blocklist"]` ensures older events with the same `d` are replaced when new ones arrive.
|
||||
|
||||
### Encryption
|
||||
- Use [NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) for any private or sensitive data in `content` or tags.
|
||||
- Public reasons for blocking or reporting can remain in tags or clear text.
|
||||
|
||||
### Fetching User Lists
|
||||
1. **On Login**
|
||||
- Query each relay for events matching:
|
||||
```jsonc
|
||||
{
|
||||
"kinds": [10001, 30002],
|
||||
"authors": [<user-pubkey>]
|
||||
}
|
||||
```
|
||||
- Merge and deduplicate block/subscription lists from the resulting events.
|
||||
2. **Processing**
|
||||
- For blocklists, unify all “p” tags into a single set of blocked pubkeys.
|
||||
- For subscriptions, unify “p” tags or categories if the user merges multiple lists.
|
||||
|
||||
### Pushing Updates to Relays
|
||||
1. **Create Event**
|
||||
- Include kind, pubkey, timestamp, tags, and optional content.
|
||||
2. **Sign Event**
|
||||
- Use `window.nostr.signEvent` or your own signing library.
|
||||
3. **Publish**
|
||||
- Send the signed event to each configured relay.
|
||||
- Example in pseudocode:
|
||||
```javascript
|
||||
const signedEvent = await signEvent(myEvent);
|
||||
for (const relay of relays) {
|
||||
pool.publish([relay], signedEvent);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Integration
|
||||
|
||||
### Subscription Management
|
||||
- **List View**
|
||||
- Show subscribed creators with categories if applicable.
|
||||
- **Add/Remove**
|
||||
- Allow users to add an `npub` to their list.
|
||||
- Save as a new replaceable event.
|
||||
|
||||
### Block Management
|
||||
- **Block/Unblock Button**
|
||||
- Quick action to add or remove a pubkey from the user’s blocklist.
|
||||
- **Blocklist Editor**
|
||||
- Display current blocks.
|
||||
- Optionally show reasons (public or private).
|
||||
- Publish changes via a new replaceable event.
|
||||
|
||||
### Report Management
|
||||
- **Report Button**
|
||||
- Attached to each video or note.
|
||||
- Triggers a “kind:1984” event with the chosen category (spam, nudity, etc.).
|
||||
- **Displaying Reports**
|
||||
- Optionally show how many “trusted friends” have reported a user/note.
|
||||
- Let users decide whether to hide or blur content with certain flags.
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
1. **Paid Subscriptions**
|
||||
- Could layer subscription tiers on top of `30002` events.
|
||||
2. **Global Block Lists**
|
||||
- Let users publish or subscribe to a curated blocklist (or share one in a group).
|
||||
3. **Web of Trust**
|
||||
- Filter reports based on which reporters a user trusts.
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
By leveraging Nostr event kinds (`30002` for subscriptions, `10001` for blocks, and `1984` for reports), bitvid can maintain a decentralized, user-controlled moderation system. Users can sync their lists across devices through relays, while administrators can choose how to handle flagged content on a platform level. This approach keeps moderation flexible and transparent.
|
@@ -0,0 +1,243 @@
|
||||
# **bitvid: Enhanced Video Comment System Specification**
|
||||
|
||||
### **Objective**
|
||||
To implement a decentralized comment system for videos shared on a Nostr-based platform, combining multiple NIPs to provide structured, interactive, and scalable functionality for video comments, reactions, live discussions, and metadata tagging.
|
||||
|
||||
---
|
||||
|
||||
### **Features**
|
||||
1. **Post Comments**:
|
||||
- Users can post comments on videos.
|
||||
- Comments are associated with a specific video event using the `e` tag.
|
||||
|
||||
2. **Structured Threading**:
|
||||
- Comments support threading by referencing parent comments using NIP-27 conventions.
|
||||
- Threaded replies are visually nested.
|
||||
|
||||
3. **Reactions**:
|
||||
- Users can react to comments (e.g., upvote, downvote) using NIP-25.
|
||||
|
||||
4. **Live Discussions**:
|
||||
- Real-time public chat using NIP-28 for live video events.
|
||||
|
||||
5. **Optional Metadata**:
|
||||
- Extra metadata fields are included for user preferences or administrative tags using NIP-24.
|
||||
|
||||
6. **Real-Time Updates**:
|
||||
- Comments, reactions, and live chats update in real-time using Nostr subscriptions.
|
||||
|
||||
7. **Moderation**:
|
||||
- Support for flagging or hiding inappropriate comments.
|
||||
|
||||
8. **Privacy**:
|
||||
- Encrypted comments for private videos (optional).
|
||||
|
||||
---
|
||||
|
||||
### **Technical Specifications**
|
||||
|
||||
#### **1. Event Structure**
|
||||
Each component (comments, reactions, live chat) is represented as a Nostr event.
|
||||
|
||||
##### **Comment Event**:
|
||||
```json
|
||||
{
|
||||
"kind": 1311,
|
||||
"pubkey": "abcdef1234567890...",
|
||||
"created_at": 1675000000,
|
||||
"tags": [
|
||||
["e", "video-event-id"], // Reference to the video
|
||||
["e", "parent-comment-id"], // Reference to the parent comment (optional, for replies)
|
||||
["p", "commenter-pubkey"] // Optional: commenter pubkey
|
||||
],
|
||||
"content": "This is a great video!"
|
||||
}
|
||||
```
|
||||
|
||||
##### **Reaction Event**:
|
||||
```json
|
||||
{
|
||||
"kind": 7,
|
||||
"pubkey": "abcdef1234567890...",
|
||||
"created_at": 1675000000,
|
||||
"tags": [
|
||||
["e", "comment-event-id"], // Reference to the comment being reacted to
|
||||
["p", "reactor-pubkey"]
|
||||
],
|
||||
"content": "+" // + for upvote, - for downvote
|
||||
}
|
||||
```
|
||||
|
||||
##### **Live Chat Event**:
|
||||
```json
|
||||
{
|
||||
"kind": 42,
|
||||
"pubkey": "abcdef1234567890...",
|
||||
"created_at": 1675000000,
|
||||
"tags": [
|
||||
["e", "video-event-id"], // Reference to the live video
|
||||
["p", "participant-pubkey"]
|
||||
],
|
||||
"content": "What a great discussion!"
|
||||
}
|
||||
```
|
||||
|
||||
##### **Metadata (Optional)**:
|
||||
Metadata tags are added to comments or live chat events as needed:
|
||||
```json
|
||||
{
|
||||
"kind": 1311,
|
||||
"pubkey": "abcdef1234567890...",
|
||||
"created_at": 1675000000,
|
||||
"tags": [
|
||||
["e", "video-event-id"],
|
||||
["m", "featured-comment"], // Example metadata tag
|
||||
["a", "admin-tag"] // Administrative tag
|
||||
],
|
||||
"content": "Highlighted comment for this video."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Implementation Details**
|
||||
|
||||
#### **1. Posting a Comment**
|
||||
To post a comment:
|
||||
1. The client constructs a comment event using NIP-22 for `created_at` validation.
|
||||
2. The event includes references to the video (mandatory) and parent comments (optional).
|
||||
3. The event is signed and published to relays.
|
||||
|
||||
##### API Example:
|
||||
```javascript
|
||||
async function postComment(videoId, commentText, parentCommentId = null) {
|
||||
const event = {
|
||||
kind: 1311,
|
||||
pubkey: userPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', videoId],
|
||||
...(parentCommentId ? [['e', parentCommentId]] : []),
|
||||
],
|
||||
content: commentText
|
||||
};
|
||||
|
||||
const signedEvent = await nostrClient.signEvent(event);
|
||||
await nostrClient.pool.publish(nostrClient.relays, signedEvent);
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. Reacting to a Comment**
|
||||
Reactions use NIP-25.
|
||||
|
||||
##### API Example:
|
||||
```javascript
|
||||
async function reactToComment(commentId, reaction) {
|
||||
const event = {
|
||||
kind: 7,
|
||||
pubkey: userPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['e', commentId]],
|
||||
content: reaction // Use "+" for upvote, "-" for downvote
|
||||
};
|
||||
|
||||
const signedEvent = await nostrClient.signEvent(event);
|
||||
await nostrClient.pool.publish(nostrClient.relays, signedEvent);
|
||||
}
|
||||
```
|
||||
|
||||
#### **3. Fetching Comments**
|
||||
Comments are retrieved using `REQ` messages filtered by the `e` tag for the video’s event ID and optionally by parent comment IDs for threading.
|
||||
|
||||
##### API Example:
|
||||
```javascript
|
||||
async function fetchComments(videoId) {
|
||||
const filter = {
|
||||
kinds: [1311],
|
||||
'#e': [videoId],
|
||||
limit: 100
|
||||
};
|
||||
|
||||
const comments = await nostrClient.pool.list(nostrClient.relays, [filter]);
|
||||
return comments.sort((a, b) => a.created_at - b.created_at);
|
||||
}
|
||||
```
|
||||
|
||||
#### **4. Live Chat for Live Videos**
|
||||
Real-time public chats for live videos use NIP-28.
|
||||
|
||||
##### API Example:
|
||||
```javascript
|
||||
function subscribeToLiveChat(videoId) {
|
||||
const sub = nostrClient.pool.sub(nostrClient.relays, [
|
||||
{
|
||||
kinds: [42],
|
||||
'#e': [videoId]
|
||||
}
|
||||
]);
|
||||
|
||||
sub.on('event', event => {
|
||||
console.log('New chat message:', event);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### **5. Metadata Integration**
|
||||
Use NIP-24 to attach metadata to events for administrative or user-specific tags.
|
||||
|
||||
---
|
||||
|
||||
### **Data Flow**
|
||||
1. **Posting a Comment**:
|
||||
- User creates a comment event.
|
||||
- Client signs and publishes the event to relays.
|
||||
2. **Fetching Comments**:
|
||||
- Client requests comments for a video by filtering events with the `e` tag.
|
||||
3. **Reacting to Comments**:
|
||||
- Users react to comments by posting reaction events.
|
||||
4. **Live Discussions**:
|
||||
- Live chat messages are sent and received in real-time using NIP-28.
|
||||
5. **Metadata Management**:
|
||||
- Metadata is attached during event creation or editing.
|
||||
|
||||
---
|
||||
|
||||
### **UI/UX Considerations**
|
||||
|
||||
1. **Comment Form**:
|
||||
- Input field for comments.
|
||||
- Button to post comments.
|
||||
- Optional reply button for threaded comments.
|
||||
|
||||
2. **Reactions**:
|
||||
- Upvote and downvote icons next to each comment.
|
||||
- Display reaction counts dynamically.
|
||||
|
||||
3. **Live Chat**:
|
||||
- Real-time message updates below live videos.
|
||||
- Highlight important messages using metadata tags.
|
||||
|
||||
4. **Nested Threading**:
|
||||
- Indent replies to show comment hierarchy.
|
||||
|
||||
---
|
||||
|
||||
### **Testing and Validation**
|
||||
|
||||
1. Validate:
|
||||
- Posting, retrieving, and displaying threaded comments.
|
||||
- Reactions and live chat events.
|
||||
- Metadata tagging.
|
||||
2. Test:
|
||||
- Pagination for large comment threads.
|
||||
- Performance under high comment or chat volume.
|
||||
3. Simulate:
|
||||
- Various timestamp scenarios to ensure NIP-22 compliance.
|
||||
|
||||
---
|
||||
|
||||
### **Benefits**
|
||||
- Decentralized and censorship-resistant.
|
||||
- Fully interoperable with other Nostr clients and relays.
|
||||
- Extensible with reactions, live discussions, and metadata features.
|
||||
|
@@ -0,0 +1,394 @@
|
||||
# **bitvid: Updated Plan with VRR (View, Rating, and Retention) Penalty Scoring**
|
||||
|
||||
## **Project Overview**
|
||||
|
||||
**Objective**
|
||||
Enable a decentralized video platform where every view and engagement action is logged via the Nostr protocol (NIP-78). This system incorporates both interval-based watch tracking and a penalty scoring method for short watch times (potentially due to slow loading or immediate disinterest).
|
||||
|
||||
**Key Features**
|
||||
1. Track video views and user retention at regular intervals.
|
||||
2. Assign penalty scores if a viewer leaves within certain short-watch thresholds.
|
||||
3. Use this data to power more accurate recommendations.
|
||||
4. Maintain user privacy by supporting logged-in (persistent pubkey) and session-based (temporary pubkey) tracking.
|
||||
|
||||
---
|
||||
|
||||
## **Technical Requirements**
|
||||
|
||||
1. **Video View Tracking**
|
||||
- **Event Trigger**
|
||||
- A “view” event is sent to a relay whenever a user starts playing a video.
|
||||
- Subsequent engagement events (interval updates, exit events) follow.
|
||||
- **Data Logged**
|
||||
- Video ID or magnet URI.
|
||||
- Viewer’s pubkey (for logged-in) or session-based key (for guests).
|
||||
- Timestamps and session details.
|
||||
- **Nostr Protocol Usage**
|
||||
- Build on NIP-78, using `kind: 30078` events.
|
||||
|
||||
2. **Non-Logged-In User Tracking**
|
||||
- Generate an ephemeral key pair on session start.
|
||||
- Mark the event with something like `["session", "true"]` to distinguish it from a persistent account.
|
||||
- Discard the key pair when the session ends.
|
||||
|
||||
3. **Recommendations**
|
||||
- **Algorithm**
|
||||
- Combine tags, watch intervals, exit penalty scores, and metadata for ranking.
|
||||
- **Presentation**
|
||||
- Show recommended content in a sidebar or beneath the video player.
|
||||
- **Filters**
|
||||
- Exclude deleted or private content unless the user owns it.
|
||||
|
||||
4. **User Privacy**
|
||||
- Provide an opt-in or opt-out setting for tracking.
|
||||
- Collect minimal data to preserve user autonomy.
|
||||
- Consider encryption or obfuscation for private data.
|
||||
|
||||
5. **Performance Considerations**
|
||||
- Batching events to avoid overwhelming relays (e.g., every 5 seconds or in small groups).
|
||||
- Index or cache data to speed up recommendations.
|
||||
|
||||
---
|
||||
|
||||
## **Functional Components**
|
||||
|
||||
1. **Event Structure**
|
||||
|
||||
**Regular View Event (Start)**
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "user_pubkey_or_temp_key",
|
||||
"created_at": 1672531200,
|
||||
"tags": [
|
||||
["t", "view"],
|
||||
["video", "video_id"]
|
||||
],
|
||||
"content": "{\"videoId\":\"video_id\",\"timestamp\":1672531200}"
|
||||
}
|
||||
```
|
||||
|
||||
**Interval/Retention Event (5-second, 10-second, etc.)**
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "user_pubkey_or_temp_key",
|
||||
"created_at": 1672531234,
|
||||
"tags": [
|
||||
["t", "video-watch"],
|
||||
["video", "video_id"]
|
||||
],
|
||||
"content": "{\"timestamp\":1672531234,\"currentWatchSeconds\":10}"
|
||||
}
|
||||
```
|
||||
|
||||
**Exit Event (With Penalty Score)**
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "user_pubkey_or_temp_key",
|
||||
"created_at": 1672531300,
|
||||
"tags": [
|
||||
["t", "vrr-exit"],
|
||||
["video", "video_id"]
|
||||
],
|
||||
"content": "{\"watchTime\":4,\"score\":25}"
|
||||
}
|
||||
```
|
||||
|
||||
2. **UI Updates**
|
||||
- **Video Player**
|
||||
- Trigger events when the video starts, at regular intervals, and upon exit.
|
||||
- **Recommendations**
|
||||
- Fetch and rank videos based on a combination of watch intervals, exit scores, and content tags.
|
||||
|
||||
3. **Integration Points**
|
||||
- **Frontend**
|
||||
- Implement functions like `trackViewEvent()` and `applyPenaltyScore()` in a dedicated module.
|
||||
- **Backend/Relay**
|
||||
- Store, relay, and retrieve events for analytics and recommendation algorithms.
|
||||
|
||||
4. **Development Workflow**
|
||||
- **videoTracker.js**
|
||||
- Contains the VRR methods: initial view events, interval tracking, penalty scoring on exit.
|
||||
- **app.js**
|
||||
- Orchestrates user interactions, calls into `videoTracker.js` on playback, stopping, etc.
|
||||
- **HTML/CSS**
|
||||
- Builds out a recommendations display area or scoreboard for each video.
|
||||
|
||||
---
|
||||
|
||||
## **VRR (View, Rating, and Retention) Method**
|
||||
|
||||
### **Penalty (or Bonus) Scoring for Early Exits**
|
||||
|
||||
| Watch Time <br>(seconds) | Score |
|
||||
|--------------------------|-------|
|
||||
| 0–5 | +25 |
|
||||
| 5–10 | +20 |
|
||||
| 10–15 | +15 |
|
||||
| 15–20 | +10 |
|
||||
| 20–25 | +5 |
|
||||
| 25–30 | +0 |
|
||||
|
||||
- Immediately grant **+1** view when the video starts.
|
||||
- If the user leaves before the thresholds listed above, publish an exit event with the corresponding score.
|
||||
|
||||
### **Interval-Based Retention Tracking**
|
||||
- Send an update every 5 seconds (5s, 10s, 15s, etc.) while a video is actively playing.
|
||||
- Each interval event stores current watch progress for analytics.
|
||||
|
||||
### **Example Score Usage**
|
||||
- Final overall “video health” may sum interval engagement with exit penalties.
|
||||
- Videos consistently abandoned at under 5 seconds might be flagged for slow loading or poor content.
|
||||
- The combination of interval events and exit events builds a complete retention profile.
|
||||
|
||||
---
|
||||
|
||||
## **Implementation Examples**
|
||||
|
||||
### **1. `videoTracker.js`**
|
||||
|
||||
```javascript
|
||||
// videoTracker.js
|
||||
|
||||
class VideoTracker {
|
||||
constructor(videoId, totalDuration, nostrClient) {
|
||||
this.videoId = videoId;
|
||||
this.totalDuration = totalDuration;
|
||||
this.nostrClient = nostrClient;
|
||||
this.startTimestamp = null;
|
||||
this.trackingInterval = null;
|
||||
}
|
||||
|
||||
startVideo() {
|
||||
this.startTimestamp = Date.now();
|
||||
|
||||
// Send initial "view" event (+1 View)
|
||||
const startEvent = {
|
||||
kind: 30078,
|
||||
tags: [
|
||||
["t", "view"],
|
||||
["video", this.videoId]
|
||||
],
|
||||
content: JSON.stringify({
|
||||
videoId: this.videoId,
|
||||
timestamp: this.startTimestamp
|
||||
})
|
||||
};
|
||||
this.nostrClient.publish(startEvent);
|
||||
|
||||
// Kick off interval tracking every 5 seconds
|
||||
this.trackingInterval = setInterval(() => {
|
||||
const currentTime = Date.now();
|
||||
const watchSeconds = Math.floor((currentTime - this.startTimestamp) / 1000);
|
||||
|
||||
const watchEvent = {
|
||||
kind: 30078,
|
||||
tags: [
|
||||
["t", "video-watch"],
|
||||
["video", this.videoId]
|
||||
],
|
||||
content: JSON.stringify({
|
||||
timestamp: currentTime,
|
||||
currentWatchSeconds: watchSeconds
|
||||
})
|
||||
};
|
||||
this.nostrClient.publish(watchEvent);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
stopVideo() {
|
||||
// Clear interval-based tracking
|
||||
if (this.trackingInterval) {
|
||||
clearInterval(this.trackingInterval);
|
||||
this.trackingInterval = null;
|
||||
}
|
||||
|
||||
// Compute how long the user actually watched
|
||||
if (!this.startTimestamp) return;
|
||||
const stopTimestamp = Date.now();
|
||||
const totalWatchSeconds = Math.floor((stopTimestamp - this.startTimestamp) / 1000);
|
||||
|
||||
// Apply penalty scoring
|
||||
let score = 0;
|
||||
if (totalWatchSeconds < 5) {
|
||||
score = 25;
|
||||
} else if (totalWatchSeconds < 10) {
|
||||
score = 20;
|
||||
} else if (totalWatchSeconds < 15) {
|
||||
score = 15;
|
||||
} else if (totalWatchSeconds < 20) {
|
||||
score = 10;
|
||||
} else if (totalWatchSeconds < 25) {
|
||||
score = 5;
|
||||
}
|
||||
|
||||
// Publish exit event with score
|
||||
const exitEvent = {
|
||||
kind: 30078,
|
||||
tags: [
|
||||
["t", "vrr-exit"],
|
||||
["video", this.videoId]
|
||||
],
|
||||
content: JSON.stringify({
|
||||
watchTime: totalWatchSeconds,
|
||||
score
|
||||
})
|
||||
};
|
||||
this.nostrClient.publish(exitEvent);
|
||||
|
||||
// Reset startTimestamp for the next session
|
||||
this.startTimestamp = null;
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoTracker;
|
||||
```
|
||||
|
||||
### **2. Integrating with `app.js`**
|
||||
|
||||
```javascript
|
||||
// app.js
|
||||
|
||||
import VideoTracker from './videoTracker.js';
|
||||
|
||||
class BitvidApp {
|
||||
constructor(nostrClient) {
|
||||
this.nostrClient = nostrClient;
|
||||
this.videoTracker = null;
|
||||
}
|
||||
|
||||
playVideo(video) {
|
||||
// video might have id and totalDuration properties
|
||||
// e.g., { id: 'abc123', totalDuration: 300 }
|
||||
this.videoTracker = new VideoTracker(video.id, video.totalDuration, this.nostrClient);
|
||||
this.videoTracker.startVideo();
|
||||
|
||||
// ... code to actually play the video in the UI ...
|
||||
}
|
||||
|
||||
stopVideo() {
|
||||
if (this.videoTracker) {
|
||||
this.videoTracker.stopVideo();
|
||||
this.videoTracker = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BitvidApp;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **Recommendation Data Flow**
|
||||
|
||||
1. **Collect**:
|
||||
- Gather interval events and exit penalty events from each viewer.
|
||||
2. **Analyze**:
|
||||
- Identify average watch times and drop-off points.
|
||||
- Rank videos that hold attention for longer while penalizing those with frequent early exits.
|
||||
3. **Display**:
|
||||
- Show suggested content to viewers in a recommendations section.
|
||||
- Optionally expose a public graph or chart showing user retention for each video.
|
||||
|
||||
---
|
||||
|
||||
Below is a combined reference that shows two separate Mermaid flowcharts:
|
||||
|
||||
1. **Load Time Penalty** — for users who abandon the video **before** playback actually begins.
|
||||
2. **Retention Tracking** — for users who start watching and either trigger a short-watch penalty or get a “no penalty” bonus for watching beyond 25 seconds.
|
||||
|
||||
---
|
||||
|
||||
## **1. Load Time Penalty**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A((User Presses Play)) --> B[Start Measuring Load Time]
|
||||
B --> C{User Waits or Leaves?}
|
||||
C -->|Leaves Before Playback| D[+25 Slow Load Penalty]
|
||||
C -->|Playback Starts| E[Go to Retention Phase]
|
||||
```
|
||||
|
||||
**Explanation**
|
||||
- The moment the viewer clicks "Play," you begin tracking load time.
|
||||
- If the user abandons before the video actually begins, you apply a **“slow load penalty”** (e.g., +25).
|
||||
- If playback starts successfully, move on to the retention phase.
|
||||
|
||||
---
|
||||
|
||||
## **2. Retention Tracking (Penalty or Bonus)**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A([Playback Begins]) --> B([+1 View Logged])
|
||||
B --> C{"User Exits?\n(X seconds watched)"}
|
||||
C -- "<5s" --> D([+25 Penalty])
|
||||
C -- "<10s" --> E([+20 Penalty])
|
||||
C -- "<15s" --> F([+15 Penalty])
|
||||
C -- "<20s" --> G([+10 Penalty])
|
||||
C -- "<25s" --> H([+5 Penalty])
|
||||
C -- ">=25s" --> I([No Penalty or Bonus])
|
||||
```
|
||||
|
||||
**Explanation**
|
||||
1. **Immediate View (+1)**: Once playback starts, you record an initial view.
|
||||
2. **Exit Thresholds**:
|
||||
- If the viewer leaves under 5 seconds, assign +25.
|
||||
- If under 10 seconds, +20; 15 seconds, +15; etc.
|
||||
3. **Bonus at ≥ 25s**:
|
||||
- Watching at least 25 seconds yields **no penalty**, which you can treat as a “bonus” scenario in your scoring logic—meaning it doesn’t accumulate any additional negative score.
|
||||
|
||||
---
|
||||
|
||||
### **How They Fit Together**
|
||||
|
||||
1. **Load Check**: Start measuring from the moment the user hits Play. If they bail out **before** playback, log a slow-load penalty (+25).
|
||||
2. **Playback & Retention**: As soon as playback initiates, log **+1** to indicate a valid view. From here, short exits accumulate penalties; longer watch times result in less or no penalty.
|
||||
3. **Interval Notes**: During actual viewing (after playback starts), send additional events (e.g., every 5 seconds) to track retention and watch progress.
|
||||
|
||||
This two-stage approach ensures that slow-loading videos are penalized separately from videos that load quickly but are abandoned due to other reasons.
|
||||
|
||||
---
|
||||
|
||||
## **Testing Plan**
|
||||
|
||||
1. **Unit Tests**
|
||||
- Ensure `startVideo()` and `stopVideo()` generate correct event structures.
|
||||
- Verify that penalty scores are assigned according to the specified thresholds.
|
||||
2. **Integration Tests**
|
||||
- Confirm the events flow properly to relays and can be retrieved for recommendation logic.
|
||||
- Check that the UI reflects the recommended videos accurately.
|
||||
3. **Performance Tests**
|
||||
- Simulate multiple viewers to see if the 5-second interval events cause any relay overload.
|
||||
- Adjust batching or intervals if required.
|
||||
|
||||
---
|
||||
|
||||
## **Future Enhancements**
|
||||
|
||||
- **Advanced Metrics**
|
||||
- Heatmaps to show which parts of a video users often skip.
|
||||
- Correlate viewer comments or likes with retention.
|
||||
- **ML-Based Recommendations**
|
||||
- Expand beyond simple threshold scoring, possibly using collaborative filtering.
|
||||
- **Monetization Features**
|
||||
- Integrate tipping (e.g., Lightning-based zaps) for creators who maintain high average watch times.
|
||||
|
||||
---
|
||||
|
||||
## **Timeline**
|
||||
|
||||
| **Task** | **Time Estimate** |
|
||||
|----------------------------------------|-------------------|
|
||||
| Implementation of VRR + penalty logic | 1 week |
|
||||
| UI/Recommendation Updates | 1 week |
|
||||
| Testing and Optimization | 1 week |
|
||||
| Deployment/Relay Configuration | 1 week |
|
||||
|
||||
---
|
||||
|
||||
## **Conclusion**
|
||||
|
||||
This plan integrates short-watch penalties and interval-based retention metrics into bitvid’s decentralized architecture. By logging early exits with specific scores, you can better measure how each video performs—even if playback fails or loads slowly. The final result is a transparent system that respects user privacy while providing data-driven recommendations.
|
@@ -0,0 +1,245 @@
|
||||
# **bitvid: Enhanced Dynamic Home Page and Video Tracking Specification**
|
||||
|
||||
This document outlines how to implement a dynamic home page for bitvid using new video tracking methods. It focuses on reading video views via Nostr events rather than relying on active WebTorrent peer counts. The goal is to display personalized, popular, and trending content, all while preserving a single-page architecture and maintaining a consistent layout.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### Main Objectives
|
||||
1. **Personalized Feeds**: Recommend videos and channels by analyzing user subscriptions and view logs.
|
||||
2. **Video Tracking**: Log video views using Nostr events (kind `30078`) so the system can determine popularity and trending content.
|
||||
3. **Consistent Layout**: Use a single-page approach with views for home, profiles, and other sections. A templating or view-switching system will let the header, footer, and future sidebar remain unchanged.
|
||||
4. **Privacy Support**: Track views with either a logged-in public key or a temporary session-based key for non-logged-in users.
|
||||
|
||||
---
|
||||
|
||||
## 2. Home Page Views
|
||||
|
||||
The home page will be composed of multiple sections that show different collections of videos. All sections can be rendered within `index.html`, controlled by view logic. These sections might include:
|
||||
|
||||
1. **For You**
|
||||
- Videos from subscriptions.
|
||||
- Personalized based on user watch history or tags.
|
||||
2. **Trending**
|
||||
- Videos that have grown in views over a certain timeframe.
|
||||
- Uses view logs to gauge growth trends.
|
||||
3. **Popular**
|
||||
- Videos with the highest total views.
|
||||
- Sorted based on aggregated play events.
|
||||
|
||||
Each section can be wrapped in its own HTML container. JavaScript will fetch the relevant data, sort or filter it, then populate the DOM dynamically.
|
||||
|
||||
---
|
||||
|
||||
## 3. Video View Tracking
|
||||
|
||||
Rather than measuring active peers, bitvid now counts views by logging them as Nostr events. This enables a transparent and decentralized way to track engagement while still allowing for privacy controls.
|
||||
|
||||
### 3.1 Event Structure
|
||||
|
||||
- **Kind**: `30078` (existing kind used for video-related data).
|
||||
- **Tags**:
|
||||
- `["t", "view"]` to identify a view event.
|
||||
- `["video", "<video_id>"]` to map the event to a specific video.
|
||||
- `["session", "true"]` if the viewer is non-logged-in using a temporary session.
|
||||
- **Content**: May include a JSON object with the video ID, timestamp, and optional metadata.
|
||||
|
||||
**Example View Event (Logged In):**
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "user_pubkey",
|
||||
"created_at": 1672531200,
|
||||
"tags": [
|
||||
["t", "view"],
|
||||
["video", "video_abc123"]
|
||||
],
|
||||
"content": "{\"videoId\":\"video_abc123\",\"timestamp\":1672531200}"
|
||||
}
|
||||
```
|
||||
|
||||
**Example View Event (Temporary Session):**
|
||||
```json
|
||||
{
|
||||
"kind": 30078,
|
||||
"pubkey": "temporary_pubkey",
|
||||
"created_at": 1672531200,
|
||||
"tags": [
|
||||
["t", "view"],
|
||||
["video", "video_abc123"],
|
||||
["session", "true"]
|
||||
],
|
||||
"content": "{\"videoId\":\"video_abc123\",\"timestamp\":1672531200}"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Ranking Logic
|
||||
|
||||
1. **Popular Videos**
|
||||
- Sort by the total number of view events.
|
||||
- A simple approach is to query all view events for each video, then rank them by the count of events.
|
||||
|
||||
2. **Trending Videos**
|
||||
- Evaluate growth in view events over a recent window (for example, the last 24 hours).
|
||||
- Compare the count of new views against a previous period, or calculate the rate of increase.
|
||||
|
||||
3. **For You**
|
||||
- Look at what the user watched or subscribed to (via kind `30002` subscription lists).
|
||||
- Track the user’s or session’s recent view tags, then recommend videos with overlapping tags or from the same channels.
|
||||
|
||||
---
|
||||
|
||||
## 5. Home Page Layout and Rendering
|
||||
|
||||
### 5.1 HTML Structure
|
||||
|
||||
Your `index.html` might include placeholders for each section:
|
||||
|
||||
```html
|
||||
<div id="homeContainer">
|
||||
<!-- For You -->
|
||||
<section id="forYouSection">
|
||||
<h2>For You</h2>
|
||||
<div id="forYouGrid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Trending -->
|
||||
<section id="trendingSection">
|
||||
<h2>Trending</h2>
|
||||
<div id="trendingGrid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Popular -->
|
||||
<section id="popularSection">
|
||||
<h2>Popular</h2>
|
||||
<div id="popularGrid"></div>
|
||||
</section>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5.2 JavaScript Flow
|
||||
|
||||
1. **Fetch Data**
|
||||
- Pull video metadata from kind `30078` events that contain video info or from your existing approach.
|
||||
- Pull subscription data (kind `30002`).
|
||||
- Pull view events (kind `30078` with `"t", "view"`).
|
||||
|
||||
2. **Process Rankings**
|
||||
- Tally views per video.
|
||||
- Track growth rates for trending.
|
||||
- Filter or sort data.
|
||||
|
||||
3. **Render Sections**
|
||||
- Populate each section with relevant videos.
|
||||
- A typical approach:
|
||||
|
||||
```js
|
||||
async function loadHomePage() {
|
||||
const videos = await fetchAllVideos(); // from Nostr or local data
|
||||
const viewEvents = await fetchAllViews(); // filter by "t=view"
|
||||
|
||||
const forYouData = getForYouRecommendations(videos, viewEvents);
|
||||
renderVideoGrid(forYouData, "forYouGrid");
|
||||
|
||||
const trendingData = getTrendingVideos(videos, viewEvents);
|
||||
renderVideoGrid(trendingData, "trendingGrid");
|
||||
|
||||
const popularData = getPopularVideos(videos, viewEvents);
|
||||
renderVideoGrid(popularData, "popularGrid");
|
||||
}
|
||||
```
|
||||
|
||||
- Each function (`getForYouRecommendations`, `getTrendingVideos`, `getPopularVideos`) calculates the appropriate subset of videos.
|
||||
|
||||
---
|
||||
|
||||
## 6. Single-Page View Structure
|
||||
|
||||
### 6.1 Profile and Other Views
|
||||
|
||||
- Keep the header, footer, and any sidebar in place.
|
||||
- Change the visible view container to switch between the home grid, a user’s profile, or other screens.
|
||||
- Use JavaScript routing (hash-based or history API) to detect and render the correct view in `index.html`.
|
||||
|
||||
**Example:**
|
||||
```js
|
||||
function handleRouteChange() {
|
||||
const hash = window.location.hash;
|
||||
|
||||
if (hash.startsWith("#profile")) {
|
||||
const npub = hash.split("/")[1];
|
||||
loadProfileView(npub);
|
||||
} else {
|
||||
// Default: show home
|
||||
loadHomePage();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", handleRouteChange);
|
||||
window.addEventListener("load", handleRouteChange);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommendations and Personalized Feeds
|
||||
|
||||
### 7.1 Logged-In Users
|
||||
|
||||
- Use their public key to track watch history and subscription data.
|
||||
- Query view events authored by that pubkey.
|
||||
- Match frequent tags, channels, or categories.
|
||||
|
||||
### 7.2 Non-Logged-In Users
|
||||
|
||||
- Generate a session-based key pair.
|
||||
- Tag view events with `["session", "true"]`.
|
||||
- Maintain in-memory or localStorage.
|
||||
- Provide basic recommendations for the session.
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Steps
|
||||
|
||||
1. **View Logging**:
|
||||
- Update the video player code to publish a view event when a user starts or confirms playback.
|
||||
|
||||
2. **Data Fetching**:
|
||||
- Build or adapt functions to query view events from your Nostr relays.
|
||||
- Merge the resulting data with your video metadata.
|
||||
|
||||
3. **Ranking Functions**:
|
||||
- Create functions to rank videos by total views (popular) or view velocity (trending).
|
||||
- Create a function that looks at subscription data and view history for personalized feeds.
|
||||
|
||||
4. **Rendering**:
|
||||
- Implement `renderVideoGrid(data, containerId)` to fill the given section with video cards.
|
||||
- Keep the layout responsive and consistent.
|
||||
|
||||
5. **Routes and Views**:
|
||||
- Integrate the home feed with other views (e.g., user profiles, single video modals) within the same page.
|
||||
- Use a simple router or your existing JavaScript structure to swap out sections.
|
||||
|
||||
---
|
||||
|
||||
## 9. Future Enhancements
|
||||
|
||||
1. **Advanced Recommendations**
|
||||
- Add more factors like video tags, watch duration, or user engagement events (likes, zaps).
|
||||
|
||||
2. **Analytics Dashboard**
|
||||
- Provide creators with a summary of total views, trending periods, and audience insights.
|
||||
|
||||
3. **Community Features**
|
||||
- Collaborative playlists, comments, or shared watch parties, all tracked in a decentralized manner.
|
||||
|
||||
4. **Optimized Relay Usage**
|
||||
- Implement batching or caching to limit the load on relays when publishing or querying events.
|
||||
|
||||
---
|
||||
|
||||
### Conclusion
|
||||
|
||||
This updated plan replaces the old peer-count method with view event tracking. It outlines how to fetch and render dynamic sections on the home page, switch between views, and generate personalized recommendations. By tracking views via Nostr events and rendering multiple content sections in a single-page architecture, bitvid can maintain a flexible interface while providing a richer user experience.
|
Reference in New Issue
Block a user