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 { @@ -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 = { @@ -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({ @@ -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({ @@ -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<ComposerExtraTagRow[]>(() => [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({ @@ -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({ @@ -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({ @@ -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({ @@ -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({ @@ -914,8 +957,9 @@ export default function AdvancedEventLabDialog({
<Tabs
value={labBodyTab}
onValueChange={(v) => {
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({ @@ -931,6 +975,9 @@ export default function AdvancedEventLabDialog({
<TabsTrigger value="preview" className="shrink-0">
{t('Advanced lab preview')}
</TabsTrigger>
<TabsTrigger value="json" className="shrink-0">
{t('Advanced lab json preview')}
</TabsTrigger>
</TabsList>
<TabsContent
@ -958,6 +1005,20 @@ export default function AdvancedEventLabDialog({ @@ -958,6 +1005,20 @@ export default function AdvancedEventLabDialog({
/>
</div>
</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>
</div>

1
src/components/NoteOptions/EditOrCloneEventDialog.tsx

@ -636,6 +636,7 @@ export default function EditOrCloneEventDialog(props: EditOrCloneEventDialogProp @@ -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
}

1
src/components/PostEditor/PostContent.tsx

@ -3616,6 +3616,7 @@ export default function PostContent({ @@ -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={

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

@ -115,7 +115,7 @@ const PostTextarea = forwardRef< @@ -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<Editor | null>(null)
const kindDescription = useMemo(() => getKindDescription(kind), [kind])
@ -274,6 +274,9 @@ const PostTextarea = forwardRef< @@ -274,6 +274,9 @@ const PostTextarea = forwardRef<
<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">
<TabsList className="w-auto justify-start">
<TabsTrigger value="edit" title={t('Edit')}>
{t('Edit')}
</TabsTrigger>
<TabsTrigger value="preview" title={t('Preview')}>
{t('Preview')}
</TabsTrigger>
@ -284,8 +287,17 @@ const PostTextarea = forwardRef< @@ -284,8 +287,17 @@ const PostTextarea = forwardRef<
</div>
)}
</div>
<EditorContent className="tiptap" editor={editor} />
<TabsContent value="preview">
<TabsContent
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="text-xs text-muted-foreground">
kind {kindDescription.number}: {kindDescription.description}

14
src/i18n/locales/en.ts

@ -1065,6 +1065,9 @@ export default { @@ -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 { @@ -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:

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

@ -1,5 +1,9 @@ @@ -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', () => { @@ -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)
})
})

38
src/lib/advanced-event-lab-slice.ts

@ -1,9 +1,47 @@ @@ -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(
{

Loading…
Cancel
Save