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();
+ });
+});