mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #898 from PR0M3TH3AN/codex/extend-test-suites-and-update-documentation
Document hashtag preferences and extend tests
This commit is contained in:
5 files changed
+151
No files matched your search
@@ -76,6 +76,13 @@ Operators can tune thresholds and lists from the Profile modal:
|
||||
- **Report** to issue a NIP-56 moderation report.
|
||||
- When a rebroadcast hits rate limits, the app surfaces cooldown guidance (e.g., “Rebroadcast is cooling down. Try again in 30s.”) so operators know when to retry without spamming relays.
|
||||
|
||||
### Hashtag interests & disinterests
|
||||
|
||||
- The Profile modal’s **Hashtags** tab lets you maintain interests (tags you want surfaced) and disinterests (tags to downrank) with dedicated input fields and per-tag remove actions. Preferences are cached locally so list edits render instantly.
|
||||
- bitvid persists the lists by publishing a replaceable `kind 30005` event with the identifier `bitvid:tag-preferences`. The `HashtagPreferencesService` normalizes tags, keeps interests and disinterests mutually exclusive, and emits change events so UI stays in sync.【F:js/services/hashtagPreferencesService.js†L206-L276】【F:js/services/hashtagPreferencesService.js†L304-L324】【F:docs/nostr-event-schemas.md†L150-L168】
|
||||
- Preference payloads are encrypted before leaving the browser. The service attempts NIP-44 v2 first, falls back to NIP-44 or NIP-04 based on the signer/browser capabilities, records the active scheme in the `['encrypted', <scheme>]` tag, and decrypts in the same priority order when loading from relays.【F:js/services/hashtagPreferencesService.js†L520-L657】【F:js/services/hashtagPreferencesService.js†L356-L511】
|
||||
- Published ciphertext is stored on every configured write relay and accepted ones are tracked so operators can diagnose relay failures without exposing the plaintext list.【F:js/services/hashtagPreferencesService.js†L658-L711】
|
||||
|
||||
---
|
||||
|
||||
## For Developers
|
||||
|
||||
@@ -75,6 +75,12 @@ New operators should also note that fresh viewer accounts automatically inherit
|
||||
|
||||
The profile modal now exposes blur, autoplay, and hide thresholds so operators can dial in stricter or more permissive behavior. Enter a non-negative whole number to override the default or leave the field blank to fall back to the baseline values from [`config/instance-config.js`](../../config/instance-config.js) (`DEFAULT_BLUR_THRESHOLD`, `DEFAULT_AUTOPLAY_BLOCK_THRESHOLD`, `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD`, `DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD`). Adjustments are stored locally, applied immediately to the active feed, and rehydrate on every load. Update the config file to change the per-instance defaults that new viewers see.
|
||||
|
||||
### Personal hashtag preferences
|
||||
|
||||
- The Profile modal’s Hashtags tab is wired to [`HashtagPreferencesService`](../../js/services/hashtagPreferencesService.js), which maintains a local snapshot of the viewer’s interests and disinterests, normalizes every tag, and guarantees that the two sets stay mutually exclusive before broadcasting UI change events.【F:js/services/hashtagPreferencesService.js†L206-L276】【F:js/services/hashtagPreferencesService.js†L304-L324】
|
||||
- `bitvidApp` resolves the active pubkey’s preferences at login, listens for `CHANGE` events, and forwards the normalized snapshot to controllers so moderation and discovery logic can react without querying relays again.【F:js/app.js†L3811-L3906】【F:js/app.js†L3940-L4050】
|
||||
- Publishing writes a replaceable `kind 30005` list with `d=bitvid:tag-preferences`, encrypting the `{ version, interests, disinterests }` payload via the best available NIP-44/NIP-04 scheme before hitting every configured write relay.【F:docs/nostr-event-schemas.md†L150-L168】【F:js/services/hashtagPreferencesService.js†L520-L711】
|
||||
|
||||
### Runtime flags
|
||||
|
||||
- `TRUSTED_MUTE_HIDE_THRESHOLD` — numeric default for the trusted mute hide control. Initialized from `DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD` in [`config/instance-config.js`](../../config/instance-config.js); the upstream config sets this to `1`, but adjust the export to match your policy.
|
||||
|
||||
@@ -169,6 +169,13 @@ field must be a NIP-44 ciphertext representing the JSON shape
|
||||
share their preferred and muted hashtags without exposing the raw preferences on
|
||||
relays.
|
||||
|
||||
When decrypting, the client inspects both `['encrypted', ...]` and
|
||||
`['encryption', ...]` hints on the event to prioritize NIP-44 v2 payloads,
|
||||
falling back to legacy NIP-44 or NIP-04 decryptors when required. Publishing
|
||||
performs the same capability probe, updating the existing `encrypted` tag with
|
||||
the negotiated scheme before signing so other readers know which cipher to try
|
||||
first.【F:js/services/hashtagPreferencesService.js†L356-L511】【F:js/services/hashtagPreferencesService.js†L520-L657】
|
||||
|
||||
### NIP-71 rollout
|
||||
|
||||
bitvid now emits a paired NIP-71 event (kind `21` for long-form, `22` for short
|
||||
|
||||
@@ -131,6 +131,52 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"load retries decryptors when hinted scheme fails",
|
||||
{ concurrency: false },
|
||||
async () => {
|
||||
const pubkey = "d".repeat(64);
|
||||
const attempts = [];
|
||||
|
||||
setActiveSigner({
|
||||
nip44Decrypt: async () => {
|
||||
attempts.push("nip44");
|
||||
throw new Error("nip44 failed");
|
||||
},
|
||||
nip04Decrypt: async () => {
|
||||
attempts.push("nip04");
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
interests: ["Retry"],
|
||||
disinterests: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
nostrClient.pool = {
|
||||
async list() {
|
||||
return [
|
||||
{
|
||||
id: "evt-retry",
|
||||
created_at: 300,
|
||||
pubkey,
|
||||
content: "cipher", // never decrypted successfully by nip44
|
||||
tags: [["encrypted", "nip44 nip04"]],
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.retry"];
|
||||
nostrClient.writeRelays = ["wss://relay.retry"];
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
|
||||
assert.deepEqual(attempts, ["nip44", "nip04"]);
|
||||
assert.deepEqual(hashtagPreferences.getInterests(), ["retry"]);
|
||||
assert.deepEqual(hashtagPreferences.getDisinterests(), []);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"interest and disinterest lists remain exclusive",
|
||||
{ concurrency: false },
|
||||
@@ -206,3 +252,76 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"publish falls back to window nostr encryptors",
|
||||
{ concurrency: false },
|
||||
async () => {
|
||||
const pubkey = "e".repeat(64);
|
||||
const encryptCalls = [];
|
||||
|
||||
setActiveSigner({
|
||||
type: "inpage",
|
||||
signEvent: async (event) => ({ ...event, id: "signed-window" }),
|
||||
});
|
||||
|
||||
window.nostr = {
|
||||
nip44: {
|
||||
encrypt: async (target, plaintext) => {
|
||||
encryptCalls.push({ scheme: "nip44", target, plaintext });
|
||||
throw new Error("nip44 failed");
|
||||
},
|
||||
v2: {
|
||||
encrypt: async (target, plaintext) => {
|
||||
encryptCalls.push({ scheme: "nip44_v2", target, plaintext });
|
||||
return "cipher-from-window";
|
||||
},
|
||||
},
|
||||
},
|
||||
nip04: {
|
||||
encrypt: async (target, plaintext) => {
|
||||
encryptCalls.push({ scheme: "nip04", target, plaintext });
|
||||
return "cipher-nip04";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const publishedEvents = [];
|
||||
nostrClient.pool = {
|
||||
list: async () => [],
|
||||
publish(urls, event) {
|
||||
publishedEvents.push({ urls, event });
|
||||
return {
|
||||
on(eventName, handler) {
|
||||
if (eventName === "ok") {
|
||||
setImmediate(() => handler());
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
nostrClient.relays = ["wss://relay.window"];
|
||||
nostrClient.writeRelays = ["wss://relay.window"];
|
||||
|
||||
await hashtagPreferences.load(pubkey);
|
||||
hashtagPreferences.addInterest("WindowTag");
|
||||
|
||||
const signedEvent = await hashtagPreferences.publish();
|
||||
|
||||
assert.equal(signedEvent.id, "signed-window");
|
||||
assert.equal(encryptCalls.length >= 2, true);
|
||||
assert.equal(encryptCalls[0].scheme, "nip44");
|
||||
assert.equal(encryptCalls[1].scheme, "nip44_v2");
|
||||
assert.equal(encryptCalls[0].target, pubkey);
|
||||
assert.equal(encryptCalls[1].target, pubkey);
|
||||
assert.ok(encryptCalls[0].plaintext.includes("windowtag"));
|
||||
assert.ok(encryptCalls[1].plaintext.includes("windowtag"));
|
||||
assert.equal(publishedEvents.length, 1);
|
||||
assert.deepEqual(publishedEvents[0].urls, ["wss://relay.window"]);
|
||||
const encryptedTag = publishedEvents[0].event.tags.find(
|
||||
(tag) => Array.isArray(tag) && tag[0] === "encrypted",
|
||||
);
|
||||
assert.equal(encryptedTag[1], "nip44_v2");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -78,3 +78,15 @@ test("VideoListView hides popular tags when no tags are available", () => {
|
||||
assert.equal(tagsRoot.hidden, true);
|
||||
assert.equal(tagsRoot.childElementCount, 0);
|
||||
});
|
||||
|
||||
test("VideoListView setModerationHideHandler stores callable", () => {
|
||||
const { document } = createViewDom();
|
||||
const view = new VideoListView({ document });
|
||||
|
||||
const handler = () => {};
|
||||
view.setModerationHideHandler(handler);
|
||||
assert.equal(view.handlers.moderationHide, handler);
|
||||
|
||||
view.setModerationHideHandler(null);
|
||||
assert.equal(view.handlers.moderationHide, null);
|
||||
});
|
||||
Reference in new issue
Block a user