This commit is contained in:
n 2025-04-08 15:42:05 +01:00
parent 78a0a8bd18
commit fe8b589787
7 changed files with 585 additions and 11 deletions

@ -26,27 +26,64 @@
<!-- Main Content -->
<div class="content">
<div class="info-box">
<p>This tool allows you to receive and view HTTP events (kind 21120) from Nostr relays. Connect to a relay, subscribe to events, and decrypt messages.</p>
<p>This tool allows you to receive and view HTTP events (kind 21120) from Nostr relays, QR codes, or raw text input. Choose your preferred method using the tabs below.</p>
</div>
<h2>Relay Connection</h2>
<div class="relay-connection">
<div class="relay-input-container">
<label for="relayUrl">Relay URL:</label>
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
<!-- Tab Navigation - Updated data-tab attributes to match section IDs -->
<div class="tabs">
<button class="tab-button active" data-tab="relay-connection-section">From Relay</button>
<button class="tab-button" data-tab="qr-code-scanner-section">From QR Scanner</button>
<button class="tab-button" data-tab="raw-event-input-section">From Raw Text</button>
</div>
<!-- Tab Content - Each section gets a distinct ID -->
<!-- Relay Tab Content -->
<div id="relay-connection-section" class="active">
<h2>Relay Connection</h2>
<div class="relay-connection">
<div class="relay-input-container">
<label for="relayUrl">Relay URL:</label>
<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>
</div>
<!-- QR Scanner Tab Content -->
<div id="qr-code-scanner-section">
<h2>QR Code Scanner</h2>
<div class="qr-scanner-container">
<div class="qr-viewport">
<video id="qrVideo"></video>
<canvas id="qrCanvas" style="display: none;"></canvas>
</div>
<div class="qr-controls">
<button id="startScanBtn" class="action-button">Start Camera</button>
<button id="stopScanBtn" class="action-button" disabled>Stop Camera</button>
</div>
<div id="qrStatus" class="status-message">Camera inactive. Click Start Camera to begin scanning.</div>
</div>
</div>
<!-- Raw Text Tab Content -->
<div id="raw-event-input-section">
<h2>Raw Event Input</h2>
<div class="raw-input-container">
<p>Paste a raw Nostr event JSON below:</p>
<textarea id="rawEventInput" placeholder='{"id": "...", "pubkey": "...", "created_at": 1234567890, "kind": 21120, "tags": [], "content": "..."}' rows="10"></textarea>
<button id="parseRawEventBtn" class="action-button">Parse Event</button>
<div id="rawInputStatus" class="status-message"></div>
</div>
<div id="relayStatus" class="relay-status">Not connected</div>
</div>
<h2>Received Events</h2>
<div class="received-events">
<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.
No events received yet. Use one of the methods above to receive events.
</div>
<!-- Events will be displayed here -->
</div>

@ -1,4 +1,5 @@
// External dependencies
import jsQR from 'jsqr';
import * as nostrTools from 'nostr-tools';
// Internal imports
@ -706,6 +707,271 @@ Decrypted key: ${decryptedKey}`;
}
}
// Setup tab navigation
function setupTabNavigation(): void {
const tabButtons = document.querySelectorAll('.tab-button');
const contentSections = document.querySelectorAll('#relay-connection-section, #qr-code-scanner-section, #raw-event-input-section');
// Add click event listeners to tab buttons
tabButtons.forEach(button => {
button.addEventListener('click', () => {
// Get the target tab
const targetTabId = (button as HTMLElement).dataset.tab;
if (!targetTabId) {
return;
}
// Remove active class from all buttons and content sections
tabButtons.forEach(btn => btn.classList.remove('active'));
contentSections.forEach(section => section.classList.remove('active'));
// Add active class to clicked button and corresponding content section
button.classList.add('active');
const targetSection = document.getElementById(targetTabId);
if (targetSection) {
targetSection.classList.add('active');
}
// Log for debugging
console.log(`Tab switched to: ${targetTabId}, active sections: ${document.querySelectorAll('.active').length}`);
});
});
}
// Variables for QR scanner
let qrScanning = false;
let videoStream: MediaStream | null = null;
// Setup QR code scanner
function setupQRScanner(): void {
const startScanBtn = document.getElementById('startScanBtn');
const stopScanBtn = document.getElementById('stopScanBtn');
const qrVideo = document.getElementById('qrVideo') as HTMLVideoElement;
const qrCanvas = document.getElementById('qrCanvas') as HTMLCanvasElement;
const qrStatus = document.getElementById('qrStatus');
// Function to start the QR scanner
async function startQRScanner() {
// Already scanning
if (qrScanning) {
return;
}
try {
// Get access to the camera
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
// Update status
if (qrStatus) {
qrStatus.textContent = 'Camera active. Point at a QR code.';
qrStatus.className = 'status-message';
}
// Set video source to camera stream
if (qrVideo) {
qrVideo.srcObject = stream;
qrVideo.play();
videoStream = stream;
}
// Enable/disable buttons
if (startScanBtn) {
startScanBtn.setAttribute('disabled', 'true');
}
if (stopScanBtn) {
stopScanBtn.removeAttribute('disabled');
}
// Start scanning
qrScanning = true;
scanQRCode();
} catch (error) {
// Handle errors
console.error('Error accessing camera:', error);
if (qrStatus) {
qrStatus.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`;
qrStatus.className = 'status-message error';
}
}
}
// Function to stop the QR scanner
function stopQRScanner() {
if (!qrScanning) {
return;
}
// Stop all tracks
if (videoStream) {
videoStream.getTracks().forEach(track => track.stop());
videoStream = null;
}
// Update status
if (qrStatus) {
qrStatus.textContent = 'Camera inactive. Click Start Camera to begin scanning.';
qrStatus.className = 'status-message';
}
// Enable/disable buttons
if (startScanBtn) {
startScanBtn.removeAttribute('disabled');
}
if (stopScanBtn) {
stopScanBtn.setAttribute('disabled', 'true');
}
qrScanning = false;
}
// Add event listeners
if (startScanBtn) {
startScanBtn.addEventListener('click', startQRScanner);
}
if (stopScanBtn) {
stopScanBtn.addEventListener('click', stopQRScanner);
}
// Function to scan for QR codes
function scanQRCode() {
if (!qrScanning) {
return;
}
// Request animation frame for next scan
window.requestAnimationFrame(scanQRCode);
if (!qrVideo || !qrCanvas) {
return;
}
// Skip if video isn't playing
if (qrVideo.readyState !== qrVideo.HAVE_ENOUGH_DATA) {
return;
}
// Get canvas context
const context = qrCanvas.getContext('2d');
if (!context) {
return;
}
// Set canvas dimensions to match video
qrCanvas.width = qrVideo.videoWidth;
qrCanvas.height = qrVideo.videoHeight;
// Draw current video frame to canvas
context.drawImage(qrVideo, 0, 0, qrCanvas.width, qrCanvas.height);
// Get image data for QR detection
const imageData = context.getImageData(0, 0, qrCanvas.width, qrCanvas.height);
// Process with jsQR
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: 'dontInvert'
});
// Process QR code if found
if (code) {
console.log('QR code detected:', code.data);
if (qrStatus) {
qrStatus.textContent = 'QR code found! Processing...';
}
// Try to parse as a Nostr event
try {
const eventData = JSON.parse(code.data);
// Check if this is a valid Nostr event
if (eventData && eventData.id && eventData.pubkey &&
eventData.created_at && eventData.kind === 21120) {
// Stop scanning after successful detection
stopQRScanner();
if (qrStatus) {
qrStatus.textContent = 'Valid Nostr event found! Event processed.';
}
// Process the event
processEvent(eventData);
} else {
if (qrStatus) {
qrStatus.textContent = 'QR code found, but not a valid kind 21120 Nostr event.';
}
}
} catch (error) {
console.error('Error parsing QR code data:', error);
if (qrStatus) {
qrStatus.textContent = `Error parsing QR data: ${error instanceof Error ? error.message : String(error)}`;
}
}
}
}
// Add event listeners
if (startScanBtn) {
startScanBtn.addEventListener('click', startQRScanner);
}
if (stopScanBtn) {
stopScanBtn.addEventListener('click', stopQRScanner);
}
}
// Setup raw text input
function setupRawTextInput(): void {
const parseRawEventBtn = document.getElementById('parseRawEventBtn');
const rawEventInput = document.getElementById('rawEventInput') as HTMLTextAreaElement;
const rawInputStatus = document.getElementById('rawInputStatus');
if (parseRawEventBtn && rawEventInput) {
parseRawEventBtn.addEventListener('click', () => {
const rawText = rawEventInput.value.trim();
if (!rawText) {
if (rawInputStatus) {
rawInputStatus.textContent = 'Please enter a raw Nostr event.';
}
return;
}
try {
// Parse the JSON data
const eventData = JSON.parse(rawText);
// Validate as Nostr event
if (eventData && eventData.id && eventData.pubkey &&
eventData.created_at && eventData.kind === 21120) {
// Process the event
processEvent(eventData);
if (rawInputStatus) {
rawInputStatus.textContent = 'Event successfully processed!';
}
// Clear the input for convenience
rawEventInput.value = '';
} else {
if (rawInputStatus) {
rawInputStatus.textContent = 'Not a valid kind 21120 Nostr event.';
}
}
} catch (error) {
console.error('Error parsing raw event:', error);
if (rawInputStatus) {
rawInputStatus.textContent = `Error parsing: ${error instanceof Error ? error.message : String(error)}`;
}
}
});
}
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
// Get DOM elements
@ -716,6 +982,15 @@ document.addEventListener('DOMContentLoaded', () => {
const connectRelayBtn = document.getElementById('connectRelayBtn');
// Tab functionality
setupTabNavigation();
// QR code scanner setup
setupQRScanner();
// Raw text input setup
setupRawTextInput();
// Connect to relay and automatically start subscription for logged-in user
if (connectRelayBtn) {
connectRelayBtn.addEventListener('click', async () => {

@ -471,4 +471,131 @@ pre {
.content {
padding: 10px 0;
}
}
/* Tab Interface Styles */
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
margin-right: 10px;
margin-bottom: -2px;
transition: all 0.3s ease;
}
.tab-button:hover {
color: var(--accent-color);
}
.tab-button.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
/* Tab content sections */
#relay-connection-section,
#qr-code-scanner-section,
#raw-event-input-section {
display: none;
margin-top: 20px;
margin-bottom: 20px;
}
#relay-connection-section.active,
#qr-code-scanner-section.active,
#raw-event-input-section.active {
display: block;
}
/* QR Scanner Styles */
.qr-scanner-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.qr-viewport {
width: 100%;
max-width: 500px;
height: 300px;
position: relative;
overflow: hidden;
border: 3px solid var(--accent-color);
border-radius: 8px;
background-color: var(--bg-tertiary);
}
.qr-viewport video {
width: 100%;
height: 100%;
object-fit: cover;
}
.qr-controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.action-button {
padding: 8px 15px;
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.action-button:hover {
background-color: var(--button-hover);
}
.action-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-tertiary);
cursor: not-allowed;
}
.status-message {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 14px;
}
/* Raw Input Styles */
.raw-input-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.raw-input-container textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}
.raw-input-container button {
align-self: flex-start;
}

@ -1158,4 +1158,131 @@ footer {
.clear-events-button:hover {
background-color: #c82333;
}
/* Tab Interface Styles */
.tabs {
display: flex;
border-bottom: 2px solid var(--border-color);
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
margin-right: 10px;
margin-bottom: -2px;
transition: all 0.3s ease;
}
.tab-button:hover {
color: var(--accent-color);
}
.tab-button.active {
color: var(--accent-color);
border-bottom: 2px solid var(--accent-color);
}
/* Tab content sections */
#relay-connection-section,
#qr-code-scanner-section,
#raw-event-input-section {
display: none;
margin-top: 20px;
margin-bottom: 20px;
}
#relay-connection-section.active,
#qr-code-scanner-section.active,
#raw-event-input-section.active {
display: block;
}
/* QR Scanner Styles */
.qr-scanner-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.qr-viewport {
width: 100%;
max-width: 500px;
height: 300px;
position: relative;
overflow: hidden;
border: 3px solid var(--accent-color);
border-radius: 8px;
background-color: var(--bg-tertiary);
}
.qr-viewport video {
width: 100%;
height: 100%;
object-fit: cover;
}
.qr-controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.action-button {
padding: 8px 15px;
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.action-button:hover {
background-color: var(--button-hover);
}
.action-button:disabled {
background-color: var(--bg-tertiary);
color: var(--text-tertiary);
cursor: not-allowed;
}
.status-message {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 14px;
}
/* Raw Input Styles */
.raw-input-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.raw-input-container textarea {
width: 100%;
min-height: 200px;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-tertiary);
color: var(--text-primary);
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}
.raw-input-container button {
align-self: flex-start;
}

@ -25,7 +25,7 @@ module.exports = {
new NodePolyfillPlugin(),
new CopyPlugin({
patterns: [
{ from: 'src/styles.css', to: 'styles.css' },
{ from: 'styles.css', to: 'styles.css' },
{ from: 'http.png', to: 'http.png' },
{ from: 'index.html', to: 'index.html' },
{ from: 'help.html', to: 'help.html' },

7
package-lock.json generated

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"jsqr": "^1.4.0",
"qrcode": "^1.5.4"
}
},
@ -126,6 +127,12 @@
"node": ">=8"
}
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
},
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",

@ -24,6 +24,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"jsqr": "^1.4.0",
"qrcode": "^1.5.4"
}
}