diff --git a/js/ui/dm/AppShell.js b/js/ui/dm/AppShell.js index e465d772..c926234a 100644 --- a/js/ui/dm/AppShell.js +++ b/js/ui/dm/AppShell.js @@ -4,6 +4,7 @@ import { MessageThread } from "./MessageThread.js"; import { NotificationCenter } from "./NotificationCenter.js"; import { DMPrivacySettings } from "./DMPrivacySettings.js"; import { aggregateZapTotals } from "./zapHelpers.js"; +import { createZapInterface } from "./ZapInterface.js"; function createElement(doc, tag, className, text) { const element = doc.createElement(tag); @@ -233,6 +234,29 @@ export class AppShell { zapConfig, }), ); + const handleZapTrigger = () => { + const existingInterface = main.querySelector(".dm-zap-interface"); + if (existingInterface) { + existingInterface.remove(); + return; + } + + const zapInterface = createZapInterface(doc, { + contact: activeConversation, + zapReceipts: activeZapReceipts, + onSendZap, + zapConfig + }); + + // Position it relative to the composer or just absolute in main + // The extracted createZapInterface uses 'absolute bottom-16 right-4' which positions it nicely + // relative to the container if the container is relative. + // main usually has relative positioning or we can append to root. + // AppShell root has absolute positioning context maybe? + // If main is the container, we append there. + main.appendChild(zapInterface); + }; + main.appendChild( Composer({ document: doc, @@ -242,8 +266,7 @@ export class AppShell { zapRecipient: activeConversation, zapConfig, onSend: onSendMessage, - // Zap button moved to MessageThread, so onSendZap here might be redundant if triggered from thread - // But Composer still handles text message sending. + onZap: handleZapTrigger, }), ); main.appendChild( diff --git a/js/ui/dm/Composer.js b/js/ui/dm/Composer.js index f9f2a1d5..9c2f493d 100644 --- a/js/ui/dm/Composer.js +++ b/js/ui/dm/Composer.js @@ -18,6 +18,7 @@ export function Composer({ zapRecipient = null, zapConfig = null, onSend, + onZap, } = {}) { if (!doc) { throw new Error("Composer requires a document reference."); @@ -105,7 +106,38 @@ export function Composer({ button.setAttribute("aria-label", "Send message"); actions.appendChild(tools); - actions.appendChild(button); + + // Zap Button + // We use dm-composer__send as a base for layout/size compatibility, but tweak styles. + // Using accent-action-button to apply accent color on hover, and px-2 for icon squareness. + const zapBtn = createElement(doc, "button", "dm-composer__send accent-action-button px-2"); + zapBtn.type = "button"; + zapBtn.setAttribute("aria-label", "Zap"); + zapBtn.innerHTML = ``; + zapBtn.style.marginRight = "var(--space-xs)"; // Add some spacing between zap and send if needed, though flex gap usually handles it. + // Actually, actions usually has a gap? Let's check parent styles. + // dm-composer__actions usually separates tools and submit. + // If we want it NEXT to Send, we should group them or just append. + // We'll append both to a container or just actions. + + if (typeof onZap === "function") { + zapBtn.addEventListener("click", (e) => { + e.preventDefault(); + onZap(); + }); + // Create a wrapper for buttons if we want tight grouping, or just append to actions. + // actions has "flex items-center justify-between" usually? + // Let's check CSS for dm-composer__actions. + // If not, we might need a wrapper. + // tools is one child. button is another. + // We'll wrap Zap and Send in a div to keep them together on the right. + const buttonGroup = createElement(doc, "div", "flex items-center gap-2"); + buttonGroup.appendChild(zapBtn); + buttonGroup.appendChild(button); + actions.appendChild(buttonGroup); + } else { + actions.appendChild(button); + } const status = createElement(doc, "div", "dm-composer__status"); if (state === "error") { diff --git a/js/ui/dm/MessageThread.js b/js/ui/dm/MessageThread.js index f7cd01f0..17d09ef1 100644 --- a/js/ui/dm/MessageThread.js +++ b/js/ui/dm/MessageThread.js @@ -1,9 +1,6 @@ import { DayDivider } from "./DayDivider.js"; import { MessageBubble } from "./MessageBubble.js"; -import { ZapReceiptList } from "./ZapReceiptList.js"; import { formatZapAmount } from "./zapHelpers.js"; -import { createAndPublishZapRequest, resolveZapRecipient } from "../../payments/zapRequests.js"; -import { userLogger, devLogger } from "../../utils/logger.js"; function createElement(doc, tag, className, text) { const element = doc.createElement(tag); @@ -101,21 +98,6 @@ export function MessageThread({ ); header.appendChild(zapSummary); - // Zap Button - const zapBtn = createElement(doc, "button", "btn-ghost btn-icon"); - zapBtn.innerHTML = ``; - zapBtn.ariaLabel = "Zap"; - zapBtn.addEventListener("click", () => { - const existingInterface = root.querySelector(".dm-zap-interface"); - if (existingInterface) { - existingInterface.remove(); - return; - } - const zapInterface = createZapInterface(doc, { contact, zapReceipts, onSendZap, zapConfig }); - root.appendChild(zapInterface); - }); - header.appendChild(zapBtn); - if (typeof onMarkRead === "function") { const actions = createElement(doc, "div", "ml-auto flex items-center gap-2"); const markReadButton = createElement(doc, "button", "btn-ghost", "Mark read"); @@ -188,108 +170,3 @@ export function MessageThread({ root.appendChild(timeline); return root; } - -function createZapInterface(doc, { contact, zapReceipts, onSendZap, zapConfig }) { - const container = createElement(doc, "div", "dm-zap-interface card p-4 absolute bottom-16 right-4 z-20 w-80 shadow-xl border border-border bg-surface"); - - const closeBtn = createElement(doc, "button", "absolute top-2 right-2 btn-ghost btn-xs", "✕"); - closeBtn.addEventListener("click", () => container.remove()); - container.appendChild(closeBtn); - - const title = createElement(doc, "h4", "text-sm font-bold mb-2", "Send Zap"); - container.appendChild(title); - - // Zap Form - const form = createElement(doc, "form", "space-y-2 mb-4"); - const amountInput = createElement(doc, "input", "input input-sm w-full"); - amountInput.type = "number"; - amountInput.placeholder = "Amount (sats)"; - amountInput.min = "1"; - - const noteInput = createElement(doc, "input", "input input-sm w-full"); - noteInput.type = "text"; - noteInput.placeholder = "Comment (optional)"; - - const sendBtn = createElement(doc, "button", "btn btn-sm btn-primary w-full", "Zap ⚡"); - const statusDiv = createElement(doc, "div", "text-xs text-center mt-1"); - - sendBtn.addEventListener("click", async (e) => { - e.preventDefault(); - const amount = parseInt(amountInput.value); - const comment = noteInput.value; - if (!amount || amount <= 0) { - statusDiv.textContent = "Invalid amount"; - statusDiv.className = "text-error text-xs text-center mt-1"; - return; - } - - statusDiv.textContent = "Sending..."; - statusDiv.className = "text-muted text-xs text-center mt-1"; - sendBtn.disabled = true; - - try { - if (typeof onSendZap === "function") { - // Use provided callback if available (e.g. from controller) - if (!contact.lightningAddress) { - throw new Error("No lightning address for this contact"); - } - - const resolver = zapConfig?.resolveRecipient || resolveZapRecipient; - const resolved = await resolver(contact.lightningAddress, { fetcher: zapConfig?.fetcher }); - if (!resolved) throw new Error("Could not resolve lightning address"); - - const payload = { - amountSats: amount, - comment, - recipient: contact, - resolved - }; - await onSendZap(payload); - statusDiv.textContent = "Zap sent!"; - statusDiv.className = "text-success text-xs text-center mt-1"; - amountInput.value = ""; - noteInput.value = ""; - } else if (zapConfig?.signer) { - await createAndPublishZapRequest({ - address: contact.lightningAddress, - recipientPubkey: contact.pubkey, - relays: contact.relayHints || zapConfig?.relays, - amountSats: amount, - comment, - signer: zapConfig.signer, - pool: zapConfig.pool, - fetcher: zapConfig.fetcher, - }); - statusDiv.textContent = "Zap sent!"; - statusDiv.className = "text-success text-xs text-center mt-1"; - amountInput.value = ""; - noteInput.value = ""; - } else { - throw new Error("No signer available"); - } - } catch (err) { - console.error(err); - statusDiv.textContent = "Failed to send zap"; - statusDiv.className = "text-error text-xs text-center mt-1"; - } finally { - sendBtn.disabled = false; - } - }); - - form.appendChild(amountInput); - form.appendChild(noteInput); - form.appendChild(sendBtn); - form.appendChild(statusDiv); - container.appendChild(form); - - // Receipts List - container.appendChild( - ZapReceiptList({ - document: doc, - receipts: zapReceipts, - emptyLabel: "No recent zaps.", - }), - ); - - return container; -} diff --git a/js/ui/dm/ZapInterface.js b/js/ui/dm/ZapInterface.js new file mode 100644 index 00000000..eb3a3ed6 --- /dev/null +++ b/js/ui/dm/ZapInterface.js @@ -0,0 +1,118 @@ +import { ZapReceiptList } from "./ZapReceiptList.js"; +import { createAndPublishZapRequest, resolveZapRecipient } from "../../payments/zapRequests.js"; + +function createElement(doc, tag, className, text) { + const element = doc.createElement(tag); + if (className) { + element.className = className; + } + if (typeof text === "string") { + element.textContent = text; + } + return element; +} + +export function createZapInterface(doc, { contact, zapReceipts, onSendZap, zapConfig }) { + const container = createElement(doc, "div", "dm-zap-interface card p-4 absolute bottom-16 right-4 z-20 w-80 shadow-xl border border-border bg-surface"); + + const closeBtn = createElement(doc, "button", "absolute top-2 right-2 btn-ghost btn-xs", "✕"); + closeBtn.addEventListener("click", () => container.remove()); + container.appendChild(closeBtn); + + const title = createElement(doc, "h4", "text-sm font-bold mb-2", "Send Zap"); + container.appendChild(title); + + // Zap Form + const form = createElement(doc, "form", "space-y-2 mb-4"); + const amountInput = createElement(doc, "input", "input input-sm w-full"); + amountInput.type = "number"; + amountInput.placeholder = "Amount (sats)"; + amountInput.min = "1"; + + const noteInput = createElement(doc, "input", "input input-sm w-full"); + noteInput.type = "text"; + noteInput.placeholder = "Comment (optional)"; + + const sendBtn = createElement(doc, "button", "btn btn-sm btn-primary w-full", "Zap ⚡"); + const statusDiv = createElement(doc, "div", "text-xs text-center mt-1"); + + sendBtn.addEventListener("click", async (e) => { + e.preventDefault(); + const amount = parseInt(amountInput.value); + const comment = noteInput.value; + if (!amount || amount <= 0) { + statusDiv.textContent = "Invalid amount"; + statusDiv.className = "text-error text-xs text-center mt-1"; + return; + } + + statusDiv.textContent = "Sending..."; + statusDiv.className = "text-muted text-xs text-center mt-1"; + sendBtn.disabled = true; + + try { + if (typeof onSendZap === "function") { + // Use provided callback if available (e.g. from controller) + if (!contact.lightningAddress) { + throw new Error("No lightning address for this contact"); + } + + const resolver = zapConfig?.resolveRecipient || resolveZapRecipient; + const resolved = await resolver(contact.lightningAddress, { fetcher: zapConfig?.fetcher }); + if (!resolved) throw new Error("Could not resolve lightning address"); + + const payload = { + amountSats: amount, + comment, + recipient: contact, + resolved + }; + await onSendZap(payload); + statusDiv.textContent = "Zap sent!"; + statusDiv.className = "text-success text-xs text-center mt-1"; + amountInput.value = ""; + noteInput.value = ""; + } else if (zapConfig?.signer) { + await createAndPublishZapRequest({ + address: contact.lightningAddress, + recipientPubkey: contact.pubkey, + relays: contact.relayHints || zapConfig?.relays, + amountSats: amount, + comment, + signer: zapConfig.signer, + pool: zapConfig.pool, + fetcher: zapConfig.fetcher, + }); + statusDiv.textContent = "Zap sent!"; + statusDiv.className = "text-success text-xs text-center mt-1"; + amountInput.value = ""; + noteInput.value = ""; + } else { + throw new Error("No signer available"); + } + } catch (err) { + console.error(err); + statusDiv.textContent = "Failed to send zap"; + statusDiv.className = "text-error text-xs text-center mt-1"; + } finally { + sendBtn.disabled = false; + } + }); + + form.appendChild(amountInput); + form.appendChild(noteInput); + form.appendChild(sendBtn); + form.appendChild(statusDiv); + container.appendChild(form); + + // Receipts List + container.appendChild( + ZapReceiptList({ + document: doc, + receipts: zapReceipts, + emptyLabel: "No recent zaps.", + }), + ); + + return container; +} diff --git a/verification_profile_messages.png b/verification_profile_messages.png index 37ec55cb..d7132e27 100644 Binary files a/verification_profile_messages.png and b/verification_profile_messages.png differ diff --git a/verify_profile.py b/verify_profile.py index 4cd015d5..12a5c34b 100644 --- a/verify_profile.py +++ b/verify_profile.py @@ -1,53 +1,51 @@ - from playwright.sync_api import sync_playwright -def verify_profile_modal(): +def verify_profile_messages(): with sync_playwright() as p: browser = p.chromium.launch(headless=True) - context = browser.new_context(viewport={'width': 1280, 'height': 720}) - page = context.new_page() - - # Navigate to the app + page = browser.new_page() page.goto("http://localhost:8000") - # Wait for the page to load - page.wait_for_load_state("networkidle") + # 1. Close disclaimer modal if present + # The logs say #disclaimerModal intercepts events. + try: + page.wait_for_selector("#closeDisclaimerModal", timeout=5000) + page.click("#closeDisclaimerModal") + except: + print("Disclaimer modal not found or not visible") - # Make the profile modal visible - page.evaluate("""() => { + # Also could be #disclaimerAgreeBtn + try: + page.wait_for_selector("#disclaimerAgreeBtn", timeout=2000) + page.click("#disclaimerAgreeBtn") + except: + pass + + # 2. Open Profile Modal via JS + page.evaluate(""" const modal = document.getElementById('profileModal'); if (modal) { modal.classList.remove('hidden'); modal.setAttribute('aria-hidden', 'false'); + document.body.classList.add('modal-open'); + + // Ensure profile modal is on top if multiple modals + modal.style.zIndex = '9999'; } - }""") + """) - # Force the Messages pane to be visible - page.evaluate("""() => { - const pane = document.getElementById('profilePaneMessages'); - if (pane) { - pane.classList.remove('hidden'); - pane.removeAttribute('hidden'); - // Ensure other panes are hidden if needed, but not strictly necessary for content check - } - }""") + # 3. Click Messages tab + # Wait for animation/visibility + page.wait_for_timeout(500) + page.click("#profileNavMessages") - # Wait for the pane to become active/visible - page.wait_for_selector("#profilePaneMessages:not(.hidden)") + # 4. Wait for AppShell + page.wait_for_selector("#dmAppShellMount") + page.wait_for_timeout(2000) - # Take a screenshot + # 5. Take screenshot page.screenshot(path="verification_profile_messages.png") - - content = page.content() - if "Direct message privacy" in content: - print("FAILURE: 'Direct message privacy' text found in the page source.") - else: - print("SUCCESS: 'Direct message privacy' text NOT found.") - - if "Read receipts" in content and "Direct message privacy" in content: - print("FAILURE: 'Read receipts' text found.") - browser.close() if __name__ == "__main__": - verify_profile_modal() + verify_profile_messages()