Browse Source

handled weird highlights from shakespeare

added highlight quote bar and removed quotation marks

fixed highlight preview

added "create highlight" menu item
imwald
Silberengel 4 months ago
parent
commit
7d11d43cc8
  1. 109
      src/components/Note/Highlight/index.tsx
  2. 25
      src/components/NoteOptions/index.tsx
  3. 102
      src/components/NoteOptions/useMenuActions.tsx
  4. 2
      src/components/PostEditor/HighlightEditor.tsx
  5. 35
      src/components/PostEditor/PostContent.tsx
  6. 86
      src/components/PostEditor/PostTextarea/Preview.tsx
  7. 12
      src/components/PostEditor/PostTextarea/index.tsx
  8. 14
      src/components/PostEditor/index.tsx
  9. 8
      src/components/UniversalContent/HighlightSourcePreview.tsx
  10. 6
      src/components/ui/command.tsx

109
src/components/Note/Highlight/index.tsx

@ -4,6 +4,42 @@ import { nip19 } from 'nostr-tools'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import HighlightSourcePreview from '@/components/UniversalContent/HighlightSourcePreview' import HighlightSourcePreview from '@/components/UniversalContent/HighlightSourcePreview'
/**
* Check if a string is a URL or Nostr address
*/
function isUrlOrNostrAddress(value: string | undefined): boolean {
if (!value || typeof value !== 'string') {
return false
}
// Check if it's a URL (http://, https://, or starts with common URL patterns)
try {
if (value.startsWith('http://') || value.startsWith('https://') || value.startsWith('ws://') || value.startsWith('wss://')) {
new URL(value) // Validate it's a proper URL
return true
}
} catch {
// Not a valid URL
}
// Check if it's a Nostr address (nostr: prefix or bech32 encoded)
if (value.startsWith('nostr:')) {
return true
}
// Check if it's a bech32 encoded Nostr address
try {
const decoded = nip19.decode(value)
if (['npub', 'nprofile', 'nevent', 'naddr', 'note', 'nrelay'].includes(decoded.type)) {
return true
}
} catch {
// Not a valid Nostr address
}
return false
}
export default function Highlight({ export default function Highlight({
event, event,
className className
@ -15,6 +51,7 @@ export default function Highlight({
// Extract the source (e-tag, a-tag, or r-tag) with improved priority handling // Extract the source (e-tag, a-tag, or r-tag) with improved priority handling
let source = null let source = null
let quoteSource: string | null = null // For plain text r-tags that aren't URLs/Nostr addresses
let sourceTag: string[] | undefined let sourceTag: string[] | undefined
// Check for 'source' marker first (highest priority) // Check for 'source' marker first (highest priority)
@ -50,13 +87,13 @@ export default function Highlight({
// Process the selected source tag // Process the selected source tag
if (sourceTag) { if (sourceTag) {
if (sourceTag[0] === 'e') { if (sourceTag[0] === 'e' && sourceTag[1]) {
source = { source = {
type: 'event' as const, type: 'event' as const,
value: sourceTag[1], value: sourceTag[1],
bech32: nip19.noteEncode(sourceTag[1]) bech32: nip19.noteEncode(sourceTag[1])
} }
} else if (sourceTag[0] === 'a') { } else if (sourceTag[0] === 'a' && sourceTag[1]) {
const [kind, pubkey, identifier] = sourceTag[1].split(':') const [kind, pubkey, identifier] = sourceTag[1].split(':')
const relay = sourceTag[2] const relay = sourceTag[2]
source = { source = {
@ -70,10 +107,16 @@ export default function Highlight({
}) })
} }
} else if (sourceTag[0] === 'r') { } else if (sourceTag[0] === 'r') {
source = { // Check if the r-tag value is a URL or Nostr address
type: 'url' as const, if (sourceTag[1] && isUrlOrNostrAddress(sourceTag[1])) {
value: sourceTag[1], source = {
bech32: sourceTag[1] type: 'url' as const,
value: sourceTag[1],
bech32: sourceTag[1]
}
} else if (sourceTag[1]) {
// It's plain text, store it as a quote source
quoteSource = sourceTag[1]
} }
} }
} }
@ -90,30 +133,56 @@ export default function Highlight({
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Full quoted text with highlighted portion */} {/* Full quoted text with highlighted portion */}
{context && ( {context && (
<div className="text-base font-normal mb-3 whitespace-pre-wrap break-words"> <div className="text-base font-normal mb-3 whitespace-pre-wrap break-words border-l-4 border-green-500 pl-4">
{contextTag && highlightedText ? ( {contextTag && highlightedText ? (
// If we have both context and highlighted text, show the highlight within the context // If we have both context and highlighted text, show the highlight within the context
<div> <div>
{context.split(highlightedText).map((part, index) => ( {(() => {
<span key={index}> // Strip outer quotation marks if present
{part} let cleanContext = context.trim()
{index < context.split(highlightedText).length - 1 && ( if (cleanContext.startsWith('"') && cleanContext.endsWith('"')) {
<mark className="bg-green-200 dark:bg-green-800 px-1 rounded"> cleanContext = cleanContext.slice(1, -1).trim()
{highlightedText} }
</mark> // Strip outer quotation marks from highlighted text if present
)} let cleanHighlightedText = highlightedText.trim()
</span> if (cleanHighlightedText.startsWith('"') && cleanHighlightedText.endsWith('"')) {
))} cleanHighlightedText = cleanHighlightedText.slice(1, -1).trim()
}
return cleanContext.split(cleanHighlightedText).map((part, index) => (
<span key={index}>
{part}
{index < cleanContext.split(cleanHighlightedText).length - 1 && (
<mark className="bg-green-200 dark:bg-green-800 px-1 rounded">
{cleanHighlightedText}
</mark>
)}
</span>
))
})()}
</div> </div>
) : ( ) : (
// If no context tag, just show the content as a regular quote // If no context tag, just show the content as a regular quote
<blockquote className="italic"> <div>
"{context}" {(() => {
</blockquote> // Strip outer quotation marks if present
let cleanContext = context.trim()
if (cleanContext.startsWith('"') && cleanContext.endsWith('"')) {
cleanContext = cleanContext.slice(1, -1).trim()
}
return cleanContext
})()}
</div>
)} )}
</div> </div>
)} )}
{/* Quote source (plain text r-tag) */}
{quoteSource && (
<div className="mt-3 text-sm text-gray-600 dark:text-gray-400 italic">
{quoteSource.trimStart().startsWith('—') ? quoteSource : `${quoteSource}`}
</div>
)}
{/* Source preview card */} {/* Source preview card */}
{source && ( {source && (
<div className="mt-3"> <div className="mt-3">

25
src/components/NoteOptions/index.tsx

@ -7,6 +7,8 @@ import { MobileMenu } from './MobileMenu'
import RawEventDialog from './RawEventDialog' import RawEventDialog from './RawEventDialog'
import ReportDialog from './ReportDialog' import ReportDialog from './ReportDialog'
import { SubMenuAction, useMenuActions } from './useMenuActions' import { SubMenuAction, useMenuActions } from './useMenuActions'
import PostEditor from '../PostEditor'
import { HighlightData } from '../PostEditor/HighlightEditor'
export default function NoteOptions({ event, className }: { event: Event; className?: string }) { export default function NoteOptions({ event, className }: { event: Event; className?: string }) {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
@ -16,6 +18,9 @@ export default function NoteOptions({ event, className }: { event: Event; classN
const [showSubMenu, setShowSubMenu] = useState(false) const [showSubMenu, setShowSubMenu] = useState(false)
const [activeSubMenu, setActiveSubMenu] = useState<SubMenuAction[]>([]) const [activeSubMenu, setActiveSubMenu] = useState<SubMenuAction[]>([])
const [subMenuTitle, setSubMenuTitle] = useState('') const [subMenuTitle, setSubMenuTitle] = useState('')
const [isPostEditorOpen, setIsPostEditorOpen] = useState(false)
const [initialHighlightData, setInitialHighlightData] = useState<HighlightData | undefined>(undefined)
const [highlightDefaultContent, setHighlightDefaultContent] = useState<string>('')
const closeDrawer = () => { const closeDrawer = () => {
setIsDrawerOpen(false) setIsDrawerOpen(false)
@ -38,7 +43,13 @@ export default function NoteOptions({ event, className }: { event: Event; classN
showSubMenuActions, showSubMenuActions,
setIsRawEventDialogOpen, setIsRawEventDialogOpen,
setIsReportDialogOpen, setIsReportDialogOpen,
isSmallScreen isSmallScreen,
openHighlightEditor: (highlightData: HighlightData, eventContent?: string) => {
setInitialHighlightData(highlightData)
setHighlightDefaultContent(eventContent || '')
setIsPostEditorOpen(true)
closeDrawer()
}
}) })
const trigger = useMemo( const trigger = useMemo(
@ -81,6 +92,18 @@ export default function NoteOptions({ event, className }: { event: Event; classN
isOpen={isReportDialogOpen} isOpen={isReportDialogOpen}
closeDialog={() => setIsReportDialogOpen(false)} closeDialog={() => setIsReportDialogOpen(false)}
/> />
<PostEditor
open={isPostEditorOpen}
setOpen={(open) => {
setIsPostEditorOpen(open)
if (!open) {
setInitialHighlightData(undefined)
setHighlightDefaultContent('')
}
}}
defaultContent={highlightDefaultContent}
initialHighlightData={initialHighlightData}
/>
</div> </div>
) )
} }

102
src/components/NoteOptions/useMenuActions.tsx

@ -11,7 +11,7 @@ import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants' import { BIG_RELAY_URLS, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS } from '@/constants'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin, FileDown, Globe, BookOpen } from 'lucide-react' import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert, Pin, FileDown, Globe, BookOpen, Highlighter } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' import { Event, kinds } from 'nostr-tools'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
import { useMemo, useState, useEffect } from 'react' import { useMemo, useState, useEffect } from 'react'
@ -42,6 +42,7 @@ interface UseMenuActionsProps {
setIsRawEventDialogOpen: (open: boolean) => void setIsRawEventDialogOpen: (open: boolean) => void
setIsReportDialogOpen: (open: boolean) => void setIsReportDialogOpen: (open: boolean) => void
isSmallScreen: boolean isSmallScreen: boolean
openHighlightEditor?: (highlightData: import('../PostEditor/HighlightEditor').HighlightData, eventContent?: string) => void
} }
export function useMenuActions({ export function useMenuActions({
@ -50,7 +51,8 @@ export function useMenuActions({
showSubMenuActions, showSubMenuActions,
setIsRawEventDialogOpen, setIsRawEventDialogOpen,
setIsReportDialogOpen, setIsReportDialogOpen,
isSmallScreen isSmallScreen,
openHighlightEditor
}: UseMenuActionsProps) { }: UseMenuActionsProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, attemptDelete, publish } = useNostr() const { pubkey, attemptDelete, publish } = useNostr()
@ -347,6 +349,21 @@ export function useMenuActions({
} }
}, [isArticleType, event, dTag]) }, [isArticleType, event, dTag])
// Check if this is an OP event that can be highlighted
const isOPEvent = useMemo(() => {
return (
event.kind === kinds.ShortTextNote || // 1
event.kind === kinds.LongFormArticle || // 30023
event.kind === ExtendedKind.WIKI_ARTICLE || // 30818
event.kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN || // 30817
event.kind === ExtendedKind.PUBLICATION || // 30040
event.kind === ExtendedKind.PUBLICATION_CONTENT || // 30041
event.kind === ExtendedKind.DISCUSSION || // 11
event.kind === ExtendedKind.COMMENT || // 1111
(event.kind === kinds.Zap && (event.tags.some(tag => tag[0] === 'e') || event.tags.some(tag => tag[0] === 'a'))) // Zap receipt
)
}, [event.kind, event.tags])
const menuActions: MenuAction[] = useMemo(() => { const menuActions: MenuAction[] = useMemo(() => {
// Export functions for articles // Export functions for articles
const exportAsMarkdown = () => { const exportAsMarkdown = () => {
@ -443,17 +460,81 @@ export function useMenuActions({
navigator.clipboard.writeText(toNjump(getNoteBech32Id(event))) navigator.clipboard.writeText(toNjump(getNoteBech32Id(event)))
closeDrawer() closeDrawer()
} }
}, }
{ ]
icon: Code,
label: t('View raw event'), // Add "Create Highlight" action for OP events
if (isOPEvent && openHighlightEditor) {
actions.push({
icon: Highlighter,
label: t('Create Highlight'),
onClick: () => { onClick: () => {
closeDrawer() try {
setIsRawEventDialogOpen(true) // For addressable events (publications, long-form articles with d-tag), use naddr
// For regular events, use nevent
let sourceValue: string
let sourceHexId: string | undefined
if (kinds.isAddressableKind(event.kind) || kinds.isReplaceableKind(event.kind)) {
// Generate naddr for addressable/replaceable events
const dTag = event.tags.find(tag => tag[0] === 'd')?.[1] || ''
if (dTag) {
const relays = event.tags
.filter(tag => tag[0] === 'relay')
.map(tag => tag[1])
.filter(Boolean)
try {
sourceValue = nip19.naddrEncode({
kind: event.kind,
pubkey: event.pubkey,
identifier: dTag,
relays: relays.length > 0 ? relays : undefined
})
sourceHexId = undefined // naddr doesn't have a single hex ID
} catch (error) {
logger.error('Error generating naddr for highlight', { error })
// Fallback to nevent
sourceValue = getNoteBech32Id(event)
sourceHexId = event.id
}
} else {
// No d-tag, use nevent
sourceValue = getNoteBech32Id(event)
sourceHexId = event.id
}
} else {
// Regular event, use nevent
sourceValue = getNoteBech32Id(event)
sourceHexId = event.id
}
const highlightData: import('../PostEditor/HighlightEditor').HighlightData = {
sourceType: 'nostr',
sourceValue,
sourceHexId
// context field is left empty - user can add it later if needed
}
// Pass the event content as defaultContent for the main editor field
openHighlightEditor(highlightData, event.content)
} catch (error) {
logger.error('Error creating highlight from event', { error, eventId: event.id })
toast.error(t('Failed to create highlight'))
}
}, },
separator: true separator: true
} })
] }
actions.push({
icon: Code,
label: t('View raw event'),
onClick: () => {
closeDrawer()
setIsRawEventDialogOpen(true)
},
separator: true
})
// Add export options for article-type events // Add export options for article-type events
if (isArticleType) { if (isArticleType) {
@ -621,6 +702,7 @@ export function useMenuActions({
pubkey, pubkey,
isMuted, isMuted,
isSmallScreen, isSmallScreen,
openHighlightEditor,
broadcastSubMenu, broadcastSubMenu,
closeDrawer, closeDrawer,
showSubMenuActions, showSubMenuActions,

2
src/components/PostEditor/HighlightEditor.tsx

@ -149,7 +149,7 @@ export default function HighlightEditor({
value={context} value={context}
onChange={(e) => setContext(e.target.value)} onChange={(e) => setContext(e.target.value)}
placeholder={t('Paste the entire original passage that contains your highlight')} placeholder={t('Paste the entire original passage that contains your highlight')}
rows={3} rows={12}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{t('The main editor above should contain only the text you want to highlight. This field should contain the full quote or paragraph for context.')} {t('The main editor above should contain only the text you want to highlight. This field should contain the full quote or paragraph for context.')}

35
src/components/PostEditor/PostContent.tsx

@ -38,12 +38,14 @@ export default function PostContent({
defaultContent = '', defaultContent = '',
parentEvent, parentEvent,
close, close,
openFrom openFrom,
initialHighlightData
}: { }: {
defaultContent?: string defaultContent?: string
parentEvent?: Event parentEvent?: Event
close: () => void close: () => void
openFrom?: string[] openFrom?: string[]
initialHighlightData?: HighlightData
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
@ -64,11 +66,13 @@ export default function PostContent({
const [extractedMentions, setExtractedMentions] = useState<string[]>([]) const [extractedMentions, setExtractedMentions] = useState<string[]>([])
const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([]) const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [isHighlight, setIsHighlight] = useState(false) const [isHighlight, setIsHighlight] = useState(!!initialHighlightData)
const [highlightData, setHighlightData] = useState<HighlightData>({ const [highlightData, setHighlightData] = useState<HighlightData>(
sourceType: 'nostr', initialHighlightData || {
sourceValue: '' sourceType: 'nostr',
}) sourceValue: ''
}
)
const [pollCreateData, setPollCreateData] = useState<TPollCreateData>({ const [pollCreateData, setPollCreateData] = useState<TPollCreateData>({
isMultipleChoice: false, isMultipleChoice: false,
options: ['', ''], options: ['', ''],
@ -106,6 +110,22 @@ export default function PostContent({
highlightData highlightData
]) ])
// Clear highlight data when initialHighlightData changes or is removed
useEffect(() => {
if (initialHighlightData) {
// Set highlight mode and data when provided
setIsHighlight(true)
setHighlightData(initialHighlightData)
} else {
// Clear highlight mode and data when not provided
setIsHighlight(false)
setHighlightData({
sourceType: 'nostr',
sourceValue: ''
})
}
}, [initialHighlightData])
useEffect(() => { useEffect(() => {
if (isFirstRender.current) { if (isFirstRender.current) {
isFirstRender.current = false isFirstRender.current = false
@ -469,7 +489,8 @@ export default function PostContent({
onUploadStart={handleUploadStart} onUploadStart={handleUploadStart}
onUploadProgress={handleUploadProgress} onUploadProgress={handleUploadProgress}
onUploadEnd={handleUploadEnd} onUploadEnd={handleUploadEnd}
kind={isPublicMessage ? ExtendedKind.PUBLIC_MESSAGE : isPoll ? ExtendedKind.POLL : kinds.ShortTextNote} kind={isHighlight ? kinds.Highlights : isPublicMessage ? ExtendedKind.PUBLIC_MESSAGE : isPoll ? ExtendedKind.POLL : kinds.ShortTextNote}
highlightData={isHighlight ? highlightData : undefined}
/> />
{isPoll && ( {isPoll && (
<PollEditor <PollEditor

86
src/components/PostEditor/PostTextarea/Preview.tsx

@ -3,19 +3,24 @@ import { transformCustomEmojisInContent } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event' import { createFakeEvent } from '@/lib/event'
import { cleanUrl } from '@/lib/url' import { cleanUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { kinds, nip19 } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import Content from '../../Content' import Content from '../../Content'
import Highlight from '../../Note/Highlight'
import { HighlightData } from '../HighlightEditor'
export default function Preview({ export default function Preview({
content, content,
className, className,
kind = 1 kind = 1,
highlightData
}: { }: {
content: string content: string
className?: string className?: string
kind?: number kind?: number
highlightData?: HighlightData
}) { }) {
const { content: processedContent, emojiTags } = useMemo( const { content: processedContent, emojiTags, highlightTags } = useMemo(
() => { () => {
// Clean tracking parameters from URLs in the preview // Clean tracking parameters from URLs in the preview
const cleanedContent = content.replace( const cleanedContent = content.replace(
@ -28,18 +33,81 @@ export default function Preview({
} }
} }
) )
return transformCustomEmojisInContent(cleanedContent) const { content: processed, emojiTags: tags } = transformCustomEmojisInContent(cleanedContent)
// Build highlight tags if this is a highlight
let highlightTags: string[][] = []
if (kind === kinds.Highlights && highlightData) {
// Add source tag
if (highlightData.sourceType === 'url') {
highlightTags.push(['r', highlightData.sourceValue, 'source'])
} else if (highlightData.sourceType === 'nostr') {
// For preview, we'll use a simple e-tag with the source value
// The actual tag building happens in createHighlightDraftEvent
if (highlightData.sourceHexId) {
highlightTags.push(['e', highlightData.sourceHexId])
} else if (highlightData.sourceValue) {
// Try to extract hex ID from bech32 if possible
try {
const decoded = nip19.decode(highlightData.sourceValue)
if (decoded.type === 'note' || decoded.type === 'nevent') {
const hexId = decoded.type === 'note' ? decoded.data : decoded.data.id
highlightTags.push(['e', hexId])
} else if (decoded.type === 'naddr') {
const { kind, pubkey, identifier } = decoded.data
highlightTags.push(['a', `${kind}:${pubkey}:${identifier}`])
}
} catch {
// If decoding fails, just use the source value as-is for preview
highlightTags.push(['r', highlightData.sourceValue])
}
}
}
// Add context tag if provided
if (highlightData.context) {
highlightTags.push(['context', highlightData.context])
}
}
return {
content: processed,
emojiTags: tags,
highlightTags
}
}, },
[content] [content, kind, highlightData]
) )
// Combine emoji tags and highlight tags
const allTags = useMemo(() => {
return [...emojiTags, ...highlightTags]
}, [emojiTags, highlightTags])
const fakeEvent = useMemo(() => {
return createFakeEvent({
content: processedContent,
tags: allTags,
kind
})
}, [processedContent, allTags, kind])
// For highlights, use the Highlight component for proper formatting
if (kind === kinds.Highlights) {
return (
<Card className={cn('p-3', className)}>
<Highlight
event={fakeEvent}
className="pointer-events-none"
/>
</Card>
)
}
return ( return (
<Card className={cn('p-3', className)}> <Card className={cn('p-3', className)}>
<Content <Content
event={createFakeEvent({ event={fakeEvent}
content: processedContent,
tags: emojiTags,
kind
})}
className="pointer-events-none h-full" className="pointer-events-none h-full"
mustLoadMedia mustLoadMedia
/> />

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

@ -13,7 +13,7 @@ import Text from '@tiptap/extension-text'
import { TextSelection } from '@tiptap/pm/state' import { TextSelection } from '@tiptap/pm/state'
import { EditorContent, useEditor } from '@tiptap/react' import { EditorContent, useEditor } from '@tiptap/react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { Dispatch, forwardRef, SetStateAction, useImperativeHandle } from 'react' import { Dispatch, forwardRef, SetStateAction, useImperativeHandle, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ClipboardAndDropHandler } from './ClipboardAndDropHandler' import { ClipboardAndDropHandler } from './ClipboardAndDropHandler'
import Emoji from './Emoji' import Emoji from './Emoji'
@ -21,6 +21,7 @@ import emojiSuggestion from './Emoji/suggestion'
import Mention from './Mention' import Mention from './Mention'
import mentionSuggestion from './Mention/suggestion' import mentionSuggestion from './Mention/suggestion'
import Preview from './Preview' import Preview from './Preview'
import { HighlightData } from '../HighlightEditor'
export type TPostTextareaHandle = { export type TPostTextareaHandle = {
appendText: (text: string, addNewline?: boolean) => void appendText: (text: string, addNewline?: boolean) => void
@ -41,6 +42,7 @@ const PostTextarea = forwardRef<
onUploadProgress?: (file: File, progress: number) => void onUploadProgress?: (file: File, progress: number) => void
onUploadEnd?: (file: File) => void onUploadEnd?: (file: File) => void
kind?: number kind?: number
highlightData?: HighlightData
} }
>( >(
( (
@ -54,11 +56,13 @@ const PostTextarea = forwardRef<
onUploadStart, onUploadStart,
onUploadProgress, onUploadProgress,
onUploadEnd, onUploadEnd,
kind = 1 kind = 1,
highlightData
}, },
ref ref
) => { ) => {
const { t } = useTranslation() const { t } = useTranslation()
const [activeTab, setActiveTab] = useState('edit')
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
Document, Document,
@ -160,7 +164,7 @@ const PostTextarea = forwardRef<
} }
return ( return (
<Tabs defaultValue="edit" className="space-y-2"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-2">
<TabsList> <TabsList>
<TabsTrigger value="edit">{t('Edit')}</TabsTrigger> <TabsTrigger value="edit">{t('Edit')}</TabsTrigger>
<TabsTrigger value="preview">{t('Preview')}</TabsTrigger> <TabsTrigger value="preview">{t('Preview')}</TabsTrigger>
@ -169,7 +173,7 @@ const PostTextarea = forwardRef<
<EditorContent className="tiptap" editor={editor} /> <EditorContent className="tiptap" editor={editor} />
</TabsContent> </TabsContent>
<TabsContent value="preview"> <TabsContent value="preview">
<Preview content={text} className={className} kind={kind} /> <Preview content={text} className={className} kind={kind} highlightData={highlightData} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
) )

14
src/components/PostEditor/index.tsx

@ -24,26 +24,34 @@ export default function PostEditor({
parentEvent, parentEvent,
open, open,
setOpen, setOpen,
openFrom openFrom,
initialHighlightData
}: { }: {
defaultContent?: string defaultContent?: string
parentEvent?: Event parentEvent?: Event
open: boolean open: boolean
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>
openFrom?: string[] openFrom?: string[]
initialHighlightData?: import('./HighlightEditor').HighlightData
}) { }) {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
// If initialHighlightData is provided and we're creating a highlight from an event,
// we need to pass the event content as defaultContent for the main editor
// Note: This is handled separately - we'll pass the event content when opening from menu
const effectiveDefaultContent = defaultContent
const content = useMemo(() => { const content = useMemo(() => {
return ( return (
<PostContent <PostContent
defaultContent={defaultContent} defaultContent={effectiveDefaultContent}
parentEvent={parentEvent} parentEvent={parentEvent}
close={() => setOpen(false)} close={() => setOpen(false)}
openFrom={openFrom} openFrom={openFrom}
initialHighlightData={initialHighlightData}
/> />
) )
}, [defaultContent, parentEvent, openFrom, setOpen]) }, [effectiveDefaultContent, parentEvent, openFrom, setOpen, initialHighlightData])
if (isSmallScreen) { if (isSmallScreen) {
return ( return (

8
src/components/UniversalContent/HighlightSourcePreview.tsx

@ -36,7 +36,9 @@ export default function HighlightSourcePreview({ source, className }: HighlightS
const decoded = nip19.decode(source.bech32) const decoded = nip19.decode(source.bech32)
if (decoded.type === 'nevent' || decoded.type === 'note') { if (decoded.type === 'nevent' || decoded.type === 'note') {
content = ( content = (
<EmbeddedNote noteId={source.value} className="w-full" /> <div className="max-h-[300px] overflow-hidden border-b border-gray-200 dark:border-gray-700">
<EmbeddedNote noteId={source.value} className="w-full" />
</div>
) )
} }
} catch (error) { } catch (error) {
@ -67,7 +69,9 @@ export default function HighlightSourcePreview({ source, className }: HighlightS
const decoded = nip19.decode(source.bech32) const decoded = nip19.decode(source.bech32)
if (decoded.type === 'naddr') { if (decoded.type === 'naddr') {
content = ( content = (
<EmbeddedNote noteId={source.bech32} className="w-full" /> <div className="max-h-[300px] overflow-hidden border-b border-gray-200 dark:border-gray-700">
<EmbeddedNote noteId={source.bech32} className="w-full" />
</div>
) )
} }
} catch (error) { } catch (error) {

6
src/components/ui/command.tsx

@ -35,9 +35,9 @@ const CommandDialog = ({
}: DialogProps & { classNames?: { content?: string } }) => { }: DialogProps & { classNames?: { content?: string } }) => {
return ( return (
<Dialog {...props}> <Dialog {...props}>
<DialogHeader className="hidden"> <DialogHeader className="sr-only">
<DialogTitle /> <DialogTitle>Command Menu</DialogTitle>
<DialogDescription /> <DialogDescription>Search and select a command</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogContent <DialogContent
className={cn( className={cn(

Loading…
Cancel
Save