From 47e89f52d09adf9ea9c090eef724c670693cbafe Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Sat, 21 Feb 2026 16:27:20 -0600 Subject: [PATCH 1/3] package-lock --- package-lock.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/package-lock.json b/package-lock.json index 2c9d3903..dcdb36fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3062,6 +3062,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3075,6 +3076,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3088,6 +3090,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3101,6 +3104,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3114,6 +3118,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3127,6 +3132,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3140,6 +3146,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3153,6 +3160,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3166,6 +3174,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3179,6 +3188,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3192,6 +3202,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3205,6 +3216,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3218,6 +3230,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3231,6 +3244,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3244,6 +3258,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3257,6 +3272,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3270,6 +3286,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3283,6 +3300,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3296,6 +3314,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -3309,6 +3328,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ From c28797be97d1215a2220896d9629c7bd9f1f77de Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Sat, 21 Feb 2026 16:57:46 -0600 Subject: [PATCH 2/3] Add follow-prioritized search autocomplete and @mention support Search autocomplete now prioritizes followed profiles via client-side matching merged ahead of relay results. A following badge (UserRoundCheck) appears on avatars in both the sidebar dropdown and search page accounts tab. The compose box supports @mentions with an inline autocomplete dropdown that inserts nostr:npub1 URIs and adds p tags on publish. --- src/components/ComposeBox.tsx | 82 ++++-- src/components/MentionAutocomplete.tsx | 321 +++++++++++++++++++++++ src/components/ProfileSearchDropdown.tsx | 30 ++- src/hooks/useFollowedProfiles.ts | 32 +++ src/hooks/useSearchProfiles.ts | 67 ++++- src/pages/SearchPage.tsx | 30 ++- 6 files changed, 523 insertions(+), 39 deletions(-) create mode 100644 src/components/MentionAutocomplete.tsx create mode 100644 src/hooks/useFollowedProfiles.ts diff --git a/src/components/ComposeBox.tsx b/src/components/ComposeBox.tsx index 5877b243..432cd0bb 100644 --- a/src/components/ComposeBox.tsx +++ b/src/components/ComposeBox.tsx @@ -11,6 +11,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { EmojiPicker } from '@/components/EmojiPicker'; import { EmbeddedNote } from '@/components/EmbeddedNote'; +import { MentionAutocomplete } from '@/components/MentionAutocomplete'; import { NoteContent } from '@/components/NoteContent'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -289,6 +290,19 @@ export function ComposeBox({ expand(); }, [content, expand]); + const handleInsertMention = useCallback(({ start, end, replacement }: { start: number; end: number; replacement: string }) => { + const newContent = content.slice(0, start) + replacement + content.slice(end); + setContent(newContent); + requestAnimationFrame(() => { + const textarea = textareaRef.current; + if (textarea) { + textarea.focus(); + const pos = start + replacement.length; + textarea.setSelectionRange(pos, pos); + } + }); + }, [content]); + const handleFileUpload = useCallback(async (file: File) => { try { const tags = await uploadFile(file); @@ -367,6 +381,25 @@ export function ComposeBox({ const hashtags = content.match(/#\w+/g)?.map((t) => t.slice(1)) || []; const tags: string[][] = hashtags.map((t) => ['t', t.toLowerCase()]); + // NIP-27 mention p tags — extract nostr:npub1... from content + const mentionMatches = content.matchAll(/nostr:(npub1[023456789acdefghjklmnpqrstuvwxyz]+)/g); + const mentionedPubkeys = new Set(); + for (const match of mentionMatches) { + try { + const decoded = nip19.decode(match[1]); + if (decoded.type === 'npub') { + mentionedPubkeys.add(decoded.data); + } + } catch { + // Invalid bech32, skip + } + } + // Don't include ourselves + mentionedPubkeys.delete(user.pubkey); + for (const pk of mentionedPubkeys) { + tags.push(['p', pk]); + } + // NIP-10 reply tags if (replyTo) { // Determine root of the thread @@ -381,13 +414,15 @@ export function ComposeBox({ } // Add p tags: original author + all existing p tags from the parent + // Skip pubkeys already added by mention detection above const pPubkeys = new Set(); pPubkeys.add(replyTo.pubkey); for (const tag of replyTo.tags) { if (tag[0] === 'p' && tag[1]) pPubkeys.add(tag[1]); } - // Don't include ourselves + // Don't include ourselves or already-mentioned pubkeys if (user.pubkey) pPubkeys.delete(user.pubkey); + for (const pk of mentionedPubkeys) pPubkeys.delete(pk); for (const pk of pPubkeys) { tags.push(['p', pk]); } @@ -558,25 +593,32 @@ export function ComposeBox({
{!previewMode ? ( /* Edit mode - Textarea */ -