From 2deb0c4a5ce65e70485c36be7b7fb2b4d8d303ae Mon Sep 17 00:00:00 2001 From: rogerantony-dev Date: Fri, 17 Apr 2026 18:56:13 +0530 Subject: [PATCH 1/6] feat(cards): collapse bookmark image placeholder text to two states Replaces the four-way status text ("Fetching data...", "Taking screenshot....", "Cannot fetch image for this bookmark") with a simple binary: while the image is on its way (optimistic insert, server pipeline running, or preload crossfade) show "Getting screenshot"; once the pipeline is done with no image, render no text at all. The loading gif stays to indicate the placeholder state visually. --- .../cardSection/animatedBookmarkImage.tsx | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx b/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx index a75c599c5..52ac1d6ef 100644 --- a/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx +++ b/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx @@ -192,20 +192,11 @@ export const LoaderImgPlaceholder = ({ cardTypeCondition === viewValues.list, }); - const statusText = (() => { - // Image is being preloaded by AnimatedBookmarkImage — keep showing "Fetching data..." - // so the text doesn't flash to "Cannot fetch image" during the preload window - if (isPreloading) { - return "Fetching data..."; - } - if (isLoading) { - return "Taking screenshot...."; - } - if (id < 0) { - return "Fetching data..."; - } - return "Cannot fetch image for this bookmark"; - })(); + // Two states only: + // - Image is on its way (optimistic insert, server pipeline running, or + // preloading crossfade): "Getting screenshot" + // - Pipeline done and still no image (terminal failure): no text + const statusText = isPreloading || isLoading || id < 0 ? "Getting screenshot" : null; return (
@@ -215,7 +206,7 @@ export const LoaderImgPlaceholder = ({ loader={(source) => source.src} src={loaderGif} /> - {!(cardTypeCondition === viewValues.list) && ( + {statusText && cardTypeCondition !== viewValues.list && ( Date: Sat, 18 Apr 2026 01:12:05 +0530 Subject: [PATCH 2/6] refactor(realtime): drive card loading state from subscription manager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the Zustand loadingBookmarkIds set in favor of the Realtime subscription manager as the single source of truth for "is this bookmark still being enriched?". The store was a parallel signal that had to be manually kept in sync with the subscription lifecycle — now the card reads the same state the manager already tracks. Changes: - Manager exposes subscribeToBookmarkEnrichmentChanges + isBookmarkEnrichmentActive; notifyListeners fires on open, teardown, queue push. - useBookmarkEnrichmentActive(id) hook wraps useSyncExternalStore. - Add-bookmark mutation opens the subscription in onSuccess (right after the temp → real ID swap) instead of waiting for the media-type checks. Closes the ~200-1000ms gap where the card showed no loading state while checkIfUrlAnImage + getMediaType resolved. - IIFE tears down via new "not_applicable" reason on image/audio paths once the media check confirms no screenshot pipeline will run. - PDF path keeps subscription alive through handlePdfThumbnailAndUpload; terminal condition now covers PDF (ogImage set when meta_data.mediaType is application/pdf). Only tears down if both thumbnail retries fail. - Screenshot mutation drops Zustand writes; still tears down on error. - componentStore loses loadingBookmarkIds, addLoadingBookmarkId, removeLoadingBookmarkId; LoadersStoreState interface trimmed to match. --- ...d-bookmark-min-data-optimistic-mutation.ts | 45 ++++++++++++------ .../use-add-bookmark-screenshot-mutation.ts | 11 +---- src/hooks/use-bookmark-enrichment-active.ts | 22 +++++++++ .../bookmark-enrichment-subscription.ts | 46 +++++++++++++++++-- .../realtime/bookmark-realtime-payload.ts | 22 ++++++--- .../cardSection/animatedBookmarkImage.tsx | 4 +- .../dashboard/cardSection/imageCard.tsx | 4 +- src/store/componentStore.ts | 14 ------ src/types/componentStoreTypes.ts | 3 -- 9 files changed, 116 insertions(+), 55 deletions(-) create mode 100644 src/hooks/use-bookmark-enrichment-active.ts diff --git a/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts b/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts index 8aa24e880..dc91fc49c 100644 --- a/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts +++ b/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts @@ -8,7 +8,10 @@ import type { } from "../../../types/apiTypes"; import { api } from "@/lib/api-helpers/api-v2"; -import { openBookmarkEnrichmentSubscription } from "@/lib/supabase/realtime/bookmark-enrichment-subscription"; +import { + openBookmarkEnrichmentSubscription, + teardownBookmarkEnrichmentSubscription, +} from "@/lib/supabase/realtime/bookmark-enrichment-subscription"; import useGetCurrentCategoryId from "../../../hooks/useGetCurrentCategoryId"; import useGetSortBy from "../../../hooks/useGetSortBy"; @@ -44,7 +47,7 @@ export default function useAddBookmarkMinDataOptimisticMutation() { // We'll initialize the mutation with a default value and update it when we have the actual ID const { addBookmarkScreenshotMutation } = useAddBookmarkScreenshotMutation(); const { sortBy } = useGetSortBy(); - const { addLoadingBookmarkId, removeLoadingBookmarkId, setIsBookmarkAdding } = useLoadersStore(); + const { setIsBookmarkAdding } = useLoadersStore(); const addBookmarkMinDataOptimisticMutation = useMutation< SingleListData[], @@ -191,22 +194,30 @@ export default function useAddBookmarkMinDataOptimisticMutation() { const url = data?.url; // Heavy processing (media check, PDF thumbnail, screenshot) runs as - // fire-and-forget so it doesn't block the render cycle + // fire-and-forget so it doesn't block the render cycle. + // + // The enrichment subscription was opened in onSuccess (right after the + // temp → real ID swap) so the loading state is live for the full + // pipeline. Here we tear it down on paths that don't feed the + // screenshot/enrichment flow (plain image / audio URLs) and leave it + // alive for PDF (DB write happens inside handlePdfThumbnailAndUpload) + // and the regular screenshot path. void (async () => { const isUrlOfMimeType = await checkIfUrlAnImage(url); if (isUrlOfMimeType) { + void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); return; } const mediaType = await getMediaType(url); // Audio URLs already have ogImage fallback set in add-bookmark-min-data if (mediaType?.includes("audio")) { + void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); return; } if (mediaType === PDF_MIME_TYPE || URL_PDF_CHECK_PATTERN.test(url)) { try { - addLoadingBookmarkId(data.id); successToast("Generating thumbnail"); await handlePdfThumbnailAndUpload({ fileId: data.id, @@ -224,26 +235,16 @@ export default function useAddBookmarkMinDataOptimisticMutation() { } catch (retryError) { console.error("PDF thumbnail upload failed after retry:", retryError); errorToast("thumbnail generation failed"); + void teardownBookmarkEnrichmentSubscription(data.id, "screenshot_failed"); } } finally { void queryClient.invalidateQueries({ queryKey: [BOOKMARKS_KEY, session?.user?.id], }); - removeLoadingBookmarkId(data.id); } return; } - if (data?.id) { - addLoadingBookmarkId(data.id); - if (session?.user?.id) { - openBookmarkEnrichmentSubscription({ - bookmarkId: data.id, - queryClient, - userId: session.user.id, - }); - } - } addBookmarkScreenshotMutation.mutate({ id: data.id, url: data.url }); })(); }, @@ -281,6 +282,20 @@ export default function useAddBookmarkMinDataOptimisticMutation() { } as PaginatedBookmarks; }, ); + + // Open the enrichment subscription as soon as the real id is known. + // The onSettled IIFE tears it down on paths that don't feed the + // pipeline (image/audio URLs) once the async media type check + // resolves. Opening here (instead of after the media check) closes + // the ~200–1000ms gap where the card would otherwise show no + // loading indicator while we probe the URL's content type. + if (session?.user?.id) { + openBookmarkEnrichmentSubscription({ + bookmarkId: serverBookmark.id, + queryClient, + userId: session.user.id, + }); + } } if ( diff --git a/src/async/mutationHooks/bookmarks/use-add-bookmark-screenshot-mutation.ts b/src/async/mutationHooks/bookmarks/use-add-bookmark-screenshot-mutation.ts index 464f29a27..203c23773 100644 --- a/src/async/mutationHooks/bookmarks/use-add-bookmark-screenshot-mutation.ts +++ b/src/async/mutationHooks/bookmarks/use-add-bookmark-screenshot-mutation.ts @@ -6,7 +6,7 @@ import useGetCurrentCategoryId from "../../../hooks/useGetCurrentCategoryId"; import useGetSortBy from "../../../hooks/useGetSortBy"; import { api } from "../../../lib/api-helpers/api-v2"; import { teardownBookmarkEnrichmentSubscription } from "../../../lib/supabase/realtime/bookmark-enrichment-subscription"; -import { useLoadersStore, useSupabaseSession } from "../../../store/componentStore"; +import { useSupabaseSession } from "../../../store/componentStore"; import { BOOKMARKS_KEY, V2_ADD_URL_SCREENSHOT_API } from "../../../utils/constants"; import { errorToast } from "../../../utils/toastMessages"; @@ -17,8 +17,6 @@ export default function useAddBookmarkScreenshotMutation() { const { category_id: CATEGORY_ID } = useGetCurrentCategoryId(); const session = useSupabaseSession((state) => state.session); const { sortBy } = useGetSortBy(); - const { removeLoadingBookmarkId } = useLoadersStore(); - const addBookmarkScreenshotMutation = useMutation({ mutationFn: (payload: AddBookmarkScreenshotPayloadTypes) => // ky default timeout is 10s, but the server-side screenshot capture has a @@ -31,15 +29,10 @@ export default function useAddBookmarkScreenshotMutation() { onError: (error, variables) => { errorToast(`Screenshot error: ${error.message}`); if (variables.id) { - removeLoadingBookmarkId(variables.id); void teardownBookmarkEnrichmentSubscription(variables.id, "screenshot_failed"); } }, - onSettled: (response) => { - if (response?.[0]?.id) { - removeLoadingBookmarkId(response[0].id); - } - + onSettled: () => { void queryClient.invalidateQueries({ queryKey: [BOOKMARKS_KEY, session?.user?.id, CATEGORY_ID, sortBy], }); diff --git a/src/hooks/use-bookmark-enrichment-active.ts b/src/hooks/use-bookmark-enrichment-active.ts new file mode 100644 index 000000000..501e8372f --- /dev/null +++ b/src/hooks/use-bookmark-enrichment-active.ts @@ -0,0 +1,22 @@ +import { useCallback, useSyncExternalStore } from "react"; + +import { + isBookmarkEnrichmentActive, + subscribeToBookmarkEnrichmentChanges, +} from "@/lib/supabase/realtime/bookmark-enrichment-subscription"; + +/** + * Reactive read of the subscription manager's "is this bookmark still being + * enriched?" state. Used by the card placeholder to show a "Getting + * screenshot" label while the server pipeline (screenshot + AI enrichment or + * PDF thumbnail) is in flight. + * + * Returns `true` while a Realtime subscription is active or queued for the + * given bookmark id; `false` once the subscription tears down (terminal + * state, timeout, delete, sign-out, or explicit teardown on non-enriching + * media paths). + */ +export function useBookmarkEnrichmentActive(bookmarkId: number): boolean { + const getSnapshot = useCallback(() => isBookmarkEnrichmentActive(bookmarkId), [bookmarkId]); + return useSyncExternalStore(subscribeToBookmarkEnrichmentChanges, getSnapshot, getSnapshot); +} diff --git a/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts b/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts index b7dbe3f6f..e2cfacc3e 100644 --- a/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts +++ b/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts @@ -12,6 +12,7 @@ type TeardownReason = | "auth_error" | "channel_error" | "delete_event" + | "not_applicable" | "screenshot_failed" | "terminal" | "timeout"; @@ -38,6 +39,38 @@ const SENTRY_OPERATION = "realtime_bookmark_subscribe"; const active = new Map(); const waiting: OpenArgs[] = []; +const listeners = new Set<() => void>(); + +function notifyListeners(): void { + for (const listener of listeners) { + listener(); + } +} + +/** + * Subscribe to changes in the manager's active-subscription state. Called + * whenever a subscription is opened, torn down, or promoted from the waiting + * queue. Returns an unsubscribe function. Used by `useBookmarkEnrichmentActive` + * via React's `useSyncExternalStore`. + */ +export function subscribeToBookmarkEnrichmentChanges(listener: () => void): () => void { + listeners.add(listener); + return () => { + listeners.delete(listener); + }; +} + +/** + * Reads whether an enrichment subscription is currently alive for a given + * bookmark id (either active and not torn-down, or queued waiting for a slot). + * Used as the "is this bookmark still loading?" signal in card UI. + */ +export function isBookmarkEnrichmentActive(bookmarkId: number): boolean { + if (isSubscriptionAlive(bookmarkId)) { + return true; + } + return waiting.some((queued) => queued.bookmarkId === bookmarkId); +} function breadcrumb(message: string, bookmarkId: number, extra?: Record): void { Sentry.addBreadcrumb({ @@ -50,10 +83,12 @@ function breadcrumb(message: string, bookmarkId: number, extra?: Record { - const isLoading = useLoadersStore((s) => s.loadingBookmarkIds.has(id)); + const isLoading = useBookmarkEnrichmentActive(id); const loaderClassName = cn({ "flex aspect-[1.8] w-full flex-col items-center justify-center gap-2 rounded-lg bg-gray-100 text-center duration-150 group-hover:rounded-b-none": diff --git a/src/pageComponents/dashboard/cardSection/imageCard.tsx b/src/pageComponents/dashboard/cardSection/imageCard.tsx index 970147903..7041e9947 100644 --- a/src/pageComponents/dashboard/cardSection/imageCard.tsx +++ b/src/pageComponents/dashboard/cardSection/imageCard.tsx @@ -13,7 +13,7 @@ import type { BookmarkImageProps } from "./animatedBookmarkImage"; import { cn } from "@/utils/tailwind-merge"; -import { useLoadersStore } from "../../../store/componentStore"; +import { useBookmarkEnrichmentActive } from "../../../hooks/use-bookmark-enrichment-active"; import { viewValues } from "../../../utils/constants"; import { AnimatedBookmarkImage, @@ -62,7 +62,7 @@ const ImgLogicComponent = ({ cardTypeCondition === viewValues.card || cardTypeCondition === viewValues.moodboard, }); - const isLoading = useLoadersStore((s) => s.loadingBookmarkIds.has(id)); + const isLoading = useBookmarkEnrichmentActive(id); const [errorImg, setErrorImg] = useState(null); if (!hasCoverImg) { diff --git a/src/store/componentStore.ts b/src/store/componentStore.ts index 7e6811ff8..0655d6622 100644 --- a/src/store/componentStore.ts +++ b/src/store/componentStore.ts @@ -19,24 +19,10 @@ export const useModalStore = create((set) => ({ })); export const useLoadersStore = create((set) => ({ - addLoadingBookmarkId: (id: number) => { - set((state) => { - const newSet = new Set([...state.loadingBookmarkIds, id]); - return { loadingBookmarkIds: newSet }; - }); - }, isBookmarkAdding: false, isSearchLoading: false, // this is not handelled by react-query as this is a combination for 2 queries isSortByLoading: false, - loadingBookmarkIds: new Set(), - removeLoadingBookmarkId: (id: number) => { - set((state) => { - const newSet = new Set(state.loadingBookmarkIds); - newSet.delete(id); - return { loadingBookmarkIds: newSet }; - }); - }, setIsBookmarkAdding: (value: boolean) => { set(() => ({ isBookmarkAdding: value, diff --git a/src/types/componentStoreTypes.ts b/src/types/componentStoreTypes.ts index ea5a17208..331f4c587 100644 --- a/src/types/componentStoreTypes.ts +++ b/src/types/componentStoreTypes.ts @@ -7,12 +7,9 @@ export interface ModalStoreState { } export interface LoadersStoreState { - addLoadingBookmarkId: (id: number) => void; isBookmarkAdding: boolean; isSearchLoading: boolean; isSortByLoading: boolean; - loadingBookmarkIds: Set; - removeLoadingBookmarkId: (id: number) => void; setIsBookmarkAdding: (value: boolean) => void; toggleIsSearchLoading: (value: boolean) => void; toggleIsSortByLoading: () => void; From 583172d6227f38da206984142a1ce491e798babb Mon Sep 17 00:00:00 2001 From: rogerantony-dev Date: Mon, 20 Apr 2026 16:27:39 +0530 Subject: [PATCH 3/6] fix(realtime): notify listeners on waiting-queue teardown paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `useSyncExternalStore` consumers (card loading indicator) went stale when a bookmark id was dropped from `waiting` without passing through `teardown()`'s notify call: - Teardown of a queued-but-never-promoted id (rapid burst >5 adds, then non-enriching media type resolves) spliced the entry silently. Card stuck on "Getting screenshot" — queued ids don't hit the 90s timeout either, that only fires for active records. - `teardownAllBookmarkEnrichmentSubscriptions` cleared `waiting` in place on sign-out without notifying. --- .../supabase/realtime/bookmark-enrichment-subscription.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts b/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts index e47617f76..3246934ed 100644 --- a/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts +++ b/src/lib/supabase/realtime/bookmark-enrichment-subscription.ts @@ -267,6 +267,7 @@ async function teardown(bookmarkId: number, reason: TeardownReason): Promise { const ids = [...active.keys()]; + const hadWaiting = waiting.length > 0; waiting.length = 0; + if (hadWaiting) { + notifyListeners(); + } await Promise.all(ids.map((id) => teardown(id, reason))); } From b91fe4c3bc5e9c2a7cd4835544ab971e236d29ee Mon Sep 17 00:00:00 2001 From: rogerantony-dev Date: Mon, 20 Apr 2026 16:28:46 +0530 Subject: [PATCH 4/6] fix(add-bookmark): guarantee subscription teardown on pipeline throw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onSettled IIFE opened the subscription in onSuccess then did the media-type probe + PDF path + screenshot mutation. Any unexpected throw (network error in checkIfUrlAnImage/getMediaType, toast lib failure, React Query internal throw) was swallowed by `void`, leaving the subscription dangling until the 90s timeout — card stuck on "Getting screenshot" with no actual pipeline running. Wrap the body in try/catch with a "screenshot_failed" teardown so the loading indicator always clears on unexpected failures. --- ...d-bookmark-min-data-optimistic-mutation.ts | 73 +++++++++++-------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts b/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts index dc91fc49c..29424da16 100644 --- a/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts +++ b/src/async/mutationHooks/bookmarks/use-add-bookmark-min-data-optimistic-mutation.ts @@ -203,49 +203,58 @@ export default function useAddBookmarkMinDataOptimisticMutation() { // alive for PDF (DB write happens inside handlePdfThumbnailAndUpload) // and the regular screenshot path. void (async () => { - const isUrlOfMimeType = await checkIfUrlAnImage(url); - if (isUrlOfMimeType) { - void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); - return; - } + try { + const isUrlOfMimeType = await checkIfUrlAnImage(url); + if (isUrlOfMimeType) { + void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); + return; + } - const mediaType = await getMediaType(url); - // Audio URLs already have ogImage fallback set in add-bookmark-min-data - if (mediaType?.includes("audio")) { - void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); - return; - } + const mediaType = await getMediaType(url); + // Audio URLs already have ogImage fallback set in add-bookmark-min-data + if (mediaType?.includes("audio")) { + void teardownBookmarkEnrichmentSubscription(data.id, "not_applicable"); + return; + } - if (mediaType === PDF_MIME_TYPE || URL_PDF_CHECK_PATTERN.test(url)) { - try { - successToast("Generating thumbnail"); - await handlePdfThumbnailAndUpload({ - fileId: data.id, - fileUrl: data.url, - sessionUserId: session?.user?.id, - }); - } catch { + if (mediaType === PDF_MIME_TYPE || URL_PDF_CHECK_PATTERN.test(url)) { try { - errorToast("retry thumbnail generation"); + successToast("Generating thumbnail"); await handlePdfThumbnailAndUpload({ fileId: data.id, fileUrl: data.url, sessionUserId: session?.user?.id, }); - } catch (retryError) { - console.error("PDF thumbnail upload failed after retry:", retryError); - errorToast("thumbnail generation failed"); - void teardownBookmarkEnrichmentSubscription(data.id, "screenshot_failed"); + } catch { + try { + errorToast("retry thumbnail generation"); + await handlePdfThumbnailAndUpload({ + fileId: data.id, + fileUrl: data.url, + sessionUserId: session?.user?.id, + }); + } catch (retryError) { + console.error("PDF thumbnail upload failed after retry:", retryError); + errorToast("thumbnail generation failed"); + void teardownBookmarkEnrichmentSubscription(data.id, "screenshot_failed"); + } + } finally { + void queryClient.invalidateQueries({ + queryKey: [BOOKMARKS_KEY, session?.user?.id], + }); } - } finally { - void queryClient.invalidateQueries({ - queryKey: [BOOKMARKS_KEY, session?.user?.id], - }); + return; } - return; - } - addBookmarkScreenshotMutation.mutate({ id: data.id, url: data.url }); + addBookmarkScreenshotMutation.mutate({ id: data.id, url: data.url }); + } catch (pipelineError) { + // Guarantee teardown on any unexpected throw from the media-type + // probe, toast calls, or mutation.mutate — without this the + // subscription opened in onSuccess would dangle until the 90s + // timeout, keeping the card stuck on "Getting screenshot". + console.error("add-bookmark post-success pipeline failed:", pipelineError); + void teardownBookmarkEnrichmentSubscription(data.id, "screenshot_failed"); + } })(); }, onSuccess: (apiResponse, _variables, context) => { From 86468c2f1d0f000e0a3a0e52290e9fb312a7da92 Mon Sep 17 00:00:00 2001 From: rogerantony-dev Date: Mon, 20 Apr 2026 16:29:14 +0530 Subject: [PATCH 5/6] fix(cards): run placeholder exit animation when statusText clears MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Placing \`statusText &&\` outside \`AnimatePresence\` unmounted the entire presence tree when the text went null, so the \`exit\` transition on \`\` never ran — the text snapped out instantly while the surrounding placeholder faded over 0.15s, causing visible jank at the end of the loading state. Move the conditional inside \`AnimatePresence\` so the exit animation is driven by child removal, not parent unmount. --- .../cardSection/animatedBookmarkImage.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx b/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx index 600c7d915..eda8d99c5 100644 --- a/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx +++ b/src/pageComponents/dashboard/cardSection/animatedBookmarkImage.tsx @@ -206,18 +206,20 @@ export const LoaderImgPlaceholder = ({ loader={(source) => source.src} src={loaderGif} /> - {statusText && cardTypeCondition !== viewValues.list && ( + {cardTypeCondition !== viewValues.list && ( - - {statusText} - + {statusText ? ( + + {statusText} + + ) : null} )}
From 958ee4523f2f8f7de5838e7e97bfd2b5ee9b35cc Mon Sep 17 00:00:00 2001 From: rogerantony-dev Date: Mon, 20 Apr 2026 16:29:55 +0530 Subject: [PATCH 6/6] fix(realtime): declare meta_data.mediaType in schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`isRowTerminal\` branches on \`meta_data.mediaType\` to pick between the PDF and regular-URL terminal conditions, but the field was not listed in \`MetaDataRealtimeSchema\` — it passed through the catchall as \`unknown\`, forcing a \`typeof === "string"\` narrow at the use site. The field is as load-bearing as \`screenshot\` and \`ocr_status\`; declare it in the schema so the contract is explicit. --- src/lib/supabase/realtime/bookmark-realtime-payload.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/supabase/realtime/bookmark-realtime-payload.ts b/src/lib/supabase/realtime/bookmark-realtime-payload.ts index 46364f9ad..a592ba51b 100644 --- a/src/lib/supabase/realtime/bookmark-realtime-payload.ts +++ b/src/lib/supabase/realtime/bookmark-realtime-payload.ts @@ -13,6 +13,7 @@ const MetaDataRealtimeSchema = z favIcon: z.string().nullable().optional(), img_caption: z.string().nullable().optional(), isPageScreenshot: z.boolean().nullable().optional(), + mediaType: z.string().nullable().optional(), ocr_status: z.enum(["limit_reached", "no_text", "success"]).nullable().optional(), ogImgBlurUrl: z.string().nullable().optional(), screenshot: z.string().nullable().optional(), @@ -68,9 +69,8 @@ export function parseBookmarkRealtimePayload(payload: unknown): BookmarkRealtime */ export function isRowTerminal(row: BookmarkRealtimeRow): boolean { const metaData = row.meta_data && typeof row.meta_data === "object" ? row.meta_data : null; - const mediaType = typeof metaData?.mediaType === "string" ? metaData.mediaType : null; - if (mediaType === "application/pdf") { + if (metaData?.mediaType === "application/pdf") { return Boolean(row.ogImage) && isBookmarkEnrichmentDone(metaData); }