feat: end to end request to request
This commit is contained in:
parent
bf471337b0
commit
173f9d19fe
@ -3,4 +3,5 @@ don't try to modify files in the build folder (eg /dist)
|
||||
don't put any css or javascript blocks in the index.html file
|
||||
npm run lint after every code update
|
||||
don't ever use the alert function
|
||||
assume you are in the client folder
|
||||
do not ask to "cd client"
|
||||
all signing to be done using nostr-login
|
@ -40,7 +40,7 @@
|
||||
<div style="margin-bottom: 10px;">
|
||||
<label for="serverPubkey">Server Pubkey or Search Term:</label><br>
|
||||
<div class="server-input-container">
|
||||
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun" class="server-input">
|
||||
<input type="text" id="serverPubkey" placeholder="npub, username, or NIP-05 identifier" value="npub1thq3fzcw393c0tpy60sz0dvvjz4tjrgtrudxsa68sldkf78fznksgp34w8" class="server-input">
|
||||
<button id="searchServerBtn" class="server-search-button">Search</button>
|
||||
</div>
|
||||
<div id="serverSearchResult" class="server-search-result" style="display: none;">
|
||||
@ -50,7 +50,7 @@
|
||||
|
||||
<div>
|
||||
<label for="relay">Response Relay (optional):</label><br>
|
||||
<input type="text" id="relay" value="wss://relay.damus.io" style="width: 100%; padding: 8px;">
|
||||
<input type="text" id="relay" value="wss://relay.degmods.com" style="width: 100%; padding: 8px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -75,7 +75,7 @@ User-Agent: Browser/1.0
|
||||
<div class="publish-container">
|
||||
<h2>Publish to Relay:</h2>
|
||||
<div class="publish-input-container">
|
||||
<input type="text" id="publishRelay" value="wss://relay.nostrdev.com" placeholder="wss://relay.example.com" class="publish-input">
|
||||
<input type="text" id="publishRelay" value="wss://relay.degmods.com" placeholder="wss://relay.example.com" class="publish-input">
|
||||
<button id="publishButton" class="publish-button">Publish Event</button>
|
||||
</div>
|
||||
<div id="publishResult" class="publish-result" style="display: none;">
|
||||
|
@ -36,41 +36,38 @@
|
||||
<div class="relay-connection">
|
||||
<div class="relay-input-container">
|
||||
<label for="relayUrl">Relay URL:</label>
|
||||
<input type="text" id="relayUrl" value="wss://relay.damus.io" placeholder="wss://relay.example.com">
|
||||
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
|
||||
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
|
||||
</div>
|
||||
<div id="relayStatus" class="relay-status">Not connected</div>
|
||||
</div>
|
||||
|
||||
<h2>Subscription Settings</h2>
|
||||
<div class="subscription-settings">
|
||||
<p class="info-text">Automatically shows all kind 21120 events that are p-tagged to the server npub.</p>
|
||||
|
||||
<button id="startSubscriptionBtn" class="start-subscription-button">Start Subscription</button>
|
||||
<button id="stopSubscriptionBtn" class="stop-subscription-button" disabled>Stop Subscription</button>
|
||||
</div>
|
||||
|
||||
<h2>Received Events</h2>
|
||||
<div class="received-events">
|
||||
<div class="event-controls">
|
||||
<button id="clearEventsBtn" class="clear-events-button">Clear Events</button>
|
||||
</div>
|
||||
|
||||
<div id="eventsList" class="events-list">
|
||||
<div class="empty-state">
|
||||
No events received yet. Connect to a relay and start a subscription.
|
||||
<div class="events-container">
|
||||
<div class="events-sidebar">
|
||||
<div id="eventsList" class="events-list">
|
||||
<div class="empty-state">
|
||||
No events received yet. Connect to a relay to start receiving events.
|
||||
</div>
|
||||
<!-- Events will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="events-content">
|
||||
<div id="eventDetails" class="event-details">
|
||||
<div class="empty-state">
|
||||
Select an event to view details
|
||||
</div>
|
||||
<!-- Selected event details will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- Events will be displayed here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Event Details</h2>
|
||||
<div id="eventDetails" class="event-details">
|
||||
<div class="empty-state">
|
||||
Select an event to view details
|
||||
</div>
|
||||
<!-- Selected event details will be shown here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Script will be provided by bundle.js -->
|
||||
|
@ -2,7 +2,17 @@
|
||||
// This follows strict CSP policies by avoiding inline scripts
|
||||
|
||||
// Import from Node.js built-ins & external modules
|
||||
import * as nostrTools from 'nostr-tools';
|
||||
// No longer need direct nostr-tools imports for this file
|
||||
|
||||
// On page load, always fetch the latest pubkey from window.nostr
|
||||
if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
|
||||
window.nostr.getPublicKey().then(pubkey => {
|
||||
console.log(`Page load: Retrieved pubkey from window.nostr: ${pubkey.slice(0, 8)}...`);
|
||||
localStorage.setItem('userPublicKey', pubkey);
|
||||
}).catch(err => {
|
||||
console.warn("Page load: Failed to get pubkey from window.nostr:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Import type definitions
|
||||
import type { NostrEvent } from './converter';
|
||||
@ -38,37 +48,44 @@ declare global {
|
||||
* Initialize nostr-login
|
||||
*/
|
||||
function initNostrLogin(): void {
|
||||
const loginContainer = document.querySelector('.login-container');
|
||||
const loginStatusDiv = document.getElementById('loginStatus');
|
||||
|
||||
if (!loginContainer || !loginStatusDiv) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a container for the NostrLogin button
|
||||
const nostrLoginContainer = document.createElement('div');
|
||||
nostrLoginContainer.id = 'nostr-login-container';
|
||||
loginContainer.appendChild(nostrLoginContainer);
|
||||
|
||||
try {
|
||||
// Initialize NostrLogin with the container
|
||||
// Initialize NostrLogin without requiring UI elements
|
||||
if (NostrLogin && NostrLogin.init) {
|
||||
// Create a temporary container if needed
|
||||
const tempContainer = document.createElement('div');
|
||||
tempContainer.style.display = 'none';
|
||||
document.body.appendChild(tempContainer);
|
||||
|
||||
console.log("Initializing NostrLogin...");
|
||||
|
||||
NostrLogin.init({
|
||||
element: nostrLoginContainer,
|
||||
element: tempContainer,
|
||||
onConnect: (pubkey: string): void => {
|
||||
const npub = nostrTools.nip19.npubEncode(pubkey);
|
||||
loginStatusDiv.innerHTML = `<span style="color: #008800;">Connected as: ${npub.slice(0, 8)}...${npub.slice(-4)}</span>`;
|
||||
console.log(`Connected to Nostr with pubkey: ${pubkey.slice(0, 8)}...`);
|
||||
// Store pubkey in localStorage for other parts of the app
|
||||
localStorage.setItem('userPublicKey', pubkey);
|
||||
},
|
||||
onDisconnect: (): void => {
|
||||
loginStatusDiv.innerHTML = '<span>Disconnected</span>';
|
||||
console.log('Disconnected from Nostr');
|
||||
localStorage.removeItem('userPublicKey');
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we can get an existing pubkey (already connected)
|
||||
if (window.nostr) {
|
||||
window.nostr.getPublicKey().then(pubkey => {
|
||||
console.log(`Already connected with pubkey: ${pubkey.slice(0, 8)}...`);
|
||||
localStorage.setItem('userPublicKey', pubkey);
|
||||
}).catch(err => {
|
||||
console.warn("Not connected to Nostr extension:", err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
loginStatusDiv.innerHTML = '<span style="color: #cc0000;">NostrLogin initialization unavailable</span>';
|
||||
console.warn("NostrLogin initialization unavailable");
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
loginStatusDiv.innerHTML = `<span style="color: #cc0000;">Error initializing Nostr login: ${errorMessage}</span>`;
|
||||
console.error(`Error initializing Nostr login: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,6 +418,9 @@ function handleCopyEvent(): void {
|
||||
}
|
||||
|
||||
// Initialize the event handlers when the DOM is loaded
|
||||
// Initialize Nostr login as early as possible, before DOM is ready
|
||||
initNostrLogin();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(): void {
|
||||
// Set up the convert button click handler
|
||||
const convertButton = document.getElementById('convertButton');
|
||||
@ -419,8 +439,15 @@ document.addEventListener('DOMContentLoaded', function(): void {
|
||||
publishButton.addEventListener('click', handlePublishEvent);
|
||||
}
|
||||
|
||||
// Initialize Nostr login
|
||||
initNostrLogin();
|
||||
// Try to get pubkey again after DOM is ready
|
||||
if (window.nostr) {
|
||||
window.nostr.getPublicKey().then(pubkey => {
|
||||
console.log(`DOM ready: Retrieved pubkey: ${pubkey.slice(0, 8)}...`);
|
||||
localStorage.setItem('userPublicKey', pubkey);
|
||||
}).catch(err => {
|
||||
console.warn("DOM ready: Failed to get pubkey:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Set default HTTP request
|
||||
setDefaultHttpRequest();
|
||||
|
@ -7,7 +7,7 @@ export const defaultServerConfig = {
|
||||
// Server's npub (hard-coded to match the server's config)
|
||||
serverNpub: "npub1r6knexka25dn9w9jnf5kf8xh6gfq7n3p38zfl7nn7cjjjsp4umcqnk0aun",
|
||||
// Default relay for responses
|
||||
defaultRelay: "wss://relay.damus.io"
|
||||
defaultRelay: "wss://relay.degmods.com"
|
||||
};
|
||||
|
||||
// Application settings
|
||||
|
@ -25,7 +25,8 @@ interface NostrWindowExtension {
|
||||
getPublicKey: () => Promise<string>;
|
||||
signEvent: (event: NostrEvent) => Promise<NostrEvent>;
|
||||
nip44?: {
|
||||
encrypt: (content: string, pubkey: string) => string;
|
||||
encrypt(pubkey: string, plaintext: string): Promise<string>;
|
||||
decrypt(pubkey: string, ciphertext: string): Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
@ -148,20 +149,11 @@ export async function convertToEvent(
|
||||
if (!/^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
|
||||
throw new Error("Invalid server pubkey format. Must be a 64-character hex string.");
|
||||
}
|
||||
|
||||
// Create a payload object with the HTTP request
|
||||
const payload = {
|
||||
httpRequest: httpRequest,
|
||||
timestamp: Date.now(),
|
||||
serverPubkey: serverPubkeyHex
|
||||
};
|
||||
|
||||
// Stringify the payload to encrypt
|
||||
const payloadString = JSON.stringify(payload);
|
||||
|
||||
// Use Web Crypto API to encrypt the payload with decryptkey
|
||||
console.log("Encrypting with Web Crypto API");
|
||||
encryptedContent = await encryptWithWebCrypto(payloadString, decryptkey);
|
||||
// Encrypt the HTTP request directly without wrapping it in JSON
|
||||
// This ensures the decrypted content is the raw HTTP request
|
||||
console.log("Encrypting raw HTTP request with Web Crypto API");
|
||||
encryptedContent = await encryptWithWebCrypto(httpRequest, decryptkey);
|
||||
console.log("Successfully encrypted HTTP content with Web Crypto API");
|
||||
console.log("Successfully encrypted content with Web Crypto API");
|
||||
} catch (error) {
|
||||
console.error("Error in encryption:", error);
|
||||
@ -207,6 +199,43 @@ export async function convertToEvent(
|
||||
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
|
||||
const pubkey = await NostrLogin.getPublicKey();
|
||||
console.log("Retrieved public key via NostrLogin:", pubkey);
|
||||
// Sometimes this explicit call triggers the extension to connect properly
|
||||
} catch (e) {
|
||||
console.error("Failed to connect via NostrLogin:", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
console.log("Encrypting decryption key using window.nostr.nip44.encrypt");
|
||||
// According to the NIP-07 spec, the first parameter is the pubkey (recipient)
|
||||
// and the second parameter is the plaintext to encrypt
|
||||
const encryptionPromise = window.nostr.nip44.encrypt(pTagValue, decryptkey);
|
||||
// Since this is a Promise, we need to await it
|
||||
encryptedKey = await encryptionPromise;
|
||||
console.log("Successfully encrypted the decryption key with NIP-44");
|
||||
} catch (encryptError) {
|
||||
console.error("Failed to encrypt key with NIP-44:", encryptError instanceof Error ? encryptError.message : String(encryptError));
|
||||
console.log("Using unencrypted key as fallback");
|
||||
}
|
||||
}
|
||||
|
||||
const event: NostrEvent = {
|
||||
kind: 21120,
|
||||
pubkey: pubkey,
|
||||
@ -215,7 +244,7 @@ export async function convertToEvent(
|
||||
tags: [
|
||||
// Required tags per README specification
|
||||
["p", pTagValue], // P tag with hex pubkey (converted from npub if needed)
|
||||
["key", decryptkey], // Key for decryption
|
||||
["key", encryptedKey], // Key for decryption, encrypted with NIP-44
|
||||
["expiration", String(Math.floor(Date.now() / 1000) + appSettings.expirationTime)]
|
||||
]
|
||||
};
|
||||
|
@ -5,6 +5,55 @@ import * as nostrTools from 'nostr-tools';
|
||||
import { convertNpubToHex } from './relay';
|
||||
import type { NostrEvent } from './relay';
|
||||
|
||||
// Import the NostrWindowExtension interface from converter
|
||||
|
||||
// Helper function for Web Crypto API decryption (mirror of encryptWithWebCrypto in converter.ts)
|
||||
async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promise<string> {
|
||||
try {
|
||||
// Convert base64 to byte array
|
||||
const encryptedBytes = new Uint8Array(
|
||||
atob(encryptedBase64)
|
||||
.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',
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
// Decrypt the data
|
||||
const decryptedData = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: iv
|
||||
},
|
||||
cryptoKey,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
// Convert decrypted data to string
|
||||
return new TextDecoder().decode(decryptedData);
|
||||
} catch (error) {
|
||||
throw new Error(`WebCrypto decryption failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Define interfaces for our application
|
||||
interface NostrSubscription {
|
||||
unsub: () => void;
|
||||
@ -22,6 +71,34 @@ interface ReceivedEvent {
|
||||
let relayPool: any = null;
|
||||
let activeSubscription: NostrSubscription | null = null;
|
||||
let activeRelayUrl: string | null = null;
|
||||
|
||||
/**
|
||||
* Get the logged-in user's public key from localStorage
|
||||
* This is populated by the nostr-login plugin
|
||||
*/
|
||||
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");
|
||||
// Note: This returns a promise, so we can't use it synchronously
|
||||
// But we'll trigger the fetch so it might be available next time
|
||||
window.nostr.getPublicKey()
|
||||
.then(directPubkey => {
|
||||
console.log("Retrieved pubkey directly from window.nostr:", directPubkey);
|
||||
localStorage.setItem('userPublicKey', directPubkey);
|
||||
return directPubkey;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Failed to get pubkey from window.nostr:", err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
return pubkey;
|
||||
}
|
||||
const receivedEvents = new Map<string, ReceivedEvent>();
|
||||
|
||||
// DOM Elements (populated on DOMContentLoaded)
|
||||
@ -130,25 +207,51 @@ async function subscribeToEvents(options: {
|
||||
|
||||
// For now, we're going to fetch ALL kind 21120 events from the relay
|
||||
// to ensure we're getting data
|
||||
console.log('Creating subscription for ALL kind 21120 events');
|
||||
// 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: any = {
|
||||
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 {
|
||||
const hexPubkey = convertNpubToHex(loggedInPubkey);
|
||||
if (hexPubkey) {
|
||||
pubkeyHex = hexPubkey;
|
||||
}
|
||||
} catch (error) {
|
||||
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));
|
||||
|
||||
|
||||
// Define the author filter type properly
|
||||
interface AuthorFilter {
|
||||
kinds: number[];
|
||||
'#p': string[];
|
||||
authors?: string[];
|
||||
}
|
||||
|
||||
// We can also filter by author if specified
|
||||
// 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;
|
||||
|
||||
@ -164,8 +267,10 @@ async function subscribeToEvents(options: {
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional author filter
|
||||
(filter as AuthorFilter).authors = [pubkey];
|
||||
// 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
|
||||
@ -302,6 +407,8 @@ function processEvent(event: NostrEvent): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Event filtering is now handled at the subscription level
|
||||
|
||||
// Store the event
|
||||
const receivedEvent: ReceivedEvent = {
|
||||
id: event.id,
|
||||
@ -330,7 +437,7 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create event item
|
||||
// Create event item with improved styling
|
||||
const eventItem = document.createElement('div');
|
||||
eventItem.className = 'event-item';
|
||||
eventItem.dataset.id = event.id;
|
||||
@ -341,15 +448,24 @@ function addEventToUI(receivedEvent: ReceivedEvent): void {
|
||||
// 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 ? 'Request' : (hasE ? 'Response' : 'Unknown');
|
||||
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">
|
||||
<span class="event-id">ID: ${eventIdForDisplay}...</span>
|
||||
<span class="event-id">${eventIdForDisplay}...</span>
|
||||
<span class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
<div class="event-summary">Type: ${eventType} | From: ${event.pubkey.substring(0, 8)}...</div>
|
||||
<div class="event-summary">
|
||||
<span class="event-type">${eventType}</span> | From: ${event.pubkey.substring(0, 8)}... ${recipient}
|
||||
</div>
|
||||
<div class="event-actions">
|
||||
<button class="view-details-btn">View Details</button>
|
||||
</div>
|
||||
@ -421,40 +537,57 @@ function showEventDetails(eventId: string): void {
|
||||
const isResponse = event.tags.some(tag => tag[0] === 'e');
|
||||
const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
|
||||
|
||||
// Format content for display (if decrypted, use the decrypted content)
|
||||
const displayContent = receivedEvent.decrypted ?
|
||||
// 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;
|
||||
|
||||
// Try to pretty-print JSON if the content appears to be JSON
|
||||
let formattedContent = displayContent || '';
|
||||
try {
|
||||
if (formattedContent.trim().startsWith('{')) {
|
||||
const jsonObj = JSON.parse(formattedContent);
|
||||
formattedContent = JSON.stringify(jsonObj, null, 2);
|
||||
}
|
||||
} catch {
|
||||
// Not valid JSON, use as-is
|
||||
}
|
||||
|
||||
// For raw display of encrypted content if needed
|
||||
const rawContent = event.content;
|
||||
eventDetails.innerHTML = `
|
||||
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
|
||||
<div class="event-detail-item">
|
||||
<strong>ID:</strong> ${fullEventId}
|
||||
<div class="event-detail-header">
|
||||
<h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
|
||||
<div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>From:</strong> ${event.pubkey}
|
||||
|
||||
<div class="event-detail-metadata">
|
||||
<div class="event-detail-item">
|
||||
<strong>ID:</strong> <span class="metadata-value">${fullEventId}</span>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>From:</strong> <span class="metadata-value">${event.pubkey}</span>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>Created:</strong> <span class="metadata-value">${new Date(event.created_at * 1000).toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>Tags:</strong>
|
||||
<ul class="event-tags">${tagsHtml}</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>Created:</strong> ${new Date(event.created_at * 1000).toLocaleString()}
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>Tags:</strong>
|
||||
<ul>${tagsHtml}</ul>
|
||||
</div>
|
||||
<div class="event-detail-item">
|
||||
<strong>Content${receivedEvent.decrypted ? ' (Decrypted)' : ''}:</strong>
|
||||
<pre class="event-content">${formattedContent}</pre>
|
||||
|
||||
<div class="event-detail-content">
|
||||
<div class="content-row">
|
||||
<div class="event-metadata-column">
|
||||
<div class="content-header">
|
||||
<strong>Event Metadata</strong>
|
||||
</div>
|
||||
<div class="metadata-content">
|
||||
<div>ID: ${fullEventId}</div>
|
||||
<div>Pubkey: ${event.pubkey}</div>
|
||||
<div>Created: ${new Date(event.created_at * 1000).toLocaleString()}</div>
|
||||
${receivedEvent.decrypted ? '<div class="decrypted-badge">Decrypted</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="http-content-column">
|
||||
<div class="content-header">
|
||||
<strong>HTTP ${eventTypeLabel}</strong>
|
||||
</div>
|
||||
<pre class="http-content">${httpContent}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -467,32 +600,95 @@ async function decryptEvent(eventId: string): Promise<void> {
|
||||
}
|
||||
|
||||
try {
|
||||
// First try to parse as base64-encoded JSON (for unencrypted content)
|
||||
const event = receivedEvent.event;
|
||||
let decryptedContent: string;
|
||||
try {
|
||||
// Try to parse as base64-encoded JSON
|
||||
const decodedContent = atob(receivedEvent.event.content);
|
||||
const parsedContent = JSON.parse(decodedContent);
|
||||
decryptedContent = JSON.stringify(parsedContent, null, 2);
|
||||
} catch {
|
||||
// If that fails, try NIP-44 decryption with user's private key
|
||||
|
||||
// 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 {
|
||||
// Get user's private key from localStorage (would be set during login)
|
||||
const userPrivateKey = localStorage.getItem('userPrivateKey');
|
||||
// Extract the encrypted key from the tag
|
||||
const encryptedKey = keyTag[1];
|
||||
|
||||
if (userPrivateKey) {
|
||||
// In a real implementation, we would use the nostr-tools or similar library
|
||||
// to decrypt the content using NIP-44 and the user's private key
|
||||
// For now, we'll just use the original content with a note
|
||||
decryptedContent = `[Auto-decryption would happen here with NIP-44]\n${receivedEvent.event.content}`;
|
||||
} else {
|
||||
// No private key available
|
||||
decryptedContent = `[No private key available for decryption]\n${receivedEvent.event.content}`;
|
||||
// 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 {
|
||||
await window.nostr.getPublicKey();
|
||||
// Check again after getting public key
|
||||
if (typeof window.nostr.nip44 === 'object' &&
|
||||
typeof window.nostr.nip44.decrypt === 'function') {
|
||||
console.log("NIP-44 is now available after connecting");
|
||||
} else {
|
||||
throw new Error("NIP-44 decryption is still not available after connecting");
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to connect to extension: ${e}`);
|
||||
}
|
||||
} else {
|
||||
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
|
||||
const decryptedEventContent = await decryptWithWebCrypto(
|
||||
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");
|
||||
decryptedContent = decryptedEventContent;
|
||||
} catch (contentDecryptError) {
|
||||
console.error("Content decryption failed:", contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError));
|
||||
decryptedContent = `Key decryption successful, but content decryption failed: ${contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError)}
|
||||
|
||||
Encrypted content: ${event.content.substring(0, 50)}...
|
||||
Decrypted key: ${decryptedKey}`;
|
||||
}
|
||||
} catch (decryptError) {
|
||||
// If decryption fails, use the original content
|
||||
console.error("Could not decrypt content:", decryptError);
|
||||
decryptedContent = `[Decryption failed]\n${receivedEvent.event.content}`;
|
||||
console.error("Failed to decrypt with window.nostr.nip44:", decryptError instanceof Error ? decryptError.message : String(decryptError));
|
||||
decryptedContent = `[NIP-44 decryption failed: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}]\n${event.content}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -502,7 +698,7 @@ async function decryptEvent(eventId: string): Promise<void> {
|
||||
receivedEvents.set(eventId, receivedEvent);
|
||||
|
||||
// Update UI if this event is currently being viewed
|
||||
const selectedEventId = eventDetails?.querySelector('h3')?.textContent?.match(/Event (.+)\.\.\./)?.[1];
|
||||
const selectedEventId = eventDetails?.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
|
||||
if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
|
||||
showEventDetails(eventId);
|
||||
}
|
||||
@ -520,18 +716,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
eventDetails = document.getElementById('eventDetails');
|
||||
|
||||
const connectRelayBtn = document.getElementById('connectRelayBtn');
|
||||
const startSubscriptionBtn = document.getElementById('startSubscriptionBtn');
|
||||
const stopSubscriptionBtn = document.getElementById('stopSubscriptionBtn');
|
||||
const clearEventsBtn = document.getElementById('clearEventsBtn');
|
||||
const subscriptionPubkeyInput = document.getElementById('subscriptionPubkey') as HTMLInputElement;
|
||||
|
||||
/**
|
||||
* Get the logged-in user's public key from localStorage
|
||||
* This is populated by the nostr-login plugin
|
||||
*/
|
||||
function getLoggedInPubkey(): string | null {
|
||||
return localStorage.getItem('userPublicKey');
|
||||
}
|
||||
|
||||
// Connect to relay and automatically start subscription for logged-in user
|
||||
if (connectRelayBtn) {
|
||||
@ -540,6 +725,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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');
|
||||
@ -549,19 +738,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const success = await connectToRelay(relayUrl);
|
||||
|
||||
if (success) {
|
||||
// Get the logged-in user's pubkey from localStorage
|
||||
let pubkeyFilter = subscriptionPubkeyInput?.value.trim();
|
||||
// Get the logged-in user's pubkey from localStorage as the default
|
||||
const userPubkey = getLoggedInPubkey();
|
||||
const pubkeyFilter = userPubkey || '';
|
||||
|
||||
// If no pubkey is specified in the input field, use the logged-in user's pubkey
|
||||
if (!pubkeyFilter) {
|
||||
const userPubkey = getLoggedInPubkey();
|
||||
if (userPubkey) {
|
||||
pubkeyFilter = userPubkey;
|
||||
// Update the input field to show which pubkey we're using
|
||||
if (subscriptionPubkeyInput) {
|
||||
subscriptionPubkeyInput.value = 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
|
||||
@ -578,72 +763,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} catch (subError) {
|
||||
console.error("Subscription error:", subError);
|
||||
updateRelayStatus(`Subscription error: ${subError instanceof Error ? subError.message : String(subError)}`, 'error');
|
||||
// Return subscription button to enabled state
|
||||
if (startSubscriptionBtn && stopSubscriptionBtn) {
|
||||
stopSubscriptionBtn.setAttribute('disabled', 'disabled');
|
||||
startSubscriptionBtn.removeAttribute('disabled');
|
||||
}
|
||||
// No need to update subscription buttons since they're removed
|
||||
}
|
||||
|
||||
// Update UI to reflect active subscription
|
||||
if (startSubscriptionBtn && stopSubscriptionBtn) {
|
||||
startSubscriptionBtn.setAttribute('disabled', 'disabled');
|
||||
stopSubscriptionBtn.removeAttribute('disabled');
|
||||
}
|
||||
// Successful subscription message
|
||||
|
||||
console.log(`Subscribed to kind 21120 events for ${pubkeyFilter || 'all users'}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start subscription
|
||||
if (startSubscriptionBtn && stopSubscriptionBtn) {
|
||||
startSubscriptionBtn.addEventListener('click', async () => {
|
||||
if (!relayPool || !activeRelayUrl) {
|
||||
console.error('Please connect to a relay first');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update status to indicate subscription is in progress
|
||||
updateRelayStatus('Subscribing...', 'connecting');
|
||||
|
||||
try {
|
||||
// Get pubkey if specified
|
||||
const subscriptionPubkeyInput = document.getElementById('subscriptionPubkey') as HTMLInputElement;
|
||||
const pubkeyFilter = subscriptionPubkeyInput?.value.trim();
|
||||
|
||||
await subscribeToEvents({
|
||||
pubkeyFilter
|
||||
});
|
||||
|
||||
// Update UI to reflect active subscription
|
||||
if (startSubscriptionBtn && stopSubscriptionBtn) {
|
||||
startSubscriptionBtn.setAttribute('disabled', 'disabled');
|
||||
stopSubscriptionBtn.removeAttribute('disabled');
|
||||
}
|
||||
} catch (subError) {
|
||||
console.error("Subscription error:", subError);
|
||||
updateRelayStatus(`Subscription error: ${subError instanceof Error ? subError.message : String(subError)}`, 'error');
|
||||
|
||||
// Leave buttons in their current state if there's an error
|
||||
if (startSubscriptionBtn) {
|
||||
startSubscriptionBtn.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Stop subscription
|
||||
if (stopSubscriptionBtn && startSubscriptionBtn) {
|
||||
stopSubscriptionBtn.addEventListener('click', () => {
|
||||
if (activeSubscription) {
|
||||
activeSubscription.unsub();
|
||||
activeSubscription = null;
|
||||
}
|
||||
|
||||
if (stopSubscriptionBtn && startSubscriptionBtn) {
|
||||
stopSubscriptionBtn.setAttribute('disabled', 'disabled');
|
||||
startSubscriptionBtn.removeAttribute('disabled');
|
||||
// Add subscribed indication to the UI
|
||||
updateRelayStatus('Connected and subscribed ✓', 'connected');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -654,7 +782,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
receivedEvents.clear();
|
||||
|
||||
if (eventsList) {
|
||||
eventsList.innerHTML = '<div class="empty-state">No events received yet. Connect to a relay and start a subscription.</div>';
|
||||
eventsList.innerHTML = '<div class="empty-state">No events received yet. Connect to a relay to start receiving events.</div>';
|
||||
}
|
||||
|
||||
if (eventDetails) {
|
||||
|
@ -747,32 +747,126 @@ footer {
|
||||
color: var(--text-tertiary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-secondary);
|
||||
/* Events container for side-by-side layout */
|
||||
.events-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.events-sidebar {
|
||||
flex: 0 0 300px;
|
||||
}
|
||||
|
||||
.events-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-secondary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
.event-item {
|
||||
padding: 10px;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.event-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.event-item:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.event-item.selected {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-left: 4px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.event-id {
|
||||
font-weight: bold;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.event-summary {
|
||||
font-size: 13px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.event-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.view-details-btn {
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-details-btn:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
padding: 20px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.event-detail-item {
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.event-detail-item strong {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.event-detail-item ul {
|
||||
padding-left: 20px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.event-content {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
@ -803,6 +897,152 @@ footer {
|
||||
|
||||
.copy-button:hover {
|
||||
background-color: var(--button-hover);
|
||||
/* Enhanced Event Details Styling */
|
||||
.event-detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.event-detail-header h3 {
|
||||
margin: 0;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.event-timestamp {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.event-detail-metadata {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metadata-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.event-tags {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.event-detail-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
margin-bottom: 10px;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.decrypted-badge {
|
||||
background-color: var(--button-success);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Content row for side-by-side display */
|
||||
.content-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.event-metadata-column {
|
||||
flex: 0 0 250px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.http-content-column {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.metadata-content {
|
||||
margin-top: 10px;
|
||||
line-height: 1.6;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.http-content {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
border-left: 4px solid var(--accent-color);
|
||||
}
|
||||
|
||||
.raw-content-column {
|
||||
flex: 1 0 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.raw-content {
|
||||
margin-top: 10px;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-tertiary);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: pre-wrap;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.toggle-raw-btn {
|
||||
float: right;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-raw-btn:hover {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.event-item:hover .event-type {
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-button:active {
|
||||
@ -812,4 +1052,44 @@ footer {
|
||||
.copy-button.copied {
|
||||
background-color: var(--button-success);
|
||||
}
|
||||
/* Dark mode is already handled by CSS variables */
|
||||
/* Dark mode is already handled by CSS variables */
|
||||
|
||||
/* Responsive adjustments for the events container */
|
||||
@media (max-width: 768px) {
|
||||
.events-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.events-sidebar {
|
||||
flex: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.events-list {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.event-details {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear events button */
|
||||
.event-controls {
|
||||
margin-bottom: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.clear-events-button {
|
||||
padding: 8px 15px;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.clear-events-button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user