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 a64ee3c94..f0ffeee7f 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 806179442..a4599c6cf 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 89366b109..90857c391 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 = () => { - setViewState((prev) => { - const newState = { ...prev, orderByTimeAsc: !prev.orderByTimeAsc }; - persistToStorage(newState); - return newState; - }); - }; - - const setTimeBasis = (field: MemoTimeBasis) => { + const updateState = (patch: Partial | ((prev: ViewState) => Partial)) => { setViewState((prev) => { - const newState = { ...prev, timeBasis: field }; + const newState = { ...prev, ...(typeof patch === "function" ? patch(prev) : patch) }; 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 e693aa67b..bb52ad0b7 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 0197cbfe2..a25b735e9 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 2756ff5d2..bd443afc2 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 8a52ef81c..8aa0d643e 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 0cfb44059..3b00b95fd 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 272fb22b0..3e7cf72dd 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 29f827ed0..cfb0b408a 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 2c8e07459..5b7aa1152 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 16adf1874..86903f307 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 200237b6c..afb66f412 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 bf5d498aa..4f67a3e54 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 abb4ba98e..0009a0a97 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 355d627e0..cf8a0559d 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 0e2f5b2e5..1f3ff3c1a 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 def58081c..81d0d1923 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 27810ab29..f67589563 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 b12ab10a3..540e33557 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 15065f9b9..5af02e5e5 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 b315c0963..7fe656903 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 f1cd046d6..26d65e087 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 f9e40c895..a243c62d0 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 1173e6589..0e26588b8 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 12da053cb..22317e0f1 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 8993a58e0..9004e528f 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 af417861f..743024fd2 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 608feacea..bb95b667b 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 bda70ba5b..b1806254d 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 61e99ee41..8a4f575f7 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 6ce09b79b..3503da277 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 ba0ae9396..a0460c806 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 b1dcc5843..908fc9f25 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 6e5223eea..3bb264345 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 0786fb598..a98759803 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 c2523cb2e..36163314c 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 dd73a5a93..0c8c4e0ef 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 4700cc3d4..63ad8c4c9 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 b1d7dfbdb..eb16253b8 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 5c63c974c..5765628c0 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 ed9edb035..6111103cc 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 a7020ae61..ab6d92ed1 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 c096a7969..a15f91d94 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 6f3bd5266..127264a74 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 e80c39141..34f0b29e5 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 000000000..5ac028971 --- /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(); + }); +});