Browse Source

bug-fixes

imwald
Silberengel 2 weeks ago
parent
commit
e082c4ff4d
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 18
      scripts/download-piper-extra-voices.sh
  4. 32
      scripts/ensure-libretranslate-dirs.sh
  5. 92
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  6. 444
      src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx
  7. 4
      src/components/AdvancedEventLab/AdvancedEventLabPreviewPane.tsx
  8. 62
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  9. 55
      src/components/StoredAccountSwitchSelect.tsx
  10. 29
      src/hooks/useFetchEvent.tsx
  11. 58
      src/lib/event-kind1111-parent.test.ts
  12. 25
      src/lib/relay-url-priority.test.ts
  13. 21
      src/lib/relay-url-priority.ts
  14. 52
      src/pages/secondary/NotePage/index.tsx
  15. 6
      src/services/client.service.ts

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.1.0", "version": "23.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "imwald", "name": "imwald",
"version": "23.1.0", "version": "23.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@asciidoctor/core": "^3.0.4", "@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "imwald", "name": "imwald",
"version": "23.1.0", "version": "23.1.1",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true, "private": true,
"type": "module", "type": "module",

18
scripts/download-piper-extra-voices.sh

@ -1,6 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Download Piper ONNX voices — delegates to ensure-libretranslate-dirs.sh (single source of truth). # Download Piper ONNX voices (read-aloud / Wyoming) — delegates to ensure-libretranslate-dirs.sh.
# Usage: bash scripts/download-piper-extra-voices.sh [DEST_DIR] # Does not touch LibreTranslate dirs, Docker, or scripts/libretranslate-lt.default.env.
#
# Usage:
# bash scripts/download-piper-extra-voices.sh [DEST_DIR]
# Default DEST: $PIPER_DOWNLOAD_DIR if set, else <repo>/.local-piper-data
# Optional: HF_BASE (see ensure-libretranslate-dirs.sh) for a Hugging Face mirror.
#
# Voice list lives only in ensure-libretranslate-dirs.sh (download_piper_voices_to).
set -euo pipefail set -euo pipefail
__dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" __dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
exec bash "$__dir/ensure-libretranslate-dirs.sh" --download-piper-only "$@" _ensure="${__dir}/ensure-libretranslate-dirs.sh"
[[ -f "$_ensure" ]] || {
echo "[download-piper-extra-voices] Missing ${_ensure} (keep this script next to ensure-libretranslate-dirs.sh)." >&2
exit 1
}
exec bash "$_ensure" --download-piper-only "$@"

32
scripts/ensure-libretranslate-dirs.sh

@ -10,7 +10,9 @@
# as docker-compose `env_file` for libretranslate). # as docker-compose `env_file` for libretranslate).
# #
# Piper download logic lives in this file so you can copy **only** this script to a server and run it # Piper download logic lives in this file so you can copy **only** this script to a server and run it
# from the repo root (still need curl, docker; full clone is easier: bash scripts/ensure-libretranslate-dirs.sh). # from the repo root (still need curl, docker). If `scripts/libretranslate-lt.default.env` is missing,
# a built-in LT_LOAD_ONLY list is used (see load_stack_lt_load_only). Full clone is easiest:
# bash scripts/ensure-libretranslate-dirs.sh
# #
# Internal entry: bash ensure-libretranslate-dirs.sh --download-piper-only [DEST] # Internal entry: bash ensure-libretranslate-dirs.sh --download-piper-only [DEST]
# (used by scripts/download-piper-extra-voices.sh — keep Piper relpaths in sync with trinity-languages.ts) # (used by scripts/download-piper-extra-voices.sh — keep Piper relpaths in sync with trinity-languages.ts)
@ -30,21 +32,28 @@ _resolve_root() {
fi fi
} }
# Keep identical to scripts/libretranslate-lt.default.env in the repo (fallback when that file is absent).
DEFAULT_LT_LOAD_ONLY='en,de,es,fr,it,pt,ru,zh,ar,nl,pl,cs,tr'
load_stack_lt_load_only() { load_stack_lt_load_only() {
local f="${ROOT}/scripts/libretranslate-lt.default.env" local f="${ROOT}/scripts/libretranslate-lt.default.env"
[[ -f "$f" ]] || { local f_alt="${ROOT}/libretranslate-lt.default.env"
echo "[ensure] Missing ${f}" >&2 if [[ -f "$f" ]]; then
exit 1 STACK_LT_LOAD_ONLY="$(grep -E '^[[:space:]]*LT_LOAD_ONLY=' "$f" | head -1 | sed 's/^[[:space:]]*LT_LOAD_ONLY=//')"
} elif [[ -f "$f_alt" ]]; then
STACK_LT_LOAD_ONLY="$(grep -E '^[[:space:]]*LT_LOAD_ONLY=' "$f" | head -1 | sed 's/^[[:space:]]*LT_LOAD_ONLY=//')" STACK_LT_LOAD_ONLY="$(grep -E '^[[:space:]]*LT_LOAD_ONLY=' "$f_alt" | head -1 | sed 's/^[[:space:]]*LT_LOAD_ONLY=//')"
else
echo "[ensure] warn: missing ${f} (and ${f_alt}) — using built-in LT_LOAD_ONLY. Copy scripts/libretranslate-lt.default.env from the repo to customize." >&2
STACK_LT_LOAD_ONLY="$DEFAULT_LT_LOAD_ONLY"
fi
[[ -n "$STACK_LT_LOAD_ONLY" ]] || { [[ -n "$STACK_LT_LOAD_ONLY" ]] || {
echo "[ensure] LT_LOAD_ONLY empty in ${f}" >&2 echo "[ensure] LT_LOAD_ONLY empty (check ${f} or set LT_LOAD_ONLY before running)" >&2
exit 1 exit 1
} }
} }
# Keep in sync with src/lib/trinity-languages.ts (TRINITY_PIPER_VOICE + EXTRA_READ_ALOUD_PIPER_VOICE) # Keep in sync with src/lib/trinity-languages.ts (TRINITY_PIPER_VOICE + EXTRA_READ_ALOUD_PIPER_VOICE)
# and services/piper-tts-proxy/server.ts getVoiceForLanguage voiceMap. # and services/piper-tts-proxy/server.ts getVoiceForLanguage voiceMap (14 voices: 10 trinity + en-gb, ar, it, pt).
download_piper_voices_to() { download_piper_voices_to() {
local dest="${1:?destination directory}" local dest="${1:?destination directory}"
local hf="${HF_BASE:-https://huggingface.co/rhasspy/piper-voices/resolve/main}" local hf="${HF_BASE:-https://huggingface.co/rhasspy/piper-voices/resolve/main}"
@ -74,8 +83,11 @@ download_piper_voices_to() {
continue continue
fi fi
echo "Fetching ${base_name}" echo "Fetching ${base_name}"
curl -fsSL -o "$onnx" "${hf}/${relpath}.onnx" # HuggingFace can be slow or reset mid-transfer; retries + caps avoid hung deploys.
curl -fsSL -o "$json" "${hf}/${relpath}.onnx.json" curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 30 --max-time 600 \
-o "$onnx" "${hf}/${relpath}.onnx"
curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 30 --max-time 600 \
-o "$json" "${hf}/${relpath}.onnx.json"
done done
echo "Piper ONNX done → ${dest}" echo "Piper ONNX done → ${dest}"
} }

92
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -225,6 +225,9 @@ export default function AdvancedEventLabDialog({
previewEmojiTags previewEmojiTags
}: AdvancedEventLabDialogProps) { }: AdvancedEventLabDialogProps) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
/** `useTranslation().t` can change identity every render; never list it as a layout-effect dep (editor remount loop). */
const labTRef = useRef(t)
labTRef.current = t
const dark = useDarkModeFlag() const dark = useDarkModeFlag()
const markupHost = useRef<HTMLDivElement>(null) const markupHost = useRef<HTMLDivElement>(null)
const markupView = useRef<EditorView | null>(null) const markupView = useRef<EditorView | null>(null)
@ -243,6 +246,10 @@ export default function AdvancedEventLabDialog({
const [previewDoc, setPreviewDoc] = useState('') const [previewDoc, setPreviewDoc] = useState('')
/** Stable while payload matches; avoids remounting the editor when the parent passes a new `initial` object reference. */
const labEditorMountFingerprint =
initial != null ? `${initial.kind}\0${initial.content}\0${JSON.stringify(initial.tags)}` : ''
const mergedLabPreviewEmojiTags = useMemo(() => { const mergedLabPreviewEmojiTags = useMemo(() => {
if (!open || !initial) return [] if (!open || !initial) return []
const fromInitial = initial.tags.filter(([n]) => n === 'emoji').map((r) => [...r]) const fromInitial = initial.tags.filter(([n]) => n === 'emoji').map((r) => [...r])
@ -470,7 +477,7 @@ export default function AdvancedEventLabDialog({
clearInterval(pushId) clearInterval(pushId)
clearInterval(uiId) clearInterval(uiId)
} }
}, [open, initial, pushLabCheckpoint, bumpUndoUi]) }, [open, labEditorMountFingerprint, pushLabCheckpoint, bumpUndoUi])
const [translateLangs, setTranslateLangs] = useState<TranslateLanguageOption[]>([]) const [translateLangs, setTranslateLangs] = useState<TranslateLanguageOption[]>([])
const ltList = useMemo( const ltList = useMemo(
@ -610,7 +617,7 @@ export default function AdvancedEventLabDialog({
keymap.of([...defaultKeymap, ...historyKeymap]), keymap.of([...defaultKeymap, ...historyKeymap]),
lineNumbers(), lineNumbers(),
cmPlaceholder( cmPlaceholder(
t( labTRef.current(
markupMode === 'asciidoc' markupMode === 'asciidoc'
? 'Advanced lab markup placeholder asciidoc' ? 'Advanced lab markup placeholder asciidoc'
: 'Advanced lab markup placeholder markdown' : 'Advanced lab markup placeholder markdown'
@ -618,10 +625,11 @@ export default function AdvancedEventLabDialog({
), ),
markupLang, markupLang,
EditorView.theme({ EditorView.theme({
'&': { maxHeight: '100%' }, '&': { height: '100%', maxHeight: '100%', minHeight: 0 },
'.cm-scroller': { overflow: 'auto' }, '.cm-scroller': { overflow: 'auto', minHeight: 0 },
// Large dvh mins fight stacked flex/grid rows and overflow onto the preview; host + row cap height instead.
'.cm-content': { '.cm-content': {
minHeight: 'min(22dvh, 11rem)', minHeight: '11rem',
fontFamily: 'var(--font-mono, ui-monospace, monospace)' fontFamily: 'var(--font-mono, ui-monospace, monospace)'
} }
}), }),
@ -722,11 +730,10 @@ export default function AdvancedEventLabDialog({
} }
}, [ }, [
open, open,
initial, labEditorMountFingerprint,
markupMode, markupMode,
dark, dark,
destroyEditors, destroyEditors,
t,
bodyApiRef, bodyApiRef,
scheduleLabDraftPersist, scheduleLabDraftPersist,
flushLabDraftNow, flushLabDraftNow,
@ -838,7 +845,10 @@ export default function AdvancedEventLabDialog({
<SelectTrigger id="lt-lang" className="min-w-[220px] max-w-md w-auto"> <SelectTrigger id="lt-lang" className="min-w-[220px] max-w-md w-auto">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"> <SelectContent
className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div <div
className="sticky top-0 z-10 border-b border-border bg-popover p-2" className="sticky top-0 z-10 border-b border-border bg-popover p-2"
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
@ -893,7 +903,10 @@ export default function AdvancedEventLabDialog({
<SelectTrigger id="tr-src" className="min-w-[220px] max-w-md w-auto"> <SelectTrigger id="tr-src" className="min-w-[220px] max-w-md w-auto">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"> <SelectContent
className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div <div
className="sticky top-0 z-10 border-b border-border bg-popover p-2" className="sticky top-0 z-10 border-b border-border bg-popover p-2"
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
@ -942,7 +955,10 @@ export default function AdvancedEventLabDialog({
<SelectTrigger id="tr-tgt" className="min-w-[220px] max-w-md w-auto"> <SelectTrigger id="tr-tgt" className="min-w-[220px] max-w-md w-auto">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"> <SelectContent
className="max-h-64 min-w-[var(--radix-select-trigger-width)] p-0"
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div <div
className="sticky top-0 z-10 border-b border-border bg-popover p-2" className="sticky top-0 z-10 border-b border-border bg-popover p-2"
onPointerDown={(e) => e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}
@ -1002,34 +1018,36 @@ export default function AdvancedEventLabDialog({
</div> </div>
</div> </div>
<AdvancedEventLabMarkupToolbar markupMode={markupMode} viewRef={markupView} sliceRef={sliceRef} /> <div className="flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-y-contain">
<AdvancedEventLabMarkupToolbar markupMode={markupMode} viewRef={markupView} sliceRef={sliceRef} />
<div className="flex-1 min-h-0 flex flex-col gap-3 px-4 py-2 overflow-hidden lg:flex-row lg:gap-0">
<div className="flex flex-1 min-h-0 min-w-0 flex-col gap-1 lg:pr-3"> <div className="flex min-h-0 flex-1 flex-col gap-0 px-4 py-2 max-lg:grid max-lg:grid-rows-[minmax(0,1fr)_minmax(0,1fr)] lg:flex lg:flex-row lg:py-2">
<span className="text-xs font-medium text-muted-foreground shrink-0"> <div className="flex min-h-0 min-w-0 flex-col gap-2 overflow-hidden max-lg:min-h-0 lg:flex-1 lg:pr-3">
{t( <h3 className="shrink-0 text-left text-sm font-semibold leading-none text-foreground">
markupMode === 'asciidoc' {t(
? 'Advanced lab markup label asciidoc' markupMode === 'asciidoc'
: 'Advanced lab markup label markdown' ? 'Advanced lab markup label asciidoc'
)} : 'Advanced lab markup label markdown'
</span> )}
<div </h3>
ref={markupHost} <div
className="flex-1 min-h-[min(28dvh,14rem)] lg:min-h-[min(42dvh,24rem)] border rounded-md overflow-hidden bg-muted/20" ref={markupHost}
/> className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border bg-muted/20 lg:min-h-[min(42dvh,24rem)]"
</div>
<div className="flex flex-1 min-h-0 min-w-0 flex-col gap-1 border-t border-border pt-3 lg:flex-[0_1_42%] lg:max-w-[min(50%,40rem)] lg:border-l lg:border-t-0 lg:pl-3 lg:pt-0">
<span className="text-xs font-medium text-muted-foreground shrink-0">
{t('Advanced lab preview')}
</span>
<div className="flex-1 min-h-[min(24dvh,12rem)] lg:min-h-0 overflow-y-auto rounded-md border bg-muted/10 px-2 py-2">
<AdvancedEventLabPreviewPane
markupMode={markupMode}
source={previewDoc}
previewAuthorPubkey={previewAuthorPubkey}
previewEmojiTags={mergedLabPreviewEmojiTags}
/> />
</div> </div>
<div className="flex min-h-0 min-w-0 flex-col gap-2 overflow-hidden -mx-4 border-t-2 border-border bg-muted/40 px-4 pb-3 pt-4 max-lg:min-h-0 max-lg:rounded-b-lg lg:mx-0 lg:mt-0 lg:flex-[0_1_42%] lg:max-w-[min(50%,40rem)] lg:rounded-none lg:border-t-0 lg:border-l lg:border-border lg:bg-transparent lg:px-0 lg:pb-0 lg:pt-0 lg:pl-3">
<h3 className="shrink-0 text-left text-sm font-semibold leading-none text-foreground">
{t('Advanced lab preview')}
</h3>
<div className="flex min-h-0 flex-1 overflow-y-auto rounded-md border border-border bg-background py-2 pl-0 pr-0 text-left lg:bg-muted/10 lg:px-2">
<AdvancedEventLabPreviewPane
markupMode={markupMode}
source={previewDoc}
previewAuthorPubkey={previewAuthorPubkey}
previewEmojiTags={mergedLabPreviewEmojiTags}
/>
</div>
</div>
</div> </div>
</div> </div>
@ -1053,7 +1071,7 @@ export default function AdvancedEventLabDialog({
/** Responsive shell: ~5× prior max width cap and ~3× vertical use of viewport (still clamped). */ /** Responsive shell: ~5× prior max width cap and ~3× vertical use of viewport (still clamped). */
function cnDialogShell(): string { function cnDialogShell(): string {
return [ return [
'z-[250] max-w-none flex flex-col gap-0 p-0 overflow-hidden', 'z-[250] max-w-none flex min-h-0 flex-col gap-0 overflow-hidden p-0',
'w-[min(98vw,calc(72rem*5))]', 'w-[min(98vw,calc(72rem*5))]',
'h-[min(94vh,calc(28rem*3))]', 'h-[min(94vh,calc(28rem*3))]',
'max-h-[min(96vh,90dvh)]' 'max-h-[min(96vh,90dvh)]'

444
src/components/AdvancedEventLab/AdvancedEventLabMarkupToolbar.tsx

@ -41,6 +41,7 @@ import {
import type { MutableRefObject } from 'react' import type { MutableRefObject } from 'react'
import { Fragment, useMemo, useState } from 'react' import { Fragment, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
import { import {
labInsertRaw, labInsertRaw,
labInsertRawWithOptionalBlockLeadNl, labInsertRawWithOptionalBlockLeadNl,
@ -123,6 +124,8 @@ export function AdvancedEventLabMarkupToolbar({
const { t } = useTranslation() const { t } = useTranslation()
const [codeFilter, setCodeFilter] = useState('') const [codeFilter, setCodeFilter] = useState('')
const [langFilter, setLangFilter] = useState('') const [langFilter, setLangFilter] = useState('')
/** Code-fence / source-block picker uses plain Buttons; close explicitly after insert (Radix). */
const [codeLangMenuOpen, setCodeLangMenuOpen] = useState(false)
const [citationPickerOpen, setCitationPickerOpen] = useState(false) const [citationPickerOpen, setCitationPickerOpen] = useState(false)
const [citationDisplayType, setCitationDisplayType] = useState<LabCitationDisplayType>('inline') const [citationDisplayType, setCitationDisplayType] = useState<LabCitationDisplayType>('inline')
@ -154,19 +157,29 @@ export function AdvancedEventLabMarkupToolbar({
/> />
) )
const outlineTbClass =
'h-8 shrink-0 gap-1 text-xs max-md:w-9 max-md:min-w-9 max-md:justify-center max-md:gap-0 max-md:px-0'
const citationDropdown = ( const citationDropdown = (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<BookMarked className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb citations')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb citations')}
title={t('Advanced lab tb citations')}
>
<BookMarked className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb citations')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(20rem,92vw)] max-h-80 overflow-y-auto"> <DropdownMenuContent align="start" className="z-[280] w-[min(20rem,92vw)] max-h-80 overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb citationsHint')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb citationsHint')}</DropdownMenuLabel>
{LAB_CITATION_MENU_ITEMS.map(({ type, labelKey }) => ( {LAB_CITATION_MENU_ITEMS.map(({ type, labelKey }) => (
<DropdownMenuItem key={type} onClick={() => openCitationPicker(type)}> <DropdownMenuItem key={type} onSelect={() => openCitationPicker(type)}>
{t(labelKey)} {t(labelKey)}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
@ -206,17 +219,24 @@ export function AdvancedEventLabMarkupToolbar({
if (markupMode === 'markdown') { if (markupMode === 'markdown') {
return ( return (
<Fragment> <Fragment>
<div className="flex flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2"> <div className="flex max-md:sticky max-md:top-0 max-md:z-[25] max-md:bg-muted/95 max-md:backdrop-blur-sm flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2">
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0 mr-1"> <span className="mr-1 hidden shrink-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground sm:inline">
{t('Advanced lab tb markup tools')} {t('Advanced lab tb markup tools')}
</span> </span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Heading className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb headings')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb headings')}
title={t('Advanced lab tb headings')}
>
<Heading className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb headings')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] max-h-80 overflow-y-auto w-56"> <DropdownMenuContent align="start" className="z-[280] max-h-80 overflow-y-auto w-56">
@ -235,7 +255,7 @@ export function AdvancedEventLabMarkupToolbar({
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={labelKey} key={labelKey}
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -251,7 +271,7 @@ export function AdvancedEventLabMarkupToolbar({
})} })}
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -264,7 +284,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb setextH1')} {t('Advanced lab tb setextH1')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -281,34 +301,41 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Type className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb inline')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb inline')}
title={t('Advanced lab tb inline')}
>
<Type className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb inline')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-56"> <DropdownMenuContent align="start" className="z-[280] w-56">
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '**', 'bold'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '**', 'bold'))}>
{t('Advanced lab tb bold')} {t('Advanced lab tb bold')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'italic'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'italic'))}>
{t('Advanced lab tb italic')} {t('Advanced lab tb italic')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '__', 'bold'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '__', 'bold'))}>
{t('Advanced lab tb boldUnderscore')} {t('Advanced lab tb boldUnderscore')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '_', 'italic'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '_', 'italic'))}>
{t('Advanced lab tb italicUnderscore')} {t('Advanced lab tb italicUnderscore')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '~~', 'strikethrough'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '~~', 'strikethrough'))}>
{t('Advanced lab tb strike')} {t('Advanced lab tb strike')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'code'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'code'))}>
{t('Advanced lab tb inlineCode')} {t('Advanced lab tb inlineCode')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '[', 'link text', '](https://example.com)') labInsertSnippet(v, sliceRef, '[', 'link text', '](https://example.com)')
) )
@ -318,7 +345,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb link')} {t('Advanced lab tb link')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '![', 'alt text', '](https://example.com/image.png)') labInsertSnippet(v, sliceRef, '![', 'alt text', '](https://example.com/image.png)')
) )
@ -329,7 +356,7 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -345,7 +372,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb linkTitled')} {t('Advanced lab tb linkTitled')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -361,7 +388,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb imageTitled')} {t('Advanced lab tb imageTitled')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, ' \n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, ' \n'))}>
<Pilcrow className="h-3.5 w-3.5 mr-2 inline" /> <Pilcrow className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb hardBreak')} {t('Advanced lab tb hardBreak')}
</DropdownMenuItem> </DropdownMenuItem>
@ -372,27 +399,34 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<List className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb lists')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb lists')}
title={t('Advanced lab tb lists')}
>
<List className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb lists')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-56"> <DropdownMenuContent align="start" className="z-[280] w-56">
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n- item one\n- item two\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n- item one\n- item two\n'))}>
<List className="h-3.5 w-3.5 mr-2 inline" /> <List className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb bulletList')} {t('Advanced lab tb bulletList')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n* item one\n* item two\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n* item one\n* item two\n'))}>
<List className="h-3.5 w-3.5 mr-2 inline" /> <List className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb bulletListStar')} {t('Advanced lab tb bulletListStar')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n1. first\n2. second\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n1. first\n2. second\n'))}>
<ListOrdered className="h-3.5 w-3.5 mr-2 inline" /> <ListOrdered className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb orderedList')} {t('Advanced lab tb orderedList')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -406,7 +440,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb orderedListStart')} {t('Advanced lab tb orderedListStart')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -424,19 +458,26 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Quote className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb blocks')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb blocks')}
title={t('Advanced lab tb blocks')}
>
<Quote className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb blocks')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-64"> <DropdownMenuContent align="start" className="z-[280] w-64">
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n> quoted line\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n> quoted line\n'))}>
<Quote className="h-3.5 w-3.5 mr-2 inline" /> <Quote className="h-3.5 w-3.5 mr-2 inline" />
{t('Advanced lab tb blockquote')} {t('Advanced lab tb blockquote')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -450,11 +491,11 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb pipeTable')} {t('Advanced lab tb pipeTable')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n[^1]\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n[^1]\n'))}>
{t('Advanced lab tb footnoteRef')} {t('Advanced lab tb footnoteRef')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw(v, sliceRef, '\n[^1]: Footnote text goes here.\n') labInsertRaw(v, sliceRef, '\n[^1]: Footnote text goes here.\n')
) )
@ -465,12 +506,25 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu onOpenChange={(o) => !o && setCodeFilter('')}> <DropdownMenu
open={codeLangMenuOpen}
onOpenChange={(o) => {
setCodeLangMenuOpen(o)
if (!o) setCodeFilter('')
}}
>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Code2 className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb codeBlock')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb codeBlock')}
title={t('Advanced lab tb codeBlock')}
>
<Code2 className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb codeBlock')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2"> <DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2">
@ -493,6 +547,7 @@ export function AdvancedEventLabMarkupToolbar({
onClick={() => { onClick={() => {
mdFence(lang) mdFence(lang)
setCodeFilter('') setCodeFilter('')
setCodeLangMenuOpen(false)
}} }}
> >
{lang} {lang}
@ -505,10 +560,17 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Sigma className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb math')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb math')}
title={t('Advanced lab tb math')}
>
<Sigma className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb math')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto"> <DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto">
@ -516,7 +578,7 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '$', 'x^2 + y^2 = r^2', '$') labInsertSnippet(v, sliceRef, '$', 'x^2 + y^2 = r^2', '$')
) )
@ -525,7 +587,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb mathInline')} {t('Advanced lab tb mathInline')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n$$\n', 'E = mc^2', '\n$$\n') labInsertSnippet(v, sliceRef, '\n$$\n', 'E = mc^2', '\n$$\n')
) )
@ -537,25 +599,25 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel>{t('Advanced lab tb mathCommon')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb mathCommon')}</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\frac{numerator}{denominator}$'))} onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\frac{numerator}{denominator}$'))}
> >
{t('Advanced lab tb katexFrac')} {t('Advanced lab tb katexFrac')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sqrt{x}$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\sqrt{x}$'))}>
{t('Advanced lab tb katexSqrt')} {t('Advanced lab tb katexSqrt')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sum_{i=1}^{n} i$'))} onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\sum_{i=1}^{n} i$'))}
> >
{t('Advanced lab tb katexSum')} {t('Advanced lab tb katexSum')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\int_{a}^{b} f(x)\\,dx$'))} onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\int_{a}^{b} f(x)\\,dx$'))}
> >
{t('Advanced lab tb katexInt')} {t('Advanced lab tb katexInt')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -568,7 +630,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb katexMatrix')} {t('Advanced lab tb katexMatrix')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -582,34 +644,34 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\alpha$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\alpha$'))}>
<code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')} <code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\beta$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\beta$'))}>
<code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')} <code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\gamma$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\gamma$'))}>
<code className="text-xs mr-2">{'\\gamma'}</code> {t('Advanced lab tb greekGamma')} <code className="text-xs mr-2">{'\\gamma'}</code> {t('Advanced lab tb greekGamma')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\delta$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\delta$'))}>
<code className="text-xs mr-2">{'\\delta'}</code> {t('Advanced lab tb greekDelta')} <code className="text-xs mr-2">{'\\delta'}</code> {t('Advanced lab tb greekDelta')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\pi$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\pi$'))}>
<code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')} <code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\theta$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\theta$'))}>
<code className="text-xs mr-2">{'\\theta'}</code> {t('Advanced lab tb greekTheta')} <code className="text-xs mr-2">{'\\theta'}</code> {t('Advanced lab tb greekTheta')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\lambda$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\lambda$'))}>
<code className="text-xs mr-2">{'\\lambda'}</code> {t('Advanced lab tb greekLambda')} <code className="text-xs mr-2">{'\\lambda'}</code> {t('Advanced lab tb greekLambda')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\sigma$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\sigma$'))}>
<code className="text-xs mr-2">{'\\sigma'}</code> {t('Advanced lab tb greekSigma')} <code className="text-xs mr-2">{'\\sigma'}</code> {t('Advanced lab tb greekSigma')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\omega$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\omega$'))}>
<code className="text-xs mr-2">{'\\omega'}</code> {t('Advanced lab tb greekOmega')} <code className="text-xs mr-2">{'\\omega'}</code> {t('Advanced lab tb greekOmega')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '$\\infty$'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '$\\infty$'))}>
<code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')} <code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -621,22 +683,23 @@ export function AdvancedEventLabMarkupToolbar({
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 gap-1 text-xs shrink-0" className="h-8 max-md:w-9 max-md:min-w-9 max-md:justify-center max-md:px-0 gap-1 text-xs shrink-0"
aria-label={t('Advanced lab tb horizontalRules')}
title={t('Advanced lab tb horizontalRules')} title={t('Advanced lab tb horizontalRules')}
> >
<Minus className="h-3.5 w-3.5" /> <Minus className="h-3.5 w-3.5 shrink-0" />
<ChevronDown className="h-3 w-3 opacity-60" /> <ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-48"> <DropdownMenuContent align="start" className="z-[280] w-48">
<DropdownMenuLabel>{t('Advanced lab tb horizontalRules')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb horizontalRules')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n---\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n---\n'))}>
{t('Advanced lab tb hrDashes')} {t('Advanced lab tb hrDashes')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n***\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n***\n'))}>
{t('Advanced lab tb hrAsterisks')} {t('Advanced lab tb hrAsterisks')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n___\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n___\n'))}>
{t('Advanced lab tb hrUnderscores')} {t('Advanced lab tb hrUnderscores')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -650,23 +713,30 @@ export function AdvancedEventLabMarkupToolbar({
/* AsciiDoc */ /* AsciiDoc */
return ( return (
<Fragment> <Fragment>
<div className="flex flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2"> <div className="flex max-md:sticky max-md:top-0 max-md:z-[25] max-md:bg-muted/95 max-md:backdrop-blur-sm flex-wrap items-center gap-1.5 min-w-0 overflow-x-auto border-b bg-muted/30 px-2 py-2">
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0 mr-1"> <span className="mr-1 hidden shrink-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground sm:inline">
{t('Advanced lab tb markup tools')} {t('Advanced lab tb markup tools')}
</span> </span>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Heading className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb adocTitles')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb adocTitles')}
title={t('Advanced lab tb adocTitles')}
>
<Heading className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb adocTitles')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(80vh,32rem)] overflow-y-auto"> <DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(80vh,32rem)] overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb adocTitlesHint')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb adocTitlesHint')}</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRawWithOptionalBlockLeadNl(v, sliceRef, `= ${t('Advanced lab tb documentTitle')}\n`) labInsertRawWithOptionalBlockLeadNl(v, sliceRef, `= ${t('Advanced lab tb documentTitle')}\n`)
) )
@ -674,7 +744,7 @@ export function AdvancedEventLabMarkupToolbar({
> >
{t('Advanced lab tb adocLevel0')} {t('Advanced lab tb adocLevel0')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => adocInsertFullHeader(`= ${t('Advanced lab tb documentTitle')}`)}> <DropdownMenuItem onSelect={() => adocInsertFullHeader(`= ${t('Advanced lab tb documentTitle')}`)}>
{t('Advanced lab tb adocLevel0WithHeader')} {t('Advanced lab tb adocLevel0WithHeader')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
@ -691,7 +761,7 @@ export function AdvancedEventLabMarkupToolbar({
return ( return (
<DropdownMenuItem <DropdownMenuItem
key={labelKey} key={labelKey}
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -710,25 +780,32 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Type className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb inline')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb inline')}
title={t('Advanced lab tb inline')}
>
<Type className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb inline')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-56"> <DropdownMenuContent align="start" className="z-[280] w-56">
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'bold'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '*', 'bold'))}>
{t('Advanced lab tb adocBold')} {t('Advanced lab tb adocBold')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '_', 'italic'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '_', 'italic'))}>
{t('Advanced lab tb adocItalic')} {t('Advanced lab tb adocItalic')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'mono'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '`', 'mono'))}>
{t('Advanced lab tb adocMono')} {t('Advanced lab tb adocMono')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, 'link:https://example.com[', 'link text', ']') labInsertSnippet(v, sliceRef, 'link:https://example.com[', 'link text', ']')
) )
@ -738,7 +815,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocLink')} {t('Advanced lab tb adocLink')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -752,21 +829,21 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocImage')} {t('Advanced lab tb adocImage')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, ' +\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, ' +\n'))}>
{t('Advanced lab tb adocLineBreak')} {t('Advanced lab tb adocLineBreak')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '^', 'sup'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '^', 'sup'))}>
{t('Advanced lab tb adocSuperscript')} {t('Advanced lab tb adocSuperscript')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labWrapOrSnippet(v, sliceRef, '~', 'sub'))}> <DropdownMenuItem onSelect={() => run((v) => labWrapOrSnippet(v, sliceRef, '~', 'sub'))}>
{t('Advanced lab tb adocSubscript')} {t('Advanced lab tb adocSubscript')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, '+++', 'raw or HTML', '+++'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, '+++', 'raw or HTML', '+++'))}
> >
{t('Advanced lab tb adocPassthrough')} {t('Advanced lab tb adocPassthrough')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'footnote:[Footnote text]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'footnote:[Footnote text]'))}>
{t('Advanced lab tb adocFootnote')} {t('Advanced lab tb adocFootnote')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -776,25 +853,32 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<List className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb lists')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb lists')}
title={t('Advanced lab tb lists')}
>
<List className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb lists')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-56"> <DropdownMenuContent align="start" className="z-[280] w-56">
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n* item one\n* item two\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n* item one\n* item two\n'))}>
{t('Advanced lab tb adocUnordered')} {t('Advanced lab tb adocUnordered')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n. first step\n. second step\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n. first step\n. second step\n'))}>
{t('Advanced lab tb adocOrdered')} {t('Advanced lab tb adocOrdered')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\nterm:: definition line\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\nterm:: definition line\n'))}>
{t('Advanced lab tb adocLabeled')} {t('Advanced lab tb adocLabeled')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -811,15 +895,22 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Pilcrow className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb adocBlocks')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb adocBlocks')}
title={t('Advanced lab tb adocBlocks')}
>
<Pilcrow className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb adocBlocks')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-64"> <DropdownMenuContent align="start" className="z-[280] w-64">
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n____\n', 'Quoted paragraph', '\n____\n') labInsertSnippet(v, sliceRef, '\n____\n', 'Quoted paragraph', '\n____\n')
) )
@ -828,7 +919,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocQuote')} {t('Advanced lab tb adocQuote')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n....\n', 'Literal monospace block', '\n....\n') labInsertSnippet(v, sliceRef, '\n....\n', 'Literal monospace block', '\n....\n')
) )
@ -838,7 +929,7 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -853,7 +944,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocNote')} {t('Advanced lab tb adocNote')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -868,7 +959,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocTip')} {t('Advanced lab tb adocTip')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -883,7 +974,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocWarning')} {t('Advanced lab tb adocWarning')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -898,7 +989,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocImportant')} {t('Advanced lab tb adocImportant')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -914,7 +1005,7 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -929,7 +1020,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocExampleBlock')} {t('Advanced lab tb adocExampleBlock')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n****\n', 'Sidebar body', '\n****\n') labInsertSnippet(v, sliceRef, '\n****\n', 'Sidebar body', '\n****\n')
) )
@ -938,7 +1029,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocSidebar')} {t('Advanced lab tb adocSidebar')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet( labInsertSnippet(
v, v,
@ -953,7 +1044,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocListing')} {t('Advanced lab tb adocListing')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n--\n', 'Open block body', '\n--\n') labInsertSnippet(v, sliceRef, '\n--\n', 'Open block body', '\n--\n')
) )
@ -966,16 +1057,23 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Anchor className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb adocStructure')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb adocStructure')}
title={t('Advanced lab tb adocStructure')}
>
<Anchor className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb adocStructure')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto"> <DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb adocStructureHint')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb adocStructureHint')}</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -989,7 +1087,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocTable')} {t('Advanced lab tb adocTable')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => labInsertRawWithOptionalBlockLeadNl(v, sliceRef, '[#section-anchor]\n')) run((v) => labInsertRawWithOptionalBlockLeadNl(v, sliceRef, '[#section-anchor]\n'))
} }
> >
@ -997,7 +1095,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocAnchor')} {t('Advanced lab tb adocAnchor')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '<<section-anchor,', 'link label', '>>') labInsertSnippet(v, sliceRef, '<<section-anchor,', 'link label', '>>')
) )
@ -1008,7 +1106,7 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw(v, sliceRef, '\nvideo::https://example.com/video.mp4[width=640]\n') labInsertRaw(v, sliceRef, '\nvideo::https://example.com/video.mp4[width=640]\n')
) )
@ -1018,7 +1116,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocVideo')} {t('Advanced lab tb adocVideo')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw(v, sliceRef, '\naudio::https://example.com/audio.mp3[]\n') labInsertRaw(v, sliceRef, '\naudio::https://example.com/audio.mp3[]\n')
) )
@ -1028,29 +1126,42 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb adocAudio')} {t('Advanced lab tb adocAudio')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, '\n// Comment line\n'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, '\n// Comment line\n'))}>
{t('Advanced lab tb adocComment')} {t('Advanced lab tb adocComment')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'kbd:[Ctrl+T]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'kbd:[Ctrl+T]'))}>
{t('Advanced lab tb adockbd')} {t('Advanced lab tb adockbd')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertRaw(v, sliceRef, 'menu:View[Zoom > In]'))} onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'menu:View[Zoom > In]'))}
> >
{t('Advanced lab tb adocMenu')} {t('Advanced lab tb adocMenu')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'btn:[OK]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'btn:[OK]'))}>
{t('Advanced lab tb adocBtn')} {t('Advanced lab tb adocBtn')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu onOpenChange={(o) => !o && setLangFilter('')}> <DropdownMenu
open={codeLangMenuOpen}
onOpenChange={(o) => {
setCodeLangMenuOpen(o)
if (!o) setLangFilter('')
}}
>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Braces className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb adocSource')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb adocSource')}
title={t('Advanced lab tb adocSource')}
>
<Braces className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb adocSource')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2"> <DropdownMenuContent align="start" className="z-[280] w-[min(22rem,92vw)] p-2">
@ -1075,6 +1186,7 @@ export function AdvancedEventLabMarkupToolbar({
onClick={() => { onClick={() => {
adocSource(lang) adocSource(lang)
setLangFilter('') setLangFilter('')
setCodeLangMenuOpen(false)
}} }}
> >
{lang} {lang}
@ -1087,26 +1199,33 @@ export function AdvancedEventLabMarkupToolbar({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button type="button" variant="outline" size="sm" className="h-8 gap-1 text-xs shrink-0"> <Button
<Sigma className="h-3.5 w-3.5" /> type="button"
{t('Advanced lab tb adocStem')} variant="outline"
<ChevronDown className="h-3 w-3 opacity-60" /> size="sm"
className={cn(outlineTbClass)}
aria-label={t('Advanced lab tb adocStem')}
title={t('Advanced lab tb adocStem')}
>
<Sigma className="h-3.5 w-3.5 shrink-0" />
<span className="hidden md:inline">{t('Advanced lab tb adocStem')}</span>
<ChevronDown className="hidden h-3 w-3 shrink-0 opacity-60 md:inline-block" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto"> <DropdownMenuContent align="start" className="z-[280] w-[min(24rem,92vw)] max-h-[min(70vh,28rem)] overflow-y-auto">
<DropdownMenuLabel>{t('Advanced lab tb adocStemHint')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb adocStemHint')}</DropdownMenuLabel>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', 'x^2 + y^2', ']'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', 'x^2 + y^2', ']'))}
> >
{t('Advanced lab tb adocStemInline')} {t('Advanced lab tb adocStemInline')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'latexmath:[', 'x^2 + y^2', ']'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'latexmath:[', 'x^2 + y^2', ']'))}
> >
{t('Advanced lab tb adocLatexmathInline')} {t('Advanced lab tb adocLatexmathInline')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertSnippet(v, sliceRef, '\n[stem]\n++++\n', 'E = mc^2', '\n++++\n') labInsertSnippet(v, sliceRef, '\n[stem]\n++++\n', 'E = mc^2', '\n++++\n')
) )
@ -1116,25 +1235,25 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\frac{a}{b}', ']'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\frac{a}{b}', ']'))}
> >
{t('Advanced lab tb katexFrac')} {t('Advanced lab tb katexFrac')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sqrt{x}', ']'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sqrt{x}', ']'))}>
{t('Advanced lab tb katexSqrt')} {t('Advanced lab tb katexSqrt')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sum_{i=1}^{n} i', ']'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\sum_{i=1}^{n} i', ']'))}
> >
{t('Advanced lab tb katexSum')} {t('Advanced lab tb katexSum')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\int_{a}^{b} f(x)\\,dx', ']'))} onSelect={() => run((v) => labInsertSnippet(v, sliceRef, 'stem:[', '\\int_{a}^{b} f(x)\\,dx', ']'))}
> >
{t('Advanced lab tb katexInt')} {t('Advanced lab tb katexInt')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -1147,7 +1266,7 @@ export function AdvancedEventLabMarkupToolbar({
{t('Advanced lab tb katexMatrix')} {t('Advanced lab tb katexMatrix')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => onSelect={() =>
run((v) => run((v) =>
labInsertRaw( labInsertRaw(
v, v,
@ -1161,16 +1280,16 @@ export function AdvancedEventLabMarkupToolbar({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel> <DropdownMenuLabel>{t('Advanced lab tb mathGreek')}</DropdownMenuLabel>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\alpha]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\alpha]'))}>
<code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')} <code className="text-xs mr-2">{'\\alpha'}</code> {t('Advanced lab tb greekAlpha')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\beta]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\beta]'))}>
<code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')} <code className="text-xs mr-2">{'\\beta'}</code> {t('Advanced lab tb greekBeta')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\pi]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\pi]'))}>
<code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')} <code className="text-xs mr-2">{'\\pi'}</code> {t('Advanced lab tb greekPi')}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\infty]'))}> <DropdownMenuItem onSelect={() => run((v) => labInsertRaw(v, sliceRef, 'stem:[\\infty]'))}>
<code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')} <code className="text-xs mr-2">{'\\infty'}</code> {t('Advanced lab tb greekInfty')}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
@ -1180,11 +1299,12 @@ export function AdvancedEventLabMarkupToolbar({
type="button" type="button"
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 text-xs shrink-0" className="h-8 max-md:w-9 max-md:min-w-9 max-md:justify-center max-md:px-0 text-xs shrink-0"
aria-label={t('Advanced lab tb adocHrTitle')}
title={t('Advanced lab tb adocHrTitle')} title={t('Advanced lab tb adocHrTitle')}
onClick={() => run((v) => labInsertRaw(v, sliceRef, "\n'''\n"))} onClick={() => run((v) => labInsertRaw(v, sliceRef, "\n'''\n"))}
> >
<Minus className="h-3.5 w-3.5" /> <Minus className="h-3.5 w-3.5 shrink-0" />
</Button> </Button>
</div> </div>
{citationPicker} {citationPicker}

4
src/components/AdvancedEventLab/AdvancedEventLabPreviewPane.tsx

@ -37,13 +37,13 @@ export const AdvancedEventLabPreviewPane = memo(function AdvancedEventLabPreview
if (!source.trim()) { if (!source.trim()) {
return ( return (
<p className="text-sm text-muted-foreground px-1 py-2">{t('Advanced lab preview empty')}</p> <p className="px-0 py-2 text-left text-sm text-muted-foreground">{t('Advanced lab preview empty')}</p>
) )
} }
return ( return (
<Card className="border-0 bg-transparent p-0 shadow-none"> <Card className="border-0 bg-transparent p-0 shadow-none">
<div className="select-text max-w-none text-sm"> <div className="select-text min-w-0 max-w-none text-left text-sm">
{markupMode === 'asciidoc' ? ( {markupMode === 'asciidoc' ? (
<AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} /> <AsciidocArticle event={fakeEvent} hideImagesAndInfo={false} />
) : ( ) : (

62
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -3124,6 +3124,27 @@ function stripMarkdownGreentextMarker(trimmedLine: string): string {
return trimmedLine.replace(/^>(?! )/, '') return trimmedLine.replace(/^>(?! )/, '')
} }
/**
* Marked leaves unclosed `![alt](url` / `![alt](url "title` as a junk paragraph (text + autolink + text).
* When the whole trimmed paragraph is that fragment, recover alt, URL, and optional title.
*/
function tryRecoverMalformedMarkdownImageParagraph(
paragraphTrimmed: string
): { alt: string; href: string; title?: string } | null {
const m = paragraphTrimmed.match(/^!\[([^\]]*)\]\((https?:\/\/[^)\s]+)(.*)$/i)
if (!m) return null
const alt = m[1] ?? ''
const href = m[2] ?? ''
const tail = m[3] ?? ''
if (tail === '' || tail === ')') return { alt, href }
const fullTitled = /^\s+"([^"]*)"\s*\)\s*$/.exec(tail)
if (fullTitled) return { alt, href, title: fullTitled[1] }
const partialTitle = /^\s+"([^"]*)$/.exec(tail)
if (partialTitle) return { alt, href, title: partialTitle[1] }
if (/^\s*\)\s*$/.test(tail)) return { alt, href }
return null
}
/** /**
* Marked-driven markdown renderer (standard markdown blocks/inline), while keeping * Marked-driven markdown renderer (standard markdown blocks/inline), while keeping
* Nostr-specific enrichments (embeds, wikilinks, relay/profile navigation) custom. * Nostr-specific enrichments (embeds, wikilinks, relay/profile navigation) custom.
@ -3452,6 +3473,43 @@ function parseMarkdownContentMarked(
/> />
) )
} }
const recoveredMdImage = tryRecoverMalformedMarkdownImageParagraph(paragraphText)
if (recoveredMdImage) {
const cleaned = cleanUrl(recoveredMdImage.href)
if (cleaned && isImage(cleaned) && isSafeMediaUrl(cleaned)) {
const baseImeta = imetaInfoForStandaloneImageUrl(cleaned)
let imageIdx = imageIndexMap.get(cleaned)
if (imageIdx === undefined && getImageIdentifier) {
const id = getImageIdentifier(cleaned)
if (id) imageIdx = imageIndexMap.get(`__img_id:${id}`)
}
const alt = recoveredMdImage.alt || 'image'
const imageTip =
recoveredMdImage.title && recoveredMdImage.title.trim().length > 0
? recoveredMdImage.title.trim()
: undefined
return (
<div key={`${key}-recovered-md-img`} className="my-2 not-prose block max-w-[400px] mx-auto">
<Image
image={{ ...baseImeta, url: recoveredMdImage.href }}
alt={alt}
tooltipTitle={imageTip}
showAltCaption={Boolean(recoveredMdImage.alt.trim())}
className="w-full rounded-lg cursor-zoom-in"
classNames={{
wrapper: 'not-prose my-2 block max-w-[400px] mx-auto rounded-lg w-full',
errorPlaceholder: 'aspect-square h-[30vh]'
}}
holdUntilClick={lazyMedia}
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
if (typeof imageIdx === 'number') openLightbox(imageIdx)
}}
/>
</div>
)
}
}
const isNostrEventBech32 = (value: string): boolean => const isNostrEventBech32 = (value: string): boolean =>
value.startsWith('note') || value.startsWith('nevent') || value.startsWith('naddr') value.startsWith('note') || value.startsWith('nevent') || value.startsWith('naddr')
const standaloneNostr = paragraphText.match(/^nostr:([a-z0-9]{8,})$/i) const standaloneNostr = paragraphText.match(/^nostr:([a-z0-9]{8,})$/i)
@ -5762,7 +5820,9 @@ export default function MarkdownArticle({
color: #5eead4 !important; color: #5eead4 !important;
} }
`}</style> `}</style>
<div className={`prose prose-zinc max-w-none dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}> <div
className={`prose prose-zinc max-w-none min-w-0 dark:prose-invert break-words overflow-wrap-anywhere ${className || ''}`}
>
{iArticleUrl && !suppressITagArticleWebPreview && ( {iArticleUrl && !suppressITagArticleWebPreview && (
<div className="not-prose mb-4 max-w-full"> <div className="not-prose mb-4 max-w-full">
<WebPreview url={iArticleUrl} className="w-full" /> <WebPreview url={iArticleUrl} className="w-full" />

55
src/components/StoredAccountSwitchSelect.tsx

@ -1,14 +1,6 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import UserAvatar from '@/components/UserAvatar' import UserAvatar from '@/components/UserAvatar'
import Username from '@/components/Username'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -26,6 +18,10 @@ type Props = {
/** /**
* Switch {@link useNostr} session among stored accounts (same as notifications spell). * Switch {@link useNostr} session among stored accounts (same as notifications spell).
* Renders nothing when there is only one stored account or no session. * Renders nothing when there is only one stored account or no session.
*
* Uses a native {@link HTMLSelectElement} instead of Radix Select: nested `UserAvatar` /
* `Username` inside `SelectItem` composes refs in ways that have triggered
* Maximum update depth exceeded on this page (Radix `compose-refs` + frequent re-renders).
*/ */
export default function StoredAccountSwitchSelect({ export default function StoredAccountSwitchSelect({
className, className,
@ -59,8 +55,8 @@ export default function StoredAccountSwitchSelect({
const handlePick = useCallback( const handlePick = useCallback(
async (v: string) => { async (v: string) => {
const target = normalizeHexPubkey(v) const target = normalizeHexPubkey(v)
if (pubkey && hexPubkeysEqual(target, pubkey)) return if (pubkey && hexPubkeysEqual(target, normalizeHexPubkey(pubkey))) return
const nextAccount = accounts.find((a) => hexPubkeysEqual(a.pubkey, target)) const nextAccount = accounts.find((a) => hexPubkeysEqual(normalizeHexPubkey(a.pubkey), target))
if (!nextAccount) { if (!nextAccount) {
toast.error(t('notificationsSwitchAccountFailed')) toast.error(t('notificationsSwitchAccountFailed'))
return return
@ -91,32 +87,23 @@ export default function StoredAccountSwitchSelect({
> >
{t('notificationsViewAsAccount')} {t('notificationsViewAsAccount')}
</span> </span>
<Select <UserAvatar userId={sessionPubkey} size="small" className="shrink-0" />
<select
className={cn(
'h-9 min-w-0 flex-1 cursor-pointer rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm ring-offset-background focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
triggerClassName
)}
value={sessionPubkey} value={sessionPubkey}
disabled={isAccountSessionHydrating} disabled={isAccountSessionHydrating}
onValueChange={(v) => void handlePick(v)} aria-label={t('notificationsViewAsAccountAria')}
onChange={(e) => void handlePick(e.target.value)}
> >
<SelectTrigger {storedAccountPubkeys.map((pk) => (
className={cn('h-9 min-w-0 flex-1', triggerClassName)} <option key={pk} value={pk}>
aria-label={t('notificationsViewAsAccountAria')} {formatPubkey(pk)}
> </option>
<SelectValue /> ))}
</SelectTrigger> </select>
<SelectContent position="popper">
{storedAccountPubkeys.map((pk) => (
<SelectItem key={pk} value={pk}>
<span className="flex min-w-0 items-center gap-2">
<UserAvatar userId={pk} size="small" className="shrink-0" />
<Username
userId={pk}
className="min-w-0 truncate text-left font-normal"
skeletonClassName="h-4 w-24"
/>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
) )
} }

29
src/hooks/useFetchEvent.tsx

@ -19,12 +19,16 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) {
}, []) }, [])
useEffect(() => { useEffect(() => {
let cancelled = false
if (!eventId) { if (!eventId) {
setIsFetching(false) setIsFetching(false)
setEvent(undefined) setEvent(undefined)
// Do not setError here: this effect re-runs when callback deps (e.g. addReplies) change identity; // Do not setError here: this effect re-runs when callback deps (e.g. addReplies) change identity;
// allocating a new Error each time would force updates and can exceed React's max update depth. // allocating a new Error each time would force updates and can exceed React's max update depth.
return return () => {
cancelled = true
}
} }
const skipShortcuts = refetchToken > 0 const skipShortcuts = refetchToken > 0
@ -47,7 +51,9 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) {
addReplies([initialEvent]) addReplies([initialEvent])
setIsFetching(false) setIsFetching(false)
} }
return return () => {
cancelled = true
}
} }
// Check navigation event store first (events passed through navigation) // Check navigation event store first (events passed through navigation)
@ -57,7 +63,9 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) {
setEvent(navigationEvent) setEvent(navigationEvent)
addReplies([navigationEvent]) addReplies([navigationEvent])
setIsFetching(false) setIsFetching(false)
return return () => {
cancelled = true
}
} }
} }
@ -71,18 +79,27 @@ export function useFetchEvent(eventId?: string, initialEvent?: Event) {
skipShortcuts skipShortcuts
? await eventService.fetchEventForceRetry(eventId) ? await eventService.fetchEventForceRetry(eventId)
: await eventService.fetchEvent(eventId) : await eventService.fetchEvent(eventId)
if (cancelled) return
if (fetchedEvent && !isEventDeleted(fetchedEvent)) { if (fetchedEvent && !isEventDeleted(fetchedEvent)) {
setEvent(fetchedEvent) setEvent(fetchedEvent)
addReplies([fetchedEvent]) addReplies([fetchedEvent])
} }
} catch (error) { } catch (error) {
setError(error as Error) if (!cancelled) {
setError(error as Error)
}
} finally { } finally {
setIsFetching(false) if (!cancelled) {
setIsFetching(false)
}
} }
} }
fetchEvent() void fetchEvent()
return () => {
cancelled = true
}
}, [eventId, initialEvent, isEventDeleted, addReplies, refetchToken]) }, [eventId, initialEvent, isEventDeleted, addReplies, refetchToken])
useEffect(() => { useEffect(() => {

58
src/lib/event-kind1111-parent.test.ts

@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest'
import { nip19 } from 'nostr-tools'
import { getParentBech32Id, getParentEventHexId, getRootEventHexId } from './event'
/** Kind 1111 sample: E/e point at a kind-1 parent; must not resolve parent hex to the comment id. */
const fiatjafCommentSample = {
id: '10144b660d2aaf4eb65f5e60e7cc9d5e3fed7854073f57c1773929837d332dc1',
pubkey: '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d',
kind: 1111,
created_at: 1776511298,
content: 'x',
sig: '0'.repeat(128),
tags: [
[
'E',
'2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99',
'',
'1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb'
],
['P', '1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb'],
['K', '1'],
[
'e',
'2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99',
'',
'1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb'
],
['k', '1'],
['p', '1f79058c77a224e5be226c8f024cacdad4d741855d75ed9f11473ba8eb86e1cb']
]
} as const
describe('kind 1111 parent / root resolution', () => {
it('resolves parent and root hex to the threaded kind-1 id, not the comment id', () => {
const ev = { ...fiatjafCommentSample } as any
expect(getParentEventHexId(ev)).toBe(
'2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99'
)
expect(getRootEventHexId(ev)).toBe(
'2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99'
)
expect(getParentEventHexId(ev)).not.toBe(ev.id)
})
it('parent bech32 decodes to the parent hex id', () => {
const ev = { ...fiatjafCommentSample } as any
const parentBech32 = getParentBech32Id(ev)
expect(parentBech32).toBeTruthy()
const decoded = nip19.decode(parentBech32!)
expect(decoded.type).toBe('nevent')
if (decoded.type === 'nevent') {
expect(decoded.data.id).toBe(
'2c88e6bdf1d51d52037078624b21f07eefd86f3413be78efdb64e4931bb6bc99'
)
expect(decoded.data.id).not.toBe(ev.id)
}
})
})

25
src/lib/relay-url-priority.test.ts

@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest'
import { dedupeNormalizeRelayUrlsOrdered, filterContextAuthorReadRelaysForPublish } from '@/lib/relay-url-priority'
describe('filterContextAuthorReadRelaysForPublish', () => {
it('drops loopback, LAN, and .onion; keeps public relays', () => {
const out = filterContextAuthorReadRelaysForPublish([
'ws://localhost:4869/',
'wss://127.0.0.1/',
'wss://192.168.0.5/',
'wss://abcdefghijklmnop.onion/',
'wss://relay.example.com/'
])
expect(out).toEqual(['wss://relay.example.com/'])
})
it('dedupes like dedupeNormalizeRelayUrlsOrdered', () => {
const a = dedupeNormalizeRelayUrlsOrdered(['wss://relay.example.com/', 'wss://relay.example.com/'])
const b = filterContextAuthorReadRelaysForPublish([
'wss://relay.example.com/',
'wss://relay.example.com/',
'ws://localhost:4869/'
])
expect(b).toEqual(a)
})
})

21
src/lib/relay-url-priority.ts

@ -21,6 +21,25 @@ export function dedupeNormalizeRelayUrlsOrdered(urls: string[]): string[] {
return out return out
} }
/**
* NIP-65 **read** (inbox) hints from reply/mention context must never add LAN, loopback, or Tor-only
* endpoints to the publish list those are the author's private reachability, not yours.
*/
export function filterContextAuthorReadRelaysForPublish(urls: string[]): string[] {
return dedupeNormalizeRelayUrlsOrdered(urls).filter((u) => {
const n = normalizeAnyRelayUrl(u) || u.trim()
if (!n) return false
if (isLocalNetworkUrl(u) || isLocalNetworkUrl(n)) return false
try {
const host = new URL(n).hostname
if (host.endsWith('.onion')) return false
} catch {
return false
}
return true
})
}
/** LAN / local host relays first, then the rest; deduped. */ /** LAN / local host relays first, then the rest; deduped. */
export function relayUrlsLocalsFirst(urls: string[]): string[] { export function relayUrlsLocalsFirst(urls: string[]): string[] {
const local: string[] = [] const local: string[] = []
@ -171,7 +190,7 @@ function buildWriteRelayPriorityLayers(opts: {
extraRelays?: string[] extraRelays?: string[]
}): string[][] { }): string[][] {
const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays) const tier1 = relayUrlsLocalsFirst(opts.userWriteRelays)
const tier2 = dedupeNormalizeRelayUrlsOrdered(opts.authorReadRelays ?? []) const tier2 = filterContextAuthorReadRelaysForPublish(opts.authorReadRelays ?? [])
const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? []) const tier3 = dedupeNormalizeRelayUrlsOrdered(opts.favoriteRelays ?? [])
const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? []) const tier4 = dedupeNormalizeRelayUrlsOrdered(opts.extraRelays ?? [])
const tier5 = normFastWrite() const tier5 = normFastWrite()

52
src/pages/secondary/NotePage/index.tsx

@ -14,7 +14,13 @@ import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchEvent, useFetchProfile } from '@/hooks' import { useFetchEvent, useFetchProfile } from '@/hooks'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { getParentBech32Id, getParentETag, getRootBech32Id } from '@/lib/event' import {
getParentBech32Id,
getParentETag,
getParentEventHexId,
getRootBech32Id,
getRootEventHexId
} from '@/lib/event'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList } from '@/lib/link' import { toNote, toNoteList } from '@/lib/link'
import { tagNameEquals } from '@/lib/tag' import { tagNameEquals } from '@/lib/tag'
@ -109,8 +115,18 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
const [externalEvent, setExternalEvent] = useState<Event | undefined>(undefined) const [externalEvent, setExternalEvent] = useState<Event | undefined>(undefined)
const finalEvent = event || externalEvent const finalEvent = event || externalEvent
const parentEventId = useMemo(() => getParentBech32Id(finalEvent), [finalEvent]) const parentEventId = useMemo(() => {
const rootEventId = useMemo(() => getRootBech32Id(finalEvent), [finalEvent]) if (!finalEvent) return undefined
const parentHex = getParentEventHexId(finalEvent)?.toLowerCase()
if (parentHex && parentHex === finalEvent.id.toLowerCase()) return undefined
return getParentBech32Id(finalEvent)
}, [finalEvent])
const rootEventId = useMemo(() => {
if (!finalEvent) return undefined
const rootHex = getRootEventHexId(finalEvent)?.toLowerCase()
if (rootHex && rootHex === finalEvent.id.toLowerCase()) return undefined
return getRootBech32Id(finalEvent)
}, [finalEvent])
const rootITag = useMemo( const rootITag = useMemo(
() => (finalEvent?.kind === ExtendedKind.COMMENT ? finalEvent.tags.find(tagNameEquals('I')) : undefined), () => (finalEvent?.kind === ExtendedKind.COMMENT ? finalEvent.tags.find(tagNameEquals('I')) : undefined),
[finalEvent] [finalEvent]
@ -120,6 +136,12 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
const { isFetching: isFetchingParentEvent, event: parentEvent, refetch: refetchParent } = const { isFetching: isFetchingParentEvent, event: parentEvent, refetch: refetchParent } =
useFetchEvent(parentEventId) useFetchEvent(parentEventId)
const selfHex = finalEvent?.id?.toLowerCase()
const rootEventForStrip =
rootEvent && selfHex && rootEvent.id.toLowerCase() !== selfHex ? rootEvent : undefined
const parentEventForStrip =
parentEvent && selfHex && parentEvent.id.toLowerCase() !== selfHex ? parentEvent : undefined
// When viewing a kind-24 invite (e.g. from notifications), extract calendar event naddr from content and show full calendar card with RSVP // When viewing a kind-24 invite (e.g. from notifications), extract calendar event naddr from content and show full calendar card with RSVP
const calendarInviteNaddr = useMemo(() => { const calendarInviteNaddr = useMemo(() => {
if (finalEvent?.kind !== ExtendedKind.PUBLIC_MESSAGE || !finalEvent.content?.trim()) return undefined if (finalEvent?.kind !== ExtendedKind.PUBLIC_MESSAGE || !finalEvent.content?.trim()) return undefined
@ -456,20 +478,22 @@ const NotePage = forwardRef(({ id, index, hideTitlebar = false, initialEvent }:
> >
<div className="px-4 pt-3 w-full"> <div className="px-4 pt-3 w-full">
{rootITag && <ExternalRoot value={rootITag[1]} />} {rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId && rootEventId !== parentEventId && ( {rootEventId &&
<ParentNote rootEventId !== parentEventId &&
key={`root-note-${finalEvent.id}`} (isFetchingRootEvent || rootEventForStrip) && (
isFetching={isFetchingRootEvent} <ParentNote
event={rootEvent} key={`root-note-${finalEvent.id}`}
eventBech32Id={rootEventId} isFetching={isFetchingRootEvent}
isConsecutive={isConsecutive(rootEvent, parentEvent)} event={rootEventForStrip}
/> eventBech32Id={rootEventId}
)} isConsecutive={isConsecutive(rootEventForStrip, parentEventForStrip)}
{parentEventId && ( />
)}
{parentEventId && (isFetchingParentEvent || parentEventForStrip) && (
<ParentNote <ParentNote
key={`parent-note-${finalEvent.id}`} key={`parent-note-${finalEvent.id}`}
isFetching={isFetchingParentEvent} isFetching={isFetchingParentEvent}
event={parentEvent} event={parentEventForStrip}
eventBech32Id={parentEventId} eventBech32Id={parentEventId}
/> />
)} )}

6
src/services/client.service.ts

@ -101,6 +101,7 @@ import { getPubkeysFromPTags, tagNameEquals } from '@/lib/tag'
import { import {
buildPrioritizedWriteRelayUrls, buildPrioritizedWriteRelayUrls,
dedupeNormalizeRelayUrlsOrdered, dedupeNormalizeRelayUrlsOrdered,
filterContextAuthorReadRelaysForPublish,
mergeRelayPriorityLayers, mergeRelayPriorityLayers,
relayUrlsLocalsFirst relayUrlsLocalsFirst
} from '@/lib/relay-url-priority' } from '@/lib/relay-url-priority'
@ -621,7 +622,12 @@ class ClientService extends EventTarget {
const n = normalizeUrl(u) || u const n = normalizeUrl(u) || u
if (n) authorReadSet.add(n) if (n) authorReadSet.add(n)
} }
for (const u of list?.httpRead ?? []) {
const n = normalizeHttpRelayUrl(u) || u
if (n) authorReadSet.add(n)
}
} }
authorReadSet = new Set(filterContextAuthorReadRelaysForPublish([...authorReadSet]))
} }
const favSet = new Set( const favSet = new Set(

Loading…
Cancel
Save