fix: removed unneeded linkage for media from photo album pages
This commit is contained in:
@@ -242,77 +242,34 @@ const FULL_MONTH_NAMES = [
|
||||
];
|
||||
|
||||
// Track photo_archive hydration state to prevent duplicate runs
|
||||
const photoArchiveHydratingCache = new Map<string, boolean>();
|
||||
|
||||
/**
|
||||
* 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<string> {
|
||||
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<string>): void {
|
||||
try {
|
||||
localStorage.setItem(getPhotoArchiveLinkedKey(postId), JSON.stringify([...ids]));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
const photoArchiveHydratingCache = new WeakSet<HTMLElement>();
|
||||
|
||||
/**
|
||||
* 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<Element>
|
||||
) => {
|
||||
// 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<string>();
|
||||
// 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<string, ImageData[]>();
|
||||
|
||||
// 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<PostEditorProps> = ({ 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<PostEditorProps> = ({ postId }) => {
|
||||
<div className="preview-hydrating-overlay" ref={hydrationOverlayRef} style={{ display: 'none' }}>
|
||||
<div className="preview-hydrating-content">
|
||||
<div className="preview-hydrating-spinner" />
|
||||
<span>Linking images to post...</span>
|
||||
<span>Loading photo archive...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*
|
||||
* Creates a photo gallery organized by year and month.
|
||||
* Images are discovered dynamically from the media library based on their creation date.
|
||||
* When rendered, images are automatically linked to the post.
|
||||
*
|
||||
* Usage:
|
||||
* [[photo_archive]] - Newest 10 months with images (month + year label)
|
||||
@@ -37,7 +36,7 @@ function getMonthName(month: number): string {
|
||||
|
||||
const photoArchiveMacro: MacroDefinition = {
|
||||
name: 'photo_archive',
|
||||
description: 'Creates a photo archive gallery organized by year and month, automatically linking discovered images to the post',
|
||||
description: 'Creates a photo archive gallery organized by year and month',
|
||||
|
||||
validate(params: MacroParams): string | undefined {
|
||||
// Year is optional - if not provided, shows recent 10 months
|
||||
@@ -76,7 +75,7 @@ const photoArchiveMacro: MacroDefinition = {
|
||||
return `📅 Photo Archive: ${year}`;
|
||||
},
|
||||
|
||||
render(params: MacroParams, context: MacroRenderContext): string {
|
||||
render(params: MacroParams, _context: MacroRenderContext): string {
|
||||
const { year, month } = params;
|
||||
|
||||
// Build data attributes for hydration
|
||||
@@ -92,10 +91,6 @@ const photoArchiveMacro: MacroDefinition = {
|
||||
}
|
||||
}
|
||||
|
||||
if (context.postId) {
|
||||
dataAttrs.push(`data-post-id="${context.postId}"`);
|
||||
}
|
||||
|
||||
// CSS classes
|
||||
const classes = ['macro-photo-archive'];
|
||||
if (!year) {
|
||||
|
||||
@@ -261,6 +261,20 @@ describe('photo_archive hydration', () => {
|
||||
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)', () => {
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user