From 1b9d8d136d824d6bab54f17b9f75147962e09ef4 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 18 Mar 2026 15:07:10 +0100 Subject: [PATCH] change --- .../Mention/NeventNaddrPickerDialog.tsx | 10 ++- .../PostTextarea/Mention/suggestion.ts | 23 ++++++- .../PostEditor/PostTextarea/Preview.tsx | 63 +++++-------------- .../PostEditor/PostTextarea/index.tsx | 4 +- .../TextareaWithMentionAutocomplete/index.tsx | 35 ++++++++--- src/components/ui/dialog.tsx | 6 +- src/lib/tiptap.ts | 8 ++- .../DiscussionsPage/CreateThreadDialog.tsx | 3 +- src/pages/primary/DiscussionsPage/index.tsx | 2 +- 9 files changed, 88 insertions(+), 66 deletions(-) diff --git a/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx b/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx index ae223c51..81cd02d4 100644 --- a/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx +++ b/src/components/PostEditor/PostTextarea/Mention/NeventNaddrPickerDialog.tsx @@ -17,7 +17,7 @@ import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Loader2, Search } from 'lucide-react' import type { Editor } from '@tiptap/core' -import { OPEN_NEVENT_PICKER_EVENT } from './suggestion' +import { OPEN_NEVENT_PICKER_EVENT, extendMentionRangeToEndOfWord } from './suggestion' type NeventNaddrPickerDialogProps = { open: boolean @@ -88,7 +88,10 @@ export function NeventNaddrPickerDialog({ return ( - + {t('Search for event or address…')} @@ -150,8 +153,9 @@ export function NeventPickerProvider({ children }: { children: React.ReactNode } useEffect(() => { const handler = (e: Event) => { const { editor, range } = (e as CustomEvent<{ editor: Editor; range: { from: number; to: number } }>).detail + const to = extendMentionRangeToEndOfWord(editor, range) setOnSelectedRef(() => (link: string) => { - editor.chain().focus().insertContentAt(range, link + ' ').run() + editor.chain().focus().insertContentAt({ from: range.from, to }, link + ' ').run() }) setOpen(true) } diff --git a/src/components/PostEditor/PostTextarea/Mention/suggestion.ts b/src/components/PostEditor/PostTextarea/Mention/suggestion.ts index 94e164ce..61fc8eb5 100644 --- a/src/components/PostEditor/PostTextarea/Mention/suggestion.ts +++ b/src/components/PostEditor/PostTextarea/Mention/suggestion.ts @@ -14,6 +14,24 @@ const MENTION_CHAR = '@' export const OPEN_NEVENT_PICKER_EVENT = 'open-nevent-picker' +/** Extend range.to to include any trailing word chars (handle, NIP-05) so the full @handle is replaced. Exported for nevent picker. */ +export function extendMentionRangeToEndOfWord(editor: Editor, range: { from: number; to: number }): number { + const { doc } = editor.state + let pos = range.to + while (pos < doc.content.size) { + const $pos = doc.resolve(pos) + const node = $pos.nodeAfter + if (!node || !node.isText) break + const text = node.text ?? '' + const offset = pos - $pos.start() + let i = offset + while (i < text.length && /[\w.-]/.test(text[i]!)) i++ + if (i === offset) break + pos += i - offset + } + return pos +} + const suggestion = { command: ({ editor, range, props }: { editor: Editor; range: { from: number; to: number }; props: { id: string; label?: string } }) => { if (props.id === NEVENT_NADDR_PICKER_ID) { @@ -23,13 +41,14 @@ const suggestion = { ) return } + const to = extendMentionRangeToEndOfWord(editor, range) const nodeAfter = editor.view.state.selection.$to.nodeAfter const overrideSpace = nodeAfter?.text?.startsWith(' ') - const to = overrideSpace ? range.to + 1 : range.to + const toWithSpace = overrideSpace ? to + 1 : to editor .chain() .focus() - .insertContentAt({ from: range.from, to }, [ + .insertContentAt({ from: range.from, to: toWithSpace }, [ { type: MENTION_EXTENSION_NAME, attrs: { ...props, mentionSuggestionChar: MENTION_CHAR } }, { type: 'text', text: ' ' } ]) diff --git a/src/components/PostEditor/PostTextarea/Preview.tsx b/src/components/PostEditor/PostTextarea/Preview.tsx index c660e693..054fa9b4 100644 --- a/src/components/PostEditor/PostTextarea/Preview.tsx +++ b/src/components/PostEditor/PostTextarea/Preview.tsx @@ -165,14 +165,12 @@ export default function Preview({ }) }, [processedContent, allTags, kind, mediaUrl]) + const selectableClass = 'select-text' // For polls, use ContentPreview to show poll properly if (kind === ExtendedKind.POLL) { return ( - - + + ) } @@ -180,11 +178,8 @@ export default function Preview({ // For highlights, use the Highlight component for proper formatting if (kind === kinds.Highlights) { return ( - - + + ) } @@ -193,12 +188,8 @@ export default function Preview({ // This ensures preview matches the final result (no Links section, correct image placement, proper line breaks) if (kind === kinds.ShortTextNote || kind === ExtendedKind.COMMENT || kind === ExtendedKind.VOICE_COMMENT) { return ( - - + + ) } @@ -206,12 +197,8 @@ export default function Preview({ // For LongFormArticle, use MarkdownArticle if (kind === kinds.LongFormArticle) { return ( - - + + ) } @@ -219,12 +206,8 @@ export default function Preview({ // For WikiArticle (AsciiDoc), use AsciidocArticle if (kind === ExtendedKind.WIKI_ARTICLE) { return ( - - + + ) } @@ -232,12 +215,8 @@ export default function Preview({ // For WikiArticleMarkdown, use MarkdownArticle if (kind === ExtendedKind.WIKI_ARTICLE_MARKDOWN) { return ( - - + + ) } @@ -245,23 +224,15 @@ export default function Preview({ // For PublicationContent, use AsciidocArticle if (kind === ExtendedKind.PUBLICATION_CONTENT) { return ( - - + + ) } return ( - - + + ) } diff --git a/src/components/PostEditor/PostTextarea/index.tsx b/src/components/PostEditor/PostTextarea/index.tsx index 502f45aa..ed1dea9f 100644 --- a/src/components/PostEditor/PostTextarea/index.tsx +++ b/src/components/PostEditor/PostTextarea/index.tsx @@ -247,11 +247,11 @@ const PostTextarea = forwardRef< -
+
{isLoadingJson ? (
{t('Loading...')}
) : ( -
+              
                 {draftEventJson || t('No JSON available')}
               
)} diff --git a/src/components/TextareaWithMentionAutocomplete/index.tsx b/src/components/TextareaWithMentionAutocomplete/index.tsx index 3218df34..054da71b 100644 --- a/src/components/TextareaWithMentionAutocomplete/index.tsx +++ b/src/components/TextareaWithMentionAutocomplete/index.tsx @@ -44,7 +44,9 @@ const TextareaWithMentionAutocomplete = forwardRef(null) const searchTimeoutRef = useRef | null>(null) const emojiSearchTimeoutRef = useRef | null>(null) + const mentionQueryRef = useRef(mentionQuery) const neventPicker = useNeventPicker() + mentionQueryRef.current = mentionQuery const [dropdownRect, setDropdownRect] = useState<{ top: number; left: number; width: number } | null>(null) const closeMention = useCallback(() => { @@ -59,27 +61,38 @@ const TextareaWithMentionAutocomplete = forwardRef { if (!value) { closeMention() closeEmoji() return } - if (mentionOpen && (value.length <= mentionStart || value[mentionStart] !== '@')) { - closeMention() + if (mentionOpen) { + if (value.length <= mentionStart || value[mentionStart] !== '@' || !value.includes('@')) { + closeMention() + } } - if (emojiOpen && (value.length <= emojiStart || value[emojiStart] !== ':')) { - closeEmoji() + if (emojiOpen) { + if (value.length <= emojiStart || value[emojiStart] !== ':') { + closeEmoji() + } } }, [value, mentionOpen, emojiOpen, mentionStart, emojiStart, closeMention, closeEmoji]) + /** Find end of @-mention segment in value (from start, after the @): alphanumeric, underscore, hyphen, dot (NIP-05). */ + const findMentionSegmentEnd = useCallback((val: string, from: number) => { + let i = from + 1 + while (i < val.length && /[\w.-]/.test(val[i]!)) i++ + return i + }, []) + const insertMention = useCallback( (id: string) => { const ta = textareaRef.current if (!ta) return const start = mentionStart - const end = start + 1 + mentionQuery.length + const end = findMentionSegmentEnd(value, start) const before = value.slice(0, start) const after = value.slice(end) @@ -106,7 +119,7 @@ const TextareaWithMentionAutocomplete = forwardRef { + const q = mentionQueryRef.current.trim().toLowerCase() + if (q === 'nevent' || q === 'naddr' || q.startsWith('nevent') || q.startsWith('naddr')) { + return + } const list = npubs ?? [] setMentionItems(list) setMentionOpen(list.length > 0) setSelectedIndex(0) }) .catch(() => { + const q = mentionQueryRef.current.trim().toLowerCase() + if (q === 'nevent' || q === 'naddr' || q.startsWith('nevent') || q.startsWith('naddr')) { + return + } setMentionItems([]) setMentionOpen(false) }) diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 9a88e75d..4ee3973f 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -72,10 +72,12 @@ const DialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef & { withoutClose?: boolean + /** Optional overlay className (e.g. z-[10001] so this dialog appears above other modals). */ + overlayClassName?: string } ->(({ className, children, withoutClose, ...props }, ref) => ( +>(({ className, children, withoutClose, overlayClassName, ...props }, ref) => ( - + { + text = text.replace(regex, (match) => { const trimmed = match.trim() const leadingSpace = match.startsWith(' ') ? ' ' : '' @@ -18,6 +18,10 @@ export function parseEditorJsonToText(node?: JSONContent) { return match } }) + + // Ensure space before nostr: when not already preceded by space (fixes "Like:nostr:npub" and "Like:\nnostr:npub") + text = text.replace(/(.)(?=nostr:)/g, (_, prev) => (prev === ' ' ? prev : prev + ' ')) + return text } function _parseEditorJsonToText(node?: JSONContent): string { diff --git a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx index 62bdeed0..a76da551 100644 --- a/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx +++ b/src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx @@ -207,6 +207,7 @@ export default function CreateThreadDialog({ const favoriteKey = favoriteRelays.join(',') const blockedKey = blockedRelays.join(',') const relaySetsKey = relaySets.map(s => `${s.id}:${s.relayUrls.join(',')}`).join(';') + const availableRelaysKey = availableRelays.join(',') // Initialize selected relays using the centralized relay selection service (once per meaningful change) useEffect(() => { @@ -249,7 +250,7 @@ export default function CreateThreadDialog({ } initializeRelays() - }, [initialRelay, availableRelays, writeKey, readKey, favoriteKey, blockedKey, relaySetsKey, pubkey]) + }, [initialRelay, availableRelaysKey, writeKey, readKey, favoriteKey, blockedKey, relaySetsKey, pubkey]) // Load cached thread draft when dialog opens useEffect(() => { diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx index 5cce8a57..267efc01 100644 --- a/src/pages/primary/DiscussionsPage/index.tsx +++ b/src/pages/primary/DiscussionsPage/index.tsx @@ -1042,7 +1042,7 @@ const DiscussionsPage = forwardRef((_, ref) => { titlebar={} displayScrollToTopButton > -
+