This commit is contained in:
2025-01-03 17:18:45 -05:00
parent 3cb1cb2613
commit a0d4762343
4 changed files with 245 additions and 290 deletions

150
src/app.js Normal file
View File

@@ -0,0 +1,150 @@
// Global variables
let archiveData = [];
let sortedData = [];
let currentSort = { column: 'created_at', order: 'desc' };
const rowsPerPage = 100;
let currentPage = 1;
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('fileInput').addEventListener('change', handleFileLoad);
// Add click listeners to sortable headers
document.querySelectorAll('#dataTable th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
sortTable(th.dataset.sort);
});
});
});
// File handling
function handleFileLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) throw new Error('Invalid archive format: Expected an array.');
archiveData = data;
sortedData = [...archiveData];
updateStats();
sortTable(currentSort.column); // Initial sort
} catch (err) {
alert(`Failed to load archive: ${err.message}`);
}
};
reader.readAsText(file);
}
// Statistics
function updateStats() {
const statsDiv = document.getElementById('stats');
const kindCounts = archiveData.reduce((acc, event) => {
acc[event.kind] = (acc[event.kind] || 0) + 1;
return acc;
}, {});
const totalEvents = archiveData.length;
let statsText = `Total Events: ${totalEvents}<br>`;
statsText += 'Most Common Types: ';
const topKinds = Object.entries(kindCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([kind, count]) => `${getKindLabel(parseInt(kind))}: ${count}`);
statsText += topKinds.join(' | ');
statsDiv.innerHTML = statsText;
}
// Table rendering
function renderTable() {
const tbody = document.querySelector('#dataTable tbody');
tbody.innerHTML = '';
const start = (currentPage - 1) * rowsPerPage;
const end = start + rowsPerPage;
const pageData = sortedData.slice(start, end);
if (pageData.length === 0) {
tbody.innerHTML = `<tr class="empty-message">
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
</tr>`;
return;
}
pageData.forEach((event, index) => {
const row = document.createElement('tr');
row.innerHTML = `
<td class="number-column">${start + index + 1}</td>
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
<td>${getKindLabel(event.kind)}</td>
<td class="content-cell">${getContentDisplay(event)}</td>
`;
tbody.appendChild(row);
});
updatePagination(end);
}
// Pagination
function updatePagination(end) {
document.getElementById('prevPage').disabled = currentPage === 1;
document.getElementById('nextPage').disabled = end >= sortedData.length;
document.getElementById('pageInfo').innerText = `Page ${currentPage} of ${Math.ceil(sortedData.length / rowsPerPage)}`;
}
function changePage(offset) {
currentPage += offset;
renderTable();
}
// Sorting
function sortTable(column) {
if (currentSort.column === column) {
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.order = 'desc';
}
const order = currentSort.order === 'asc' ? 1 : -1;
sortedData.sort((a, b) => {
if (a[column] < b[column]) return -1 * order;
if (a[column] > b[column]) return 1 * order;
return 0;
});
currentPage = 1;
renderTable();
}
// Helper functions
function getKindLabel(kind) {
const kinds = {
0: 'Profile Metadata',
1: 'Short Text Note',
2: 'Recommend Relay',
3: 'Contacts',
4: 'Encrypted DM',
6: 'Repost',
7: 'Reaction',
40: 'Channel Creation',
41: 'Channel Metadata',
42: 'Channel Message',
30023: 'Long-form Content',
// Add more as needed
};
return `${kind} (${kinds[kind] || 'Unknown'})`;
}
function getContentDisplay(event) {
if (event.kind === 4) {
const recipient = event.tags.find(tag => tag[0] === 'p');
return `Encrypted Message: ${event.content || 'N/A'}<br>Recipient: ${recipient ? recipient[1] : 'Unknown'}`;
}
return event.content || 'No content available';
}

View File

@@ -52,11 +52,10 @@ document.addEventListener('DOMContentLoaded', () => {
// Update the filter to include additional kinds
const filter = {
kinds: [0, 1, 2, 3, 4, 6, 7, 10002, 30023], // Include multiple event kinds
authors: [pubkey],
limit: 1000,
kinds: [0, 1, 2, 3, 4, 6, 7, 10002, 30023, 10509], // Include all relevant kinds
authors: [pubkey], // Fetch events from the specified pubkey
};
relayUrls.forEach((url) => {
try {
const sub = pool.sub([url], [filter]);
@@ -78,12 +77,16 @@ document.addEventListener('DOMContentLoaded', () => {
case 4:
console.log('Encrypted DM captured:', event);
break;
case 10509:
console.log('Ephemeral DM captured:', event);
break;
case 30023:
console.log('Long-Form Content captured:', event);
break;
default:
console.log('Other event captured:', event);
}
collectedEvents.push(event); // Store the raw event data
eventIds.add(event.id);
}

View File

@@ -1,50 +1,110 @@
/* Base styles */
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
.container {
width: 90%;
max-width: 600px;
margin: 50px auto;
background: #fff;
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
/* Header and Stats */
h1 {
text-align: center;
margin-bottom: 10px;
}
label {
.stats {
text-align: center;
color: #666;
margin-bottom: 20px;
font-size: 1.1em;
}
input[type="file"] {
display: block;
margin-top: 15px;
margin: 20px auto;
}
input[type="text"],
textarea {
/* Table styles */
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
margin-top: 5px;
box-sizing: border-box;
border: 1px solid #ddd;
text-align: left;
vertical-align: top;
}
button {
margin-top: 20px;
padding: 10px 15px;
background-color: #007BFF;
border: none;
color: #fff;
th {
cursor: pointer;
background: #f4f4f4;
position: relative;
}
button:hover {
background-color: #0056b3;
th:hover {
background: #e0e0e0;
}
#status {
.empty-message {
text-align: center;
color: #888;
margin-top: 20px;
min-height: 20px;
}
.content-cell {
max-width: 600px;
white-space: pre-wrap;
word-break: break-word;
}
/* Sort indicators */
.sort-indicator::after {
content: '⬍';
margin-left: 5px;
}
.sort-asc::after {
content: '↑';
}
.sort-desc::after {
content: '↓';
}
.number-column {
width: 50px;
text-align: right;
color: #666;
}
/* Pagination */
.pagination {
text-align: center;
margin: 20px 0;
}
.pagination button {
margin: 0 5px;
padding: 5px 10px;
background: #007BFF;
color: white;
border: none;
cursor: pointer;
border-radius: 3px;
}
.pagination button:disabled {
background: #ccc;
cursor: not-allowed;
}

View File

@@ -4,79 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nostr Archive Viewer</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f9f9f9;
}
.container {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
margin-bottom: 10px;
}
.stats {
text-align: center;
color: #666;
margin-bottom: 20px;
font-size: 1.1em;
}
input[type="file"] {
display: block;
margin: 20px auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 10px;
border: 1px solid #ddd;
text-align: left;
vertical-align: top;
}
th {
cursor: pointer;
background: #f4f4f4;
position: relative;
}
th:hover {
background: #e0e0e0;
}
.empty-message {
text-align: center;
color: #888;
margin-top: 20px;
}
.content-cell {
max-width: 600px;
white-space: pre-wrap;
word-break: break-word;
}
.sort-indicator::after {
content: '⬍';
margin-left: 5px;
}
.sort-asc::after {
content: '↑';
}
.sort-desc::after {
content: '↓';
}
.number-column {
width: 50px;
text-align: right;
color: #666;
}
</style>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
@@ -98,198 +26,12 @@
</tr>
</tbody>
</table>
<div class="pagination">
<button id="prevPage" onclick="changePage(-1)" disabled>Previous</button>
<span id="pageInfo">Page 1</span>
<button id="nextPage" onclick="changePage(1)" disabled>Next</button>
</div>
</div>
<script>
document.getElementById('fileInput').addEventListener('change', handleFileLoad);
let archiveData = [];
let currentSort = { column: 'created_at', order: 'desc' };
function handleFileLoad(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
if (!Array.isArray(data)) throw new Error('Invalid archive format: Expected an array.');
archiveData = data;
updateStats();
sortTable(currentSort.column); // Initial sort
} catch (err) {
alert(`Failed to load archive: ${err.message}`);
}
};
reader.readAsText(file);
}
function updateStats() {
const statsDiv = document.getElementById('stats');
const kindCounts = archiveData.reduce((acc, event) => {
acc[event.kind] = (acc[event.kind] || 0) + 1;
return acc;
}, {});
const totalEvents = archiveData.length;
let statsText = `Total Events: ${totalEvents}<br>`;
statsText += 'Most Common Types: ';
// Get top 3 most common kinds
const topKinds = Object.entries(kindCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([kind, count]) => `${getKindLabel(parseInt(kind))}: ${count}`);
statsText += topKinds.join(' | ');
statsDiv.innerHTML = statsText;
}
function renderTable() {
const tbody = document.querySelector('#dataTable tbody');
tbody.innerHTML = '';
if (archiveData.length === 0) {
tbody.innerHTML = `<tr class="empty-message">
<td colspan="4">No data loaded. Please load a JSON archive file.</td>
</tr>`;
return;
}
archiveData.forEach((event, index) => {
const row = document.createElement('tr');
try {
row.innerHTML = `
<td class="number-column">${index + 1}</td>
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
<td>${getKindLabel(event.kind)}</td>
<td class="content-cell">${getContentDisplay(event)}</td>
`;
} catch (err) {
console.error(`Error rendering event ${event.id}:`, err);
row.innerHTML = `
<td class="number-column">${index + 1}</td>
<td>${new Date(event.created_at * 1000).toLocaleString()}</td>
<td>${getKindLabel(event.kind)}</td>
<td class="content-cell">Error loading content</td>
`;
}
tbody.appendChild(row);
});
// Update sort indicators
document.querySelectorAll('#dataTable th[data-sort] .sort-indicator').forEach(indicator => {
indicator.className = 'sort-indicator';
});
const currentHeader = document.querySelector(`th[data-sort="${currentSort.column}"] .sort-indicator`);
if (currentHeader) {
currentHeader.classList.add(`sort-${currentSort.order}`);
}
}
function getKindLabel(kind) {
const kinds = {
0: 'Profile Metadata',
1: 'Short Text Note',
2: 'Recommend Relay',
3: 'Contacts',
4: 'Encrypted DM',
5: 'Event Deletion',
6: 'Repost',
7: 'Reaction',
8: 'Badge Award',
40: 'Channel Creation',
41: 'Channel Metadata',
42: 'Channel Message',
43: 'Channel Hide Message',
44: 'Channel Mute User',
1984: 'Reporting',
9734: 'Zap Request',
9735: 'Zap',
10000: 'Mute List',
10001: 'Pin List',
10002: 'Relay List Metadata',
30000: 'Follow List',
30001: 'Generic List',
30002: 'Relay List',
30003: 'Bookmark List',
30004: 'Communities',
30008: 'Profile Badges',
30009: 'Badge Definition',
30017: 'Create or update a stall',
30018: 'Create or update a product',
30023: 'Long-form Content',
30078: 'Application-specific Data',
30402: 'Classified Listing',
31989: 'Handler Recommendation',
31990: 'Handler Information'
};
return `${kind} (${kinds[kind] || 'Reserved/Custom'})`;
}
function getContentDisplay(event) {
try {
if (event.kind === 0) {
const profile = JSON.parse(event.content || '{}');
return `Name: ${profile.name || 'N/A'}<br>
About: ${profile.about || 'N/A'}<br>
Picture: ${profile.picture || 'N/A'}`;
} else if (event.kind === 30023) {
// Display full long-form content
const content = event.content || '';
return content.replace(/\n/g, '<br>');
} else if (event.kind === 4) {
return `Encrypted Message: ${event.content || 'N/A'}`;
} else if (event.kind === 7) {
// Special handling for reactions
return `Reaction: ${event.content || '+'}`;
} else if (event.kind === 6) {
// Special handling for reposts
return `Repost: ${event.content || 'No content'}`;
} else if (event.kind === 3) {
// Special handling for contact lists
try {
const contacts = JSON.parse(event.content || '[]');
return `Contact List: ${contacts.length} contacts`;
} catch {
return `Contact List: Unable to parse`;
}
} else if (event.kind === 1) {
// Display full short text note
return event.content ? event.content.replace(/\n/g, '<br>') : 'No content available';
}
return event.content || 'No content available';
} catch (err) {
console.error(`Error processing event ${event.id}:`, err);
return 'Error processing content';
}
}
function sortTable(column) {
if (currentSort.column === column) {
currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.order = 'desc'; // Default to newest first
}
const order = currentSort.order === 'asc' ? 1 : -1;
archiveData.sort((a, b) => {
if (a[column] < b[column]) return -1 * order;
if (a[column] > b[column]) return 1 * order;
return 0;
});
renderTable();
}
document.querySelectorAll('#dataTable th[data-sort]').forEach(th => {
th.addEventListener('click', () => {
sortTable(th.dataset.sort);
});
});
</script>
<script src="app.js"></script>
</body>
</html>