diff --git a/web/src/components/ChatArea.tsx b/web/src/components/ChatArea.tsx index 95f2d39..75a0617 100644 --- a/web/src/components/ChatArea.tsx +++ b/web/src/components/ChatArea.tsx @@ -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(null); + const [commandQuery, setCommandQuery] = useState(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 && ( )} + {commandQuery !== null && ( + { + setInput('/' + name + ' '); + setCommandQuery(null); + inputRef.current?.focus(); + }} + /> + )} + ))} + + ); +} diff --git a/web/src/lib/slashCommands.ts b/web/src/lib/slashCommands.ts new file mode 100644 index 0000000..1663f8b --- /dev/null +++ b/web/src/lib/slashCommands.ts @@ -0,0 +1,46 @@ +export interface SlashCommand { + name: string; + description: string; + transform: (args: string, username: string) => string; +} + +const SHRUG = '\u00AF\\_(\u30C4)_/\u00AF'; +const TABLEFLIP = '(\u256F\u00B0\u25A1\u00B0\uFF09\u256F\uFE35 \u253B\u2501\u253B'; +const UNFLIP = '\u252C\u2500\u252C\u30CE( \u00BA _ \u00BA\u30CE)'; +const LENNY = '( \u0361\u00B0 \u035C\u0325 \u0361\u00B0)'; +const BEAR = '\u0295\u2022\u1D25\u2022\u0294'; +const DISAPPROVE = '\u0CA0_\u0CA0'; +const FACEPALM = '(\uFF0D\u1DD9\uFF4D)'; +const CRY = '(\u2568_\u2568)'; +const DANCE = '\u250C(\u30FB\u30FB\u30FB)\u2518\u266A'; +const HUG = '(\u2283\u301C\u203F\u2022\u032F\u2022\uFF65\u0300)\u2283'; +const GREET = '(\u30CE\u00B4\uFF89`)\u30CE'; + +function appendOrReturn(kaomoji: string, args: string): string { + return args ? args + ' ' + kaomoji : kaomoji; +} + +export const SLASH_COMMANDS: SlashCommand[] = [ + { name: 'shrug', description: 'Append shrug kaomoji', transform: (a) => appendOrReturn(SHRUG, a) }, + { name: 'tableflip', description: 'Flip a table', transform: (a) => appendOrReturn(TABLEFLIP, a) }, + { name: 'unflip', description: 'Put the table back', transform: (a) => appendOrReturn(UNFLIP, a) }, + { name: 'lenny', description: '( \u0361\u00B0 \u035C\u0325 \u0361\u00B0)', transform: (a) => appendOrReturn(LENNY, a) }, + { name: 'bear', description: 'Kaomoji bear', transform: (a) => a ? BEAR + ' ' + a : BEAR }, + { name: 'disapprove', description: 'Look of disapproval', transform: (a) => appendOrReturn(DISAPPROVE, a) }, + { name: 'facepalm', description: 'Facepalm', transform: (a) => appendOrReturn(FACEPALM, a) }, + { name: 'cry', description: 'Crying', transform: (a) => appendOrReturn(CRY, a) }, + { name: 'dance', description: 'Dance', transform: (a) => appendOrReturn(DANCE, a) }, + { name: 'hug', description: 'Hug', transform: (a) => a ? HUG + ' ' + a : HUG }, + { name: 'greet', description: 'Friendly wave hello', transform: (a) => a ? GREET + ' ' + a : GREET + ' Hello!' }, + { name: 'me', description: 'Send an action message', transform: (a, u) => '*' + u + ' ' + a + '*' }, + { name: 'spoiler', description: 'Hide text as a spoiler', transform: (a) => '||' + a + '||' }, +]; + +export function findCommand(name: string): SlashCommand | undefined { + return SLASH_COMMANDS.find((c) => c.name === name.toLowerCase()); +} + +export function matchingCommands(query: string): SlashCommand[] { + const q = query.toLowerCase(); + return SLASH_COMMANDS.filter((c) => c.name.startsWith(q)); +} diff --git a/web/tsconfig.app.tsbuildinfo b/web/tsconfig.app.tsbuildinfo index 4e6c59b..2a750ca 100644 --- a/web/tsconfig.app.tsbuildinfo +++ b/web/tsconfig.app.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandManager.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/PinnedMessages.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/App.tsx","./src/main.tsx","./src/components/BotManager.tsx","./src/components/CalendarView.tsx","./src/components/ChannelList.tsx","./src/components/ChannelSettingsModal.tsx","./src/components/ChatArea.tsx","./src/components/CommandDropdown.tsx","./src/components/CommandManager.tsx","./src/components/ContextMenu.tsx","./src/components/ConversationList.tsx","./src/components/CreateChannelModal.tsx","./src/components/CreateServerModal.tsx","./src/components/DMChat.tsx","./src/components/DocsView.tsx","./src/components/EmojiPicker.tsx","./src/components/ForgotPasswordPage.tsx","./src/components/ForumView.tsx","./src/components/GiphyPicker.tsx","./src/components/InstallPrompt.tsx","./src/components/InviteModal.tsx","./src/components/JoinServer.tsx","./src/components/JoinServerModal.tsx","./src/components/Layout.tsx","./src/components/ListView.tsx","./src/components/LoginForm.tsx","./src/components/MemberContextMenu.tsx","./src/components/MemberList.tsx","./src/components/MemberRoleAssign.tsx","./src/components/MentionDropdown.tsx","./src/components/MentionPopup.tsx","./src/components/MessageSearch.tsx","./src/components/MobileDrawer.tsx","./src/components/MobileNav.tsx","./src/components/NewConversationModal.tsx","./src/components/PinnedMessages.tsx","./src/components/ReactionBar.tsx","./src/components/ReplyBar.tsx","./src/components/ResetPasswordPage.tsx","./src/components/RoleManager.tsx","./src/components/ServerBar.tsx","./src/components/ServerSettingsModal.tsx","./src/components/SlashCommandPopup.tsx","./src/components/ThemeToggle.tsx","./src/components/ThreadListPanel.tsx","./src/components/ThreadPanel.tsx","./src/components/TypingIndicator.tsx","./src/components/UserProfileModal.tsx","./src/components/UserSettings.tsx","./src/components/VideoGrid.tsx","./src/components/VoiceChannel.tsx","./src/components/VoiceControls.tsx","./src/components/VoicePanel.tsx","./src/lib/api.ts","./src/lib/kaomojiData.ts","./src/lib/slashCommands.ts","./src/lib/usePermissions.ts","./src/stores/auth.ts","./src/stores/bot.ts","./src/stores/channel.ts","./src/stores/conversation.ts","./src/stores/layout.ts","./src/stores/member.ts","./src/stores/message.ts","./src/stores/moderation.ts","./src/stores/notificationSettings.ts","./src/stores/permissions.ts","./src/stores/presence.ts","./src/stores/push.ts","./src/stores/readStates.ts","./src/stores/role.ts","./src/stores/server.ts","./src/stores/thread.ts","./src/stores/typing.ts","./src/stores/voice.ts","./src/stores/ws.ts"],"version":"5.9.3"} \ No newline at end of file