feat: slash commands with autocomplete dropdown
Type / at start of message to see available commands. Commands: /shrug, /tableflip, /unflip, /lenny, /bear, /disapprove, /facepalm, /cry, /dance, /hug, /greet, /me (action text), /spoiler (hidden text). Autocomplete dropdown with tab-complete. Commands transform input text client-side before sending. Unknown /commands pass through as regular messages.
This commit is contained in:
@@ -8,6 +8,8 @@ import { useAuthStore } from "../stores/auth.ts";
|
||||
import { useMemberStore } from "../stores/member.ts";
|
||||
import { useThreadStore } from "../stores/thread.ts";
|
||||
import { GiphyPicker, type Gif } from "./GiphyPicker";
|
||||
import { CommandDropdown } from "./CommandDropdown";
|
||||
import { findCommand } from "../lib/slashCommands";
|
||||
import { MentionDropdown } from "./MentionDropdown";
|
||||
import { useReadStatesStore } from "../stores/readStates.ts";
|
||||
import { MessageSearch } from "./MessageSearch";
|
||||
@@ -269,6 +271,7 @@ export function ChatArea() {
|
||||
const [showKaomoji, setShowKaomoji] = useState(false);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
const [mentionQuery, setMentionQuery] = useState<string | null>(null);
|
||||
const [commandQuery, setCommandQuery] = useState<string | null>(null);
|
||||
const [slowmodeRemaining, setSlowmodeRemaining] = useState(0);
|
||||
const [selectMode, setSelectMode] = useState(false);
|
||||
const [showThreads, setShowThreads] = useState(false);
|
||||
@@ -434,6 +437,13 @@ export function ChatArea() {
|
||||
}
|
||||
|
||||
const beforeCursor = value.slice(0, cursor);
|
||||
// Slash command detection (only at start of input)
|
||||
if (beforeCursor.startsWith('/') && !beforeCursor.includes(' ')) {
|
||||
setCommandQuery(beforeCursor.slice(1));
|
||||
setMentionQuery(null);
|
||||
return;
|
||||
}
|
||||
setCommandQuery(null);
|
||||
const atIndex = beforeCursor.lastIndexOf("@");
|
||||
if (atIndex === -1) {
|
||||
setMentionQuery(null);
|
||||
@@ -478,7 +488,18 @@ export function ChatArea() {
|
||||
if (!activeChannelId || !input.trim() || slowmodeRemaining > 0) return;
|
||||
setError(null);
|
||||
try {
|
||||
await sendMessage(activeChannelId, input.trim(), replyToMessage?.id);
|
||||
let messageText = input.trim();
|
||||
// Slash command transform
|
||||
if (messageText.startsWith('/')) {
|
||||
const spaceIdx = messageText.indexOf(' ');
|
||||
const cmdName = spaceIdx === -1 ? messageText.slice(1) : messageText.slice(1, spaceIdx);
|
||||
const args = spaceIdx === -1 ? '' : messageText.slice(spaceIdx + 1).trim();
|
||||
const cmd = findCommand(cmdName);
|
||||
if (cmd) {
|
||||
messageText = cmd.transform(args, currentUser?.username || 'user');
|
||||
}
|
||||
}
|
||||
await sendMessage(activeChannelId, messageText, replyToMessage?.id);
|
||||
setInput("");
|
||||
setReplyToMessage(null);
|
||||
setMentionQuery(null);
|
||||
@@ -714,6 +735,16 @@ export function ChatArea() {
|
||||
{mentionQuery !== null && (
|
||||
<MentionDropdown query={mentionQuery} members={members} onSelect={handleMentionSelect} />
|
||||
)}
|
||||
{commandQuery !== null && (
|
||||
<CommandDropdown
|
||||
query={commandQuery}
|
||||
onSelect={(name) => {
|
||||
setInput('/' + name + ' ');
|
||||
setCommandQuery(null);
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { SLASH_COMMANDS } from "../lib/slashCommands";
|
||||
|
||||
interface CommandDropdownProps {
|
||||
query: string;
|
||||
onSelect: (command: string) => void;
|
||||
}
|
||||
|
||||
export function CommandDropdown({ query, onSelect }: CommandDropdownProps) {
|
||||
const q = query.toLowerCase();
|
||||
const filtered = SLASH_COMMANDS.filter((c) => c.name.startsWith(q)).slice(0, 8);
|
||||
|
||||
if (filtered.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute bottom-full left-0 mb-1 z-50 w-72 max-h-48 overflow-y-auto bg-gb-bg-s border border-gb-bg-t shadow-lg">
|
||||
<div className="px-2 py-1 text-xs text-gb-fg-s font-mono border-b border-gb-bg-t">
|
||||
COMMANDS
|
||||
</div>
|
||||
{filtered.map((cmd) => (
|
||||
<button
|
||||
key={cmd.name}
|
||||
type="button"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
onSelect(cmd.name);
|
||||
}}
|
||||
className="w-full text-left px-2 py-1 text-sm font-mono hover:bg-gb-bg-t text-gb-fg flex items-center gap-2"
|
||||
>
|
||||
<span className="text-gb-orange">/{cmd.name}</span>
|
||||
<span className="text-gb-fg-f text-xs truncate">{cmd.description}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user