Add waiting indicator for event responses and improve UI feedback

This commit is contained in:
complex 2025-04-09 15:46:20 +02:00
parent 9454a8a1b2
commit aad74cf1a7
4 changed files with 525 additions and 260 deletions

@ -44,6 +44,160 @@ declare global {
}
}
// Track active response subscriptions
const activeResponseSubscriptions = new Map<string, () => void>();
// Add a new function to update the UI for waiting for a response
function updateUIForWaitingResponse(eventId: string, isWaiting: boolean) {
// Find the event element in the UI
const eventElement = document.querySelector(`[data-event-id="${eventId}"]`);
if (!eventElement) {
console.warn(`Event element not found for ID: ${eventId}`);
return;
}
// Check if waiting indicator already exists
let waitingIndicator = eventElement.querySelector('.waiting-indicator');
if (isWaiting) {
// Create or update waiting indicator
if (!waitingIndicator) {
waitingIndicator = document.createElement('div');
waitingIndicator.className = 'waiting-indicator waiting';
eventElement.appendChild(waitingIndicator);
} else {
waitingIndicator.className = 'waiting-indicator waiting';
}
waitingIndicator.textContent = 'Waiting for response...';
} else {
// Update to received state
if (waitingIndicator) {
waitingIndicator.className = 'waiting-indicator received';
waitingIndicator.textContent = 'Response received';
// Remove the indicator after a delay
setTimeout(() => {
if (waitingIndicator && waitingIndicator.parentNode) {
waitingIndicator.parentNode.removeChild(waitingIndicator);
}
}, 5000);
}
}
}
/**
* Subscribe to events that reference a specific event ID via E tag
* @param eventId The event ID to watch for references
* @param relayUrl The relay URL to subscribe to
* @returns A function to unsubscribe from the subscription
*/
async function subscribeToEventResponses(eventId: string, relayUrl: string): Promise<() => void> {
return new Promise<() => void>((resolve, reject) => {
try {
// Create a direct WebSocket connection
const ws = new WebSocket(relayUrl);
let connected = false;
let unsubscribed = false;
// Set up event handlers
ws.onopen = () => {
console.log(`WebSocket connected to ${relayUrl} for response subscription`);
connected = true;
// Create a filter for events with an E tag matching our event ID
const filter = {
kinds: [21121], // HTTP Response event kind
'#e': [eventId] // Filter for events that reference our event ID
};
// Send a REQ message to subscribe
const reqId = `resp-${Date.now()}`;
const reqMsg = JSON.stringify(["REQ", reqId, filter]);
console.log(`Sending response subscription request: ${reqMsg}`);
ws.send(reqMsg);
// Update UI to show we're waiting for a response
updateUIForWaitingResponse(eventId, true);
};
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data as string);
// Handle different message types
if (Array.isArray(data)) {
console.log('Received message in response subscription:', JSON.stringify(data).substring(0, 100) + '...');
if (data[0] === "EVENT" && data.length >= 3) {
console.log('Processing response event:', data[2].id);
// Update UI to show we received a response
updateUIForWaitingResponse(eventId, false);
// Process the event using the receiver module's functionality
// We need to access the processEvent function from receiver.ts
// Since it's not exported, we'll use a custom event to communicate
const customEvent = new CustomEvent('processNostrEvent', {
detail: data[2]
});
document.dispatchEvent(customEvent);
// Unsubscribe after receiving a response
if (!unsubscribed) {
unsubscribe();
}
}
}
} catch (e) {
console.error('Error processing response message:', e);
}
};
ws.onerror = (err) => {
console.error('WebSocket error in response subscription:', err);
// Update UI to show error state
updateUIForWaitingResponse(eventId, false);
};
ws.onclose = () => {
console.log('Response subscription WebSocket connection closed');
// Update UI to show connection closed
updateUIForWaitingResponse(eventId, false);
};
// Function to unsubscribe
const unsubscribe = () => {
if (connected && !unsubscribed) {
try {
ws.close();
unsubscribed = true;
console.log(`Unsubscribed from response subscription for event ${eventId}`);
} catch (e) {
console.error('Error closing response subscription WebSocket:', e);
}
}
};
// Wait for connection to establish
const timeout = setTimeout(() => {
if (!connected) {
reject(new Error('Response subscription connection timeout'));
// Update UI to show timeout
updateUIForWaitingResponse(eventId, false);
}
}, 5000);
// Resolve with the unsubscribe function
resolve(unsubscribe);
} catch (error) {
reject(new Error(`Error setting up response subscription: ${String(error)}`));
// Update UI to show error
updateUIForWaitingResponse(eventId, false);
}
});
}
/**
* Initialize nostr-login
*/
@ -55,9 +209,9 @@ function initNostrLogin(): void {
const tempContainer = document.createElement('div');
tempContainer.style.display = 'none';
document.body.appendChild(tempContainer);
console.log("Initializing NostrLogin...");
NostrLogin.init({
element: tempContainer,
onConnect: (pubkey: string): void => {
@ -70,7 +224,7 @@ function initNostrLogin(): void {
localStorage.removeItem('userPublicKey');
}
});
// Check if we can get an existing pubkey (already connected)
if (window.nostr) {
window.nostr.getPublicKey().then(pubkey => {
@ -95,17 +249,17 @@ function initNostrLogin(): void {
async function handleServerSearch(): Promise<void> {
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
const resultDiv = document.getElementById('serverSearchResult');
if (!serverPubkeyInput || !resultDiv) {
return;
}
const searchTerm = serverPubkeyInput.value.trim();
if (!searchTerm) {
showError(resultDiv, 'Please enter a search term');
return;
}
// If it's a valid npub, no need to search
if (searchTerm.startsWith('npub')) {
try {
@ -119,13 +273,13 @@ async function handleServerSearch(): Promise<void> {
// Not a valid npub, continue with search
}
}
// Display loading state
showLoading(resultDiv, 'Searching relays...');
try {
const results = await searchUsers(searchTerm);
if (results.length > 0) {
// If there's only one result and it's a valid npub, use it directly
if (results.length === 1 && results[0].name === 'Valid npub') {
@ -133,10 +287,10 @@ async function handleServerSearch(): Promise<void> {
resultDiv.style.display = 'none';
return;
}
// Create the results list
let resultsHtml = '<div class="search-results-list">';
results.forEach(result => {
const truncatedNpub = `${result.npub.substring(0, 10)}...${result.npub.substring(result.npub.length - 5)}`;
resultsHtml += `
@ -148,10 +302,10 @@ async function handleServerSearch(): Promise<void> {
</div>
`;
});
resultsHtml += '</div>';
resultDiv.innerHTML = resultsHtml;
// Add click handlers for the "Use" buttons
document.querySelectorAll('.use-npub-btn').forEach(button => {
button.addEventListener('click', (e) => {
@ -180,29 +334,55 @@ async function handlePublishEvent(): Promise<void> {
const eventOutputPre = document.getElementById('eventOutput') as HTMLElement;
const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
const publishResultDiv = document.getElementById('publishResult') as HTMLElement;
if (!eventOutputPre || !publishRelayInput || !publishResultDiv) {
return;
}
// Check if we have a stored event from the creation process
if (window.currentSignedEvent) {
try {
// Type assertion needed since window.currentSignedEvent is optional
const event = window.currentSignedEvent as NostrEvent;
const relayUrl = publishRelayInput.value.trim();
if (!relayUrl || !relayUrl.startsWith('wss://')) {
showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
return;
}
// Display loading state
showLoading(publishResultDiv, 'Publishing to relay...');
try {
const result = await publishToRelay(event, relayUrl);
showSuccess(publishResultDiv, result);
// Add data-event-id attribute to the event output element
if (event.id) {
eventOutputPre.setAttribute('data-event-id', event.id);
}
// Subscribe to responses for this event
if (event.id) {
try {
// Unsubscribe from any existing subscription for this event
if (activeResponseSubscriptions.has(event.id)) {
const unsubscribe = activeResponseSubscriptions.get(event.id);
if (unsubscribe) {
unsubscribe();
activeResponseSubscriptions.delete(event.id);
}
}
// Create a new subscription
const unsubscribe = await subscribeToEventResponses(event.id, relayUrl);
activeResponseSubscriptions.set(event.id, unsubscribe);
} catch (subError) {
console.error('Error subscribing to responses:', subError);
publishResultDiv.innerHTML += '<br><span style="color: #cc0000;">Failed to subscribe to responses</span>';
}
}
} catch (publishError) {
showError(publishResultDiv, String(publishError));
// Don't rethrow, just handle it here so the UI shows the error
@ -212,30 +392,30 @@ async function handlePublishEvent(): Promise<void> {
// Continue with normal flow if this fails
}
}
const eventText = eventOutputPre.textContent || '';
if (!eventText) {
showError(publishResultDiv, 'No event to publish');
return;
}
let event;
try {
// Check for non-printable characters or hidden characters that might cause issues
const sanitizedEventText = sanitizeText(eventText);
event = JSON.parse(sanitizedEventText);
// Validate that it's a proper Nostr event
if (!event || typeof event !== 'object') {
throw new Error('Invalid event object');
}
// Check if it has id, pubkey, and sig properties which are required for a valid Nostr event
if (!event.id || !event.pubkey || !event.sig) {
throw new Error('Event is missing required properties (id, pubkey, or sig)');
}
// Check if pubkey is in npub format and convert it if needed
if (event.pubkey.startsWith('npub')) {
const hexPubkey = convertNpubToHex(event.pubkey);
@ -245,38 +425,64 @@ async function handlePublishEvent(): Promise<void> {
throw new Error('Invalid npub format in pubkey');
}
}
// Create a clean event with exactly the fields we need
event = standardizeEvent(event);
} catch (error) {
showError(publishResultDiv, `Invalid event: ${String(error)}`);
return;
}
const relayUrl = publishRelayInput.value.trim();
if (!relayUrl || !relayUrl.startsWith('wss://')) {
showError(publishResultDiv, 'Please enter a valid relay URL (must start with wss://)');
return;
}
// Display loading state
showLoading(publishResultDiv, 'Publishing to relay...');
try {
// Verify event first
const isValid = verifyEvent(event);
if (!isValid) {
// Just continue, the relay will validate
}
// Proceed with publish even if verification failed - the relay will validate
publishResultDiv.innerHTML += '<br><span>Attempting to publish...</span>';
try {
const result = await publishToRelay(event, relayUrl);
showSuccess(publishResultDiv, result);
// Add data-event-id attribute to the event output element
if (event.id) {
eventOutputPre.setAttribute('data-event-id', event.id);
}
// Subscribe to responses for this event
if (event.id) {
try {
// Unsubscribe from any existing subscription for this event
if (activeResponseSubscriptions.has(event.id)) {
const unsubscribe = activeResponseSubscriptions.get(event.id);
if (unsubscribe) {
unsubscribe();
activeResponseSubscriptions.delete(event.id);
}
}
// Create a new subscription
const unsubscribe = await subscribeToEventResponses(event.id, relayUrl);
activeResponseSubscriptions.set(event.id, unsubscribe);
} catch (subError) {
console.error('Error subscribing to responses:', subError);
publishResultDiv.innerHTML += '<br><span style="color: #cc0000;">Failed to subscribe to responses</span>';
}
}
} catch (publishError) {
showError(publishResultDiv, String(publishError));
// Don't rethrow, just handle it here so the UI shows the error
@ -296,14 +502,14 @@ function toggleTheme(): void {
// const themeToggleBtn = document.getElementById('themeToggleBtn');
const themeIcon = document.getElementById('themeIcon');
const themeText = document.getElementById('themeText');
const isDarkMode = body.getAttribute('data-theme') === 'dark';
if (isDarkMode) {
// Switch to light theme
body.removeAttribute('data-theme');
window.localStorage.setItem('theme', 'light');
// Update old toggle if it exists
if (themeToggle) {
const toggleText = themeToggle.querySelector('.theme-toggle-text');
@ -315,7 +521,7 @@ function toggleTheme(): void {
toggleIcon.textContent = '🌓';
}
}
// Update new toggle button if it exists
if (themeIcon) {
themeIcon.textContent = '🌙';
@ -327,7 +533,7 @@ function toggleTheme(): void {
// Switch to dark theme
body.setAttribute('data-theme', 'dark');
window.localStorage.setItem('theme', 'dark');
// Update old toggle if it exists
if (themeToggle) {
const toggleText = themeToggle.querySelector('.theme-toggle-text');
@ -339,7 +545,7 @@ function toggleTheme(): void {
toggleIcon.textContent = '☀️';
}
}
// Update new toggle button if it exists
if (themeIcon) {
themeIcon.textContent = '☀️';
@ -356,21 +562,21 @@ function toggleTheme(): void {
function setupTabSwitching(): void {
const tabs = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
// Remove active class from all tabs
tabs.forEach(t => t.classList.remove('active'));
// Add active class to clicked tab
tab.classList.add('active');
// Hide all tab panes
tabPanes.forEach(pane => {
pane.classList.add('hidden');
pane.classList.remove('active');
});
// Show the corresponding tab pane
const targetPane = document.getElementById((tab as HTMLElement).dataset.tab as string);
if (targetPane) {
@ -387,16 +593,16 @@ function setupTabSwitching(): void {
function handleCopyEvent(): void {
const copyButton = document.getElementById('copyEventButton');
const eventOutput = document.getElementById('eventOutput');
if (!copyButton || !eventOutput) {
return;
}
copyButton.addEventListener('click', () => {
if (!eventOutput.textContent) {
return;
}
// Copy text to clipboard
navigator.clipboard.writeText(eventOutput.textContent)
.then(() => {
@ -404,7 +610,7 @@ function handleCopyEvent(): void {
copyButton.classList.add('copied');
const originalText = copyButton.innerHTML;
copyButton.innerHTML = '<span>Copied!</span>';
// Reset after 2 seconds
setTimeout(() => {
copyButton.classList.remove('copied');
@ -421,24 +627,24 @@ function handleCopyEvent(): void {
// Initialize Nostr login as early as possible, before DOM is ready
initNostrLogin();
document.addEventListener('DOMContentLoaded', function(): void {
document.addEventListener('DOMContentLoaded', function (): void {
// Set up the convert button click handler
const convertButton = document.getElementById('convertButton');
const searchButton = document.getElementById('searchServerBtn');
const publishButton = document.getElementById('publishButton');
if (convertButton) {
convertButton.addEventListener('click', displayConvertedEvent);
}
if (searchButton) {
searchButton.addEventListener('click', handleServerSearch);
}
if (publishButton) {
publishButton.addEventListener('click', handlePublishEvent);
}
// Try to get pubkey again after DOM is ready
if (window.nostr) {
window.nostr.getPublicKey().then(pubkey => {
@ -448,23 +654,23 @@ document.addEventListener('DOMContentLoaded', function(): void {
console.warn("DOM ready: Failed to get pubkey:", err);
});
}
// Set default HTTP request
setDefaultHttpRequest();
// Initialize theme toggle
const themeToggle = document.getElementById('themeToggle');
const themeToggleBtn = document.getElementById('themeToggleBtn');
// First try the new button, then fall back to the old toggle
const toggleElement = themeToggleBtn || themeToggle;
if (toggleElement) {
// Set initial theme based on local storage
const savedTheme = window.localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.setAttribute('data-theme', 'dark');
// Update UI for whichever toggle we're using
if (themeToggleBtn) {
const themeIcon = document.getElementById('themeIcon');
@ -486,14 +692,14 @@ document.addEventListener('DOMContentLoaded', function(): void {
}
}
}
// Add click handler
toggleElement.addEventListener('click', toggleTheme);
}
// Initialize copy button
handleCopyEvent();
// Setup tab switching
setupTabSwitching();
});

@ -48,15 +48,15 @@ let standalonePublicKey: string | null = null;
// Initialize the keypair
function initStandaloneKeypair(): { publicKey: string, secretKey: Uint8Array } {
if (!standaloneSecretKey) {
standaloneSecretKey = nostrTools.generateSecretKey();
standalonePublicKey = nostrTools.getPublicKey(standaloneSecretKey);
}
return {
publicKey: standalonePublicKey!,
secretKey: standaloneSecretKey
};
if (!standaloneSecretKey) {
standaloneSecretKey = nostrTools.generateSecretKey();
standalonePublicKey = nostrTools.getPublicKey(standaloneSecretKey);
}
return {
publicKey: standalonePublicKey!,
secretKey: standaloneSecretKey
};
}
/**
@ -74,13 +74,13 @@ function initStandaloneKeypair(): { publicKey: string, secretKey: Uint8Array } {
async function encryptWithWebCrypto(data: string, key: string): Promise<string> {
// Convert text to bytes
const dataBytes = new TextEncoder().encode(data);
// Create a key from the provided key (using SHA-256 hash)
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);
// Import the key
const cryptoKey = await crypto.subtle.importKey(
'raw',
@ -89,10 +89,10 @@ async function encryptWithWebCrypto(data: string, key: string): Promise<string>
false,
['encrypt']
);
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt the data
const encryptedData = await crypto.subtle.encrypt(
{
@ -102,12 +102,12 @@ async function encryptWithWebCrypto(data: string, key: string): Promise<string>
cryptoKey,
dataBytes
);
// Combine IV and ciphertext
const encryptedArray = new Uint8Array(iv.length + encryptedData.byteLength);
encryptedArray.set(iv);
encryptedArray.set(new Uint8Array(encryptedData), iv.length);
// Convert to base64 string
return btoa(String.fromCharCode.apply(null, Array.from(encryptedArray)));
}
@ -120,7 +120,7 @@ export async function convertToEvent(
relayUrl?: string
): Promise<string | null> {
console.log("convertToEvent called with httpRequest:", httpRequest.substring(0, 50) + "...");
if (!httpRequest || httpRequest.trim() === '') {
alert('Please enter an HTTP request message.');
return null;
@ -128,13 +128,13 @@ export async function convertToEvent(
// Create a single kind 21120 event with encrypted HTTP request as content
// Using Web Crypto API for proper encryption
let encryptedContent: string;
try {
// Convert server pubkey to hex if it's an npub
let serverPubkeyHex = serverPubkey;
if (serverPubkey.startsWith('npub')) {
const hexPubkey = convertNpubToHex(serverPubkey);
if (hexPubkey) {
@ -144,7 +144,7 @@ export async function convertToEvent(
throw new Error("Failed to decode npub. Please use a valid npub format.");
}
}
// Validate that we have a hex string of the right length
if (!/^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
throw new Error("Invalid server pubkey format. Must be a 64-character hex string.");
@ -160,11 +160,11 @@ export async function convertToEvent(
// Fall back to unencrypted content if encryption fails
encryptedContent = httpRequest;
}
// Debug log the content
console.log("Final encryptedContent before creating event:",
encryptedContent.substring(0, 50) + "...");
encryptedContent.substring(0, 50) + "...");
// Ensure the serverPubkey is in valid hex format for the p tag
let pTagValue = serverPubkey;
@ -198,19 +198,19 @@ export async function convertToEvent(
// Ensure encryptedContent is a string
const finalContent = typeof encryptedContent === 'string' ?
encryptedContent : JSON.stringify(encryptedContent);
// Encrypt the decryption key using NIP-44 through nostr-signers
let encryptedKey = decryptkey;
// First ensure we're properly connected to nostr-signers
if (!window.nostr) {
console.warn("window.nostr not available - ensure a NIP-07 extension is installed and connected");
} else if (typeof window.nostr.nip44 !== 'object' || typeof window.nostr.nip44.encrypt !== 'function') {
console.warn("NIP-44 encryption not available - connect to a compatible NIP-07 extension");
// Log additional diagnostic information
console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
if (NostrLogin && typeof NostrLogin.getPublicKey === 'function') {
try {
// Try to explicitly connect using NostrLogin
@ -235,7 +235,7 @@ export async function convertToEvent(
console.log("Using unencrypted key as fallback");
}
}
const event: NostrEvent = {
kind: 21120,
pubkey: pubkey,
@ -248,14 +248,14 @@ export async function convertToEvent(
["expiration", String(Math.floor(Date.now() / 1000) + appSettings.expirationTime)]
]
};
console.log("Created event object:", JSON.stringify(event, null, 2));
// Add optional relay tag if provided
if (relayUrl) {
event.tags.push(["r", relayUrl]);
}
// Double-check that all p tags are in hex format
for (let i = 0; i < event.tags.length; i++) {
const tag = event.tags[i];
@ -275,7 +275,7 @@ export async function convertToEvent(
throw new Error(`Invalid npub in p tag: ${tag[1]}`);
}
}
// Verify the tag is now in hex format
if (!/^[0-9a-f]{64}$/i.test(event.tags[i][1])) {
console.error(`Invalid hex format in p tag: ${event.tags[i][1]}`);
@ -283,7 +283,7 @@ export async function convertToEvent(
}
}
}
return JSON.stringify(event, null, 2);
}
@ -295,7 +295,7 @@ export async function displayConvertedEvent(): Promise<void> {
const outputDiv = document.getElementById('output') as HTMLElement;
const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
const relayInput = document.getElementById('relay') as HTMLInputElement;
if (httpRequestBox && eventOutputPre && outputDiv) {
// Get server pubkey and relay values from inputs
const serverPubkey = serverPubkeyInput && serverPubkeyInput.value ?
@ -330,7 +330,7 @@ export async function displayConvertedEvent(): Promise<void> {
console.log("HTTP request textarea value:", httpRequestValue);
console.log("HTTP request length:", httpRequestValue.length);
console.log("HTTP request first 50 chars:", httpRequestValue.substring(0, 50));
// If the request is empty, try to use the placeholder value
let requestToUse = httpRequestValue;
if (!requestToUse || requestToUse.trim() === '') {
@ -345,13 +345,13 @@ export async function displayConvertedEvent(): Promise<void> {
console.log("Using hardcoded default request");
}
}
// Generate a random key for encryption
const randomKey = Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
Math.random().toString(36).substring(2, 15);
console.log("Generated random encryption key:", randomKey);
// Call the async convertToEvent function and await its result
const convertedEvent = await convertToEvent(
requestToUse,
@ -360,22 +360,22 @@ export async function displayConvertedEvent(): Promise<void> {
randomKey,
relay
);
if (convertedEvent) {
// Store the original event in case we need to reference it
(window as any).originalEvent = convertedEvent;
// Variable to hold the Nostr event
let nostrEvent: NostrEvent;
try {
// Parse the event to create a proper Nostr event object for signing
console.log("convertedEvent to parse:", convertedEvent);
const parsedEvent = JSON.parse(convertedEvent);
// Debug the content field
console.log("Event content from parsedEvent:", typeof parsedEvent.content, parsedEvent.content);
// Create the nostrEvent, ensuring content is a string
nostrEvent = {
kind: 21120,
@ -384,7 +384,7 @@ export async function displayConvertedEvent(): Promise<void> {
created_at: Math.floor(Date.now() / 1000),
pubkey: parsedEvent.pubkey
};
// Log the event being signed
console.log("Content field of nostrEvent:", typeof nostrEvent.content, nostrEvent.content.substring(0, 50) + "...");
console.log("Event to be signed:", JSON.stringify(nostrEvent, null, 2));
@ -426,7 +426,7 @@ export async function displayConvertedEvent(): Promise<void> {
} else {
throw new Error("No signing method available");
}
console.log("Event signed successfully");
console.log("Event ID:", signedEvent.id);
console.log("Content field type:", typeof signedEvent.content);
@ -444,57 +444,62 @@ export async function displayConvertedEvent(): Promise<void> {
outputDiv.hidden = false;
return;
}
// Store the event in a global variable for easier access during publishing
(window as any).currentSignedEvent = signedEvent;
// Store the event in a global variable for easier access during publishing
(window as any).currentSignedEvent = signedEvent;
// Display the event JSON
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
// Display the event JSON
eventOutputPre.textContent = JSON.stringify(signedEvent, null, 2);
// Add the data-event-id attribute to the event output element
if (signedEvent.id) {
eventOutputPre.setAttribute('data-event-id', signedEvent.id);
}
// Add a helpful message about publishing the event
const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
if (publishRelayInput) {
const publishResult = document.getElementById('publishResult');
if (publishResult) {
showSuccess(publishResult, 'Event created successfully. Ready to publish!');
}
}
// Add a helpful message about publishing the event
const publishRelayInput = document.getElementById('publishRelay') as HTMLInputElement;
if (publishRelayInput) {
const publishResult = document.getElementById('publishResult');
if (publishResult) {
showSuccess(publishResult, 'Event created successfully. Ready to publish!');
}
}
// Generate animated QR code using multiple frames
const qrCodeContainer = document.getElementById('qrCode') as HTMLElement;
if (qrCodeContainer) {
try {
// Convert the event to a JSON string
const eventJson = JSON.stringify(signedEvent);
// Calculate how many QR codes we need based on the data size
// Use a much smaller chunk size to ensure it fits in the QR code
const maxChunkSize = 500; // Significantly reduced chunk size
const chunks = [];
// Split the data into chunks
for (let i = 0; i < eventJson.length; i += maxChunkSize) {
chunks.push(eventJson.slice(i, i + maxChunkSize));
}
// Prepare container for the animated QR code
qrCodeContainer.innerHTML = '';
const qrFrameContainer = document.createElement('div');
qrFrameContainer.className = 'qr-frame-container';
qrCodeContainer.appendChild(qrFrameContainer);
// Create QR codes for each chunk with chunk number and total chunks
const qrFrames: HTMLElement[] = [];
chunks.forEach((chunk, index) => {
// Add metadata to identify part of sequence: [current/total]:[data]
const dataWithMeta = `${index + 1}/${chunks.length}:${chunk}`;
try {
// Create QR code with maximum version and lower error correction
const qr = qrcode(15, 'L'); // Version 15 (higher capacity), Low error correction
qr.addData(dataWithMeta);
qr.make();
// Create frame
const frameDiv = document.createElement('div');
frameDiv.className = 'qr-frame';
@ -503,7 +508,7 @@ if (publishRelayInput) {
cellSize: 3, // Smaller cell size
margin: 2
});
qrFrameContainer.appendChild(frameDiv);
qrFrames.push(frameDiv);
} catch (qrError) {
@ -521,9 +526,9 @@ if (publishRelayInput) {
qrFrameContainer.appendChild(errorDiv);
qrFrames.push(errorDiv);
}
});
// Add information about the animated QR code
const infoElement = document.createElement('div');
infoElement.innerHTML = `
@ -532,7 +537,7 @@ if (publishRelayInput) {
<p class="qr-info"><small>The QR code will cycle through all frames automatically</small></p>
`;
qrCodeContainer.appendChild(infoElement);
// Animation controls
const controlsDiv = document.createElement('div');
controlsDiv.className = 'qr-controls';
@ -542,19 +547,19 @@ if (publishRelayInput) {
<button id="qrNextBtn">Next </button>
`;
qrCodeContainer.appendChild(controlsDiv);
// Set up animation
let currentFrame = 0;
let animationInterval: number | null = null;
let isPaused = false;
const updateFrameInfo = () => {
const frameInfo = qrCodeContainer.querySelector('.current-frame');
if (frameInfo) {
frameInfo.textContent = `Showing frame ${currentFrame + 1} of ${chunks.length}`;
}
};
const showFrame = (index: number) => {
qrFrames.forEach((frame, i) => {
frame.style.display = i === index ? 'block' : 'none';
@ -562,23 +567,23 @@ if (publishRelayInput) {
currentFrame = index;
updateFrameInfo();
};
const nextFrame = () => {
showFrame((currentFrame + 1) % qrFrames.length);
};
const prevFrame = () => {
showFrame((currentFrame - 1 + qrFrames.length) % qrFrames.length);
};
// Start the animation
animationInterval = window.setInterval(nextFrame, 2000); // Change frame every 2 seconds
// Set up button event handlers
const pauseBtn = document.getElementById('qrPauseBtn');
const prevBtn = document.getElementById('qrPrevBtn');
const nextBtn = document.getElementById('qrNextBtn');
if (pauseBtn) {
pauseBtn.addEventListener('click', () => {
if (isPaused) {
@ -594,7 +599,7 @@ if (publishRelayInput) {
isPaused = !isPaused;
});
}
if (prevBtn) {
prevBtn.addEventListener('click', () => {
if (isPaused) {
@ -602,7 +607,7 @@ if (publishRelayInput) {
}
});
}
if (nextBtn) {
nextBtn.addEventListener('click', () => {
if (isPaused) {
@ -612,20 +617,20 @@ if (publishRelayInput) {
}
} catch (error) {
console.error("Error generating QR code:", error);
// Create a fallback display with error information and a more compact representation
qrCodeContainer.innerHTML = '';
// Create a backup QR code with just the event ID and relay
try {
const eventId = signedEvent.id || '';
const relay = encodeURIComponent(defaultServerConfig.defaultRelay);
const nostrUri = `nostr:${eventId}?relay=${relay}`;
const qr = qrcode(10, 'M');
qr.addData(nostrUri);
qr.make();
qrCodeContainer.innerHTML = `
<div class="qr-error-container">
<h3>Event Too Large for Animated QR</h3>
@ -659,21 +664,21 @@ document.addEventListener('DOMContentLoaded', () => {
// Set up the click event handler without any automatic encryption
const convertButton = document.getElementById('convertButton');
const publishButton = document.getElementById('publishButton');
if (convertButton) {
convertButton.addEventListener('click', displayConvertedEvent);
}
// Add a handler for the publish button to check if an event is available
if (publishButton) {
publishButton.addEventListener('click', () => {
const eventOutput = document.getElementById('eventOutput');
const publishResult = document.getElementById('publishResult');
if (!eventOutput || !publishResult) {
return;
}
if (!eventOutput.textContent || eventOutput.textContent.trim() === '') {
publishResult.innerHTML = '<span style="color: #cc0000;">You need to convert an HTTP request first</span>';
publishResult.style.display = 'block';

@ -16,18 +16,18 @@ async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promi
.split('')
.map(char => char.charCodeAt(0))
);
// Extract IV (first 12 bytes)
const iv = encryptedBytes.slice(0, 12);
// Extract ciphertext (remaining bytes)
const ciphertext = encryptedBytes.slice(12);
// Create key material from the decryption key
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);
// Import the key for AES-GCM decryption
const cryptoKey = await crypto.subtle.importKey(
'raw',
@ -36,7 +36,7 @@ async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promi
false,
['decrypt']
);
// Decrypt the data
const decryptedData = await crypto.subtle.decrypt(
{
@ -46,7 +46,7 @@ async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promi
cryptoKey,
ciphertext
);
// Convert decrypted data to string
return new TextDecoder().decode(decryptedData);
} catch (error) {
@ -79,7 +79,7 @@ let activeRelayUrl: string | null = null;
function getLoggedInPubkey(): string | null {
const pubkey = localStorage.getItem('userPublicKey');
console.log("Retrieved pubkey from localStorage:", pubkey);
// If no pubkey in localStorage, try to get it from window.nostr directly
if (!pubkey && window.nostr && typeof window.nostr.getPublicKey === 'function') {
console.log("No pubkey in localStorage, trying to get from window.nostr");
@ -96,7 +96,7 @@ function getLoggedInPubkey(): string | null {
return null;
});
}
return pubkey;
}
const receivedEvents = new Map<string, ReceivedEvent>();
@ -121,14 +121,14 @@ async function connectToRelay(relayUrl: string): Promise<boolean> {
}
relayPool = null;
}
updateRelayStatus('Connecting to relay...', 'connecting');
try {
// Create a direct WebSocket connection to test connectivity first
const ws = new WebSocket(relayUrl);
let connected = false;
// Wait for connection to establish
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
@ -137,35 +137,35 @@ async function connectToRelay(relayUrl: string): Promise<boolean> {
reject(new Error('Connection timeout after 5 seconds'));
}
}, 5000);
ws.onopen = () => {
clearTimeout(timeout);
connected = true;
resolve();
};
ws.onerror = (err) => {
clearTimeout(timeout);
reject(new Error(`WebSocket error: ${err.toString()}`));
};
});
// Connection successful, close test connection
ws.close();
// Now create the relay pool for later use
relayPool = new nostrTools.SimplePool();
console.log(`Successfully connected to relay: ${relayUrl}`);
activeRelayUrl = relayUrl;
updateRelayStatus('Connected', 'connected');
return true;
} catch (connectionError) {
console.error(`Error connecting to relay: ${connectionError instanceof Error ? connectionError.message : String(connectionError)}`);
// Clean up
relayPool = null;
updateRelayStatus('Connection failed', 'error');
return false;
}
@ -192,7 +192,7 @@ async function subscribeToEvents(options: {
console.error('Cannot subscribe: Relay pool or URL not set');
return;
}
// Unsubscribe if there's an active subscription
if (activeSubscription) {
try {
@ -202,32 +202,32 @@ async function subscribeToEvents(options: {
}
activeSubscription = null;
}
console.log('Creating subscription for kind 21120 events');
// For now, we're going to fetch ALL kind 21120 events from the relay
// to ensure we're getting data
// Get the logged-in user's pubkey if available
const loggedInPubkey = getLoggedInPubkey();
console.log('Creating subscription for kind 21120 events addressed to the user');
// Define the filter type properly
interface NostrFilter {
kinds: number[];
'#p'?: string[];
authors?: string[];
}
// Create filter for kind 21120 events
const filter: NostrFilter = {
kinds: [21120], // HTTP Messages event kind
};
// If the user is logged in, filter for events addressed to them
if (loggedInPubkey) {
let pubkeyHex = loggedInPubkey;
// Convert npub to hex if needed
if (loggedInPubkey.startsWith('npub')) {
try {
@ -239,22 +239,22 @@ async function subscribeToEvents(options: {
console.error("Failed to convert npub to hex:", error);
}
}
// Add p-tag filter for events addressed to the logged-in user
filter['#p'] = [pubkeyHex];
console.log(`Filtering for events addressed to user: ${pubkeyHex}`);
} else {
console.log('No user logged in, showing all kind 21120 events');
}
// Log the filter we're using
console.log('Using filter:', JSON.stringify(filter));
// If a specific pubkey filter is provided in options, use it for p-tag filtering
// This replaces the logged-in user's pubkey filter with the specified one
if (options.pubkeyFilter) {
let pubkey = options.pubkeyFilter;
// Convert npub to hex if needed
if (pubkey.startsWith('npub')) {
try {
@ -266,53 +266,53 @@ async function subscribeToEvents(options: {
console.error("Failed to convert npub to hex:", error);
}
}
// Set the p-tag filter to the specified pubkey
// This replaces any existing p-tag filter set by the logged-in user
filter['#p'] = [pubkey];
console.log(`Overriding p-tag filter to: ${pubkey}`);
}
// Skip using nostr-tools SimplePool for subscription to avoid issues
// Instead, create a direct WebSocket connection
console.log(`Creating direct WebSocket subscription to ${activeRelayUrl}`);
try {
// Create a direct WebSocket connection
// Make sure activeRelayUrl is not null before using it
if (!activeRelayUrl) {
throw new Error('Relay URL is not set');
}
const ws = new WebSocket(activeRelayUrl);
let connected = false;
// Set up event handlers
ws.onopen = () => {
console.log(`WebSocket connected to ${activeRelayUrl}`);
connected = true;
// Send a REQ message to subscribe
const reqId = `req-${Date.now()}`;
const reqMsg = JSON.stringify(["REQ", reqId, filter]);
console.log(`Sending subscription request: ${reqMsg}`);
// Make sure to log when messages are received
console.log('Waiting for events from relay...');
ws.send(reqMsg);
// Update status
updateRelayStatus('Subscription active ✓', 'connected');
};
ws.onmessage = (msg) => {
try {
const data = JSON.parse(msg.data as string);
// Handle different message types
if (Array.isArray(data)) {
console.log('Received message:', JSON.stringify(data).substring(0, 100) + '...');
if (data[0] === "EVENT" && data.length >= 3) {
console.log('Processing event:', data[2].id);
processEvent(data[2] as NostrEvent);
@ -322,12 +322,12 @@ async function subscribeToEvents(options: {
console.error('Error processing message:', e);
}
};
ws.onerror = (err) => {
console.error('WebSocket error:', err);
updateRelayStatus(`WebSocket error`, 'error');
};
ws.onclose = () => {
console.log('WebSocket connection closed');
if (connected) {
@ -336,7 +336,7 @@ async function subscribeToEvents(options: {
updateRelayStatus('Failed to connect', 'error');
}
};
// Wait for connection to establish
await new Promise<void>((resolve, reject) => {
// Set a timeout to prevent hanging
@ -347,13 +347,13 @@ async function subscribeToEvents(options: {
resolve();
}
}, 5000);
// Resolve immediately if already connected
if (connected) {
clearTimeout(timeout);
resolve();
}
// Override onopen to resolve promise
const originalOnOpen = ws.onopen;
ws.onopen = (ev) => {
@ -363,7 +363,7 @@ async function subscribeToEvents(options: {
}
resolve();
};
// Override onerror to reject promise
const originalOnError = ws.onerror;
ws.onerror = (ev) => {
@ -374,7 +374,7 @@ async function subscribeToEvents(options: {
reject(new Error('WebSocket connection error'));
};
});
// Store the subscription for later unsubscription
activeSubscription = {
unsub: () => {
@ -385,7 +385,7 @@ async function subscribeToEvents(options: {
}
}
};
console.log(`Successfully subscribed to ${activeRelayUrl} for kind 21120 events`);
} catch (error) {
console.error('Error creating subscription:', error);
@ -401,14 +401,14 @@ function processEvent(event: NostrEvent): void {
console.error('Received event with no ID, skipping');
return;
}
// Check if this is a new event
if (receivedEvents.has(event.id)) {
return;
}
// Event filtering is now handled at the subscription level
// Store the event
const receivedEvent: ReceivedEvent = {
id: event.id,
@ -416,9 +416,9 @@ function processEvent(event: NostrEvent): void {
receivedAt: Date.now(),
decrypted: false
};
receivedEvents.set(event.id, receivedEvent);
// Add event to UI
addEventToUI(receivedEvent);
}
@ -428,35 +428,35 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
if (!eventsList) {
return;
}
const event = receivedEvent.event;
// Ensure the event has an ID (should already be checked by processEvent)
if (!event.id) {
console.error('Event has no ID, cannot add to UI');
return;
}
// Create event item with improved styling
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
eventItem.dataset.id = event.id;
// Get event ID for display
const eventIdForDisplay = event.id.substring(0, 8);
// Determine if it's a request or response
const hasP = event.tags.some(tag => tag[0] === 'p');
const hasE = event.tags.some(tag => tag[0] === 'e');
const eventType = hasP ? 'HTTP Request' : (hasE ? 'HTTP Response' : 'Unknown');
// Find recipient if available
let recipient = '';
const pTag = event.tags.find(tag => tag[0] === 'p');
if (pTag && pTag.length > 1) {
recipient = `| To: ${pTag[1].substring(0, 8)}...`;
}
// Format the event item
eventItem.innerHTML = `
<div class="event-header">
@ -470,27 +470,27 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
<button class="view-details-btn">View Details</button>
</div>
`;
// Add event listeners
const viewDetailsBtn = eventItem.querySelector('.view-details-btn');
if (viewDetailsBtn) {
viewDetailsBtn.addEventListener('click', () => {
showEventDetails(event.id as string);
// Also trigger decryption when viewing details
decryptEvent(event.id as string);
});
}
// Auto-decrypt the event when it's added to the UI
// Use timeout for better UI responsiveness
setTimeout(() => {
decryptEvent(event.id as string);
}, 100);
// Add to list
eventsList.appendChild(eventItem);
// Clear empty state if present
const emptyState = eventsList.querySelector('.empty-state');
if (emptyState) {
@ -503,18 +503,18 @@ function showEventDetails(eventId: string): void {
if (!eventDetails) {
return;
}
const receivedEvent = receivedEvents.get(eventId);
if (!receivedEvent) {
return;
}
const event = receivedEvent.event;
// Ensure event has an ID (should already be verified)
const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
const fullEventId = event.id || 'unknown';
// Generate tags HTML with better formatting
let tagsHtml = '';
event.tags.forEach((tag: string[]) => {
@ -531,18 +531,18 @@ function showEventDetails(eventId: string): void {
tagsHtml += `<li><strong>${tag[0]}</strong></li>`;
}
});
// Determine if this is a request or response
const isRequest = event.tags.some(tag => tag[0] === 'p');
const isResponse = event.tags.some(tag => tag[0] === 'e');
const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
// Get the decrypted or original content
// Since we now encrypt just the HTTP request directly, this is just the HTTP content
const httpContent = receivedEvent.decrypted ?
(receivedEvent.decryptedContent || event.content) :
event.content;
// Display the event details
eventDetails.innerHTML = `
<div class="event-detail-header">
@ -597,32 +597,32 @@ async function decryptEvent(eventId: string): Promise<void> {
if (!receivedEvent || receivedEvent.decrypted) {
return;
}
try {
const event = receivedEvent.event;
let decryptedContent: string;
// Look for a "key" tag in the event
const keyTag = event.tags.find(tag => tag[0] === 'key');
if (!keyTag || keyTag.length < 2) {
decryptedContent = `[No key tag found for decryption]\n${event.content}`;
} else {
try {
// Extract the encrypted key from the tag
const encryptedKey = keyTag[1];
// Check if window.nostr and nip44.decrypt are available
if (!window.nostr) {
throw new Error("window.nostr is not available - ensure a NIP-07 extension is installed");
}
if (!window.nostr.nip44 || !window.nostr.nip44.decrypt) {
console.warn("NIP-44 decryption not available - trying to connect to a compatible extension");
// Log diagnostic information
console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
// Check if we can trigger a connection via NostrLogin
if (typeof window.nostr.getPublicKey === 'function') {
try {
@ -641,31 +641,31 @@ async function decryptEvent(eventId: string): Promise<void> {
throw new Error("window.nostr.nip44.decrypt is not available");
}
}
// Use window.nostr to decrypt the key tag with NIP-44
console.log("Using window.nostr.nip44.decrypt for key decryption");
// Note that according to the NIP-07 spec, the first parameter is the pubkey (sender)
// and the second parameter is the ciphertext
// Declare decryptedKey outside the try block so it's available in the outer scope
let decryptedKey: string;
try {
// Log the actual values being passed to help debug
console.log(`Attempting to decrypt with pubkey: ${event.pubkey.substring(0, 8)}... and encrypted key: ${encryptedKey.substring(0, 10)}...`);
decryptedKey = await window.nostr.nip44.decrypt(
event.pubkey, // The pubkey that encrypted the key
encryptedKey // The encrypted key from the "key" tag
);
console.log("Decryption successful, decrypted key:", decryptedKey.substring(0, 5) + "...");
} catch (decryptKeyError) {
console.error("Error in direct decryption call:", decryptKeyError);
throw decryptKeyError; // Re-throw to be caught by the outer catch block
}
console.log("Key decryption successful");
// Now use the decrypted key to decrypt the content using Web Crypto API
try {
// Decrypt the content using Web Crypto API and the decrypted key
@ -673,7 +673,7 @@ async function decryptEvent(eventId: string): Promise<void> {
event.content,
decryptedKey
);
// The decrypted content is the direct HTTP request/response
// No need to try parsing as JSON anymore
console.log("Successfully decrypted HTTP content");
@ -690,12 +690,12 @@ Decrypted key: ${decryptedKey}`;
decryptedContent = `[NIP-44 decryption failed: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}]\n${event.content}`;
}
}
// Update the event
receivedEvent.decrypted = true;
receivedEvent.decryptedContent = decryptedContent;
receivedEvents.set(eventId, receivedEvent);
// Update UI if this event is currently being viewed
const selectedEventId = eventDetails?.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
@ -713,49 +713,49 @@ document.addEventListener('DOMContentLoaded', () => {
relayStatus = document.getElementById('relayStatus');
eventsList = document.getElementById('eventsList');
eventDetails = document.getElementById('eventDetails');
const connectRelayBtn = document.getElementById('connectRelayBtn');
// Connect to relay and automatically start subscription for logged-in user
if (connectRelayBtn) {
connectRelayBtn.addEventListener('click', async () => {
if (!relayUrlInput) {
return;
}
// Check if we're logged in before connecting
const userPubkey = getLoggedInPubkey();
console.log("User pubkey when connecting to relay:", userPubkey);
const relayUrl = relayUrlInput.value.trim();
if (!relayUrl || !relayUrl.startsWith('wss://')) {
updateRelayStatus('Invalid relay URL. Must start with wss://', 'error');
return;
}
const success = await connectToRelay(relayUrl);
if (success) {
// Get the logged-in user's pubkey from localStorage as the default
const userPubkey = getLoggedInPubkey();
const pubkeyFilter = userPubkey || '';
// Log the pubkey we're using for subscription
if (userPubkey) {
console.log(`Using logged-in user's pubkey for subscription: ${userPubkey.substring(0, 8)}...`);
} else {
console.log('No user pubkey found, subscribing to all kind 21120 events');
}
// Automatically subscribe to kind 21120 events
try {
// Update status to indicate subscription is in progress
updateRelayStatus('Subscribing...', 'connecting');
await subscribeToEvents({
pubkeyFilter
});
// If no error was thrown, the subscription was successful
console.log(`Subscription initiated to ${activeRelayUrl}`);
} catch (subError) {
@ -763,17 +763,26 @@ document.addEventListener('DOMContentLoaded', () => {
updateRelayStatus(`Subscription error: ${subError instanceof Error ? subError.message : String(subError)}`, 'error');
// No need to update subscription buttons since they're removed
}
// Successful subscription message
// Subscribed to kind 21120 events
// Add subscribed indication to the UI
updateRelayStatus('Connected and subscribed ✓', 'connected');
}
});
}
// No clear events functionality
// Listen for custom events from client.ts for processing response events
document.addEventListener('processNostrEvent', ((event: CustomEvent) => {
const nostrEvent = event.detail;
if (nostrEvent && nostrEvent.id) {
console.log('Processing response event from custom event:', nostrEvent.id);
processEvent(nostrEvent);
}
}) as EventListener);
});

@ -78,14 +78,15 @@ h2 {
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
height: 50px;
display: flex;
width: 100%;
}
body {
padding-top: 70px; /* Added padding to account for fixed navbar */
padding-top: 70px;
/* Added padding to account for fixed navbar */
}
.nav-left {
@ -166,11 +167,13 @@ body {
/* Legacy theme toggle styles for backward compatibility */
.theme-toggle-container {
display: none; /* Hide the old toggle container */
display: none;
/* Hide the old toggle container */
}
.theme-toggle {
display: none; /* Hide the old toggle */
display: none;
/* Hide the old toggle */
}
.theme-toggle-text {
@ -187,7 +190,8 @@ body {
}
/* Form elements */
input[type="text"], textarea {
input[type="text"],
textarea {
width: 100%;
padding: 8px 10px;
margin-bottom: 15px;
@ -271,7 +275,7 @@ button:hover {
background-color: var(--bg-secondary);
border-radius: 4px;
border: 1px solid var(--border-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
pre {
@ -361,7 +365,8 @@ pre {
height: 300px;
margin: 0 auto;
position: relative;
background-color: white; /* QR codes need white background */
background-color: white;
/* QR codes need white background */
}
.qr-frame {
@ -442,6 +447,46 @@ pre {
margin: 10px 0;
}
/* Waiting indicator styles */
.waiting-indicator {
margin-top: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
transition: all 0.3s ease;
}
.waiting-indicator.waiting {
background-color: #e6f3ff;
color: #0088cc;
border: 1px solid #0088cc;
}
.waiting-indicator.received {
background-color: #e6ffe6;
color: #008800;
border: 1px solid #008800;
}
/* Add a subtle animation for the waiting state */
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
.waiting-indicator.waiting {
animation: pulse 2s infinite;
}
/* Responsive adjustments */
@media (max-width: 768px) {
body {
@ -449,25 +494,25 @@ pre {
padding-left: 10px;
padding-right: 10px;
}
.top-nav {
height: 45px;
}
.nav-left {
margin-left: 5px;
}
.nav-right {
right: 5px;
height: 45px;
gap: 10px;
}
.nav-link {
padding: 0 15px;
}
.content {
padding: 10px 0;
}