feat: rich command templates with public links + media text-first flow

- Command templates now match notification template style: type icons,
  linked filenames via album shared links, location, favorite status
- Media mode sends text message first, then media as reply (was media-only)
- Search/find/person/place resolve asset public URLs from tracked albums'
  shared links (share/{key}/photos/{id})
- Albums/summary commands include album public_url in context
- Enriched command template preview sample context with public_url, city,
  country, is_favorite
- Extract sanitizePreview to shared lib/sanitize.ts
- Command template preview now renders HTML links (was raw text)
- Global provider filter moved above search in sidebar
- CLAUDE.md: template consistency + context variable sync rules
This commit is contained in:
2026-03-24 16:48:57 +03:00
parent f90cc36ebd
commit d0bc767e98
26 changed files with 253 additions and 116 deletions
+40
View File
@@ -0,0 +1,40 @@
/**
* Sanitize HTML preview output — allows only safe Telegram-compatible tags.
* Used by notification and command template preview renderers.
*/
const ALLOWED_TAGS = new Set(['B', 'I', 'CODE', 'PRE', 'A', 'BR']);
function walkNodes(parent: Node, target: Node): void {
for (const node of Array.from(parent.childNodes)) {
if (node.nodeType === Node.TEXT_NODE) {
target.appendChild(document.createTextNode(node.textContent || ''));
} else if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
if (ALLOWED_TAGS.has(el.tagName)) {
const safe = document.createElement(el.tagName);
if (el.tagName === 'A') {
const href = el.getAttribute('href') || '';
if (/^https?:\/\//i.test(href)) {
safe.setAttribute('href', href);
safe.setAttribute('target', '_blank');
safe.setAttribute('rel', 'noopener noreferrer');
}
}
walkNodes(el, safe);
target.appendChild(safe);
} else {
walkNodes(el, target);
}
}
}
}
export function sanitizePreview(html: string): string {
const doc = new DOMParser().parseFromString(html, 'text/html');
const fragment = document.createDocumentFragment();
walkNodes(doc.body, fragment);
const wrapper = document.createElement('div');
wrapper.appendChild(fragment);
return wrapper.innerHTML;
}