diff --git a/public/healthz.json b/public/healthz.json index 730eefb..17d6fcd 100644 --- a/public/healthz.json +++ b/public/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.3.1", - "buildTime": "2026-02-12T11:34:23.480Z", + "buildTime": "2026-02-12T12:55:08.903Z", "gitCommit": "unknown", - "timestamp": 1770896063480 + "timestamp": 1770900908903 } \ No newline at end of file diff --git a/src/lib/components/EventMenu.svelte b/src/lib/components/EventMenu.svelte index 2c1d5db..a9929fd 100644 --- a/src/lib/components/EventMenu.svelte +++ b/src/lib/components/EventMenu.svelte @@ -418,7 +418,7 @@ {#if isReplaceable} {/if} @@ -508,7 +508,7 @@ {#if isLoggedIn && !isOwnEvent} {/if} @@ -646,6 +646,12 @@ flex-shrink: 0; font-size: 1rem; line-height: 1; + display: inline-flex; + align-items: center; + } + + .menu-item-icon :global(.icon-wrapper) { + display: inline-block; } .menu-item span { diff --git a/src/lib/components/content/FileExplorer.svelte b/src/lib/components/content/FileExplorer.svelte index dcaa3c4..3b20aa1 100644 --- a/src/lib/components/content/FileExplorer.svelte +++ b/src/lib/components/content/FileExplorer.svelte @@ -3,6 +3,7 @@ // @ts-ignore - highlight.js default export works at runtime import hljs from 'highlight.js'; import 'highlight.js/styles/vs2015.css'; + import Icon from '../ui/Icon.svelte'; interface Props { files: GitFile[]; @@ -137,20 +138,25 @@ } } - function getFileIcon(file: GitFile): string { + function getFileIconName(file: GitFile): string { const ext = file.name.split('.').pop()?.toLowerCase() || ''; - const icons: Record = { - 'js': '📜', 'ts': '📘', 'jsx': '⚛️', 'tsx': '⚛️', - 'py': '🐍', 'java': '☕', 'cpp': '⚙️', 'c': '⚙️', - 'html': '🌐', 'css': '🎨', 'scss': '🎨', 'sass': '🎨', - 'json': '📋', 'yaml': '📋', 'yml': '📋', 'toml': '📋', - 'md': '📝', 'txt': '📄', 'adoc': '📝', - 'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', 'svg': '🖼️', - 'pdf': '📕', 'zip': '📦', 'tar': '📦', 'gz': '📦', - 'sh': '💻', 'bash': '💻', 'zsh': '💻', - 'rs': '🦀', 'go': '🐹', 'php': '🐘', 'rb': '💎' + const iconMap: Record = { + // Code files + 'js': 'code', 'ts': 'code', 'jsx': 'code', 'tsx': 'code', + 'py': 'code', 'java': 'code', 'cpp': 'code', 'c': 'code', + 'html': 'code', 'css': 'code', 'scss': 'code', 'sass': 'code', + 'rs': 'code', 'go': 'code', 'php': 'code', 'rb': 'code', + 'sh': 'code', 'bash': 'code', 'zsh': 'code', + // Data/config files + 'json': 'file-text', 'yaml': 'file-text', 'yml': 'file-text', 'toml': 'file-text', + // Text files + 'md': 'file-text', 'txt': 'file-text', 'adoc': 'file-text', + // Images + 'png': 'image', 'jpg': 'image', 'jpeg': 'image', 'gif': 'image', 'svg': 'image', + // Other files + 'pdf': 'file-text', 'zip': 'file-text', 'tar': 'file-text', 'gz': 'file-text' }; - return icons[ext] || '📄'; + return iconMap[ext] || 'file-text'; } function formatFileSize(bytes?: number): string { @@ -326,7 +332,7 @@ onclick={() => fetchFileContent(file)} class="tree-file-btn" > - {getFileIcon(file)} + {subName} {#if file.size} {formatFileSize(file.size)} @@ -364,7 +370,7 @@ onclick={() => fetchFileContent(nestedFile)} class="tree-file-btn" > - {getFileIcon(nestedFile)} + {nestedName} {#if nestedFile.size} {formatFileSize(nestedFile.size)} @@ -401,7 +407,7 @@ onclick={() => fetchFileContent(deepFile)} class="tree-file-btn" > - {getFileIcon(deepFile)} + {deepName} {#if deepFile.size} {formatFileSize(deepFile.size)} @@ -436,7 +442,7 @@ onclick={() => fetchFileContent(file)} class="tree-file-btn" > - {getFileIcon(file)} + {name} {#if file.size} {formatFileSize(file.size)} diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte index 2775b62..4ff8f1a 100644 --- a/src/lib/components/content/MarkdownRenderer.svelte +++ b/src/lib/components/content/MarkdownRenderer.svelte @@ -124,68 +124,6 @@ } } - // Convert plain media URLs (images, videos, audio) to HTML tags - function convertMediaUrls(text: string): string { - // Match media URLs (http/https URLs ending in media extensions) - const imageExtensions = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i; - const videoExtensions = /\.(mp4|webm|ogg|mov|avi|mkv)(\?[^\s<>"']*)?$/i; - const audioExtensions = /\.(mp3|wav|ogg|flac|aac|m4a)(\?[^\s<>"']*)?$/i; - const urlPattern = /https?:\/\/[^\s<>"']+/g; - - // Normalize exclude URLs for comparison - const normalizedExcludeUrls = new Set(excludeMediaUrls.map(url => normalizeUrl(url))); - - let result = text; - const matches: Array<{ url: string; index: number; endIndex: number; type: 'image' | 'video' | 'audio' }> = []; - - // Find all URLs - let match; - while ((match = urlPattern.exec(text)) !== null) { - const url = match[0]; - const index = match.index; - const endIndex = index + url.length; - - // Skip if this URL is already displayed by MediaAttachments - if (normalizedExcludeUrls.has(normalizeUrl(url))) { - continue; - } - - // Check if this URL is already in markdown or HTML - const before = text.substring(Math.max(0, index - 10), index); - const after = text.substring(endIndex, Math.min(text.length, endIndex + 10)); - - // Skip if it's already in markdown or HTML tags - if (before.includes('![') || before.includes('') || after.startsWith('') || after.startsWith('')) { - continue; - } - - // Determine media type - if (imageExtensions.test(url)) { - matches.push({ url, index, endIndex, type: 'image' }); - } else if (videoExtensions.test(url)) { - matches.push({ url, index, endIndex, type: 'video' }); - } else if (audioExtensions.test(url)) { - matches.push({ url, index, endIndex, type: 'audio' }); - } - } - - // Replace from end to start to preserve indices - for (let i = matches.length - 1; i >= 0; i--) { - const { url, index, endIndex, type } = matches[i]; - const escapedUrl = escapeHtml(url); - - if (type === 'image') { - result = result.substring(0, index) + `` + result.substring(endIndex); - } else if (type === 'video') { - result = result.substring(0, index) + `` + result.substring(endIndex); - } else if (type === 'audio') { - result = result.substring(0, index) + `` + result.substring(endIndex); - } - } - - return result; - } // Resolve custom emojis in content async function resolveContentEmojis(text: string): Promise { @@ -502,10 +440,144 @@ return result; } + // Process and convert media URLs: handles markdown, AsciiDoc, and plain URLs + // Removes excluded URLs (displayed by MediaAttachments) and converts others to HTML tags + function processMediaUrls(text: string): string { + // Normalize exclude URLs for comparison + const normalizedExcludeUrls = excludeMediaUrls.length > 0 + ? new Set(excludeMediaUrls.map(url => normalizeUrl(url))) + : new Set(); + + // Debug: Log excluded URLs and check if they're being found in text + if (normalizedExcludeUrls.size > 0 && typeof console !== 'undefined') { + console.debug('MarkdownRenderer: Excluding URLs:', Array.from(normalizedExcludeUrls)); + console.debug('MarkdownRenderer: Original text:', text.substring(0, 200)); + // Check if any excluded URLs appear in the text + for (const excludedUrl of normalizedExcludeUrls) { + // Try to find the URL in the text (check both normalized and original) + const urlInText = text.includes(excludedUrl) || + excludeMediaUrls.some(orig => text.includes(orig)); + if (urlInText) { + console.debug('MarkdownRenderer: Found excluded URL in text:', excludedUrl); + // Find where it appears + const index = text.indexOf(excludeMediaUrls.find(orig => text.includes(orig)) || ''); + if (index >= 0) { + console.debug('MarkdownRenderer: URL found at index:', index, 'context:', text.substring(Math.max(0, index - 20), Math.min(text.length, index + 100))); + } + } + } + } + + let result = text; + const replacements: Array<{ fullMatch: string; replacement: string; index: number }> = []; + const processedIndices = new Set(); // Track processed indices to avoid duplicates + + // 1. Match markdown image syntax: ![alt](url) + const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; + let match; + while ((match = markdownImageRegex.exec(text)) !== null) { + const fullMatch = match[0]; + const alt = match[1]; + const url = match[2]; + const index = match.index; + + if (processedIndices.has(index)) continue; + processedIndices.add(index); + + const normalizedUrl = normalizeUrl(url); + if (normalizedExcludeUrls.has(normalizedUrl)) { + // Remove excluded markdown images, leaving just alt text + replacements.push({ fullMatch, replacement: alt || '', index }); + } else { + // Convert non-excluded markdown images to HTML (marked will handle this, but we ensure it's preserved) + // Marked will convert this, so we don't need to do anything here + } + } + + // 2. Match AsciiDoc image syntax: image::url[] or image:url[] + const asciidocImageRegex = /image::?([^\s\[\]]+)(\[[^\]]*\])?/g; + asciidocImageRegex.lastIndex = 0; + let asciidocMatch; + while ((asciidocMatch = asciidocImageRegex.exec(text)) !== null) { + const fullMatch = asciidocMatch[0]; + const url = asciidocMatch[1]; + const index = asciidocMatch.index; + + if (processedIndices.has(index)) continue; + + // Only process if it's a URL (starts with http:// or https://) + if (url.startsWith('http://') || url.startsWith('https://')) { + processedIndices.add(index); + const normalizedUrl = normalizeUrl(url); + if (normalizedExcludeUrls.has(normalizedUrl)) { + // Remove excluded AsciiDoc images + replacements.push({ fullMatch, replacement: '', index }); + } + // Non-excluded AsciiDoc images will be handled by AsciiDoc renderer + } + } + + // 3. Match plain media URLs (using the SAME pattern as getMediaAttachmentUrls) + const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+\.(jpg|jpeg|png|gif|webp|svg|bmp|mp4|webm|ogg|mov|avi|mkv|mp3|wav|flac|aac|m4a)(\?[^\s<>"{}|\\^`\[\]]*)?)/gi; + urlRegex.lastIndex = 0; + while ((match = urlRegex.exec(text)) !== null) { + const url = match[1]; // The URL part (group 1) + const fullMatch = match[0]; // Full match + const index = match.index; + + if (processedIndices.has(index)) continue; + processedIndices.add(index); + + // Check if it's already in markdown or HTML + const before = text.substring(Math.max(0, index - 10), index); + const after = text.substring(index + fullMatch.length, Math.min(text.length, index + fullMatch.length + 10)); + + if (before.includes('![') || before.includes('') || after.startsWith('') || after.startsWith('')) { + continue; // Already handled by markdown/HTML + } + + const normalizedUrl = normalizeUrl(url); + if (normalizedExcludeUrls.has(normalizedUrl)) { + // Remove excluded plain URLs + if (typeof console !== 'undefined') { + console.debug('MarkdownRenderer: Found excluded URL, removing:', url, 'normalized:', normalizedUrl); + } + replacements.push({ fullMatch, replacement: '', index }); + } else { + if (typeof console !== 'undefined' && normalizedExcludeUrls.size > 0) { + console.debug('MarkdownRenderer: URL not excluded, will convert:', url, 'normalized:', normalizedUrl, 'excluded set:', Array.from(normalizedExcludeUrls)); + } + // Convert non-excluded plain URLs to HTML tags + const ext = match[2].toLowerCase(); + let htmlTag: string; + if (['mp4', 'webm', 'ogg', 'mov', 'avi', 'mkv'].includes(ext)) { + htmlTag = ``; + } else if (['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'].includes(ext)) { + htmlTag = ``; + } else { + htmlTag = ``; + } + replacements.push({ fullMatch, replacement: htmlTag, index }); + } + } + + // Apply all replacements from end to start to preserve indices + replacements.sort((a, b) => b.index - a.index); + for (const { fullMatch, replacement, index } of replacements) { + result = result.substring(0, index) + replacement + result.substring(index + fullMatch.length); + } + + return result; + } + // Process content: replace nostr URIs with HTML span elements and convert media URLs function processContent(text: string): string { - // First, convert greentext (must be before markdown processing) - let processed = convertGreentext(text); + // Process media URLs (removes excluded ones, converts others to HTML) + let processed = processMediaUrls(text); + + // Then, convert greentext (must be before markdown processing) + processed = convertGreentext(processed); // Convert wikilinks to markdown links (before other processing) processed = convertWikilinks(processed); @@ -516,9 +588,6 @@ // Convert hashtags to links processed = convertHashtags(processed); - // Then, convert plain media URLs (images, videos, audio) to HTML tags - processed = convertMediaUrls(processed); - // Find all NIP-21 links (nostr:npub, nostr:nprofile, nostr:nevent, etc.) const links = findNIP21Links(processed); @@ -688,6 +757,48 @@ }); } + // Helper function to apply exclusion filtering to HTML (used for both fresh and cached content) + function applyExclusionFiltering(htmlContent: string): string { + if (excludeMediaUrls.length === 0) return htmlContent; + + const normalizedExcludeUrls = new Set(excludeMediaUrls.map(url => normalizeUrl(url))); + let filtered = htmlContent; + + // Remove ALL img tags with excluded URLs + filtered = filtered.replace(/]*>/gi, (match) => { + const srcMatch = match.match(/src=["']([^"']+)["']/i); + if (srcMatch) { + const src = srcMatch[1]; + const normalizedSrc = normalizeUrl(src); + if (normalizedExcludeUrls.has(normalizedSrc)) { + if (typeof console !== 'undefined') { + console.debug('MarkdownRenderer: Removing excluded img tag from content:', src); + } + return ''; + } + } + return match; + }); + + // Remove links pointing to excluded image URLs + filtered = filtered.replace(/]*?)href=["']([^"']+)["']([^>]*?)>([^<]*?)<\/a>/gi, (match, beforeAttrs, url, afterAttrs, linkText) => { + if (normalizedExcludeUrls.has(normalizeUrl(url)) && /\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?|$)/i.test(url)) { + return linkText; + } + return match; + }); + + // Remove excluded image URLs from text nodes + filtered = filtered.replace(/>([^<]*?)(https?:\/\/[^\s<>"{}|\\^`\[\]]+\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?[^\s<>"{}|\\^`\[\]]*)?)([^<]*?) { + if (normalizedExcludeUrls.has(normalizeUrl(url))) { + return `>${before}${after}<`; + } + return match; + }); + + return filtered; + } + // Render markdown or AsciiDoc to HTML async function renderMarkdown(text: string): Promise { if (!content) return ''; @@ -710,13 +821,15 @@ } } markdownCache.set(cacheKey, cachedFromDB); - return cachedFromDB; + // Apply exclusion filtering to cached content (exclusion list may have changed) + return applyExclusionFiltering(cachedFromDB); } // Check in-memory cache (faster for same session) const cached = markdownCache.get(cacheKey); if (cached !== undefined) { - return cached; + // Apply exclusion filtering to cached content (exclusion list may have changed) + return applyExclusionFiltering(cached); } const processed = processContent(contentToRender); @@ -744,6 +857,9 @@ // Post-process to fix any greentext that markdown converted to blockquotes html = postProcessGreentext(html); + // Apply exclusion filtering to the rendered HTML + html = applyExclusionFiltering(html); + // Remove emoji images that are inside code blocks (they should be plain text) // This handles cases where emojis were replaced before markdown/AsciiDoc parsing // Handle tags (inline code) @@ -773,9 +889,34 @@ // Normalize exclude URLs for comparison const normalizedExcludeUrls = new Set(excludeMediaUrls.map(url => normalizeUrl(url))); + // AGGRESSIVE CLEANUP: Remove ALL img tags with excluded URLs first (before any other processing) + // This is the most important step - it catches images regardless of how they were created + if (normalizedExcludeUrls.size > 0) { + const beforeCleanup = html; + html = html.replace(/]*>/gi, (match) => { + // Extract src attribute + const srcMatch = match.match(/src=["']([^"']+)["']/i); + if (srcMatch) { + const src = srcMatch[1]; + const normalizedSrc = normalizeUrl(src); + // Remove if this image is already displayed by MediaAttachments + if (normalizedExcludeUrls.has(normalizedSrc)) { + if (typeof console !== 'undefined') { + console.debug('MarkdownRenderer: Removing excluded img tag:', src, 'normalized:', normalizedSrc); + } + return ''; // Remove the img tag completely + } + } + return match; + }); + if (typeof console !== 'undefined' && beforeCleanup !== html) { + console.debug('MarkdownRenderer: Cleaned up img tags. Before:', beforeCleanup.match(/]*>/gi)?.length || 0, 'After:', html.match(/]*>/gi)?.length || 0); + } + } + // Fix malformed image tags - ensure all img src attributes are absolute URLs // This prevents the browser from trying to fetch markdown syntax or malformed tags as relative URLs - // Also filter out images that are already displayed by MediaAttachments + // Also filter out images that are already displayed by MediaAttachments (defensive check) html = html.replace(/]*?)>/gi, (match, attributes) => { // Extract src attribute const srcMatch = attributes.match(/src=["']([^"']+)["']/i); @@ -846,6 +987,11 @@ const urlMatch = pattern.match(/(https?:\/\/[^\s<>"')]+)/i); if (urlMatch) { const url = urlMatch[1]; + // Skip if this URL is already displayed by MediaAttachments + if (normalizedExcludeUrls.has(normalizeUrl(url))) { + // Remove the URL pattern completely + return `>${before}${after}<`; + } // If it's an image URL, convert to proper img tag if (/\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)(\?[^\s<>"']*)?$/i.test(url)) { const escapedUrl = escapeHtml(url); @@ -856,6 +1002,32 @@ return `>${before}${after}<`; }); + // Final aggressive cleanup: Remove ANY reference to excluded URLs from the HTML + // This catches URLs that might have been converted to links by the markdown renderer + // or appeared in any other form + if (normalizedExcludeUrls.size > 0) { + // Remove links that point to excluded image URLs (marked might auto-link them) + html = html.replace(/]*?)href=["']([^"']+)["']([^>]*?)>([^<]*?)<\/a>/gi, (match, beforeAttrs, url, afterAttrs, linkText) => { + if (normalizedExcludeUrls.has(normalizeUrl(url))) { + // Remove the link but keep just the text content + return linkText; + } + return match; + }); + + // Remove excluded image URLs from text nodes (final safety net) + // Match URLs in text content (between > and <) + html = html.replace(/>([^<]*?)(https?:\/\/[^\s<>"{}|\\^`\[\]]+\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?[^\s<>"{}|\\^`\[\]]*)?)([^<]*?) { + // Check if this URL is excluded (already displayed by MediaAttachments) + if (normalizedExcludeUrls.has(normalizeUrl(url))) { + // Remove the URL from the text completely + return `>${before}${after}<`; + } + // Otherwise, leave it as-is + return match; + }); + } + // Filter out invalid relative links (like /a, /b, etc.) that cause 404s // These are likely malformed markdown links html = html.replace(/]*>.*?<\/a>/gi, (match, char) => { @@ -869,8 +1041,51 @@ return textMatch ? textMatch[1] : ''; }); + // FINAL PASS: Remove any remaining references to excluded URLs + // This is the absolute last step before sanitization - catches anything we might have missed + if (normalizedExcludeUrls.size > 0) { + // Remove any img tags with excluded URLs (one more time, just to be sure) + html = html.replace(/]*src=["']([^"']+)["'][^>]*>/gi, (match, src) => { + if (normalizedExcludeUrls.has(normalizeUrl(src))) { + return ''; + } + return match; + }); + + // Remove any links pointing to excluded image URLs + html = html.replace(/]*href=["']([^"']+)["'][^>]*>([^<]*?)<\/a>/gi, (match, href, linkText) => { + if (normalizedExcludeUrls.has(normalizeUrl(href))) { + // Check if it's an image URL (ends with image extension) + if (/\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?|$)/i.test(href)) { + return linkText; // Remove link, keep text + } + } + return match; + }); + + // Remove excluded image URLs from any remaining text content + html = html.replace(/(https?:\/\/[^\s<>"{}|\\^`\[\]]+\.(jpg|jpeg|png|gif|webp|svg|bmp)(\?[^\s<>"{}|\\^`\[\]]*)?)/gi, (match, url) => { + if (normalizedExcludeUrls.has(normalizeUrl(url))) { + return ''; + } + return match; + }); + } + // Sanitize HTML (but preserve our data attributes and image src) - const sanitized = sanitizeMarkdown(html); + let sanitized = sanitizeMarkdown(html); + + // ONE MORE FINAL PASS after sanitization - remove any excluded URLs that might have survived + // This is the absolute last chance to catch excluded images + if (normalizedExcludeUrls.size > 0) { + sanitized = sanitized.replace(/]*>/gi, (match) => { + const srcMatch = match.match(/src=["']([^"']+)["']/i); + if (srcMatch && normalizedExcludeUrls.has(normalizeUrl(srcMatch[1]))) { + return ''; + } + return match; + }); + } // Cache the result (with size limit to prevent memory bloat) if (markdownCache.size >= MAX_CACHE_SIZE) { diff --git a/src/lib/components/content/MediaAttachments.svelte b/src/lib/components/content/MediaAttachments.svelte index 11ef411..d3e4cc2 100644 --- a/src/lib/components/content/MediaAttachments.svelte +++ b/src/lib/components/content/MediaAttachments.svelte @@ -2,6 +2,7 @@ import type { NostrEvent } from '../../types/nostr.js'; import { decode } from 'blurhash'; import { onMount } from 'svelte'; + import Icon from '../ui/Icon.svelte'; interface Props { event: NostrEvent; @@ -685,7 +686,8 @@ rel="noopener noreferrer" class="file-link" > - 📎 {item.mimeType || 'File'} {item.size ? `(${(item.size / 1024).toFixed(1)} KB)` : ''} + + {item.mimeType || 'File'} {item.size ? `(${(item.size / 1024).toFixed(1)} KB)` : ''} {/if} @@ -772,6 +774,10 @@ align-items: center; gap: 0.5rem; } + + .file-link :global(.icon-wrapper) { + flex-shrink: 0; + } .file-link:hover { text-decoration: underline; diff --git a/src/lib/components/modals/UpdateModal.svelte b/src/lib/components/modals/UpdateModal.svelte index 02b3e15..ec6ab0c 100644 --- a/src/lib/components/modals/UpdateModal.svelte +++ b/src/lib/components/modals/UpdateModal.svelte @@ -45,38 +45,64 @@ }; try { - // Step 1: Ensure database is up to date + // Step 1: Ensure database is up to date (with timeout) progress = { step: 'Updating database structure...', progress: 5, - total: 7, + total: 8, current: 1 }; - await getDB(); // This will trigger IndexedDB upgrade if needed + + // Add timeout to prevent hanging + await Promise.race([ + getDB(), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Database update timed out after 10 seconds')), 10000) + ) + ]).catch(async (error) => { + // If timeout, try to continue anyway (database might still work) + console.warn('Database update timeout, continuing:', error); + try { + // Try to get DB one more time without timeout + await getDB(); + } catch (e) { + console.warn('Failed to get database after timeout:', e); + // Continue anyway - the app might still work + } + }); + + // Step 2: Clean up service workers and caches + progress = { + step: 'Cleaning up service workers...', + progress: 10, + total: 8, + current: 2 + }; + await cleanupServiceWorkers(); - // Step 2: Prewarm caches + // Step 3: Prewarm caches await prewarmCaches((prog) => { progress = { ...prog, - progress: 10 + Math.round((prog.progress * 80) / 100) // Map 0-100 to 10-90 + progress: 15 + Math.round((prog.progress * 75) / 100) // Map 0-100 to 15-90 }; }); - // Step 3: Mark version as updated + // Step 4: Mark version as updated progress = { step: 'Finalizing update...', progress: 95, - total: 7, - current: 7 + total: 8, + current: 8 }; await markVersionUpdated(); - // Step 4: Complete + // Step 5: Complete progress = { step: 'Update complete!', progress: 100, - total: 7, - current: 7 + total: 8, + current: 8 }; // Small delay to show completion @@ -103,6 +129,61 @@ } } + /** + * Clean up service workers and caches to prevent corrupted content errors + */ + async function cleanupServiceWorkers(): Promise { + if (typeof window === 'undefined' || !('serviceWorker' in navigator)) { + return; + } + + try { + // Get all service worker registrations + const registrations = await navigator.serviceWorker.getRegistrations(); + + // Unregister all service workers + await Promise.all( + registrations.map(async (registration) => { + try { + // Clear all caches for this registration + if (registration.active) { + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames.map(cacheName => caches.delete(cacheName)) + ); + } + + // Unregister the service worker + const unregistered = await registration.unregister(); + if (unregistered) { + console.log('Service worker unregistered successfully'); + } + } catch (error) { + console.warn('Failed to unregister service worker:', error); + // Continue with other registrations even if one fails + } + }) + ); + + // Also try to clear all caches (in case some weren't associated with registrations) + try { + const cacheNames = await caches.keys(); + await Promise.all( + cacheNames.map(cacheName => caches.delete(cacheName)) + ); + if (cacheNames.length > 0) { + console.log(`Cleared ${cacheNames.length} cache(s)`); + } + } catch (error) { + console.warn('Failed to clear some caches:', error); + // Non-critical, continue + } + } catch (error) { + console.warn('Service worker cleanup encountered errors (non-critical):', error); + // Non-critical - continue with update even if cleanup fails + } + } + async function handlePWAUpdate() { try { // Check for service worker update @@ -181,7 +262,10 @@ Update Now {#if isLoggedIn && profileEvent} {/if} {#if isOwnProfile} {/if} {#if isLoggedIn && !isOwnProfile} {/if} @@ -446,7 +446,7 @@ + + + + +{/if} diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index c93cbb7..6e7ab64 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -25,6 +25,7 @@ 'Media attachments rendering in all feeds and views', 'NIP-92/NIP-94 image tags support', 'Blossom support for media attachments', + 'OP-only view added to feed', ], '0.3.0': [ 'Version history modal added to event menu', diff --git a/src/routes/discussions/+page.svelte b/src/routes/discussions/+page.svelte index 2b1dd12..c9ff0ba 100644 --- a/src/routes/discussions/+page.svelte +++ b/src/routes/discussions/+page.svelte @@ -11,6 +11,7 @@ import { page } from '$app/stores'; import Pagination from '../../lib/components/ui/Pagination.svelte'; import { getPaginatedItems, getCurrentPage, ITEMS_PER_PAGE } from '../../lib/utils/pagination.js'; + import Icon from '../../lib/components/ui/Icon.svelte'; let filterResult = $state<{ type: 'event' | 'pubkey' | 'text' | null; value: string | null }>({ type: null, value: null }); let searchResults = $state<{ events: NostrEvent[]; profiles: string[] }>({ events: [], profiles: [] }); @@ -74,7 +75,8 @@ {#if discussionListComponent && !searchResults.events.length && !searchResults.profiles.length}
- Write + + Write