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
This commit is contained in:
Claude
2026-01-13 11:21:39 +00:00
parent 280a395187
commit a65f9073e5
2 changed files with 215 additions and 1 deletions

View File

@@ -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<SuggestionOptions, "editor"> | 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);
},

View File

@@ -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<void> {
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<void> {
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
*/