Browse Source

bug-fixes for new post editor

imwald
Silberengel 3 weeks ago
parent
commit
1aff6d8eaa
  1. 69
      src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx
  2. 1
      src/components/NoteOptions/EditOrCloneEventDialog.tsx
  3. 1
      src/components/PostEditor/PostContent.tsx
  4. 18
      src/components/PostEditor/PostTextarea/index.tsx
  5. 14
      src/i18n/locales/en.ts
  6. 23
      src/lib/advanced-event-lab-slice.test.ts
  7. 38
      src/lib/advanced-event-lab-slice.ts

69
src/components/AdvancedEventLab/AdvancedEventLabDialog.tsx

@ -25,7 +25,11 @@ import {
} from '@/lib/language-display-meta' } from '@/lib/language-display-meta'
import { LanguageSelectOptionLines } from '@/lib/language-select-option-lines' import { LanguageSelectOptionLines } from '@/lib/language-select-option-lines'
import { buildLabLanguageToolPreferenceList } from '@/lib/trinity-languages' 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 { translateAdvancedLabMarkup } from '@/lib/advanced-lab-markup-protect'
import { import {
warmTranslateLanguagesOnce, warmTranslateLanguagesOnce,
@ -195,6 +199,8 @@ export type AdvancedEventLabDialogProps = {
previewAuthorPubkey?: string | null previewAuthorPubkey?: string | null
/** Lab preview: `emoji` tags on the fake event (e.g. copied from the event being edited). */ /** Lab preview: `emoji` tags on the fake event (e.g. copied from the event being edited). */
previewEmojiTags?: string[][] previewEmojiTags?: string[][]
/** When true (default), JSON preview includes the Imwald `client` tag like publish. */
addClientTag?: boolean
} }
function useDarkModeFlag(): boolean { function useDarkModeFlag(): boolean {
@ -227,7 +233,8 @@ export default function AdvancedEventLabDialog({
formatToolbar, formatToolbar,
draftPersistenceKey = null, draftPersistenceKey = null,
previewAuthorPubkey = null, previewAuthorPubkey = null,
previewEmojiTags previewEmojiTags,
addClientTag = true
}: 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). */ /** `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 LAB_DRAFT_DEBOUNCE_MS = 500
const [previewDoc, setPreviewDoc] = useState('') 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<ComposerExtraTagRow[]>(() => [newComposerTagRow()]) const [labTagRows, setLabTagRows] = useState<ComposerExtraTagRow[]>(() => [newComposerTagRow()])
/** Stable while payload matches; avoids remounting the editor when the parent passes a new `initial` object reference. */ /** 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) 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(() => { useEffect(() => {
schedulePreviewUpdateRef.current = schedulePreviewUpdate schedulePreviewUpdateRef.current = schedulePreviewUpdate
}, [schedulePreviewUpdate]) }, [schedulePreviewUpdate])
@ -354,11 +389,17 @@ export default function AdvancedEventLabDialog({
previewDebounceTimerRef.current = null previewDebounceTimerRef.current = null
} }
setPreviewDoc('') setPreviewDoc('')
setLabJsonPreview('')
} else { } else {
setLabBodyTab('edit') setLabBodyTab('edit')
} }
}, [open]) }, [open])
useEffect(() => {
if (!open || labBodyTab !== 'json') return
refreshLabJsonPreview()
}, [open, labBodyTab, labTagRows, refreshLabJsonPreview])
const handleDialogOpenChange = useCallback( const handleDialogOpenChange = useCallback(
(next: boolean) => { (next: boolean) => {
if (!next) { if (!next) {
@ -400,6 +441,7 @@ export default function AdvancedEventLabDialog({
s.tags = editableRowsToLabTags(rows) s.tags = editableRowsToLabTags(rows)
scheduleLabDraftPersist() scheduleLabDraftPersist()
bumpUndoUi() bumpUndoUi()
if (labBodyTabRef.current === 'json') refreshLabJsonPreviewRef.current()
}, },
[scheduleLabDraftPersist, bumpUndoUi] [scheduleLabDraftPersist, bumpUndoUi]
) )
@ -686,6 +728,7 @@ export default function AdvancedEventLabDialog({
s.content = content s.content = content
schedulePreviewUpdateRef.current(content) schedulePreviewUpdateRef.current(content)
scheduleLabDraftPersist() scheduleLabDraftPersist()
if (labBodyTabRef.current === 'json') refreshLabJsonPreviewRef.current()
}) })
] ]
if (isLanguageToolConfigured()) { if (isLanguageToolConfigured()) {
@ -914,8 +957,9 @@ export default function AdvancedEventLabDialog({
<Tabs <Tabs
value={labBodyTab} value={labBodyTab}
onValueChange={(v) => { onValueChange={(v) => {
const next = v as 'edit' | 'preview' const next = v as 'edit' | 'preview' | 'json'
if (next === 'preview') flushPreviewDocNow() if (next === 'preview') flushPreviewDocNow()
if (next === 'json') refreshLabJsonPreview()
setLabBodyTab(next) setLabBodyTab(next)
}} }}
className="flex flex-col gap-2" className="flex flex-col gap-2"
@ -931,6 +975,9 @@ export default function AdvancedEventLabDialog({
<TabsTrigger value="preview" className="shrink-0"> <TabsTrigger value="preview" className="shrink-0">
{t('Advanced lab preview')} {t('Advanced lab preview')}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="json" className="shrink-0">
{t('Advanced lab json preview')}
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent <TabsContent
@ -958,6 +1005,20 @@ export default function AdvancedEventLabDialog({
/> />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent
value="json"
className="mt-0 data-[state=inactive]:hidden focus-visible:ring-0 focus-visible:ring-offset-0"
>
<p className="text-xs text-muted-foreground mb-2">
{t('Advanced lab json preview hint')}
</p>
<div className="min-h-[24rem] h-[min(84vh,56rem)] overflow-auto rounded-md border border-border bg-muted/20 p-3">
<pre className="text-xs whitespace-pre-wrap break-words font-mono select-text text-foreground">
{labJsonPreview || '{}'}
</pre>
</div>
</TabsContent>
</Tabs> </Tabs>
</div> </div>

1
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -636,6 +636,7 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp
contextEventId={!isCreate && sourceEvent ? sourceEvent.id : null} contextEventId={!isCreate && sourceEvent ? sourceEvent.id : null}
previewAuthorPubkey={pubkey ?? null} previewAuthorPubkey={pubkey ?? null}
previewEmojiTags={labPreviewEmojiTags} previewEmojiTags={labPreviewEmojiTags}
addClientTag={storage.getAddClientTag()}
draftPersistenceKey={ draftPersistenceKey={
advancedLabOpen && advancedLabDraftPersistenceKey ? advancedLabDraftPersistenceKey : null advancedLabOpen && advancedLabDraftPersistenceKey ? advancedLabDraftPersistenceKey : null
} }

1
src/components/PostEditor/PostContent.tsx

@ -3616,6 +3616,7 @@ export default function PostContent({
i18nLanguage={i18n.language} i18nLanguage={i18n.language}
contextEventId={parentEvent?.id ?? null} contextEventId={parentEvent?.id ?? null}
previewAuthorPubkey={pubkey ?? null} previewAuthorPubkey={pubkey ?? null}
addClientTag={addClientTag}
draftPersistenceKey={advancedLabOpen ? advancedLabPersistenceKey : null} draftPersistenceKey={advancedLabOpen ? advancedLabPersistenceKey : null}
bodyApiRef={advancedLabBodyApiRef} bodyApiRef={advancedLabBodyApiRef}
formatToolbar={ formatToolbar={

18
src/components/PostEditor/PostTextarea/index.tsx

@ -115,7 +115,7 @@ const PostTextarea = forwardRef<
onUploadCompressPhaseRef.current = onUploadCompressPhase onUploadCompressPhaseRef.current = onUploadCompressPhase
const onUploadCompressProgressRef = useRef(onUploadCompressProgress) const onUploadCompressProgressRef = useRef(onUploadCompressProgress)
onUploadCompressProgressRef.current = onUploadCompressProgress onUploadCompressProgressRef.current = onUploadCompressProgress
const [activeTab, setActiveTab] = useState('preview') const [activeTab, setActiveTab] = useState('edit')
const editorRef = useRef<Editor | null>(null) const editorRef = useRef<Editor | null>(null)
const kindDescription = useMemo(() => getKindDescription(kind), [kind]) const kindDescription = useMemo(() => getKindDescription(kind), [kind])
@ -274,6 +274,9 @@ const PostTextarea = forwardRef<
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-2"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-2">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<TabsList className="w-auto justify-start"> <TabsList className="w-auto justify-start">
<TabsTrigger value="edit" title={t('Edit')}>
{t('Edit')}
</TabsTrigger>
<TabsTrigger value="preview" title={t('Preview')}> <TabsTrigger value="preview" title={t('Preview')}>
{t('Preview')} {t('Preview')}
</TabsTrigger> </TabsTrigger>
@ -284,8 +287,17 @@ const PostTextarea = forwardRef<
</div> </div>
)} )}
</div> </div>
<EditorContent className="tiptap" editor={editor} /> <TabsContent
<TabsContent value="preview"> value="edit"
forceMount
className="mt-0 data-[state=inactive]:hidden focus-visible:ring-0 focus-visible:ring-offset-0"
>
<EditorContent className="tiptap" editor={editor} />
</TabsContent>
<TabsContent
value="preview"
className="mt-0 data-[state=inactive]:hidden focus-visible:ring-0 focus-visible:ring-offset-0"
>
<div className="space-y-2"> <div className="space-y-2">
<div className="text-xs text-muted-foreground"> <div className="text-xs text-muted-foreground">
kind {kindDescription.number}: {kindDescription.description} kind {kindDescription.number}: {kindDescription.description}

14
src/i18n/locales/en.ts

@ -1065,6 +1065,9 @@ export default {
"Advanced lab markup label asciidoc": "AsciiDoc", "Advanced lab markup label asciidoc": "AsciiDoc",
"Advanced lab preview": "Preview", "Advanced lab preview": "Preview",
"Advanced lab preview empty": "Nothing to preview yet.", "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 markdown": "Note body (Markdown)",
"Advanced lab markup placeholder asciidoc": "Note body (AsciiDoc)", "Advanced lab markup placeholder asciidoc": "Note body (AsciiDoc)",
"Advanced lab tags JSON": "Kind, content, and tags (JSON)", "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 translate done": "Translation inserted into the editor.",
"Advanced lab use translation read aloud": "Use body for read-aloud (this note)", "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).", "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", advancedLabTagsTitle: "Event tags",
advancedLabTagsCount: "{{count}} tags", advancedLabTagsCount: "{{count}} tags",
advancedLabTagsHint: advancedLabTagsHint:

23
src/lib/advanced-event-lab-slice.test.ts

@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest' 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', () => { describe('parseLabSlice', () => {
it('round-trips', () => { it('round-trips', () => {
@ -13,3 +17,20 @@ describe('parseLabSlice', () => {
expect(parseLabSlice('{"kind":"x","content":"","tags":[]}').ok).toBe(false) 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)
})
})

38
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 = { export type AdvancedEventLabSlice = {
kind: number kind: number
content: string content: string
tags: 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 { export function serializeLabSlice(slice: AdvancedEventLabSlice): string {
return JSON.stringify( return JSON.stringify(
{ {

Loading…
Cancel
Save