diff --git a/js/ui/dm/AppShell.js b/js/ui/dm/AppShell.js
index e465d772..7ab36971 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,32 @@ 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);
+ };
+
+ const conversationZapTotalSats = totalsByConversation.get(activeConversation.id) || 0;
+ const profileZapTotalSats = totalsByProfile.get(activeConversation.pubkey) || 0;
+
main.appendChild(
Composer({
document: doc,
@@ -242,8 +269,11 @@ 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,
+ zapStats: {
+ conversationTotal: conversationZapTotalSats,
+ profileTotal: profileZapTotalSats
+ }
}),
);
main.appendChild(
diff --git a/js/ui/dm/Composer.js b/js/ui/dm/Composer.js
index f9f2a1d5..1fa39090 100644
--- a/js/ui/dm/Composer.js
+++ b/js/ui/dm/Composer.js
@@ -9,6 +9,8 @@ function createElement(doc, tag, className, text) {
return element;
}
+import { formatZapAmount } from "./zapHelpers.js";
+
export function Composer({
document: doc,
placeholder = "Write a message…",
@@ -18,6 +20,8 @@ export function Composer({
zapRecipient = null,
zapConfig = null,
onSend,
+ onZap,
+ zapStats = null,
} = {}) {
if (!doc) {
throw new Error("Composer requires a document reference.");
@@ -105,7 +109,65 @@ 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 = ``;
+
+ if (typeof onZap === "function") {
+ zapBtn.addEventListener("click", (e) => {
+ e.preventDefault();
+ onZap();
+ });
+
+ const buttonGroup = createElement(doc, "div", "flex items-center gap-2");
+
+ // Zap Stats Pill
+ if (zapStats && typeof zapStats === "object") {
+ // Reuse class from MessageThread for consistent styling
+ const zapSummary = createElement(doc, "div", "dm-message-thread__zap-summary");
+
+ // Zaps Label
+ const labelSpan = createElement(doc, "span", "dm-message-thread__zap-label", "Zaps");
+ zapSummary.appendChild(labelSpan);
+
+ // Conversation Total
+ // We need formatZapAmount. Since we can't import dynamically easily here (without changing module type or build),
+ // we assume formatZapAmount is available or we pass formatted strings.
+ // BUT, looking at file structure, Composer is a module. I should import it at the top.
+ // I will add the import in a separate block.
+ // Assuming formatZapAmount is imported.
+
+ const conversationTotal = createElement(
+ doc,
+ "span",
+ "dm-message-thread__zap-total",
+ formatZapAmount(zapStats.conversationTotal || 0)
+ );
+ zapSummary.appendChild(conversationTotal);
+
+ // Profile Total
+ const profileTotal = createElement(
+ doc,
+ "span",
+ "dm-message-thread__zap-profile",
+ `Profile: ${formatZapAmount(zapStats.profileTotal || 0)}`
+ );
+ zapSummary.appendChild(profileTotal);
+
+ buttonGroup.appendChild(zapSummary);
+ }
+
+ 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..beb593b0 100644
--- a/js/ui/dm/MessageThread.js
+++ b/js/ui/dm/MessageThread.js
@@ -1,9 +1,5 @@
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);
@@ -79,43 +75,6 @@ export function MessageThread({
privacyBadge.title = privacyHint;
header.appendChild(privacyBadge);
- const zapSummary = createElement(doc, "div", "dm-message-thread__zap-summary");
- zapSummary.appendChild(
- createElement(doc, "span", "dm-message-thread__zap-label", "Zaps"),
- );
- zapSummary.appendChild(
- createElement(
- doc,
- "span",
- "dm-message-thread__zap-total",
- formatZapAmount(conversationZapTotalSats),
- ),
- );
- zapSummary.appendChild(
- createElement(
- doc,
- "span",
- "dm-message-thread__zap-profile",
- `Profile: ${formatZapAmount(profileZapTotalSats)}`,
- ),
- );
- 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 +147,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..b0f96650 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()