From 46030ef1fe48f12a9110f1242a93769c04e8208f Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Fri, 26 Jun 2026 16:56:34 +0300 Subject: [PATCH 1/3] feat: add parallel covers loading --- src/domain/noteList.service.ts | 45 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/src/domain/noteList.service.ts b/src/domain/noteList.service.ts index 87367b41..bb9827cb 100644 --- a/src/domain/noteList.service.ts +++ b/src/domain/noteList.service.ts @@ -30,43 +30,38 @@ export default class NoteListService { const noteList = await this.repository.getNoteList(page, onlyCreatedByUser); /** - * Note list with valid image urls in cover + * Load cover image for a single note in parallel + * @param note - Note entity + * @returns Note with cover blob URL (or null cover on error) */ - const parsedNoteList: NoteList = { - items: [], - }; - - for (const note of noteList.items) { - /** - * If note has no cover, we have no need to load it - */ + const loadCover = async (note: NoteList['items'][number]): Promise => { if (note.cover === null) { - parsedNoteList.items.push(note); - continue; + return note; } - /** - * Cover object url for passing to the element - */ - let objUrl: string | null = null; - try { const imageData = await this.noteAttachmentRepository.load(note.id, note.cover); - /** - * Make url from blob data - */ // eslint-disable-next-line n/no-unsupported-features/node-builtins - objUrl = URL.createObjectURL(imageData); + const objUrl = URL.createObjectURL(imageData); + + return { + ...note, + cover: objUrl, + }; } catch { console.log('Error while loading cover for note ', note.id); + + return note; } + }; - parsedNoteList.items.push({ - ...note, - cover: objUrl, - }); - } + /** + * Load all cover images in parallel + */ + const parsedNoteList: NoteList = { + items: await Promise.all(noteList.items.map(loadCover)), + }; return parsedNoteList; } From c359154d13352b217cdb9a0f412fea965f283e25 Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Fri, 26 Jun 2026 20:14:40 +0300 Subject: [PATCH 2/3] feat: add reactive covers update --- src/application/services/useNoteList.ts | 49 +++++++++++++++++++++- src/domain/noteList.service.ts | 54 +++++++++---------------- 2 files changed, 66 insertions(+), 37 deletions(-) diff --git a/src/application/services/useNoteList.ts b/src/application/services/useNoteList.ts index 1d29b6ea..5b96a03d 100644 --- a/src/application/services/useNoteList.ts +++ b/src/application/services/useNoteList.ts @@ -69,7 +69,7 @@ export default function (onlyCreatedByUser = false): UseNoteListComposableState const isLoading = ref(false); /** - * Get note list + * Get note list (metadata only, covers are not downloaded) * @param page - number of pages */ const load = async (page: number): Promise => { @@ -82,6 +82,47 @@ export default function (onlyCreatedByUser = false): UseNoteListComposableState return list; }; + /** + * Load cover images for all notes in the list in the background + * Updates each note's cover reactively as it arrives + */ + const loadCovers = async (): Promise => { + if (isEmpty(noteList.value)) { + return; + } + + const list = noteList.value; + const items = list.items; + + await Promise.all(items.map(async (item, index) => { + /** + * If cover is null, the note has no cover image + */ + if (item.cover === null) { + return; + } + + /** + * If cover is already a blob URL, it was already loaded + */ + if (item.cover.startsWith('blob:')) { + return; + } + + const url = await noteListService.loadCover(item.id, item.cover); + + if (url !== null) { + /** + * Update the specific note's cover reactively so the card renders the image + */ + list.items[index] = { + ...item, + cover: url, + }; + } + })); + }; + /** * Load next page of the notes */ @@ -102,6 +143,12 @@ export default function (onlyCreatedByUser = false): UseNoteListComposableState } else { noteList.value = loadedNotes; } + + /** + * Kick off cover downloads in the background + * List is already rendered, covers will appear one by one as they load + */ + loadCovers().catch(console.error); }; /** diff --git a/src/domain/noteList.service.ts b/src/domain/noteList.service.ts index bb9827cb..941161c0 100644 --- a/src/domain/noteList.service.ts +++ b/src/domain/noteList.service.ts @@ -20,49 +20,31 @@ export default class NoteListService { } /** - * Returns note list - * @todo - move loading images data logic to separate service for optimization + * Returns note list with metadata only (covers are not downloaded) * @param page - number of current pages * @param onlyCreatedByUser - if true, returns notes created by the user * @returns list of notes */ public async getNoteList(page: number, onlyCreatedByUser = false): Promise { - const noteList = await this.repository.getNoteList(page, onlyCreatedByUser); - - /** - * Load cover image for a single note in parallel - * @param note - Note entity - * @returns Note with cover blob URL (or null cover on error) - */ - const loadCover = async (note: NoteList['items'][number]): Promise => { - if (note.cover === null) { - return note; - } - - try { - const imageData = await this.noteAttachmentRepository.load(note.id, note.cover); - - // eslint-disable-next-line n/no-unsupported-features/node-builtins - const objUrl = URL.createObjectURL(imageData); - - return { - ...note, - cover: objUrl, - }; - } catch { - console.log('Error while loading cover for note ', note.id); + return await this.repository.getNoteList(page, onlyCreatedByUser); + } - return note; - } - }; + /** + * Load cover image for a single note and return its blob URL + * @param noteId - Note identifier + * @param coverKey - Cover file key on server + * @returns Blob URL for the cover image, or null on error + */ + public async loadCover(noteId: string, coverKey: string): Promise { + try { + const imageData = await this.noteAttachmentRepository.load(noteId, coverKey); - /** - * Load all cover images in parallel - */ - const parsedNoteList: NoteList = { - items: await Promise.all(noteList.items.map(loadCover)), - }; + // eslint-disable-next-line n/no-unsupported-features/node-builtins + return URL.createObjectURL(imageData); + } catch { + console.log('Error while loading cover for note ', noteId); - return parsedNoteList; + return null; + } } } From 5022ddf72d1423d6aaa8376d561c080c48f6f44b Mon Sep 17 00:00:00 2001 From: 7eliassen Date: Wed, 8 Jul 2026 16:59:31 +0300 Subject: [PATCH 3/3] fix: prevent extra requests --- src/presentation/components/note-list/NoteList.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/components/note-list/NoteList.vue b/src/presentation/components/note-list/NoteList.vue index e1574f92..d51ac81e 100644 --- a/src/presentation/components/note-list/NoteList.vue +++ b/src/presentation/components/note-list/NoteList.vue @@ -15,7 +15,7 @@