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:
2026-07-02 12:22:43 -04:00
parent 722eab8e94
commit d8b4defaff
4 changed files with 114 additions and 2 deletions
+32 -1
View File
@@ -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"