jacksource:<br><br><!DOCTYPE html><br><html lang="en"><br><head><br> <meta charset="UTF-8"><br> <meta name="viewport" content="width=device-width, initial-scale=1.0"><br> <title>Nostr Media Feed</title><br> <style><br> :root {<br> --bg-color: <a class="mention hashtag" href="https://mostr.pub/tags/ffffff" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>ffffff</a>;<br> --text-color: #333333;<br> }<br><br> [data-theme="dark"] {<br> --bg-color: <a class="mention hashtag" href="https://mostr.pub/tags/1a1a1a" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>1a1a1a</a>;<br> --text-color: <a class="mention hashtag" href="https://mostr.pub/tags/ffffff" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>ffffff</a>;<br> }<br><br> body {<br> font-family: -apple-system, system-ui, sans-serif;<br> margin: 0;<br> padding: 0;<br> background: var(--bg-color);<br> color: var(--text-color);<br> }<br><br> <a class="mention hashtag" href="https://mostr.pub/tags/header" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>header</a> {<br> position: fixed;<br> top: 0;<br> left: 0;<br> right: 0;<br> padding: 15px 20px;<br> background: var(--bg-color);<br> display: flex;<br> justify-content: space-between;<br> align-items: center;<br> z-index: 1000;<br> font-size: 14px;<br> }<br><br> <a class="mention hashtag" href="https://mostr.pub/tags/feed" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>feed</a> {<br> margin-top: 52px;<br> }<br><br> .note {<br> margin-bottom: 0;<br> }<br><br> .media-container {<br> background: #000;<br> line-height: 0;<br> width: 100%;<br> }<br><br> .media-container a {<br> display: block;<br> line-height: 0;<br> }<br><br> .media-container img,<br> .media-container video {<br> width: 100%;<br> height: auto;<br> object-fit: contain;<br> opacity: 0;<br> transition: opacity 0.5s ease-in;<br> }<br><br> .media-container img.loaded,<br> .media-container video.loaded {<br> opacity: 1;<br> }<br><br> <a class="mention hashtag" href="https://mostr.pub/tags/status" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>status</a> {<br> display: flex;<br> align-items: center;<br> gap: 6px;<br> opacity: 0.7;<br> }<br><br> .status-dot {<br> width: 6px;<br> height: 6px;<br> border-radius: 50%;<br> }<br><br> .status-live .status-dot {<br> background: <a class="mention hashtag" href="https://mostr.pub/tags/4CAF50" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>4CAF50</a>;<br> }<br><br> .status-paused .status-dot {<br> background: <a class="mention hashtag" href="https://mostr.pub/tags/ff9800" rel="nofollow noopener noreferrer" target="_blank"><span>#</span>ff9800</a>;<br> }<br><br> @media (min-width: 800px) {<br> .media-container img,<br> .media-container video {<br> max-height: 100vh;<br> }<br> }<br><br> @media (max-width: 799px) {<br> .media-container img,<br> .media-container video {<br> max-height: none;<br> }<br> }<br> </style><br></head><br><body><br> <div id="header"><br> <div id="status" class="status-live"><br> <div class="status-dot"></div><br> <span>Live</span><br> </div><br> </div><br> <div id="feed"></div><br><br> <script><br> // Auto dark mode<br> if (window.matchMedia('(prefers-color-scheme: dark)').matches) {<br> document.body.setAttribute('data-theme', 'dark');<br> }<br> window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {<br> document.body.setAttribute('data-theme', e.matches ? 'dark' : 'light');<br> });<br><br> // Initialize<br> const feed = document.getElementById('feed');<br> const status = document.getElementById('status');<br> const seenNotes = new Set();<br> const seenMedia = new Set();<br> let isPaused = false;<br><br> // Relays<br> const RELAYS = [<br> 'wss://<a href="http://relay.damus.io" rel="nofollow noopener noreferrer" target="_blank">relay.damus.io</a>',<br> 'wss://<a href="http://relay.nostr.band" rel="nofollow noopener noreferrer" target="_blank">relay.nostr.band</a>',<br> 'wss://<a href="http://nos.lol" rel="nofollow noopener noreferrer" target="_blank">nos.lol</a>',<br> 'wss://<a href="http://relay.nostr.info" rel="nofollow noopener noreferrer" target="_blank">relay.nostr.info</a>'<br> ];<br> <br> const relayPool = new Map();<br><br> // Function to update status display<br> function updateStatus(paused) {<br> status.className = paused ? 'status-paused' : 'status-live';<br> status.querySelector('span').textContent = paused ? 'Paused' : 'Live';<br> }<br><br> // Pause/Resume based on scroll position<br> let lastScrollTop = 0;<br> window.addEventListener('scroll', () => {<br> const st = window.pageYOffset || document.documentElement.scrollTop;<br> if (st > lastScrollTop && st > 100) {<br> // Scrolling down<br> if (!isPaused) {<br> isPaused = true;<br> updateStatus(true);<br> }<br> } else if (st === 0) {<br> // At top<br> if (isPaused) {<br> isPaused = false;<br> updateStatus(false);<br> }<br> }<br> lastScrollTop = st;<br> });<br><br> // Connect to relays<br> function connect() {<br> let connectedRelays = 0;<br><br> RELAYS.forEach(relayUrl => {<br> const socket = new WebSocket(relayUrl);<br> relayPool.set(relayUrl, socket);<br><br> socket.onopen = () => {<br> connectedRelays++;<br> if (connectedRelays === 1) {<br> updateStatus(false);<br> }<br> <br> // Subscribe to notes with media<br> const recentSub = JSON.stringify([<br> "REQ",<br> "recent_" + relayUrl,<br> {<br> "kinds": [1],<br> "limit": 500<br> }<br> ]);<br> socket.send(recentSub);<br> };<br><br> socket.onclose = () => {<br> relayPool.delete(relayUrl);<br> connectedRelays--;<br> if (connectedRelays === 0) {<br> setTimeout(() => connect(), 2000);<br> }<br> };<br><br> socket.onerror = (error) => {<br> console.error('WebSocket error:', error);<br> };<br><br> // Handle incoming messages<br> socket.onmessage = async (event) => {<br> if (isPaused) return;<br> <br> const data = JSON.parse(<a href="http://event.data" rel="nofollow noopener noreferrer" target="_blank">event.data</a>);<br> if (data[0] !== 'EVENT') return;<br> <br> const msg = data[2];<br> <br> // Handle notes<br> if (msg.kind !== 1) return;<br> if (seenNotes.has(<a href="http://msg.id" rel="nofollow noopener noreferrer" target="_blank">msg.id</a>)) return;<br> seenNotes.add(<a href="http://msg.id" rel="nofollow noopener noreferrer" target="_blank">msg.id</a>);<br><br> // Look for media URLs<br> const mediaUrls = [];<br> const urlRegex = /(https?:\/\/[^\s<]+\.(jpg|jpeg|png|gif|mp4|webm))/gi;<br> let match;<br> while ((match = urlRegex.exec(msg.content)) !== null) {<br> mediaUrls.push(match[0]);<br> }<br> if (mediaUrls.length === 0) return;<br><br> // Check for duplicate media<br> const mediaKey = mediaUrls.sort().join(',');<br> if (seenMedia.has(mediaKey)) return;<br> seenMedia.add(mediaKey);<br><br> try {<br> // Create note element<br> const noteEl = document.createElement('div');<br> noteEl.className = 'note';<br> <br> // Add media<br> const mediaContainer = document.createElement('div');<br> mediaContainer.className = 'media-container';<br> <br> // Make media container clickable<br> const mediaLink = document.createElement('a');<br> mediaLink.href = `<a href="https://njump.me/${msg.id}`" rel="nofollow noopener noreferrer" target="_blank">https://njump.me/${msg.id}`</a>;<br> <a href="http://mediaLink.target" rel="nofollow noopener noreferrer" target="_blank">mediaLink.target</a> = '_blank';<br> <a href="http://mediaLink.style" rel="nofollow noopener noreferrer" target="_blank">mediaLink.style</a>.cursor = 'pointer';<br> mediaContainer.appendChild(mediaLink);<br> <br> for (const url of mediaUrls) {<br> if (url.match(/\.(jpg|jpeg|png|gif)$/i)) {<br> try {<br> // Create and preload image<br> const img = document.createElement('img');<br> <a href="http://img.style" rel="nofollow noopener noreferrer" target="_blank">img.style</a>.opacity = '0';<br> img.src = url;<br> img.loading = 'lazy';<br> <br> // Wait for image to load<br> await new Promise((resolve, reject) => {<br> img.onload = resolve;<br> img.onerror = reject;<br> });<br> <br> // Add to container and fade in<br> mediaLink.appendChild(img);<br> requestAnimationFrame(() => {<br> <a href="http://img.style" rel="nofollow noopener noreferrer" target="_blank">img.style</a>.opacity = '1';<br> });<br> } catch (e) {<br> console.error('Failed to load image:', url);<br> }<br> } else {<br> const video = document.createElement('video');<br> video.src = url;<br> video.controls = true;<br> video.autoplay = true;<br> video.muted = true;<br> video.loop = true;<br> video.playsInline = true;<br> <a href="http://video.style" rel="nofollow noopener noreferrer" target="_blank">video.style</a>.opacity = '0';<br> <br> // Fade in once video starts playing<br> video.addEventListener('playing', () => {<br> requestAnimationFrame(() => {<br> <a href="http://video.style" rel="nofollow noopener noreferrer" target="_blank">video.style</a>.opacity = '1';<br> });<br> }, { once: true });<br> <br> video.onerror = () => {<br> video.remove();<br> };<br> mediaLink.appendChild(video);<br> // Try to start playing<br> <a href="http://video.play" rel="nofollow noopener noreferrer" target="_blank">video.play</a>().catch(e => console.log('Auto-play prevented:', e));<br> }<br> }<br> <br> noteEl.appendChild(mediaContainer);<br> feed.insertBefore(noteEl, feed.firstChild);<br> } catch (e) {<br> console.error('Error creating note element:', e);<br> }<br> };<br> });<br> }<br><br> // Initial connection<br> connect();<br> </script><br></body><br></html>