From a65f9073e50670a79acb768a2bbcdbf3785d9736 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Jan 2026 11:21:39 +0000 Subject: [PATCH] fix: slash autocomplete only at input start + add bookmark commands - Fix slash command autocomplete to only trigger when / is at the beginning of input text (position 1 in TipTap), not in the middle of messages - Add /bookmark command to add NIP-29 group to user's kind 10009 list - Add /unbookmark command to remove group from user's kind 10009 list --- src/components/editor/MentionEditor.tsx | 3 + src/lib/chat/adapters/nip-29-adapter.ts | 213 +++++++++++++++++++++++- 2 files changed, 215 insertions(+), 1 deletion(-) diff --git a/src/components/editor/MentionEditor.tsx b/src/components/editor/MentionEditor.tsx index dfb5cee..a7f6a3b 100644 --- a/src/components/editor/MentionEditor.tsx +++ b/src/components/editor/MentionEditor.tsx @@ -345,6 +345,7 @@ export const MentionEditor = forwardRef< ); // Create slash command suggestion configuration for / commands + // Only triggers when / is at the very beginning of the input const slashCommandSuggestion: Omit | null = useMemo( () => @@ -352,6 +353,8 @@ export const MentionEditor = forwardRef< ? { char: "/", allowSpaces: false, + // Only allow slash commands at the start of input (position 1 in TipTap = first char) + allow: ({ range }) => range.from === 1, items: async ({ query }) => { return await searchCommands(query); }, diff --git a/src/lib/chat/adapters/nip-29-adapter.ts b/src/lib/chat/adapters/nip-29-adapter.ts index 35433c2..0a7c6a4 100644 --- a/src/lib/chat/adapters/nip-29-adapter.ts +++ b/src/lib/chat/adapters/nip-29-adapter.ts @@ -16,7 +16,7 @@ import type { NostrEvent } from "@/types/nostr"; import type { ChatAction, GetActionsOptions } from "@/types/chat-actions"; import eventStore from "@/services/event-store"; import pool from "@/services/relay-pool"; -import { publishEventToRelays } from "@/services/hub"; +import { publishEventToRelays, publishEvent } from "@/services/hub"; import accountManager from "@/services/accounts"; import { getTagValues } from "@/lib/nostr-utils"; import { EventFactory } from "applesauce-core/event-factory"; @@ -496,6 +496,8 @@ export class Nip29Adapter extends ChatProtocolAdapter { * Filters actions based on user's membership status: * - /join: only shown when user is NOT a member/admin * - /leave: only shown when user IS a member + * - /bookmark: only shown when group is NOT in user's kind 10009 list + * - /unbookmark: only shown when group IS in user's kind 10009 list */ getActions(options?: GetActionsOptions): ChatAction[] { const actions: ChatAction[] = []; @@ -563,6 +565,55 @@ export class Nip29Adapter extends ChatProtocolAdapter { }); } + // Add bookmark/unbookmark actions + // These are always available - the handler checks current state + actions.push({ + name: "bookmark", + description: "Add group to your group list", + handler: async (context) => { + try { + await this.bookmarkGroup(context.conversation, context.activePubkey); + return { + success: true, + message: "Group added to your list", + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to bookmark group", + }; + } + }, + }); + + actions.push({ + name: "unbookmark", + description: "Remove group from your group list", + handler: async (context) => { + try { + await this.unbookmarkGroup( + context.conversation, + context.activePubkey, + ); + return { + success: true, + message: "Group removed from your list", + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to unbookmark group", + }; + } + }, + }); + return actions; } @@ -612,6 +663,54 @@ export class Nip29Adapter extends ChatProtocolAdapter { } }, }, + { + name: "bookmark", + description: "Add group to your group list", + handler: async (context) => { + try { + await this.bookmarkGroup( + context.conversation, + context.activePubkey, + ); + return { + success: true, + message: "Group added to your list", + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to bookmark group", + }; + } + }, + }, + { + name: "unbookmark", + description: "Remove group from your group list", + handler: async (context) => { + try { + await this.unbookmarkGroup( + context.conversation, + context.activePubkey, + ); + return { + success: true, + message: "Group removed from your list", + }; + } catch (error) { + return { + success: false, + message: + error instanceof Error + ? error.message + : "Failed to unbookmark group", + }; + } + }, + }, ]; } @@ -755,6 +854,118 @@ export class Nip29Adapter extends ChatProtocolAdapter { await publishEventToRelays(event, [relayUrl]); } + /** + * Add a group to the user's group list (kind 10009) + */ + async bookmarkGroup( + conversation: Conversation, + activePubkey: string, + ): Promise { + const activeSigner = accountManager.active$.value?.signer; + + if (!activeSigner) { + throw new Error("No active signer"); + } + + const groupId = conversation.metadata?.groupId; + const relayUrl = conversation.metadata?.relayUrl; + + if (!groupId || !relayUrl) { + throw new Error("Group ID and relay URL required"); + } + + // Fetch current kind 10009 event (group list) + const currentEvent = await firstValueFrom( + eventStore.replaceable(10009, activePubkey, ""), + { defaultValue: undefined }, + ); + + // Build new tags array + let tags: string[][] = []; + + if (currentEvent) { + // Copy existing tags + tags = [...currentEvent.tags]; + + // Check if group is already in the list + const existingGroup = tags.find( + (t) => t[0] === "group" && t[1] === groupId && t[2] === relayUrl, + ); + + if (existingGroup) { + throw new Error("Group is already in your list"); + } + } + + // Add the new group tag + tags.push(["group", groupId, relayUrl]); + + // Create and publish the updated event + const factory = new EventFactory(); + factory.setSigner(activeSigner); + + const draft = await factory.build({ + kind: 10009, + content: "", + tags, + }); + const event = await factory.sign(draft); + await publishEvent(event); + } + + /** + * Remove a group from the user's group list (kind 10009) + */ + async unbookmarkGroup( + conversation: Conversation, + activePubkey: string, + ): Promise { + const activeSigner = accountManager.active$.value?.signer; + + if (!activeSigner) { + throw new Error("No active signer"); + } + + const groupId = conversation.metadata?.groupId; + const relayUrl = conversation.metadata?.relayUrl; + + if (!groupId || !relayUrl) { + throw new Error("Group ID and relay URL required"); + } + + // Fetch current kind 10009 event (group list) + const currentEvent = await firstValueFrom( + eventStore.replaceable(10009, activePubkey, ""), + { defaultValue: undefined }, + ); + + if (!currentEvent) { + throw new Error("No group list found"); + } + + // Find and remove the group tag + const originalLength = currentEvent.tags.length; + const tags = currentEvent.tags.filter( + (t) => !(t[0] === "group" && t[1] === groupId && t[2] === relayUrl), + ); + + if (tags.length === originalLength) { + throw new Error("Group is not in your list"); + } + + // Create and publish the updated event + const factory = new EventFactory(); + factory.setSigner(activeSigner); + + const draft = await factory.build({ + kind: 10009, + content: "", + tags, + }); + const event = await factory.sign(draft); + await publishEvent(event); + } + /** * Helper: Convert Nostr event to Message */