import { Card } from '@/components/ui/card' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { ScrollArea } from '@/components/ui/scroll-area' import Content from '@/components/Content' import ContentPreview from '@/components/ContentPreview' import Highlight from '@/components/Note/Highlight' import MarkdownArticle from '@/components/Note/MarkdownArticle/MarkdownArticle' import AsciidocArticle from '@/components/Note/AsciidocArticle/AsciidocArticle' import ClientTag from '@/components/ClientTag' import { ExtendedKind } from '@/constants' import { applyImwaldAttributionTags, stripImwaldAttributionTags } from '@/lib/draft-event' import { createFakeEvent } from '@/lib/event' import logger from '@/lib/logger' import { showPublishingError, showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import storage from '@/services/local-storage.service' import type { TDraftEvent } from '@/types' import dayjs from 'dayjs' import { Plus, Trash2 } from 'lucide-react' import { Event, kinds } from 'nostr-tools' import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { useTranslation } from 'react-i18next' function normalizeTagRow(row: string[]): string[] | null { const trimmed = row.map((c) => c.trim()) if (!trimmed[0]) return null let end = trimmed.length while (end > 1 && trimmed[end - 1] === '') end-- return trimmed.slice(0, end) } function tagsFromRows(rows: string[][]): string[][] { const out: string[][] = [] for (const row of rows) { const n = normalizeTagRow(row) if (n) out.push(n) } return out } function StaticEventPreview({ event, className }: { event: Event; className?: string }) { const k = event.kind const wrap = (node: ReactNode) => ( {node} ) if (k === ExtendedKind.POLL) { return wrap() } if (k === kinds.Highlights) { return wrap() } if ( k === kinds.ShortTextNote || k === ExtendedKind.COMMENT || k === ExtendedKind.VOICE_COMMENT ) { return wrap() } if (k === kinds.LongFormArticle) { return wrap() } if (k === ExtendedKind.WIKI_ARTICLE) { return wrap() } if (k === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { return wrap() } if (k === ExtendedKind.PUBLICATION_CONTENT) { return wrap() } return wrap() } export type TEditOrCloneMode = 'edit' | 'clone' export default function EditOrCloneEventDialog({ open, onOpenChange, sourceEvent, mode }: { open: boolean onOpenChange: (open: boolean) => void sourceEvent: Event mode: TEditOrCloneMode }) { const { t } = useTranslation() const { pubkey, publish, checkLogin } = useNostr() const [content, setContent] = useState(sourceEvent.content) const [tagRows, setTagRows] = useState([['', '']]) const [activeTab, setActiveTab] = useState('edit') const [publishing, setPublishing] = useState(false) const prevOpenRef = useRef(false) const kind = sourceEvent.kind useEffect(() => { if (open && !prevOpenRef.current) { setContent(sourceEvent.content) setTagRows( sourceEvent.tags?.length ? sourceEvent.tags.map((row) => [...row]) : [['', '']] ) setActiveTab('edit') } prevOpenRef.current = open }, [open, sourceEvent]) const normalizedTags = useMemo(() => tagsFromRows(tagRows), [tagRows]) const previewEvent = useMemo(() => { const now = Math.floor(Date.now() / 1000) const base: TDraftEvent = { kind, content, tags: normalizedTags, created_at: now } const withAttribution = applyImwaldAttributionTags(base, { addClientTag: storage.getAddClientTag() }) return createFakeEvent({ kind, content, tags: withAttribution.tags, pubkey: pubkey ?? '', created_at: now }) }, [kind, content, normalizedTags, pubkey]) const buildDraftJson = useCallback(() => { const base: TDraftEvent = { kind, content, tags: normalizedTags, created_at: dayjs().unix() } const withAttribution = applyImwaldAttributionTags(base, { addClientTag: storage.getAddClientTag() }) const draft = { pubkey: pubkey ?? t('Log in to publish'), kind: withAttribution.kind, content: withAttribution.content, tags: withAttribution.tags, created_at: t('Set when you publish'), _note: t('id and sig are assigned when you publish') } return JSON.stringify(draft, null, 2) }, [pubkey, kind, content, normalizedTags, t]) const draftJson = activeTab === 'json' ? buildDraftJson() : '' const updateRow = (i: number, j: number, value: string) => { setTagRows((rows) => { const next = rows.map((r) => [...r]) if (!next[i]) return rows next[i][j] = value return next }) } const addRow = () => setTagRows((rows) => [...rows, ['', '']]) const removeRow = (i: number) => { setTagRows((rows) => (rows.length <= 1 ? [['', '']] : rows.filter((_, idx) => idx !== i))) } const addCell = (i: number) => { setTagRows((rows) => { const next = rows.map((r) => [...r]) next[i] = [...next[i], ''] return next }) } const removeCell = (i: number, j: number) => { setTagRows((rows) => { const next = rows.map((r) => [...r]) if (next[i].length <= 1) return rows next[i] = next[i].filter((_, idx) => idx !== j) return next }) } const handlePublish = async () => { await checkLogin(async () => { if (!pubkey) return setPublishing(true) try { const draft = { kind, content, tags: normalizedTags, created_at: dayjs().unix() } const newEvent = await publish(draft, { addClientTag: storage.getAddClientTag() }) if ((newEvent as any)?.relayStatuses) { const rs = (newEvent as any).relayStatuses showPublishingFeedback( { success: true, relayStatuses: rs, successCount: rs.filter((s: any) => s.success).length, totalCount: rs.length }, { message: t('Post published'), duration: 6000 } ) } else { showSimplePublishSuccess(t('Post published')) } onOpenChange(false) } catch (e) { if (e instanceof AggregateError && (e as any).relayStatuses) { const relayStatuses = (e as any).relayStatuses const successCount = relayStatuses.filter((s: any) => s.success).length const totalCount = relayStatuses.length showPublishingFeedback( { success: successCount > 0, relayStatuses, successCount, totalCount }, { message: successCount > 0 ? t('Published to some relays only') : t('Failed to post'), duration: 6000 } ) if (successCount > 0) onOpenChange(false) } else { logger.error('Edit/clone publish failed', { error: e }) showPublishingError(e instanceof Error ? e : String(e)) } } finally { setPublishing(false) } }) } const title = mode === 'edit' ? t('Edit this event') : t('Clone or fork this event') return ( {title} {t('Edit content and tags, then publish a new signed event.')}
{t('Edit')} {t('Preview')} {t('Json')}