Files
MindSift/frontend/src/lib/linkPreview.ts
T
sandrodachandClaude Opus 5.5 6a855fc32c
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m22s
feat(workspace): slash commands ranked by name, taller link cards with a link marker
- /li lists /link first: name prefix, aliases, name, then description
- link cards about twice as tall; the bare address below becomes an accent
  link marker; the link chip bar under bullets is gone, bare web addresses
  in text are links themselves

chore(release): v0.14.3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:19:19 +01:00

97 lines
3.1 KiB
TypeScript

// Link previews in notes: a card above a bullet that contains a plain web address. The server fetches
// the page (https only) and keeps title, description, picture and icon; the browser only talks to
// MindSift. Hiding the card for one link turns the address into a named link [Titel](url), so the
// choice syncs with the note; a global switch (this device) turns all previews off.
import { useEffect, useState } from 'react';
import { getToken } from './api';
export interface LinkPreview {
key: string;
url: string;
title: string;
description: string;
siteName: string;
hasImage: boolean;
hasIcon: boolean;
ok: boolean;
}
// Entire bullet content (trimmed) is a single plain https address
const CARD_URL_RE = /^https:\/\/[^\s<>()\]]+[^\s<>()\].,;:!?'"]$/;
export function linkCardUrl(content: string): string | null {
const trimmed = (content || '').trim();
return CARD_URL_RE.test(trimmed) ? trimmed : null;
}
/** Replaces the address by a named link, which shows no preview. */
/** "example.com/pfad" for a short link marker (no scheme, no www.) */
export function linkHost(url: string): string {
try {
const u = new URL(url);
const path = u.pathname === '/' ? '' : u.pathname;
return u.host.replace(/^www\./, '') + path;
} catch {
return url;
}
}
export function hidePreviewContent(content: string, url: string, title: string): string {
const label = (title || new URL(url).hostname).replace(/[[\]]/g, '').trim() || url;
return content.replace(url, `[${label}](${url})`);
}
const cache = new Map<string, Promise<LinkPreview | null>>();
export function loadLinkPreview(url: string): Promise<LinkPreview | null> {
let p = cache.get(url);
if (!p) {
p = (async () => {
const token = getToken();
const res = await fetch(`/api/v1/link-preview?url=${encodeURIComponent(url)}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!res.ok) return null; // demo, http link, offline: just no card
const data = (await res.json()) as LinkPreview;
return data.ok ? data : null;
})().catch(() => null);
cache.set(url, p);
}
return p;
}
const SETTING_KEY = 'mindsift_link_previews';
const SETTING_EVENT = 'mindsift:link-previews';
export function linkPreviewsEnabled(): boolean {
try {
return localStorage.getItem(SETTING_KEY) !== 'off';
} catch {
return true;
}
}
export function setLinkPreviewsEnabled(on: boolean): void {
try {
localStorage.setItem(SETTING_KEY, on ? 'on' : 'off');
} catch {
// private mode: only for this page
}
window.dispatchEvent(new CustomEvent(SETTING_EVENT));
}
/** The global switch, kept in sync across all open notes and tabs. */
export function useLinkPreviewsEnabled(): boolean {
const [on, setOn] = useState(linkPreviewsEnabled);
useEffect(() => {
const update = () => setOn(linkPreviewsEnabled());
window.addEventListener(SETTING_EVENT, update);
window.addEventListener('storage', update);
return () => {
window.removeEventListener(SETTING_EVENT, update);
window.removeEventListener('storage', update);
};
}, []);
return on;
}