mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 10:52:27 +00:00
Refactor DM Zap button to Composer area
Moves the Zap button from the message thread header to the message composer area, adjacent to the Send button. Refactors the Zap interface creation logic into a standalone component `ZapInterface.js` to decouple it from `MessageThread.js`. Updates `Composer.js` to support an optional `onZap` handler and renders the button with an icon-only style matching the video player's accent interaction. Updates `AppShell.js` to orchestrate the Zap interface display.
This commit is contained in:
1 parent
34d57c2072
commit
d16808bcdd
6 files changed
+208
-160
No files matched your search
+25
-2
@@ -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(
|
||||
|
||||
+33
-1
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`;
|
||||
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") {
|
||||
|
||||
@@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>`;
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 80 KiB |
+32
-34
@@ -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()
|
||||
Reference in new issue
Block a user