From 52aed4c4201907a71c22f61df17eff6b48af586f Mon Sep 17 00:00:00 2001 From: hugo Date: Mon, 16 Feb 2026 06:03:56 +0100 Subject: [PATCH] fix: removed unneeded linkage for media from photo album pages --- src/renderer/components/Editor/Editor.tsx | 116 ++---------------- .../macros/definitions/photo_archive.ts | 9 +- .../components/PhotoArchiveHydration.test.ts | 14 +++ tests/renderer/macros/photo_archive.test.ts | 8 +- 4 files changed, 33 insertions(+), 114 deletions(-) diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index c9b1891..0c3aa1a 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -242,77 +242,34 @@ const FULL_MONTH_NAMES = [ ]; // Track photo_archive hydration state to prevent duplicate runs -const photoArchiveHydratingCache = new Map(); - -/** - * Get the storage key for photo_archive linked media IDs for a post - */ -function getPhotoArchiveLinkedKey(postId: string): string { - return `photoArchive:${postId}:linkedIds`; -} - -/** - * Load previously linked media IDs from localStorage - */ -function loadPreviouslyLinkedIds(postId: string): Set { - try { - const stored = localStorage.getItem(getPhotoArchiveLinkedKey(postId)); - if (stored) { - const ids = JSON.parse(stored) as string[]; - return new Set(ids); - } - } catch { - // Ignore parse errors - } - return new Set(); -} - -/** - * Save currently linked media IDs to localStorage - */ -function saveLinkedIds(postId: string, ids: Set): void { - try { - localStorage.setItem(getPhotoArchiveLinkedKey(postId), JSON.stringify([...ids])); - } catch { - // Ignore storage errors - } -} +const photoArchiveHydratingCache = new WeakSet(); /** * Hydrate photo_archive elements in the preview with actual media from the given year/month. - * Also manages linking/unlinking of media based on what the macros cover. */ const hydratePhotoArchive = async ( container: HTMLElement, - postId: string, onImageClick: (index: number, images: { src: string; alt: string }[]) => void ) => { // Match both year-based and recent-based archives const archives = container.querySelectorAll('.macro-photo-archive[data-year], .macro-photo-archive[data-recent]'); if (archives.length === 0) { - // No photo_archive macros - unlink any previously linked and clear state - const previouslyLinked = loadPreviouslyLinkedIds(postId); - if (previouslyLinked.size > 0) { - console.log(`[photo_archive] No macros found, batch unlinking ${previouslyLinked.size} previously linked media`); - await window.electronAPI?.postMedia.unlinkMany(postId, Array.from(previouslyLinked)); - localStorage.removeItem(getPhotoArchiveLinkedKey(postId)); - } return; } // Check if we're already hydrating (prevent duplicate runs) - if (photoArchiveHydratingCache.get(postId)) { - console.log(`[photo_archive] Skipping duplicate hydration for ${postId}`); + if (photoArchiveHydratingCache.has(container)) { + console.log('[photo_archive] Skipping duplicate hydration for container'); return; } - photoArchiveHydratingCache.set(postId, true); + photoArchiveHydratingCache.add(container); try { - await doHydratePhotoArchive(container, postId, onImageClick, archives); + await doHydratePhotoArchive(container, onImageClick, archives); } finally { // Clear the hydrating flag after a delay to allow for content changes - setTimeout(() => photoArchiveHydratingCache.delete(postId), 500); + setTimeout(() => photoArchiveHydratingCache.delete(container), 500); } }; @@ -321,15 +278,10 @@ const hydratePhotoArchive = async ( */ const doHydratePhotoArchive = async ( _container: HTMLElement, - postId: string, onImageClick: (index: number, images: { src: string; alt: string }[]) => void, archives: NodeListOf ) => { - // Load previously linked IDs to detect what needs unlinking - const previouslyLinkedIds = loadPreviouslyLinkedIds(postId); - - // Phase 1: Collect all media IDs that should be linked based on current macros - const shouldBeLinkedIds = new Set(); + // Collect media for archive rendering based on current macros. type ImageData = { id: string; originalName: string; alt?: string; mimeType: string; createdAt?: Date }; const archiveData: Array<{ element: Element; @@ -342,7 +294,7 @@ const doHydratePhotoArchive = async ( showYearInLabel?: boolean; }> = []; - console.log(`[photo_archive] Processing ${archives.length} archive macro(s), previously linked: ${previouslyLinkedIds.size} IDs`); + console.log(`[photo_archive] Processing ${archives.length} archive macro(s)`); for (const archive of archives) { const recentStr = archive.getAttribute('data-recent'); @@ -371,21 +323,15 @@ const doHydratePhotoArchive = async ( monthlyMap.set(key, []); } monthlyMap.get(key)!.push(img); - shouldBeLinkedIds.add(img.id); } // Sort by key descending (newest first) and take top N const sortedKeys = Array.from(monthlyMap.keys()).sort().reverse().slice(0, recentCount); const recentMonthlyImages = new Map(); - // Clear shouldBeLinkedIds and only add ones that are in top N months - shouldBeLinkedIds.clear(); for (const key of sortedKeys) { const images = monthlyMap.get(key)!; recentMonthlyImages.set(key, images); - for (const img of images) { - shouldBeLinkedIds.add(img.id); - } } const totalImages = Array.from(recentMonthlyImages.values()).reduce((sum, imgs) => sum + imgs.length, 0); @@ -411,11 +357,6 @@ const doHydratePhotoArchive = async ( }); const images = (mediaItems || []).filter(m => m.mimeType?.startsWith('image/')); - // Add to the set of IDs that should be linked - for (const img of images) { - shouldBeLinkedIds.add(img.id); - } - archiveData.push({ element: archive, mode: 'single-month', year, month, images }); console.log(`[photo_archive] Year ${year} month ${month}: ${images.length} images`); } else { @@ -431,11 +372,6 @@ const doHydratePhotoArchive = async ( if (images.length > 0) { monthlyImages.set(m + 1, images); // Store with 1-based month key - - // Add to the set of IDs that should be linked - for (const img of images) { - shouldBeLinkedIds.add(img.id); - } } } @@ -445,34 +381,8 @@ const doHydratePhotoArchive = async ( } } } - - console.log(`[photo_archive] Should link ${shouldBeLinkedIds.size} media IDs`); - - // Phase 2: Batch unlink media that was previously linked but is no longer needed - const idsToUnlink: string[] = []; - for (const mediaId of previouslyLinkedIds) { - if (!shouldBeLinkedIds.has(mediaId)) { - idsToUnlink.push(mediaId); - } - } - - if (idsToUnlink.length > 0) { - console.log(`[photo_archive] Batch unlinking ${idsToUnlink.length} media items`); - await window.electronAPI?.postMedia.unlinkMany(postId, idsToUnlink); - } - - // Save current linked IDs for next hydration - saveLinkedIds(postId, shouldBeLinkedIds); - - // Phase 3: Batch link all media that should be linked and render - // Use linkMany which internally skips already linked items - const idsToLink = Array.from(shouldBeLinkedIds); - if (idsToLink.length > 0) { - console.log(`[photo_archive] Batch linking ${idsToLink.length} media items`); - await window.electronAPI?.postMedia.linkMany(postId, idsToLink); - } - - // Phase 4: Render galleries (no more link/unlink calls here) + + // Render galleries for (const { element, mode, year, month, images, monthlyImages, showYearInLabel } of archiveData) { const archiveContainer = element.querySelector('.photo-archive-container'); if (!archiveContainer) continue; @@ -532,7 +442,7 @@ const doHydratePhotoArchive = async ( } } - console.log(`[photo_archive] Hydration complete. ${shouldBeLinkedIds.size} images should be linked.`); + console.log('[photo_archive] Hydration complete.'); }; /** @@ -763,7 +673,7 @@ const PostEditor: React.FC = ({ postId }) => { try { await hydrateGalleries(previewRef.current, postId, lightboxHandler); if (!cancelled) { - await hydratePhotoArchive(previewRef.current, postId, lightboxHandler); + await hydratePhotoArchive(previewRef.current, lightboxHandler); } } finally { // Always reset hydration state when complete - the ref is global to the component @@ -1466,7 +1376,7 @@ const PostEditor: React.FC = ({ postId }) => {
- Linking images to post... + Loading photo archive...
{ expect(monthKeys[0]).toBe('2020-06'); expect(monthKeys[monthKeys.length - 1]).toBe('2019-09'); }); + + it('should not require post-media linking side effects during preview hydration', async () => { + const linkMany = vi.fn(); + const unlinkMany = vi.fn(); + + const result = await hydratePhotoArchive( + { recent: '10' }, + mockMediaFilter + ); + + expect(result.mode).toBe('recent'); + expect(linkMany).not.toHaveBeenCalled(); + expect(unlinkMany).not.toHaveBeenCalled(); + }); }); describe('year mode (year parameter only)', () => { diff --git a/tests/renderer/macros/photo_archive.test.ts b/tests/renderer/macros/photo_archive.test.ts index 1bded2c..7dc1ce7 100644 --- a/tests/renderer/macros/photo_archive.test.ts +++ b/tests/renderer/macros/photo_archive.test.ts @@ -129,13 +129,13 @@ describe('photo_archive macro', () => { expect(html).not.toContain('data-month='); }); - it('should include post-id data attribute when available', () => { + it('should not include post-id data attribute when available', () => { const macro = getMacro('photo_archive'); const context: MacroRenderContext = { isPreview: true, postId: 'post-123' }; const html = macro!.render({ year: '2024' }, context); - expect(html).toContain('data-post-id="post-123"'); + expect(html).not.toContain('data-post-id="post-123"'); }); it('should include loading placeholder', () => { @@ -213,13 +213,13 @@ describe('photo_archive macro', () => { expect(preview).toBe('📅 Photo Archive: Recent'); }); - it('should include post-id data attribute in recent mode', () => { + it('should not include post-id data attribute in recent mode', () => { const macro = getMacro('photo_archive'); const context: MacroRenderContext = { isPreview: true, postId: 'post-123' }; const html = macro!.render({}, context); - expect(html).toContain('data-post-id="post-123"'); + expect(html).not.toContain('data-post-id="post-123"'); }); });