-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnote1.js
584 lines (488 loc) · 20.5 KB
/
note1.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
// Default relays if none are specified
const defaultRelays = [
"wss://relay.nostr.band",
"wss://relay.damus.io",
"wss://njump.me",
"wss://relay.snort.social",
"wss://nos.lol"
];
// Decode note1 from a bech32 string (Based on NIP-19)
function decodeNote1(input) {
// Match only "nostr:note1" strings
const note1Match = input.match(/nostr:note1\w+/);
if (!note1Match) return null;
const note1 = note1Match[0];
console.log("Decoding note1:", note1);
try {
const { words } = bech32.decode(note1.replace(/^nostr:/, "")); // Remove "nostr:" prefix for decoding
const bytes = bech32.fromWords(words);
if (bytes.length !== 32) {
throw new Error("Invalid nostr:note1 string. Incorrect byte length.");
}
const eventId = bytesToHex(bytes);
console.log(`Decoded note1 Event ID (Hex): ${eventId}`);
return { eventId, note1String: note1 };
} catch (error) {
console.error("Failed to decode nostr:note1:", error);
return null;
}
}
// Convert bytes to hex string
function bytesToHex(bytes) {
return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
}
// Cache to store fetched events
const fetchedEvents = new Set();
// Fetch the note1 event from relays (stop after first success)
// If event not found, retry with default relays
async function fetchNote1Event(eventId, relays, note1String) {
// If the event is already fetched, skip fetching it again
if (fetchedEvents.has(eventId)) {
console.log(`Event ${eventId} already fetched. Skipping...`);
return;
}
const relayList = relays && relays.length ? relays : defaultRelays;
console.log("Using relays:", relayList);
let eventFetched = false;
const paragraph = document.querySelector('#customParagraph');
// Replace note1 string with colored placeholder
if (paragraph.innerHTML.includes(note1String)) {
paragraph.innerHTML = paragraph.innerHTML.replace(
note1String,
'<span style="color: #bbc013;">[Event catching...]</span>'
);
}
// Timeout to replace placeholder with "Event not found"
const timeoutId = setTimeout(() => {
if (!eventFetched) {
console.warn(`Event ${eventId} not found. Replacing placeholder.`);
// Locate the specific placeholder and replace it without re-rendering the entire paragraph
const placeholder = paragraph.querySelector('span[style="color: #bbc013;"]');
if (placeholder) {
placeholder.outerHTML = '<span style="color: #FF6347;">[Event not found]</span>';
console.log("Replaced placeholder with 'Event not found'.");
}
// Reapply modal listeners
applyModalListenersToMedia(paragraph);
}
}, 20000); // 20-second timeout for fallback
for (const relay of relayList) {
if (eventFetched) break;
try {
const socket = new WebSocket(relay);
socket.onopen = () => {
const sub = ["REQ", "subscriptionId", { ids: [eventId] }];
console.log(`Requesting event ${eventId} from ${relay}`);
socket.send(JSON.stringify(sub));
};
socket.onmessage = (message) => {
const data = JSON.parse(message.data);
if (data[0] === "EVENT") {
if (!eventFetched) {
clearTimeout(timeoutId); // Clear timeout when event is fetched
displayNote1Event(data[2], note1String);
fetchedEvents.add(eventId); // Cache the fetched event ID
eventFetched = true;
}
socket.close(); // Close after first successful fetch
}
};
socket.onclose = () => {
console.log(`Relay ${relay} closed connection.`);
if (!eventFetched && relay === relayList[relayList.length - 1]) {
console.warn("No event found on provided relays. Retrying with default relays...");
if (relayList !== defaultRelays) {
fetchNote1Event(eventId, defaultRelays, note1String);
}
}
};
socket.onerror = (error) => {
console.error(`Error connecting to ${relay}:`, error);
};
} catch (error) {
console.error(`Failed to fetch event from ${relay}:`, error);
}
}
}
// Toggle Like State and Send Reaction to Relays
async function toggleLike(eventId, pubkey) {
const likeIcon = document.getElementById(`like-icon-${eventId}`);
const privateKey = localStorage.getItem("nostr_pk");
const publicKey = localStorage.getItem("nostr_pub");
if (!privateKey || !publicKey) {
alert("Private/Public key not found. Please log in.");
return;
}
// Update local liked_events
updateLocalLikedEvents(eventId);
// Sync to HEX{pubkey}.json
syncLikedEventsToHex(publicKey);
// Construct Reaction Event (NIP-25)
const reactionEvent = {
pubkey: publicKey,
created_at: Math.floor(Date.now() / 1000),
kind: 7,
tags: [
["e", eventId], // Event ID being liked
["p", pubkey] // Pubkey of the event creator
// ["emoji", "+"]
],
content: "+"
};
// Sign and Send the Event
const signedEvent = await signEvent(reactionEvent, privateKey);
propagateReaction(signedEvent);
// Disable the like button after liking
likeIcon.src = "img/othrIcon/like_1.png";
likeIcon.dataset.liked = "true";
likeIcon.style.pointerEvents = "none"; // Disable clicks
likeIcon.title = "Already Liked";
}
// Update liked events in localStorage (persistent)
function updateLocalLikedEvents(eventId) {
let likedEvents = JSON.parse(localStorage.getItem("liked_events")) || [];
// Avoid duplicates
if (!likedEvents.includes(eventId)) {
likedEvents.push(eventId);
}
// Store updated liked events
localStorage.setItem("liked_events", JSON.stringify(likedEvents));
}
// Sync liked events to HEX{pubkey}.json
function syncLikedEventsToHex(publicKey) {
const fileName = `HEX${publicKey}.json`;
let storedData = localStorage.getItem(fileName);
let data;
if (storedData) {
data = JSON.parse(storedData);
} else {
data = { events: [], pk: publicKey };
}
// Retrieve liked events from localStorage
const likedEvents = JSON.parse(localStorage.getItem("liked_events")) || [];
console.log("Liked Events in localStorage:", likedEvents);
// Merge liked events back into HEX file
data.likedEvents = likedEvents;
// Save updated HEX file
localStorage.setItem(fileName, JSON.stringify(data));
}
// Store liked events in the user's HEX{pubkey}.json
function storeLikedEvent(eventId, publicKey, eventCreatorPubkey) {
const fileName = `HEX${publicKey}.json`;
let storedData = localStorage.getItem(fileName);
let data;
// Initialize or parse existing data
if (storedData) {
data = JSON.parse(storedData);
} else {
data = { events: [], likedEvents: [], pk: publicKey };
}
// Ensure likedEvents array exists
if (!data.likedEvents) {
data.likedEvents = [];
}
// Avoid duplicate likes
if (!data.likedEvents.includes(eventId)) {
data.likedEvents.push(eventId);
}
// Save back to localStorage
localStorage.setItem(fileName, JSON.stringify(data));
}
// On Post (kind 1) - Preserve liked events
function saveEvent(event) {
const publicKey = localStorage.getItem("nostr_pub");
const fileName = `HEX${publicKey}.json`;
let storedData = localStorage.getItem(fileName);
let data;
if (storedData) {
data = JSON.parse(storedData);
} else {
data = { events: [], pk: publicKey };
}
// Add the new event to the events array
data.events.push(event);
// Preserve liked events by syncing them back
data.likedEvents = data.likedEvents || [];
const localLikedEvents = JSON.parse(localStorage.getItem("liked_events")) || [];
data.likedEvents = [...new Set([...data.likedEvents, ...localLikedEvents])];
// Save updated HEX file
localStorage.setItem(fileName, JSON.stringify(data));
}
// Send the Signed Reaction Event to Relays (Prioritize URL relays)
function propagateReaction(event) {
const params = new URLSearchParams(window.location.search);
const relayParam = params.getAll('r');
// Use relays from URL if they exist; fallback to default relays
const relayList = relayParam.length ? relayParam : defaultRelays;
let relaysTried = 0;
let relaysFailed = 0;
for (const relay of relayList) {
try {
const socket = new WebSocket(relay);
socket.onopen = () => {
socket.send(JSON.stringify(["EVENT", event]));
console.log(`Reaction propagated to ${relay}`);
};
socket.onmessage = (message) => {
const data = JSON.parse(message.data);
console.log(`Relay response from ${relay}:`, data);
// Check if relay returned 'OK' and propagate to others if not
if (data[0] === 'OK' && data[2] === true) {
socket.close();
} else {
relaysFailed++;
}
};
socket.onerror = (error) => {
console.error(`Failed to send reaction to ${relay}:`, error);
relaysFailed++;
};
socket.onclose = () => {
relaysTried++;
console.log(`Connection to ${relay} closed.`);
// Retry default relays if all custom relays fail
if (relaysTried === relayList.length && relaysFailed > 0) {
console.warn("All custom relays failed. Retrying with default relays...");
fallbackToDefaultRelaysForReactions(event);
}
};
} catch (error) {
console.error(`WebSocket Error for ${relay}:`, error);
relaysFailed++;
}
}
}
// Fallback to Default Relays if Custom Relays Fail
function fallbackToDefaultRelaysForReactions(event) {
defaultRelays.forEach(relay => {
try {
const socket = new WebSocket(relay);
socket.onopen = () => {
socket.send(JSON.stringify(["EVENT", event]));
console.log(`Reaction propagated to default relay: ${relay}`);
socket.close();
};
socket.onerror = (error) => {
console.error(`Default relay failed: ${relay}`, error);
};
} catch (error) {
console.error(`Error sending to default relay ${relay}:`, error);
}
});
}
// Sign the Reaction Event
async function signEvent(event, privateKey) {
const encoder = new TextEncoder();
const message = JSON.stringify([
0, event.pubkey, event.created_at, event.kind, event.tags, event.content
]);
const hash = await crypto.subtle.digest("SHA-256", encoder.encode(message));
const id = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join("");
event.id = id;
event.sig = await schnorr.sign(id, privateKey);
return event;
}
// Helper function to generate a random color
function generateRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
// Display the fetched note1 event in the paragraph section
function displayNote1Event(eventData, note1String) {
const paragraph = document.querySelector('#customParagraph');
// Preserve the original full content if not already saved
if (!paragraph.getAttribute('data-full-content')) {
paragraph.setAttribute('data-full-content', paragraph.innerHTML);
}
// Generate a random color for a pencil icon
const randomColor = generateRandomColor();
// Get the current full content
const fullContent = paragraph.getAttribute('data-full-content');
// Replace the matching placeholder or `note1String` with the styled noteBox
const noteBox = `
<div class="note-box">
<a href="https://njump.me/${eventData.id}" target="_blank" title="Show event in external source njump.me">
<span style="font-size:30px; cursor: pointer; color: ${randomColor};">⇝</span>
</a>
${replaceMediaLinks(eventData.content)}
<div class="note-details" id="note-details-${eventData.id}">
<strong>Event JSON:</strong>
<pre>${JSON.stringify(eventData, null, 2)}</pre>
</div>
<div class="note-header">
<img src="img/othrIcon/like_0.png" id="like-icon-${eventData.id}"
class="icon like-icon" alt="Like" title="Like Event"
onclick="toggleLike('${eventData.id}', '${eventData.pubkey}')"
data-liked="false">
<img src="img/othrIcon/details.png" class="icon details-icon"
alt="Details" title="View Details"
onclick="toggleDetails('${eventData.id}')">
</div>
</div>
`;
// Replace placeholder or `note1String` with the styled noteBox
const updatedContent = fullContent.replace(note1String, noteBox);
paragraph.setAttribute('data-full-content', updatedContent);
// Update the paragraph based on its current state (truncated or full)
if (paragraph.getAttribute('data-expanded') === "true") {
paragraph.innerHTML = updatedContent; // Expand to full content
} else {
truncateParagraph(paragraph); // Keep it truncated
}
// Apply modal listeners to media in the updated content
applyModalListenersToMedia(paragraph);
// Apply likes after rendering the noteBox
markLikedEvents();
}
function markLikedEvents() {
const publicKey = localStorage.getItem("nostr_pub");
const fileName = `HEX${publicKey}.json`;
const storedData = localStorage.getItem(fileName);
if (storedData) {
const data = JSON.parse(storedData);
if (data.likedEvents) {
// Delay to ensure DOM is fully rendered
setTimeout(() => {
data.likedEvents.forEach(eventId => {
const likeIcon = document.getElementById(`like-icon-${eventId}`);
if (likeIcon) {
likeIcon.src = "img/othrIcon/like_1.png";
likeIcon.dataset.liked = "true";
likeIcon.style.pointerEvents = "none"; // Disable clicks
likeIcon.title = "Already Liked";
}
});
}, 500); // 500ms delay
}
}
}
// Toggle function for showing/hiding JSON details
function toggleDetails(eventId) {
console.log(`Toggling details for eventId: ${eventId}`);
// Close any currently open details first
const openDetails = document.querySelectorAll('.note-details.visible');
openDetails.forEach(detail => {
if (detail.id !== `note-details-${eventId}`) {
console.log(`Closing other open details: ${detail.id}`);
detail.classList.remove('visible');
}
});
// Toggle the current detail's visibility
const details = document.getElementById(`note-details-${eventId}`);
if (details) {
details.classList.toggle('visible');
console.log(`Details for eventId ${eventId} are now ${details.classList.contains('visible') ? 'open' : 'closed'}`);
} else {
console.warn(`Details for eventId ${eventId} not found!`);
}
}
// Close note details when clicking outside
document.addEventListener('click', (event) => {
console.log("Click detected outside, checking for open details...");
const noteBoxes = document.querySelectorAll('.note-box');
let clickedInsideAnyNoteBox = false;
noteBoxes.forEach(box => {
const details = box.querySelector('.note-details');
if (details && details.classList.contains('visible')) {
console.log(`Checking visibility for details: ${details.id}`);
}
// Check if the click is inside any visible note-box
if (box.contains(event.target)) {
console.log(`Click detected inside note-box: ${box}`);
clickedInsideAnyNoteBox = true;
}
});
// Close all details if the click was outside any note-box
if (!clickedInsideAnyNoteBox) {
console.log("Click outside all note-boxes. Closing all visible details.");
const visibleDetails = document.querySelectorAll('.note-details.visible');
visibleDetails.forEach(details => {
console.log(`Closing visible details: ${details.id}`);
details.classList.remove('visible');
});
}
});
// Prevent event propagation when clicking on details icons
document.querySelectorAll('.details-icon').forEach(icon => {
icon.addEventListener('click', (e) => {
e.stopPropagation();
console.log("Click event on details-icon stopped from propagating.");
});
});
// On page load, extract and handle note1 from the p= attribute and if there's no note1 string display p= as is
// window.onload = () => {
// const params = new URLSearchParams(window.location.search);
// const pValue = params.get('p') || '';
// const relayParam = params.getAll('r');
// const customParagraph = document.getElementById('customParagraph');
// if (!pValue.match(/(?:nostr:)?note1/)) { // Match either "note1" or "nostr:note1"
// // No note1 string, simply display the paragraph as normal
// customParagraph.setAttribute('data-full-content', pValue || "");
// truncateParagraph(customParagraph); // Ensure truncation applies
// } else {
// const note1Matches = pValue.match(/(?:nostr:)?note1\w+/g) || []; // Match both formats
// if (note1Matches.length > 0) {
// note1Matches.forEach(note1String => {
// const { eventId } = decodeNote1(note1String) || {};
// if (eventId) {
// fetchNote1Event(eventId, relayParam, note1String);
// }
// });
// } else {
// customParagraph.textContent = "No valid note1 found in URL.";
// truncateParagraph(customParagraph); // Ensure truncation applies
// }
// }
// };
window.onload = () => {
const params = new URLSearchParams(window.location.search);
const pValue = params.get('p') || '';
const relayParam = params.getAll('r');
const customParagraph = document.getElementById('customParagraph');
// Check for "nostr:note1" strings explicitly
const note1Matches = pValue.match(/nostr:note1\w+/g) || [];
const plainNote1Matches = pValue.match(/\bnote1\w+\b(?!:)/g) || []; // Match "note1" without "nostr:"
if (plainNote1Matches.length > 0) {
// Handle plain "note1" strings as normal text
plainNote1Matches.forEach(note1String => {
pValue = pValue.replace(note1String, `<span>${note1String}</span>`); // Display as raw text
});
}
if (note1Matches.length > 0) {
// Handle "nostr:note1" strings for fetching
note1Matches.forEach(note1String => {
const { eventId } = decodeNote1(note1String) || {};
if (eventId) {
fetchNote1Event(eventId, relayParam, note1String);
}
});
} else if (plainNote1Matches.length === 0) {
// No "nostr:note1" or "note1" strings
customParagraph.setAttribute('data-full-content', pValue || "");
truncateParagraph(customParagraph);
}
};
// On page load, mark liked events
document.addEventListener("DOMContentLoaded", () => {
const publicKey = localStorage.getItem("nostr_pub");
const fileName = `HEX${publicKey}.json`;
const storedData = localStorage.getItem(fileName);
if (storedData) {
const data = JSON.parse(storedData);
if (data.likedEvents) {
setTimeout(() => {
data.likedEvents.forEach(eventId => {
const likeIcon = document.getElementById(`like-icon-${eventId}`);
if (likeIcon) {
likeIcon.src = "img/othrIcon/like_1.png";
likeIcon.dataset.liked = "true";
}
});
}, 1500); // 1500ms delay);
}
}
});