Why Nostr? What is Njump?
2025-05-11 05:55:35
in reply to

jack on Nostr: source: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ...

source:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nostr Media Feed</title>
<style>
:root {
--bg-color: #ffffff;
--text-color: #333333;
}

[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #ffffff;
}

body {
font-family: -apple-system, system-ui, sans-serif;
margin: 0;
padding: 0;
background: var(--bg-color);
color: var(--text-color);
}

#header {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 15px 20px;
background: var(--bg-color);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
font-size: 14px;
}

#feed {
margin-top: 52px;
}

.note {
margin-bottom: 0;
}

.media-container {
background: #000;
line-height: 0;
width: 100%;
}

.media-container a {
display: block;
line-height: 0;
}

.media-container img,
.media-container video {
width: 100%;
height: auto;
object-fit: contain;
opacity: 0;
transition: opacity 0.5s ease-in;
}

.media-container img.loaded,
.media-container video.loaded {
opacity: 1;
}

#status {
display: flex;
align-items: center;
gap: 6px;
opacity: 0.7;
}

.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}

.status-live .status-dot {
background: #4CAF50;
}

.status-paused .status-dot {
background: #ff9800;
}

@media (min-width: 800px) {
.media-container img,
.media-container video {
max-height: 100vh;
}
}

@media (max-width: 799px) {
.media-container img,
.media-container video {
max-height: none;
}
}
</style>
</head>
<body>
<div id="header">
<div id="status" class="status-live">
<div class="status-dot"></div>
<span>Live</span>
</div>
</div>
<div id="feed"></div>

<script>
// Auto dark mode
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.setAttribute('data-theme', 'dark');
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');
});

// Initialize
const feed = document.getElementById('feed');
const status = document.getElementById('status');
const seenNotes = new Set();
const seenMedia = new Set();
let isPaused = false;

// Relays
const RELAYS = [
'wss://relay.damus.io',
'wss://relay.nostr.band',
'wss://nos.lol',
'wss://relay.nostr.info'
];

const relayPool = new Map();

// Function to update status display
function updateStatus(paused) {
status.className = paused ? 'status-paused' : 'status-live';
status.querySelector('span').textContent = paused ? 'Paused' : 'Live';
}

// Pause/Resume based on scroll position
let lastScrollTop = 0;
window.addEventListener('scroll', () => {
const st = window.pageYOffset || document.documentElement.scrollTop;
if (st > lastScrollTop && st > 100) {
// Scrolling down
if (!isPaused) {
isPaused = true;
updateStatus(true);
}
} else if (st === 0) {
// At top
if (isPaused) {
isPaused = false;
updateStatus(false);
}
}
lastScrollTop = st;
});

// Connect to relays
function connect() {
let connectedRelays = 0;

RELAYS.forEach(relayUrl => {
const socket = new WebSocket(relayUrl);
relayPool.set(relayUrl, socket);

socket.onopen = () => {
connectedRelays++;
if (connectedRelays === 1) {
updateStatus(false);
}

// Subscribe to notes with media
const recentSub = JSON.stringify([
"REQ",
"recent_" + relayUrl,
{
"kinds": [1],
"limit": 500
}
]);
socket.send(recentSub);
};

socket.onclose = () => {
relayPool.delete(relayUrl);
connectedRelays--;
if (connectedRelays === 0) {
setTimeout(() => connect(), 2000);
}
};

socket.onerror = (error) => {
console.error('WebSocket error:', error);
};

// Handle incoming messages
socket.onmessage = async (event) => {
if (isPaused) return;

const data = JSON.parse(event.data);
if (data[0] !== 'EVENT') return;

const msg = data[2];

// Handle notes
if (msg.kind !== 1) return;
if (seenNotes.has(msg.id)) return;
seenNotes.add(msg.id);

// Look for media URLs
const mediaUrls = [];
const urlRegex = /(https?:\/\/[^\s<]+\.(jpg|jpeg|png|gif|mp4|webm))/gi;
let match;
while ((match = urlRegex.exec(msg.content)) !== null) {
mediaUrls.push(match[0]);
}
if (mediaUrls.length === 0) return;

// Check for duplicate media
const mediaKey = mediaUrls.sort().join(',');
if (seenMedia.has(mediaKey)) return;
seenMedia.add(mediaKey);

try {
// Create note element
const noteEl = document.createElement('div');
noteEl.className = 'note';

// Add media
const mediaContainer = document.createElement('div');
mediaContainer.className = 'media-container';

// Make media container clickable
const mediaLink = document.createElement('a');
mediaLink.href = `https://njump.me/${msg.id}`;
mediaLink.target = '_blank';
mediaLink.style.cursor = 'pointer';
mediaContainer.appendChild(mediaLink);

for (const url of mediaUrls) {
if (url.match(/\.(jpg|jpeg|png|gif)$/i)) {
try {
// Create and preload image
const img = document.createElement('img');
img.style.opacity = '0';
img.src = url;
img.loading = 'lazy';

// Wait for image to load
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});

// Add to container and fade in
mediaLink.appendChild(img);
requestAnimationFrame(() => {
img.style.opacity = '1';
});
} catch (e) {
console.error('Failed to load image:', url);
}
} else {
const video = document.createElement('video');
video.src = url;
video.controls = true;
video.autoplay = true;
video.muted = true;
video.loop = true;
video.playsInline = true;
video.style.opacity = '0';

// Fade in once video starts playing
video.addEventListener('playing', () => {
requestAnimationFrame(() => {
video.style.opacity = '1';
});
}, { once: true });

video.onerror = () => {
video.remove();
};
mediaLink.appendChild(video);
// Try to start playing
video.play().catch(e => console.log('Auto-play prevented:', e));
}
}

noteEl.appendChild(mediaContainer);
feed.insertBefore(noteEl, feed.firstChild);
} catch (e) {
console.error('Error creating note element:', e);
}
};
});
}

// Initial connection
connect();
</script>
</body>
</html>
Author Public Key
npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m