From a649c341e036563e8a6d22b824756034d29f24b8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:14:29 +0000 Subject: [PATCH] I have updated the `nostr-tools` usage and resolved the UI bugs. Here are the details of the changes: - Updated `js/nostr/adapters/nsecAdapter.js` to ensure private keys are `Uint8Array` bytes before passing to `getPublicKey`, fixing compatibility with `nostr-tools` v2. - Updated `tests/nostr/nip46Client.test.js` to use byte arrays for keys in crypto operations. - Implemented missing `setupModalMorePopover`, `setupModalSharePopover`, and `refreshModalMoreMenuPanel` in `VideoModal.js`. - Fixed a bug in `VideoModal.js` where `setCommentComposerState` was overriding `CommentsController` send button logic; it now delegates to the controller if available. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/nostr/adapters/nsecAdapter.js | 28 ++++++-- js/ui/components/VideoModal.js | 110 +++++++++++++++++++++++++++++-- tests/nostr/nip46Client.test.js | 46 ++++++------- 3 files changed, 148 insertions(+), 36 deletions(-) diff --git a/js/nostr/adapters/nsecAdapter.js b/js/nostr/adapters/nsecAdapter.js index b8dca77b..68fd4fec 100644 --- a/js/nostr/adapters/nsecAdapter.js +++ b/js/nostr/adapters/nsecAdapter.js @@ -6,8 +6,6 @@ import { ensureNostrTools, getCachedNostrTools } from "../toolkit.js"; import { normalizeActorKey } from "../watchHistory.js"; import { HEX64_REGEX } from "../../utils/hex.js"; - - function normalizePrivateKey(privateKey) { if (typeof privateKey !== "string") { return ""; @@ -31,10 +29,28 @@ async function resolvePublicKey(privateKey, pubkey) { const tools = (await ensureNostrTools()) || getCachedNostrTools(); if (tools?.getPublicKey && typeof tools.getPublicKey === "function") { - const derived = tools.getPublicKey(privateKey); - const normalized = normalizeActorKey(derived); - if (normalized && HEX64_REGEX.test(normalized)) { - return normalized; + try { + // nostr-tools v2 requires Uint8Array + let keyInput = privateKey; + if (typeof privateKey === "string" && tools.utils?.hexToBytes) { + keyInput = tools.utils.hexToBytes(privateKey); + } + const derived = tools.getPublicKey(keyInput); + const normalized = normalizeActorKey(derived); + if (normalized && HEX64_REGEX.test(normalized)) { + return normalized; + } + } catch (e) { + // Fallback for v1 or other errors + try { + const derived = tools.getPublicKey(privateKey); + const normalized = normalizeActorKey(derived); + if (normalized && HEX64_REGEX.test(normalized)) { + return normalized; + } + } catch (e2) { + return ""; + } } } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 8c363a8b..162eae41 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -3025,20 +3025,23 @@ export class VideoModal { setCommentComposerState(state) { this._commentComposerState = state; if (this.commentsController) { - // Also update the internal controller to keep them in sync if possible, - // but primarily we trust the caller (which might be a test controller) + // Delegate entirely to the controller if it exists to avoid fighting over DOM state + // (especially submit button enablement which depends on input value) if (typeof this.commentsController.setCommentComposerState === 'function') { - // Avoid infinite recursion if controller calls back if (this.commentsController.composerState !== state) { this.commentsController.setCommentComposerState(state); } } + return; } - // Reflect state to DOM immediately if elements exist + + // Fallback: Manually reflect state to DOM if no controller is attached (e.g. in isolated tests) if (this.commentsInput) { this.commentsInput.disabled = !!state?.disabled; } if (this.commentsSubmitButton) { + // Note: This naive check doesn't account for empty input, + // but without a controller we can't easily validte. this.commentsSubmitButton.disabled = !!state?.disabled; } if (this.commentsComposer) { @@ -3169,14 +3172,107 @@ export class VideoModal { } setupModalMorePopover() { - // Stub + if (!this.playerModal || !this.modalMoreBtn) { + return; + } + + if (this.modalMorePopover?.destroy) { + this.modalMorePopover.destroy(); + } + + this.modalMorePopover = createPopover({ + trigger: this.modalMoreBtn, + content: (container) => { + if (!this.activeVideo) { + container.innerHTML = ""; + return; + } + const panel = createVideoMoreMenuPanel({ + document: this.document, + video: this.activeVideo, + pointerInfo: this.modalMoreMenuContext.pointerInfo, + playbackUrl: this.modalMoreMenuContext.playbackUrl, + playbackMagnet: this.modalMoreMenuContext.playbackMagnet, + canManageBlacklist: this.modalMoreMenuContext.canManageBlacklist, + context: "modal", + }); + if (panel) { + container.appendChild(panel); + this.modalMoreMenuPanel = panel; + } + }, + placement: "bottom-end", + offset: 8, + onOpen: () => { + if (this.activeVideo) { + this.syncMoreMenuData({ + currentVideo: this.activeVideo, + canManageBlacklist: this.modalMoreMenuContext.canManageBlacklist, + }); + } + }, + }); } setupModalSharePopover() { - // Stub + if (!this.playerModal || !this.shareBtn) { + return; + } + + // Reuse popover engine for share button + // Using a simpler on-click binding for now to match legacy behavior, + // or instantiate a popover if we want the full menu experience. + // The E2E test expects a popover with [data-menu="video-share"]. + + // We'll create a local property for share popover to clean up later + if (this.modalSharePopover?.destroy) { + this.modalSharePopover.destroy(); + } + + this.modalSharePopover = createPopover({ + trigger: this.shareBtn, + content: (container) => { + if (!this.activeVideo) { + container.innerHTML = ""; + return; + } + const panel = createVideoShareMenuPanel({ + document: this.document, + video: this.activeVideo, + isLoggedIn: this.shareNostrAuthState.isLoggedIn, + hasSigner: this.shareNostrAuthState.hasSigner, + hasMagnet: Boolean(this.modalMoreMenuContext.playbackMagnet), // Re-use magnet from context + hasCdn: Boolean(this.modalMoreMenuContext.playbackUrl), + }); + if (panel) { + container.appendChild(panel); + } + }, + placement: "top", // or "top-start" based on layout + offset: 8, + }); } refreshModalMoreMenuPanel() { - // Stub + if (this.modalMoreMenuPanel && this.activeVideo) { + // Re-render panel content if open + const container = this.modalMoreMenuPanel.parentElement; + if(container) { + container.innerHTML = ""; + const panel = createVideoMoreMenuPanel({ + document: this.document, + video: this.activeVideo, + pointerInfo: this.modalMoreMenuContext.pointerInfo, + playbackUrl: this.modalMoreMenuContext.playbackUrl, + playbackMagnet: this.modalMoreMenuContext.playbackMagnet, + canManageBlacklist: this.modalMoreMenuContext.canManageBlacklist, + context: "modal", + }); + if (panel) { + container.appendChild(panel); + this.modalMoreMenuPanel = panel; + } + } + } } } diff --git a/tests/nostr/nip46Client.test.js b/tests/nostr/nip46Client.test.js index bb47a053..04ada38b 100644 --- a/tests/nostr/nip46Client.test.js +++ b/tests/nostr/nip46Client.test.js @@ -25,8 +25,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () = const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const clientPublicKey = getPublicKey(clientSecret); - const remotePubkey = getPublicKey(remoteSecret); + const clientPublicKey = getPublicKey(utils.hexToBytes(clientSecret)); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); const rpcClient = new Nip46RpcClient({ nostrClient: { relays: [] }, @@ -43,8 +43,8 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () = const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(clientSecret, remotePubkey) - : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); + ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) + : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); const decryptedWithTools = nip44.v2.decrypt(ciphertext, conversationKey); assert.equal( decryptedWithTools, @@ -71,13 +71,13 @@ test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", async () => { const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(remoteSecret); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(clientSecret, remotePubkey) - : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); + ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) + : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const { plaintext, algorithm } = await decryptNip46PayloadWithKeys( @@ -104,13 +104,13 @@ test("decryptNip46PayloadWithKeys coerces structured handshake payloads", async const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(remoteSecret); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(clientSecret, remotePubkey) - : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); + ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) + : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const [ciphertextPart, nonce] = ciphertext.split("\n"); @@ -154,13 +154,13 @@ test("decryptNip46PayloadWithKeys decodes buffer-based handshake payloads", asyn const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(remoteSecret); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); const serialized = JSON.stringify({ message: "hello", count: 42 }); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(clientSecret, remotePubkey) - : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); + ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey) + : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remotePubkey); const ciphertext = nip44.v2.encrypt(serialized, conversationKey); const [ciphertextPart, nonce] = ciphertext.split("\n"); const ciphertextBuffer = Buffer.from(ciphertext, "utf8"); @@ -197,7 +197,7 @@ test("decryptNip46PayloadWithKeys supports nip04-style ciphertext wrappers", asy const clientSecret = utils.bytesToHex(generateSecretKey()); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(remoteSecret); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)); const serialized = JSON.stringify({ message: "hello", count: 42 }); const ciphertext = await nip04.encrypt(clientSecret, remotePubkey, serialized); @@ -254,8 +254,8 @@ test("parseNip46ConnectionString handles remote signer key hints", async () => { const signerSecret = utils.bytesToHex(generateSecretKey()); const userSecret = utils.bytesToHex(generateSecretKey()); - const signerPubkey = getPublicKey(signerSecret).toLowerCase(); - const userPubkey = getPublicKey(userSecret).toLowerCase(); + const signerPubkey = getPublicKey(utils.hexToBytes(signerSecret)).toLowerCase(); + const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase(); const signerNpub = nip19.npubEncode(signerPubkey); const userNpub = nip19.npubEncode(userPubkey); @@ -295,13 +295,13 @@ test( const remoteSignerSecret = utils.bytesToHex(generateSecretKey()); const userSecret = utils.bytesToHex(generateSecretKey()); - const remoteSignerPubkey = getPublicKey(remoteSignerSecret).toLowerCase(); - const userPubkey = getPublicKey(userSecret).toLowerCase(); + const remoteSignerPubkey = getPublicKey(utils.hexToBytes(remoteSignerSecret)).toLowerCase(); + const userPubkey = getPublicKey(utils.hexToBytes(userSecret)).toLowerCase(); const conversationKey = typeof nip44.v2.getConversationKey === "function" - ? nip44.v2.getConversationKey(clientSecret, remoteSignerPubkey) - : nip44.v2.utils.getConversationKey(clientSecret, remoteSignerPubkey); + ? nip44.v2.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey) + : nip44.v2.utils.getConversationKey(utils.hexToBytes(clientSecret), remoteSignerPubkey); const payload = JSON.stringify({ id: "ack", result: "ok" }); const ciphertext = nip44.v2.encrypt(payload, conversationKey); @@ -333,12 +333,12 @@ test( const { attemptDecryptNip46HandshakePayload } = await loadNip46Module(); const clientSecret = utils.bytesToHex(generateSecretKey()); - const clientPubkey = getPublicKey(clientSecret).toLowerCase(); + const clientPubkey = getPublicKey(utils.hexToBytes(clientSecret)).toLowerCase(); const remoteSecret = utils.bytesToHex(generateSecretKey()); - const remotePubkey = getPublicKey(remoteSecret).toLowerCase(); + const remotePubkey = getPublicKey(utils.hexToBytes(remoteSecret)).toLowerCase(); const payload = JSON.stringify({ id: "ack", result: "ack" }); - const ciphertext = nip04.encrypt(remoteSecret, clientPubkey, payload); + const ciphertext = await nip04.encrypt(remoteSecret, clientPubkey, payload); const [ciphertextPart, ivPart] = ciphertext.split("?iv="); assert.ok(ivPart, "nip04 encryption should emit an iv segment");