From e3e4ae10512f514f71729779b5096d0d591c8cf4 Mon Sep 17 00:00:00 2001 From: boojack Date: Thu, 25 Jun 2026 20:45:51 +0800 Subject: [PATCH] feat(web): add link preview view setting Add a persisted "Link preview" toggle (default on) to the memo display-settings popover, mirroring the compact-mode setting. When turned off, link-preview cards are hidden across all memo views and the metadata request is skipped (the gate sits at the paragraph renderer and reads the preference from ViewContext). Resolves the request in #6045. --- .../MemoContent/markdown/Paragraph.tsx | 4 +- web/src/components/MemoDisplaySettingMenu.tsx | 8 ++- web/src/contexts/ViewContext.tsx | 44 +++++++------- web/src/locales/ar.json | 1 + web/src/locales/bg.json | 1 + web/src/locales/ca.json | 1 + web/src/locales/cs.json | 1 + web/src/locales/da.json | 1 + web/src/locales/de.json | 1 + web/src/locales/el.json | 1 + web/src/locales/en-GB.json | 1 + web/src/locales/en.json | 1 + web/src/locales/es.json | 1 + web/src/locales/et.json | 1 + web/src/locales/fa.json | 1 + web/src/locales/fi.json | 1 + web/src/locales/fr.json | 1 + web/src/locales/gl.json | 1 + web/src/locales/hi.json | 1 + web/src/locales/hr.json | 1 + web/src/locales/hu.json | 1 + web/src/locales/id.json | 1 + web/src/locales/it.json | 1 + web/src/locales/ja.json | 1 + web/src/locales/ka-GE.json | 1 + web/src/locales/ko.json | 1 + web/src/locales/lt.json | 1 + web/src/locales/lv.json | 1 + web/src/locales/mr.json | 1 + web/src/locales/nb.json | 1 + web/src/locales/nl.json | 1 + web/src/locales/pl.json | 1 + web/src/locales/pt-BR.json | 1 + web/src/locales/pt-PT.json | 1 + web/src/locales/ro.json | 1 + web/src/locales/ru.json | 1 + web/src/locales/sk.json | 1 + web/src/locales/sl.json | 1 + web/src/locales/sr.json | 1 + web/src/locales/sv.json | 1 + web/src/locales/th.json | 1 + web/src/locales/tr.json | 1 + web/src/locales/uk.json | 1 + web/src/locales/vi.json | 1 + web/src/locales/zh-Hans.json | 1 + web/src/locales/zh-Hant.json | 1 + web/tests/memo-content-link-preview.test.tsx | 57 +++++++++++++++++++ 47 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 web/tests/memo-content-link-preview.test.tsx diff --git a/web/src/components/MemoContent/markdown/Paragraph.tsx b/web/src/components/MemoContent/markdown/Paragraph.tsx index a64ee3c9..f0ffeee7 100644 --- a/web/src/components/MemoContent/markdown/Paragraph.tsx +++ b/web/src/components/MemoContent/markdown/Paragraph.tsx @@ -1,4 +1,5 @@ import type { Element } from "hast"; +import { useLinkPreviewEnabled } from "@/contexts/ViewContext"; import { markdownStyles } from "@/lib/markdownStyles"; import { cn } from "@/lib/utils"; import LinkMetadataCard from "../LinkMetadataCard"; @@ -46,7 +47,8 @@ export function getSingleLinkHref(node?: Element): string | undefined { export const Paragraph = ({ children, className, node, ...props }: ParagraphProps) => { const { blockDepth } = useMarkdownRenderContext(); - const href = blockDepth === 0 ? getSingleLinkHref(node) : undefined; + const linkPreviewEnabled = useLinkPreviewEnabled(); + const href = blockDepth === 0 && linkPreviewEnabled ? getSingleLinkHref(node) : undefined; const paragraph = (

{children} diff --git a/web/src/components/MemoDisplaySettingMenu.tsx b/web/src/components/MemoDisplaySettingMenu.tsx index 80617944..a4599c6c 100644 --- a/web/src/components/MemoDisplaySettingMenu.tsx +++ b/web/src/components/MemoDisplaySettingMenu.tsx @@ -12,8 +12,8 @@ interface Props { function MemoDisplaySettingMenu({ className }: Props) { const t = useTranslate(); - const { orderByTimeAsc, timeBasis, compactMode, setTimeBasis, toggleSortOrder, setCompactMode } = useView(); - const isApplying = orderByTimeAsc !== false || timeBasis !== "create_time" || compactMode; + const { orderByTimeAsc, timeBasis, compactMode, linkPreview, setTimeBasis, toggleSortOrder, setCompactMode, setLinkPreview } = useView(); + const isApplying = orderByTimeAsc !== false || timeBasis !== "create_time" || compactMode || !linkPreview; return ( @@ -57,6 +57,10 @@ function MemoDisplaySettingMenu({ className }: Props) { {t("memo.compact-mode")} +

+ {t("memo.link-preview")} + +
diff --git a/web/src/contexts/ViewContext.tsx b/web/src/contexts/ViewContext.tsx index 89366b10..90857c39 100644 --- a/web/src/contexts/ViewContext.tsx +++ b/web/src/contexts/ViewContext.tsx @@ -7,21 +7,26 @@ interface ViewState { timeBasis?: MemoTimeBasis; sortTimeField?: MemoTimeBasis; compactMode: boolean; + linkPreview: boolean; } interface ViewContextValue { orderByTimeAsc: boolean; timeBasis: MemoTimeBasis; compactMode: boolean; + linkPreview: boolean; toggleSortOrder: () => void; setTimeBasis: (field: MemoTimeBasis) => void; setCompactMode: (value: boolean) => void; + setLinkPreview: (value: boolean) => void; } const ViewContext = createContext(null); const LOCAL_STORAGE_KEY = "memos-view-setting"; +const DEFAULT_VIEW_STATE: ViewState = { orderByTimeAsc: false, compactMode: false, linkPreview: true }; + export function ViewProvider({ children }: { children: ReactNode }) { const getInitialState = (): ViewState => { try { @@ -31,15 +36,16 @@ export function ViewProvider({ children }: { children: ReactNode }) { const cachedTimeBasis = data.timeBasis ?? data.sortTimeField; const timeBasis = cachedTimeBasis === "create_time" || cachedTimeBasis === "update_time" ? cachedTimeBasis : undefined; return { - orderByTimeAsc: Boolean(data.orderByTimeAsc ?? false), + orderByTimeAsc: Boolean(data.orderByTimeAsc ?? DEFAULT_VIEW_STATE.orderByTimeAsc), timeBasis, - compactMode: Boolean(data.compactMode ?? false), + compactMode: Boolean(data.compactMode ?? DEFAULT_VIEW_STATE.compactMode), + linkPreview: Boolean(data.linkPreview ?? DEFAULT_VIEW_STATE.linkPreview), }; } } catch (error) { console.warn("Failed to load view settings from localStorage:", error); } - return { orderByTimeAsc: false, compactMode: false }; + return { ...DEFAULT_VIEW_STATE }; }; const [viewState, setViewState] = useState(getInitialState); @@ -53,29 +59,18 @@ export function ViewProvider({ children }: { children: ReactNode }) { } }; - const toggleSortOrder = () => { + const updateState = (patch: Partial | ((prev: ViewState) => Partial)) => { setViewState((prev) => { - const newState = { ...prev, orderByTimeAsc: !prev.orderByTimeAsc }; + const newState = { ...prev, ...(typeof patch === "function" ? patch(prev) : patch) }; persistToStorage(newState); return newState; }); }; - const setTimeBasis = (field: MemoTimeBasis) => { - setViewState((prev) => { - const newState = { ...prev, timeBasis: field }; - persistToStorage(newState); - return newState; - }); - }; - - const setCompactMode = (value: boolean) => { - setViewState((prev) => { - const newState = { ...prev, compactMode: value }; - persistToStorage(newState); - return newState; - }); - }; + const toggleSortOrder = () => updateState((prev) => ({ orderByTimeAsc: !prev.orderByTimeAsc })); + const setTimeBasis = (field: MemoTimeBasis) => updateState({ timeBasis: field }); + const setCompactMode = (value: boolean) => updateState({ compactMode: value }); + const setLinkPreview = (value: boolean) => updateState({ linkPreview: value }); return ( {children} @@ -100,3 +97,10 @@ export function useView() { } return context; } + +// Read the link-preview preference from deep inside the markdown renderer, which +// can render outside a ViewProvider (tests, isolated previews). Defaults to +// enabled when no provider is present, preserving the historical behavior. +export function useLinkPreviewEnabled() { + return useContext(ViewContext)?.linkPreview ?? DEFAULT_VIEW_STATE.linkPreview; +} diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index e693aa67..bb52ad0b 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -195,6 +195,7 @@ "label": "المرشحات" }, "compact-mode": "الوضع المضغوط", + "link-preview": "معاينة الرابط", "links": "روابط", "load-more": "تحميل المزيد", "no-archived-memos": "لا يوجد مذكرات مؤرشفة", diff --git a/web/src/locales/bg.json b/web/src/locales/bg.json index 0197cbfe..a25b735e 100644 --- a/web/src/locales/bg.json +++ b/web/src/locales/bg.json @@ -247,6 +247,7 @@ "label": "Филтри" }, "compact-mode": "Компактен режим", + "link-preview": "Преглед на връзка", "links": "Връзки", "load-more": "Заредете повече", "no-archived-memos": "Няма архивирани бележки.", diff --git a/web/src/locales/ca.json b/web/src/locales/ca.json index 2756ff5d..bd443afc 100644 --- a/web/src/locales/ca.json +++ b/web/src/locales/ca.json @@ -209,6 +209,7 @@ "label": "Filtres" }, "compact-mode": "Mode compacte", + "link-preview": "Previsualització d'enllaços", "links": "Enllaços", "load-more": "Carrega més", "no-archived-memos": "No hi ha notes arxivades.", diff --git a/web/src/locales/cs.json b/web/src/locales/cs.json index 8a52ef81..8aa0d643 100644 --- a/web/src/locales/cs.json +++ b/web/src/locales/cs.json @@ -209,6 +209,7 @@ "label": "Filtry" }, "compact-mode": "Kompaktní režim", + "link-preview": "Náhled odkazu", "links": "Odkazy", "load-more": "Načíst více", "no-archived-memos": "Žádné archivované poznámky.", diff --git a/web/src/locales/da.json b/web/src/locales/da.json index 0cfb4405..3b00b95f 100644 --- a/web/src/locales/da.json +++ b/web/src/locales/da.json @@ -247,6 +247,7 @@ "label": "Filtre" }, "compact-mode": "Kompakt tilstand", + "link-preview": "Linkforhåndsvisning", "links": "Links", "load-more": "Indlæs mere", "no-archived-memos": "Ingen arkiverede notater.", diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 272fb22b..3e7cf72d 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -209,6 +209,7 @@ "label": "Filter" }, "compact-mode": "Kompaktmodus", + "link-preview": "Linkvorschau", "links": "Links", "load-more": "Mehr laden", "no-archived-memos": "Keine archivierten Notizen.", diff --git a/web/src/locales/el.json b/web/src/locales/el.json index 29f827ed..cfb0b408 100644 --- a/web/src/locales/el.json +++ b/web/src/locales/el.json @@ -247,6 +247,7 @@ "label": "Φίλτρα" }, "compact-mode": "Συμπαγής λειτουργία", + "link-preview": "Προεπισκόπηση συνδέσμου", "links": "Εδαφος διά παιγνίδι γκολφ", "load-more": "Φορτώστε περισσότερα", "no-archived-memos": "Δεν υπάρχουν αρχειοθετημένα σημειώματα.", diff --git a/web/src/locales/en-GB.json b/web/src/locales/en-GB.json index 2c8e0745..5b7aa115 100644 --- a/web/src/locales/en-GB.json +++ b/web/src/locales/en-GB.json @@ -209,6 +209,7 @@ "label": "Filters" }, "compact-mode": "Compact mode", + "link-preview": "Link preview", "links": "Links", "load-more": "Load more", "no-archived-memos": "No archived memos.", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 16adf187..86903f30 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -265,6 +265,7 @@ "label": "Filters" }, "compact-mode": "Compact mode", + "link-preview": "Link preview", "links": "Links", "load-more": "Load more", "no-archived-memos": "No archived memos.", diff --git a/web/src/locales/es.json b/web/src/locales/es.json index 200237b6..afb66f41 100644 --- a/web/src/locales/es.json +++ b/web/src/locales/es.json @@ -209,6 +209,7 @@ "label": "Filtros" }, "compact-mode": "Modo compacto", + "link-preview": "Vista previa de enlaces", "links": "Enlaces", "load-more": "Cargar más", "no-archived-memos": "No hay memos archivados.", diff --git a/web/src/locales/et.json b/web/src/locales/et.json index bf5d498a..4f67a3e5 100644 --- a/web/src/locales/et.json +++ b/web/src/locales/et.json @@ -247,6 +247,7 @@ "label": "Filtrid" }, "compact-mode": "Kompaktne režiim", + "link-preview": "Lingi eelvaade", "links": "Lingid", "load-more": "Laadige rohkem", "no-archived-memos": "Arhiveeritud memosid pole.", diff --git a/web/src/locales/fa.json b/web/src/locales/fa.json index abb4ba98..0009a0a9 100644 --- a/web/src/locales/fa.json +++ b/web/src/locales/fa.json @@ -195,6 +195,7 @@ "label": "فیلترها" }, "compact-mode": "حالت فشرده", + "link-preview": "پیش‌نمایش پیوند", "links": "پیوندها", "load-more": "بارگذاری بیشتر", "no-archived-memos": "یادداشت آرشیو شده‌ای وجود ندارد.", diff --git a/web/src/locales/fi.json b/web/src/locales/fi.json index 355d627e..cf8a0559 100644 --- a/web/src/locales/fi.json +++ b/web/src/locales/fi.json @@ -247,6 +247,7 @@ "label": "Suodattimet" }, "compact-mode": "Tiivis tila", + "link-preview": "Linkin esikatselu", "links": "Linkit", "load-more": "Lataa lisää", "no-archived-memos": "Ei arkistoituja muistioita.", diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json index 0e2f5b2e..1f3ff3c1 100644 --- a/web/src/locales/fr.json +++ b/web/src/locales/fr.json @@ -209,6 +209,7 @@ "label": "Filtres" }, "compact-mode": "Mode compact", + "link-preview": "Aperçu des liens", "links": "Liens", "load-more": "Charger plus", "no-archived-memos": "Aucune note archivée.", diff --git a/web/src/locales/gl.json b/web/src/locales/gl.json index def58081..81d0d192 100644 --- a/web/src/locales/gl.json +++ b/web/src/locales/gl.json @@ -209,6 +209,7 @@ "label": "Filtros" }, "compact-mode": "Modo compacto", + "link-preview": "Vista previa de ligazóns", "links": "Ligazóns", "load-more": "Cargar máis", "no-archived-memos": "Sen memos arquivadas.", diff --git a/web/src/locales/hi.json b/web/src/locales/hi.json index 27810ab2..f6758956 100644 --- a/web/src/locales/hi.json +++ b/web/src/locales/hi.json @@ -195,6 +195,7 @@ "label": "फिल्टर" }, "compact-mode": "कॉम्पैक्ट मोड", + "link-preview": "लिंक पूर्वावलोकन", "links": "लिंक", "load-more": "और लोड करें", "no-archived-memos": "कोई संग्रहीत मेमो नहीं।", diff --git a/web/src/locales/hr.json b/web/src/locales/hr.json index b12ab10a..540e3355 100644 --- a/web/src/locales/hr.json +++ b/web/src/locales/hr.json @@ -209,6 +209,7 @@ "label": "Filteri" }, "compact-mode": "Kompaktni način", + "link-preview": "Pregled poveznice", "links": "Linkovi", "load-more": "Učitaj više", "no-archived-memos": "Nema arhiviranih memoa.", diff --git a/web/src/locales/hu.json b/web/src/locales/hu.json index 15065f9b..5af02e5e 100644 --- a/web/src/locales/hu.json +++ b/web/src/locales/hu.json @@ -209,6 +209,7 @@ "label": "Szűrők" }, "compact-mode": "Kompakt mód", + "link-preview": "Hivatkozás előnézete", "links": "Hivatkozások", "load-more": "Több betöltése", "no-archived-memos": "Nincsenek archivált jegyzetek.", diff --git a/web/src/locales/id.json b/web/src/locales/id.json index b315c096..7fe65690 100644 --- a/web/src/locales/id.json +++ b/web/src/locales/id.json @@ -195,6 +195,7 @@ "label": "Filter" }, "compact-mode": "Mode ringkas", + "link-preview": "Pratinjau tautan", "links": "Tautan", "load-more": "Muat lebih banyak", "no-archived-memos": "Tidak ada memo yang diarsipkan.", diff --git a/web/src/locales/it.json b/web/src/locales/it.json index f1cd046d..26d65e08 100644 --- a/web/src/locales/it.json +++ b/web/src/locales/it.json @@ -209,6 +209,7 @@ "label": "Filtri" }, "compact-mode": "Modalità compatta", + "link-preview": "Anteprima dei link", "links": "Link", "load-more": "Carica altro", "no-archived-memos": "Nessun memo archiviato.", diff --git a/web/src/locales/ja.json b/web/src/locales/ja.json index f9e40c89..a243c62d 100644 --- a/web/src/locales/ja.json +++ b/web/src/locales/ja.json @@ -227,6 +227,7 @@ "label": "フィルター" }, "compact-mode": "コンパクト表示", + "link-preview": "リンクプレビュー", "links": "リンク", "load-more": "さらに読み込む", "no-archived-memos": "アーカイブされたメモはありません。", diff --git a/web/src/locales/ka-GE.json b/web/src/locales/ka-GE.json index 1173e658..0e26588b 100644 --- a/web/src/locales/ka-GE.json +++ b/web/src/locales/ka-GE.json @@ -209,6 +209,7 @@ "label": "ფილტრები" }, "compact-mode": "კომპაქტური რეჟიმი", + "link-preview": "ბმულის გადახედვა", "links": "ლინკები", "load-more": "მეტის ჩატვირთვა", "no-archived-memos": "დაარქივებული მემოები არ არის.", diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json index 12da053c..22317e0f 100644 --- a/web/src/locales/ko.json +++ b/web/src/locales/ko.json @@ -195,6 +195,7 @@ "label": "필터" }, "compact-mode": "간략 보기", + "link-preview": "링크 미리보기", "links": "링크", "load-more": "더보기", "no-archived-memos": "보관처리된 메모가 없습니다.", diff --git a/web/src/locales/lt.json b/web/src/locales/lt.json index 8993a58e..9004e528 100644 --- a/web/src/locales/lt.json +++ b/web/src/locales/lt.json @@ -247,6 +247,7 @@ "label": "Filtrai" }, "compact-mode": "Kompaktiškas režimas", + "link-preview": "Nuorodos peržiūra", "links": "Nuorodos", "load-more": "Įkelti daugiau", "no-archived-memos": "Nėra archyvuotų atmintinių.", diff --git a/web/src/locales/lv.json b/web/src/locales/lv.json index af417861..743024fd 100644 --- a/web/src/locales/lv.json +++ b/web/src/locales/lv.json @@ -247,6 +247,7 @@ "label": "Filtri" }, "compact-mode": "Kompaktais režīms", + "link-preview": "Saites priekšskatījums", "links": "Saites", "load-more": "Ielādēt vairāk", "no-archived-memos": "Nav arhivētu piezīmju.", diff --git a/web/src/locales/mr.json b/web/src/locales/mr.json index 608feace..bb95b667 100644 --- a/web/src/locales/mr.json +++ b/web/src/locales/mr.json @@ -195,6 +195,7 @@ "label": "फिल्टर" }, "compact-mode": "संक्षिप्त मोड", + "link-preview": "दुवा पूर्वावलोकन", "links": "लिंक्स", "load-more": "अधिक लोड करा", "no-archived-memos": "कोणतेही संग्रहित मेमो नाहीत.", diff --git a/web/src/locales/nb.json b/web/src/locales/nb.json index bda70ba5..b1806254 100644 --- a/web/src/locales/nb.json +++ b/web/src/locales/nb.json @@ -209,6 +209,7 @@ "label": "Filtre" }, "compact-mode": "Kompakt modus", + "link-preview": "Forhåndsvisning av lenke", "links": "Linker", "load-more": "Last inn mer", "no-archived-memos": "Ingen arkiverte memoer.", diff --git a/web/src/locales/nl.json b/web/src/locales/nl.json index 61e99ee4..8a4f575f 100644 --- a/web/src/locales/nl.json +++ b/web/src/locales/nl.json @@ -209,6 +209,7 @@ "label": "Filters" }, "compact-mode": "Compacte modus", + "link-preview": "Linkvoorbeeld", "links": "Links", "load-more": "Meer laden", "no-archived-memos": "Geen gearchiveerde memos.", diff --git a/web/src/locales/pl.json b/web/src/locales/pl.json index 6ce09b79..3503da27 100644 --- a/web/src/locales/pl.json +++ b/web/src/locales/pl.json @@ -209,6 +209,7 @@ "label": "Filtry" }, "compact-mode": "Tryb kompaktowy", + "link-preview": "Podgląd linków", "links": "Linki", "load-more": "Załaduj więcej", "no-archived-memos": "Brak zarchiwizowanych notatek.", diff --git a/web/src/locales/pt-BR.json b/web/src/locales/pt-BR.json index ba0ae939..a0460c80 100644 --- a/web/src/locales/pt-BR.json +++ b/web/src/locales/pt-BR.json @@ -195,6 +195,7 @@ "label": "Filtros" }, "compact-mode": "Modo compacto", + "link-preview": "Pré-visualização de links", "links": "Links", "load-more": "Carregar mais", "no-archived-memos": "Nenhum memo arquivado.", diff --git a/web/src/locales/pt-PT.json b/web/src/locales/pt-PT.json index b1dcc584..908fc9f2 100644 --- a/web/src/locales/pt-PT.json +++ b/web/src/locales/pt-PT.json @@ -209,6 +209,7 @@ "label": "Filtros" }, "compact-mode": "Modo compacto", + "link-preview": "Pré-visualização de hiperligações", "links": "Links", "load-more": "Carregar mais", "no-archived-memos": "Não existem memos arquivados.", diff --git a/web/src/locales/ro.json b/web/src/locales/ro.json index 6e5223ee..3bb26434 100644 --- a/web/src/locales/ro.json +++ b/web/src/locales/ro.json @@ -247,6 +247,7 @@ "label": "Filtre" }, "compact-mode": "Mod compact", + "link-preview": "Previzualizare link", "links": "Legături", "load-more": "Încărcați mai multe", "no-archived-memos": "Fără memorii arhivate.", diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json index 0786fb59..a9875980 100644 --- a/web/src/locales/ru.json +++ b/web/src/locales/ru.json @@ -209,6 +209,7 @@ "label": "Фильтры" }, "compact-mode": "Компактный режим", + "link-preview": "Предпросмотр ссылок", "links": "Ссылки", "load-more": "Загрузить еще", "no-archived-memos": "Нет заархивированных записей.", diff --git a/web/src/locales/sk.json b/web/src/locales/sk.json index c2523cb2..36163314 100644 --- a/web/src/locales/sk.json +++ b/web/src/locales/sk.json @@ -247,6 +247,7 @@ "label": "Filtre" }, "compact-mode": "Kompaktný režim", + "link-preview": "Náhľad odkazu", "links": "Odkazy", "load-more": "Načítať viac", "no-archived-memos": "Žiadne archivované poznámky.", diff --git a/web/src/locales/sl.json b/web/src/locales/sl.json index dd73a5a9..0c8c4e0e 100644 --- a/web/src/locales/sl.json +++ b/web/src/locales/sl.json @@ -209,6 +209,7 @@ "label": "Filtri" }, "compact-mode": "Strnjeni način", + "link-preview": "Predogled povezave", "links": "Povezave", "load-more": "Naloži več", "no-archived-memos": "Ni arhiviranih beležk.", diff --git a/web/src/locales/sr.json b/web/src/locales/sr.json index 4700cc3d..63ad8c4c 100644 --- a/web/src/locales/sr.json +++ b/web/src/locales/sr.json @@ -247,6 +247,7 @@ "label": "Филтери" }, "compact-mode": "Компактни режим", + "link-preview": "Преглед везе", "links": "Линкови", "load-more": "Учитај више", "no-archived-memos": "Нема архивираних белешки.", diff --git a/web/src/locales/sv.json b/web/src/locales/sv.json index b1d7dfbd..eb16253b 100644 --- a/web/src/locales/sv.json +++ b/web/src/locales/sv.json @@ -209,6 +209,7 @@ "label": "Filter" }, "compact-mode": "Kompakt läge", + "link-preview": "Förhandsvisning av länk", "links": "Länkar", "load-more": "Ladda mer", "no-archived-memos": "Inga arkiverade anteckningar.", diff --git a/web/src/locales/th.json b/web/src/locales/th.json index 5c63c974..5765628c 100644 --- a/web/src/locales/th.json +++ b/web/src/locales/th.json @@ -195,6 +195,7 @@ "label": "ตัวกรอง" }, "compact-mode": "โหมดกะทัดรัด", + "link-preview": "ตัวอย่างลิงก์", "links": "ลิงก์", "load-more": "โหลดเพิ่มเติม", "no-archived-memos": "ไม่มีบันทึกช่วยจำที่ถูกเก็บถาวร", diff --git a/web/src/locales/tr.json b/web/src/locales/tr.json index ed9edb03..6111103c 100644 --- a/web/src/locales/tr.json +++ b/web/src/locales/tr.json @@ -209,6 +209,7 @@ "label": "Filtreler" }, "compact-mode": "Kompakt mod", + "link-preview": "Bağlantı önizlemesi", "links": "Bağlantılar", "load-more": "Daha fazla yükle", "no-archived-memos": "Arşivlenmiş not yok.", diff --git a/web/src/locales/uk.json b/web/src/locales/uk.json index a7020ae6..ab6d92ed 100644 --- a/web/src/locales/uk.json +++ b/web/src/locales/uk.json @@ -209,6 +209,7 @@ "label": "Фільтри" }, "compact-mode": "Компактний режим", + "link-preview": "Попередній перегляд посилань", "links": "Посилання", "load-more": "Завантажити більше", "no-archived-memos": "Немає архівованих нотаток.", diff --git a/web/src/locales/vi.json b/web/src/locales/vi.json index c096a796..a15f91d9 100644 --- a/web/src/locales/vi.json +++ b/web/src/locales/vi.json @@ -195,6 +195,7 @@ "label": "Bộ lọc" }, "compact-mode": "Chế độ thu gọn", + "link-preview": "Xem trước liên kết", "links": "Liên kết", "load-more": "Tải thêm", "no-archived-memos": "Không có ghi chú nào được lưu trữ.", diff --git a/web/src/locales/zh-Hans.json b/web/src/locales/zh-Hans.json index 6f3bd526..127264a7 100644 --- a/web/src/locales/zh-Hans.json +++ b/web/src/locales/zh-Hans.json @@ -211,6 +211,7 @@ "label": "过滤器" }, "compact-mode": "紧凑模式", + "link-preview": "链接预览", "links": "链接", "load-more": "加载更多", "no-archived-memos": "没有已归档备忘录。", diff --git a/web/src/locales/zh-Hant.json b/web/src/locales/zh-Hant.json index e80c3914..34f0b29e 100644 --- a/web/src/locales/zh-Hant.json +++ b/web/src/locales/zh-Hant.json @@ -247,6 +247,7 @@ "label": "過濾器" }, "compact-mode": "精簡模式", + "link-preview": "連結預覽", "links": "連結", "load-more": "載入更多", "no-archived-memos": "無已封存的備忘錄", diff --git a/web/tests/memo-content-link-preview.test.tsx b/web/tests/memo-content-link-preview.test.tsx new file mode 100644 index 00000000..5ac02897 --- /dev/null +++ b/web/tests/memo-content-link-preview.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from "@testing-library/react"; +import type { Element } from "hast"; +import { describe, expect, it, vi } from "vitest"; +import { Paragraph } from "@/components/MemoContent/markdown/Paragraph"; + +const viewState = vi.hoisted(() => ({ linkPreview: true })); + +// Paragraph reads the global preference via this hook. +vi.mock("@/contexts/ViewContext", () => ({ + useLinkPreviewEnabled: () => viewState.linkPreview, +})); + +// The link-preview card fetches metadata via this hook; stub it so the card would +// render whenever it is allowed to. +vi.mock("@/hooks/useMemoQueries", () => ({ + useLinkMetadata: () => ({ + data: { url: "https://example.com", title: "Example Title", description: "An example site", image: "" }, + isSuccess: true, + }), +})); + +// A single bare-link paragraph node, the shape that triggers a preview card. +const singleLinkNode = { + type: "element", + tagName: "p", + properties: {}, + children: [ + { + type: "element", + tagName: "a", + properties: { href: "https://example.com" }, + children: [{ type: "text", value: "https://example.com" }], + }, + ], +} as unknown as Element; + +const renderParagraph = () => + render( + + https://example.com + , + ); + +describe(" link preview gating", () => { + it("renders the link preview card when the setting is enabled (default)", () => { + viewState.linkPreview = true; + renderParagraph(); + expect(screen.getByText("Example Title")).toBeInTheDocument(); + }); + + it("renders the plain link without a card when the setting is disabled", () => { + viewState.linkPreview = false; + renderParagraph(); + expect(screen.queryByText("Example Title")).not.toBeInTheDocument(); + expect(screen.getByText("https://example.com")).toBeInTheDocument(); + }); +});