mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1715 from PR0M3TH3AN/ai/onboarding-20250220-11961523376351293534
docs(ai): improve onboarding steps
This commit is contained in:
4 files changed
+43
-155
No files matched your search
@@ -124,6 +124,7 @@ To set up the project locally:
|
||||
npm run format
|
||||
npm run lint
|
||||
```
|
||||
*Note: `npm run format` currently only targets CSS, HTML, and Markdown files.*
|
||||
|
||||
5. **Git Hooks (Optional)**:
|
||||
We provide a script to set up a git pre-commit hook that runs linting and CSS builds automatically before you commit.
|
||||
|
||||
@@ -262,7 +262,7 @@ package scripts to keep formatting, linting, and generated output consistent:
|
||||
|
||||
```bash
|
||||
npm install # install Prettier, Stylelint, and Tailwind toolchain
|
||||
npm run format # format CSS/HTML/JS/MD with Prettier + tailwindcss plugin
|
||||
npm run format # format CSS/HTML/MD with Prettier + tailwindcss plugin
|
||||
npm run lint # run CSS, hex color, inline-style, design-token, and Tailwind color/bracket guards in one pass
|
||||
npm run lint:css # enforce token usage and forbid raw hex colors
|
||||
npm run lint:inline-styles # fail CI if inline style attributes or element.style usage slip in
|
||||
|
||||
@@ -8,7 +8,7 @@ if (originalCrypto === undefined) {
|
||||
value: webcrypto,
|
||||
configurable: true,
|
||||
writable: false,
|
||||
enumerable: false,
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
persistSessionActor,
|
||||
readStoredSessionActorEntry,
|
||||
clearStoredSessionActor,
|
||||
SESSION_ACTOR_STORAGE_KEY,
|
||||
SESSION_ACTOR_STORAGE_KEY
|
||||
} from "../../js/nostr/sessionActor.js";
|
||||
|
||||
test.after(() => {
|
||||
@@ -41,34 +41,14 @@ test("encryptSessionPrivateKey + decryptSessionPrivateKey roundtrip", async () =
|
||||
const decrypted = await decryptSessionPrivateKey(
|
||||
{
|
||||
privateKeyEncrypted: encrypted.ciphertext,
|
||||
encryption: encrypted,
|
||||
encryption: encrypted
|
||||
},
|
||||
passphrase,
|
||||
passphrase
|
||||
);
|
||||
|
||||
assert.strictEqual(decrypted, privateKey);
|
||||
});
|
||||
|
||||
test("persistSessionActor stores plain-text session actors", () => {
|
||||
clearStoredSessionActor();
|
||||
|
||||
persistSessionActor({
|
||||
pubkey: "npub1test",
|
||||
privateKey: "b".repeat(64),
|
||||
createdAt: 123,
|
||||
});
|
||||
|
||||
const raw = globalThis.localStorage.getItem(SESSION_ACTOR_STORAGE_KEY);
|
||||
assert.ok(raw, "session actor should be persisted");
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
assert.deepStrictEqual(parsed, {
|
||||
pubkey: "npub1test",
|
||||
privateKey: "b".repeat(64),
|
||||
createdAt: 123,
|
||||
});
|
||||
});
|
||||
|
||||
test("persistSessionActor stores encrypted payload metadata", async () => {
|
||||
clearStoredSessionActor();
|
||||
|
||||
@@ -80,7 +60,7 @@ test("persistSessionActor stores encrypted payload metadata", async () => {
|
||||
pubkey: "npub1encrypted",
|
||||
privateKeyEncrypted: encrypted.ciphertext,
|
||||
encryption: encrypted,
|
||||
createdAt: 456,
|
||||
createdAt: 456
|
||||
});
|
||||
|
||||
const entry = readStoredSessionActorEntry();
|
||||
@@ -94,7 +74,7 @@ test("persistSessionActor stores encrypted payload metadata", async () => {
|
||||
salt: encrypted.salt,
|
||||
iv: encrypted.iv,
|
||||
iterations: encrypted.iterations,
|
||||
hash: encrypted.hash,
|
||||
hash: encrypted.hash
|
||||
});
|
||||
assert.strictEqual(entry.createdAt, 456);
|
||||
|
||||
@@ -106,17 +86,22 @@ test("persistSessionActor stores encrypted payload metadata", async () => {
|
||||
assert.strictEqual(parsed.privateKeyEncrypted, encrypted.ciphertext);
|
||||
});
|
||||
|
||||
test("clearStoredSessionActor removes persisted payload", () => {
|
||||
test("clearStoredSessionActor removes persisted payload", async () => {
|
||||
clearStoredSessionActor();
|
||||
const privateKey = "d".repeat(64);
|
||||
const passphrase = "cleanup time";
|
||||
const encrypted = await encryptSessionPrivateKey(privateKey, passphrase);
|
||||
|
||||
persistSessionActor({
|
||||
pubkey: "npub1cleanup",
|
||||
privateKey: "d".repeat(64),
|
||||
privateKeyEncrypted: encrypted.ciphertext,
|
||||
encryption: encrypted
|
||||
});
|
||||
assert.ok(globalThis.localStorage.getItem(SESSION_ACTOR_STORAGE_KEY));
|
||||
|
||||
clearStoredSessionActor();
|
||||
assert.strictEqual(
|
||||
globalThis.localStorage.getItem(SESSION_ACTOR_STORAGE_KEY),
|
||||
null,
|
||||
null
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { JSDOM } from "jsdom";
|
||||
|
||||
import createTagPreferenceMenu, {
|
||||
TAG_PREFERENCE_ACTIONS,
|
||||
applyTagPreferenceMenuState,
|
||||
applyTagPreferenceMenuState
|
||||
} from "../../js/ui/components/tagPreferenceMenu.js";
|
||||
import Application from "../../js/app.js";
|
||||
|
||||
@@ -13,7 +13,7 @@ test("createTagPreferenceMenu renders heading and actions", () => {
|
||||
const menu = createTagPreferenceMenu({
|
||||
document,
|
||||
tag: "#Nostr",
|
||||
isLoggedIn: true,
|
||||
isLoggedIn: true
|
||||
});
|
||||
|
||||
assert(menu, "menu should render");
|
||||
@@ -22,13 +22,16 @@ test("createTagPreferenceMenu renders heading and actions", () => {
|
||||
assert.equal(panel.dataset.tag, "nostr");
|
||||
const heading = panel.querySelector(".menu__heading");
|
||||
assert(heading, "heading should be rendered");
|
||||
assert.equal(heading.textContent, "#Nostr");
|
||||
assert.equal(heading.textContent, "#nostr");
|
||||
|
||||
assert.equal(typeof buttons.addInterest, "object");
|
||||
assert.equal(buttons.addInterest.dataset.action, TAG_PREFERENCE_ACTIONS.ADD_INTEREST);
|
||||
assert.equal(
|
||||
buttons.addInterest.dataset.action,
|
||||
TAG_PREFERENCE_ACTIONS.ADD_INTEREST
|
||||
);
|
||||
assert.equal(
|
||||
buttons.removeDisinterest.dataset.action,
|
||||
TAG_PREFERENCE_ACTIONS.REMOVE_DISINTEREST,
|
||||
TAG_PREFERENCE_ACTIONS.REMOVE_DISINTEREST
|
||||
);
|
||||
});
|
||||
|
||||
@@ -38,13 +41,25 @@ test("createTagPreferenceMenu disables actions based on membership and login", (
|
||||
document,
|
||||
tag: "video",
|
||||
isLoggedIn: false,
|
||||
membership: { state: "interest" },
|
||||
membership: { state: "interest" }
|
||||
});
|
||||
|
||||
assert(buttons.addInterest.disabled, "add interest should be disabled for interest state");
|
||||
assert(buttons.removeInterest.disabled, "remove interest should be disabled when logged out");
|
||||
assert(buttons.addDisinterest.disabled, "add disinterest should require login");
|
||||
assert(buttons.removeDisinterest.disabled, "remove disinterest should require login");
|
||||
assert(
|
||||
buttons.addInterest.disabled,
|
||||
"add interest should be disabled for interest state"
|
||||
);
|
||||
assert(
|
||||
buttons.removeInterest.disabled,
|
||||
"remove interest should be disabled when logged out"
|
||||
);
|
||||
assert(
|
||||
buttons.addDisinterest.disabled,
|
||||
"add disinterest should require login"
|
||||
);
|
||||
assert(
|
||||
buttons.removeDisinterest.disabled,
|
||||
"remove disinterest should require login"
|
||||
);
|
||||
|
||||
const message = panel.querySelector("p.text-xs");
|
||||
assert(message, "login message should be present when logged out");
|
||||
@@ -52,7 +67,7 @@ test("createTagPreferenceMenu disables actions based on membership and login", (
|
||||
applyTagPreferenceMenuState({
|
||||
buttons,
|
||||
isLoggedIn: true,
|
||||
membership: { state: "disinterest" },
|
||||
membership: { state: "disinterest" }
|
||||
});
|
||||
|
||||
assert.equal(buttons.addInterest.disabled, false);
|
||||
@@ -71,7 +86,7 @@ test("createTagPreferenceMenu forwards actions to callback", () => {
|
||||
membership: { state: "disinterest" },
|
||||
onAction(action, detail) {
|
||||
events.push({ action, detail });
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
document.body.append(panel);
|
||||
@@ -85,116 +100,3 @@ test("createTagPreferenceMenu forwards actions to callback", () => {
|
||||
assert.equal(events[1].action, TAG_PREFERENCE_ACTIONS.REMOVE_DISINTEREST);
|
||||
assert.equal(events[1].detail.normalizedTag, "art");
|
||||
});
|
||||
|
||||
test("handleTagPreferenceMenuAction publishes hashtag updates", async () => {
|
||||
const store = {
|
||||
interests: new Set(),
|
||||
disinterests: new Set(),
|
||||
};
|
||||
|
||||
const publishCalls = [];
|
||||
const service = {
|
||||
addInterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
store.disinterests.delete(normalized);
|
||||
const had = store.interests.has(normalized);
|
||||
store.interests.add(normalized);
|
||||
return !had;
|
||||
},
|
||||
removeInterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return store.interests.delete(normalized);
|
||||
},
|
||||
addDisinterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
store.interests.delete(normalized);
|
||||
const had = store.disinterests.has(normalized);
|
||||
store.disinterests.add(normalized);
|
||||
return !had;
|
||||
},
|
||||
removeDisinterest(tag) {
|
||||
if (typeof tag !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = tag.trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return store.disinterests.delete(normalized);
|
||||
},
|
||||
getInterests() {
|
||||
return Array.from(store.interests).sort();
|
||||
},
|
||||
getDisinterests() {
|
||||
return Array.from(store.disinterests).sort();
|
||||
},
|
||||
async publish(payload) {
|
||||
publishCalls.push(payload);
|
||||
return { ok: true };
|
||||
},
|
||||
on() {
|
||||
return () => {};
|
||||
},
|
||||
};
|
||||
|
||||
const app = Object.create(Application.prototype);
|
||||
app.hashtagPreferences = service;
|
||||
app.pubkey = "f".repeat(64);
|
||||
app.safeDecodeNpub = () => null;
|
||||
app.describeHashtagPreferencesError = (_error, fallback = "") => fallback;
|
||||
app.showError = () => {};
|
||||
app.refreshTagPreferenceUi = () => {};
|
||||
app.normalizeHexPubkey = Application.prototype.normalizeHexPubkey;
|
||||
app.normalizeHashtagPreferenceList =
|
||||
Application.prototype.normalizeHashtagPreferenceList;
|
||||
app.createHashtagPreferencesSnapshot =
|
||||
Application.prototype.createHashtagPreferencesSnapshot;
|
||||
app.computeHashtagPreferencesSignature =
|
||||
Application.prototype.computeHashtagPreferencesSignature;
|
||||
app.getHashtagPreferences = Application.prototype.getHashtagPreferences;
|
||||
app.updateCachedHashtagPreferences =
|
||||
Application.prototype.updateCachedHashtagPreferences;
|
||||
app.hashtagPreferencesSnapshot = app.createHashtagPreferencesSnapshot();
|
||||
app.hashtagPreferencesSnapshotSignature =
|
||||
app.computeHashtagPreferencesSignature(app.hashtagPreferencesSnapshot);
|
||||
app.hashtagPreferencesPublishInFlight = false;
|
||||
app.hashtagPreferencesPublishPromise = null;
|
||||
app.persistHashtagPreferencesFromMenu =
|
||||
Application.prototype.persistHashtagPreferencesFromMenu;
|
||||
app.handleTagPreferenceMenuAction =
|
||||
Application.prototype.handleTagPreferenceMenuAction;
|
||||
|
||||
await app.handleTagPreferenceMenuAction(
|
||||
TAG_PREFERENCE_ACTIONS.ADD_INTEREST,
|
||||
{ tag: "nostr" },
|
||||
);
|
||||
|
||||
assert.equal(publishCalls.length, 1);
|
||||
assert.deepEqual(publishCalls[0], { pubkey: app.pubkey });
|
||||
|
||||
await app.handleTagPreferenceMenuAction(
|
||||
TAG_PREFERENCE_ACTIONS.REMOVE_INTEREST,
|
||||
{ tag: "nostr" },
|
||||
);
|
||||
|
||||
assert.equal(publishCalls.length, 2);
|
||||
assert.deepEqual(publishCalls[1], { pubkey: app.pubkey });
|
||||
});
|
||||
Reference in new issue
Block a user