From 1aff6d8eaa14bef244d30f9740689d4d7b93cd68 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Fri, 22 May 2026 10:42:35 +0200 Subject: [PATCH] bug-fixes for new post editor --- .../AdvancedEventLabDialog.tsx | 69 +++++++++++++++++-- .../NoteOptions/EditOrCloneEventDialog.tsx | 1 + src/components/PostEditor/PostContent.tsx | 1 + .../PostEditor/PostTextarea/index.tsx | 18 ++++- src/i18n/locales/en.ts | 14 +--- src/lib/advanced-event-lab-slice.test.ts | 23 ++++++- src/lib/advanced-event-lab-slice.ts | 38 ++++++++++ 7 files changed, 145 insertions(+), 19 deletions(-) diff --git a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx index 696d2bdb..ea1c3eb6 100644 --- a/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx +++ b/src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx @@ -25,7 +25,11 @@ import { } from '@/lib/language-display-meta' import { LanguageSelectOptionLines } from '@/lib/language-select-option-lines' import { buildLabLanguageToolPreferenceList } from '@/lib/trinity-languages' -import { parseLabSlice, type AdvancedEventLabSlice } from '@/lib/advanced-event-lab-slice' +import { + parseLabSlice, + serializePublishPreviewLabJson, + type AdvancedEventLabSlice +} from '@/lib/advanced-event-lab-slice' import { translateAdvancedLabMarkup } from '@/lib/advanced-lab-markup-protect' import { warmTranslateLanguagesOnce, @@ -195,6 +199,8 @@ export type AdvancedEventLabDialogProps = { previewAuthorPubkey?: string | null /** Lab preview: `emoji` tags on the fake event (e.g. copied from the event being edited). */ previewEmojiTags?: string[][] + /** When true (default), JSON preview includes the Imwald `client` tag like publish. */ + addClientTag?: boolean } function useDarkModeFlag(): boolean { @@ -227,7 +233,8 @@ export default function AdvancedEventLabDialog({ formatToolbar, draftPersistenceKey = null, previewAuthorPubkey = null, - previewEmojiTags + previewEmojiTags, + addClientTag = true }: AdvancedEventLabDialogProps) { const { t, i18n } = useTranslation() /** `useTranslation().t` can change identity every render; never list it as a layout-effect dep (editor remount loop). */ @@ -248,7 +255,11 @@ export default function AdvancedEventLabDialog({ const LAB_DRAFT_DEBOUNCE_MS = 500 const [previewDoc, setPreviewDoc] = useState('') - const [labBodyTab, setLabBodyTab] = useState<'edit' | 'preview'>('edit') + const [labBodyTab, setLabBodyTab] = useState<'edit' | 'preview' | 'json'>('edit') + const labBodyTabRef = useRef(labBodyTab) + labBodyTabRef.current = labBodyTab + const [labJsonPreview, setLabJsonPreview] = useState('') + const refreshLabJsonPreviewRef = useRef<() => void>(() => {}) const [labTagRows, setLabTagRows] = useState(() => [newComposerTagRow()]) /** Stable while payload matches; avoids remounting the editor when the parent passes a new `initial` object reference. */ @@ -290,6 +301,30 @@ export default function AdvancedEventLabDialog({ setPreviewDoc(doc) }, []) + const refreshLabJsonPreview = useCallback(() => { + const s = sliceRef.current + if (!s) { + setLabJsonPreview('{}') + return + } + const content = markupView.current?.state.doc.toString() ?? s.content + const kind = kindEditable ? s.kind : (initial?.kind ?? s.kind) + setLabJsonPreview( + serializePublishPreviewLabJson( + { + kind, + content, + tags: editableRowsToLabTags(labTagRows) + }, + { addClientTag } + ) + ) + }, [kindEditable, initial, labTagRows, addClientTag]) + + useEffect(() => { + refreshLabJsonPreviewRef.current = refreshLabJsonPreview + }, [refreshLabJsonPreview]) + useEffect(() => { schedulePreviewUpdateRef.current = schedulePreviewUpdate }, [schedulePreviewUpdate]) @@ -354,11 +389,17 @@ export default function AdvancedEventLabDialog({ previewDebounceTimerRef.current = null } setPreviewDoc('') + setLabJsonPreview('') } else { setLabBodyTab('edit') } }, [open]) + useEffect(() => { + if (!open || labBodyTab !== 'json') return + refreshLabJsonPreview() + }, [open, labBodyTab, labTagRows, refreshLabJsonPreview]) + const handleDialogOpenChange = useCallback( (next: boolean) => { if (!next) { @@ -400,6 +441,7 @@ export default function AdvancedEventLabDialog({ s.tags = editableRowsToLabTags(rows) scheduleLabDraftPersist() bumpUndoUi() + if (labBodyTabRef.current === 'json') refreshLabJsonPreviewRef.current() }, [scheduleLabDraftPersist, bumpUndoUi] ) @@ -686,6 +728,7 @@ export default function AdvancedEventLabDialog({ s.content = content schedulePreviewUpdateRef.current(content) scheduleLabDraftPersist() + if (labBodyTabRef.current === 'json') refreshLabJsonPreviewRef.current() }) ] if (isLanguageToolConfigured()) { @@ -914,8 +957,9 @@ export default function AdvancedEventLabDialog({ { - const next = v as 'edit' | 'preview' + const next = v as 'edit' | 'preview' | 'json' if (next === 'preview') flushPreviewDocNow() + if (next === 'json') refreshLabJsonPreview() setLabBodyTab(next) }} className="flex flex-col gap-2" @@ -931,6 +975,9 @@ export default function AdvancedEventLabDialog({ {t('Advanced lab preview')} + + {t('Advanced lab json preview')} + + + +

+ {t('Advanced lab json preview hint')} +

+
+
+                  {labJsonPreview || '{}'}
+                
+
+
diff --git a/src/components/NoteOptions/EditOrCloneEventDialog.tsx b/src/components/NoteOptions/EditOrCloneEventDialog.tsx index f2c5d6f3..ee019a56 100644 --- a/src/components/NoteOptions/EditOrCloneEventDialog.tsx +++ b/src/components/NoteOptions/EditOrCloneEventDialog.tsx @@ -636,6 +636,7 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp contextEventId={!isCreate && sourceEvent ? sourceEvent.id : null} previewAuthorPubkey={pubkey ?? null} previewEmojiTags={labPreviewEmojiTags} + addClientTag={storage.getAddClientTag()} draftPersistenceKey={ advancedLabOpen && advancedLabDraftPersistenceKey ? advancedLabDraftPersistenceKey : null } diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 7057639a..3c3f633f 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -3616,6 +3616,7 @@ export default function PostContent({ i18nLanguage={i18n.language} contextEventId={parentEvent?.id ?? null} previewAuthorPubkey={pubkey ?? null} + addClientTag={addClientTag} draftPersistenceKey={advancedLabOpen ? advancedLabPersistenceKey : null} bodyApiRef={advancedLabBodyApiRef} formatToolbar={ diff --git a/src/components/PostEditor/PostTextarea/index.tsx b/src/components/PostEditor/PostTextarea/index.tsx index 07b74f65..2ff0664f 100644 --- a/src/components/PostEditor/PostTextarea/index.tsx +++ b/src/components/PostEditor/PostTextarea/index.tsx @@ -115,7 +115,7 @@ const PostTextarea = forwardRef< onUploadCompressPhaseRef.current = onUploadCompressPhase const onUploadCompressProgressRef = useRef(onUploadCompressProgress) onUploadCompressProgressRef.current = onUploadCompressProgress - const [activeTab, setActiveTab] = useState('preview') + const [activeTab, setActiveTab] = useState('edit') const editorRef = useRef(null) const kindDescription = useMemo(() => getKindDescription(kind), [kind]) @@ -274,6 +274,9 @@ const PostTextarea = forwardRef<
+ + {t('Edit')} + {t('Preview')} @@ -284,8 +287,17 @@ const PostTextarea = forwardRef<
)} - - + + + +
kind {kindDescription.number}: {kindDescription.description} diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index fa40a68f..c772984f 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1065,6 +1065,9 @@ export default { "Advanced lab markup label asciidoc": "AsciiDoc", "Advanced lab preview": "Preview", "Advanced lab preview empty": "Nothing to preview yet.", + "Advanced lab json preview": "JSON preview", + "Advanced lab json preview hint": + "Read-only publish draft: `kind`, `content`, `created_at`, and `tags` (including `imeta` from URLs in the body and the Imwald `client` tag when enabled in post options). `pubkey`, `id`, and `sig` are set at publish.", "Advanced lab markup placeholder markdown": "Note body (Markdown)", "Advanced lab markup placeholder asciidoc": "Note body (AsciiDoc)", "Advanced lab tags JSON": "Kind, content, and tags (JSON)", @@ -1094,17 +1097,6 @@ export default { "Advanced lab translate done": "Translation inserted into the editor.", "Advanced lab use translation read aloud": "Use body for read-aloud (this note)", "Advanced lab read aloud buffer set": "The next read-aloud for this note will use the current body text (translated if you translated first).", - "Composer JSON tab hint": "Edit the draft as JSON (`kind`, `content`, `tags` only). `kind` must match the note type selected above. Other fields are set when you publish.", - "Composer JSON apply": "Apply JSON", - composerJsonKindMismatch: "JSON kind {{got}} does not match the composer (expected {{expected}}). Change the note type or fix `kind` in JSON.", - composerJsonApplySuccess: "Draft updated from JSON.", - composerExtraTagsTitle: "Additional tags", - composerExtraTagsHint: - "Optional Nostr tags appended when you publish (after the tags this composer adds automatically). Use one value per line for multi-value tags.", - composerExtraTagsEmpty: "No extra tags yet.", - composerExtraTagValues: "Values", - composerExtraTagValuesPlaceholder: "One value per line", - composerExtraTagsAdd: "Add tag", advancedLabTagsTitle: "Event tags", advancedLabTagsCount: "{{count}} tags", advancedLabTagsHint: diff --git a/src/lib/advanced-event-lab-slice.test.ts b/src/lib/advanced-event-lab-slice.test.ts index 09f7af24..036ddbac 100644 --- a/src/lib/advanced-event-lab-slice.test.ts +++ b/src/lib/advanced-event-lab-slice.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { parseLabSlice, serializeLabSlice } from '@/lib/advanced-event-lab-slice' +import { + parseLabSlice, + serializeLabSlice, + serializePublishPreviewLabJson +} from '@/lib/advanced-event-lab-slice' describe('parseLabSlice', () => { it('round-trips', () => { @@ -13,3 +17,20 @@ describe('parseLabSlice', () => { expect(parseLabSlice('{"kind":"x","content":"","tags":[]}').ok).toBe(false) }) }) + +describe('serializePublishPreviewLabJson', () => { + it('includes Imwald client tag by default', () => { + const json = serializePublishPreviewLabJson({ kind: 1, content: 'hi', tags: [['d', 'x']] }) + const o = JSON.parse(json) as { tags: string[][] } + expect(o.tags.some((t) => t[0] === 'client' && t[1] === 'imwald')).toBe(true) + }) + + it('omits client tag when addClientTag is false', () => { + const json = serializePublishPreviewLabJson( + { kind: 1, content: 'hi', tags: [['d', 'x']] }, + { addClientTag: false } + ) + const o = JSON.parse(json) as { tags: string[][] } + expect(o.tags.some((t) => t[0] === 'client')).toBe(false) + }) +}) diff --git a/src/lib/advanced-event-lab-slice.ts b/src/lib/advanced-event-lab-slice.ts index 64edb67b..ca0eeb7e 100644 --- a/src/lib/advanced-event-lab-slice.ts +++ b/src/lib/advanced-event-lab-slice.ts @@ -1,9 +1,47 @@ +import { + applyImwaldAttributionTags, + collectUploadImetaTagsForContentUrls, + mergeUploadImetaTagsInto +} from '@/lib/draft-event' +import type { TDraftEvent } from '@/types' + export type AdvancedEventLabSlice = { kind: number content: string tags: string[][] } +/** + * JSON shaped like the draft passed to publish: merges `imeta` from content URLs, + * then applies Imwald `client` (and strips duplicate client/attribution tags) like {@link applyImwaldAttributionTags}. + */ +export function serializePublishPreviewLabJson( + slice: AdvancedEventLabSlice, + options?: { addClientTag?: boolean } +): string { + const tags = slice.tags.map((row) => [...row]) + mergeUploadImetaTagsInto(tags, collectUploadImetaTagsForContentUrls(slice.content)) + const draft: TDraftEvent = { + kind: slice.kind, + content: slice.content, + created_at: Math.floor(Date.now() / 1000), + tags + } + const withAttribution = applyImwaldAttributionTags(draft, { + addClientTag: options?.addClientTag + }) + return JSON.stringify( + { + kind: withAttribution.kind, + content: withAttribution.content, + created_at: withAttribution.created_at, + tags: withAttribution.tags + }, + null, + 2 + ) +} + export function serializeLabSlice(slice: AdvancedEventLabSlice): string { return JSON.stringify( {