Add waiting indicator for event responses and improve UI feedback
This commit is contained in:
parent
9454a8a1b2
commit
aad74cf1a7
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user