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 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