mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 23:47:12 +02:00
feat: make chat actions context-aware and filter by membership status
Actions are now intelligently filtered based on the user's membership in the conversation, providing a cleaner and more intuitive UX. Changes: - Added GetActionsOptions type with conversation and activePubkey - Updated base adapter getActions() to accept optional context - Modified NIP-29 adapter to filter actions based on membership: - /join: only shown when user is NOT a member/admin - /leave: only shown when user IS a member - Updated ChatViewer to pass conversation and user context to searchCommands - Moved searchCommands callback after conversation is defined This prevents showing irrelevant commands like "/join" when you're already a member, or "/leave" when you haven't joined yet. The autocomplete menu now only displays actions that are actually executable in the current context. Implementation notes: - NIP-29 uses getAllActions() fallback when context unavailable - Membership determined by checking conversation.participants array - Other protocols return empty array by default (no actions yet) Tests: All 804 tests passing Build: Successful Lint: No new errors
This commit is contained in:
@@ -333,18 +333,6 @@ export function ChatViewer({
|
||||
// Get the appropriate adapter for this protocol
|
||||
const adapter = useMemo(() => getAdapter(protocol), [protocol]);
|
||||
|
||||
// Slash command search for action autocomplete
|
||||
const searchCommands = useCallback(
|
||||
async (query: string) => {
|
||||
const availableActions = adapter.getActions();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return availableActions.filter((action) =>
|
||||
action.name.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
},
|
||||
[adapter],
|
||||
);
|
||||
|
||||
// State for retry trigger
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
|
||||
@@ -377,6 +365,22 @@ export function ChatViewer({
|
||||
? conversationResult.conversation
|
||||
: null;
|
||||
|
||||
// Slash command search for action autocomplete
|
||||
// Context-aware: only shows relevant actions based on membership status
|
||||
const searchCommands = useCallback(
|
||||
async (query: string) => {
|
||||
const availableActions = adapter.getActions({
|
||||
conversation: conversation || undefined,
|
||||
activePubkey: activeAccount?.pubkey,
|
||||
});
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return availableActions.filter((action) =>
|
||||
action.name.toLowerCase().includes(lowerQuery),
|
||||
);
|
||||
},
|
||||
[adapter, conversation, activeAccount],
|
||||
);
|
||||
|
||||
// Cleanup subscriptions when conversation changes or component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
ChatAction,
|
||||
ChatActionContext,
|
||||
ChatActionResult,
|
||||
GetActionsOptions,
|
||||
} from "@/types/chat-actions";
|
||||
|
||||
/**
|
||||
@@ -150,9 +151,10 @@ export abstract class ChatProtocolAdapter {
|
||||
/**
|
||||
* Get available actions for this protocol
|
||||
* Actions are protocol-specific slash commands like /join, /leave, etc.
|
||||
* Can be filtered based on conversation and user context
|
||||
* Returns empty array by default
|
||||
*/
|
||||
getActions(): ChatAction[] {
|
||||
getActions(_options?: GetActionsOptions): ChatAction[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -164,7 +166,11 @@ export abstract class ChatProtocolAdapter {
|
||||
actionName: string,
|
||||
context: ChatActionContext,
|
||||
): Promise<ChatActionResult> {
|
||||
const action = this.getActions().find((a) => a.name === actionName);
|
||||
// Get actions with context for validation
|
||||
const action = this.getActions({
|
||||
conversation: context.conversation,
|
||||
activePubkey: context.activePubkey,
|
||||
}).find((a) => a.name === actionName);
|
||||
|
||||
if (!action) {
|
||||
return {
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
ParticipantRole,
|
||||
} from "@/types/chat";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { ChatAction } from "@/types/chat-actions";
|
||||
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";
|
||||
@@ -493,9 +493,84 @@ export class Nip29Adapter extends ChatProtocolAdapter {
|
||||
|
||||
/**
|
||||
* Get available actions for NIP-29 groups
|
||||
* Returns simple join/leave commands without parameters
|
||||
* 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
|
||||
*/
|
||||
getActions(): ChatAction[] {
|
||||
getActions(options?: GetActionsOptions): ChatAction[] {
|
||||
const actions: ChatAction[] = [];
|
||||
|
||||
// Check if we have context to filter actions
|
||||
if (!options?.conversation || !options?.activePubkey) {
|
||||
// No context - return all actions
|
||||
return this.getAllActions();
|
||||
}
|
||||
|
||||
const { conversation, activePubkey } = options;
|
||||
|
||||
// Find user's participant info
|
||||
const userParticipant = conversation.participants.find(
|
||||
(p) => p.pubkey === activePubkey,
|
||||
);
|
||||
|
||||
const isMember = !!userParticipant;
|
||||
|
||||
// Add /join if user is NOT a member
|
||||
if (!isMember) {
|
||||
actions.push({
|
||||
name: "join",
|
||||
description: "Request to join the group",
|
||||
handler: async (context) => {
|
||||
try {
|
||||
await this.joinConversation(context.conversation);
|
||||
return {
|
||||
success: true,
|
||||
message: "Join request sent",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error ? error.message : "Failed to join group",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add /leave if user IS a member
|
||||
if (isMember) {
|
||||
actions.push({
|
||||
name: "leave",
|
||||
description: "Leave the group",
|
||||
handler: async (context) => {
|
||||
try {
|
||||
await this.leaveConversation(context.conversation);
|
||||
return {
|
||||
success: true,
|
||||
message: "You left the group",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to leave group",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all possible actions (used when no context available)
|
||||
* @private
|
||||
*/
|
||||
private getAllActions(): ChatAction[] {
|
||||
return [
|
||||
{
|
||||
name: "join",
|
||||
|
||||
@@ -35,3 +35,14 @@ export interface ChatAction {
|
||||
/** Handler function */
|
||||
handler: (context: ChatActionContext) => Promise<ChatActionResult>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for filtering available actions
|
||||
*/
|
||||
export interface GetActionsOptions {
|
||||
/** Current conversation */
|
||||
conversation?: Conversation;
|
||||
|
||||
/** Active user's pubkey */
|
||||
activePubkey?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user