feat: add /mod summon

This commit is contained in:
Evan Carroll 2026-01-20 21:48:04 -06:00
parent 864cfaec54
commit 45a7e44b3a
11 changed files with 598 additions and 5 deletions

View file

@ -19,6 +19,8 @@ enum CommandMode {
ShowingColonHint,
/// Showing command hint for slash commands (`/setting`).
ShowingSlashHint,
/// Showing mod command hint (`/mod summon [nick|*]`).
ShowingModHint,
/// Showing emotion list popup.
ShowingList,
/// Showing scene list popup for teleport.
@ -98,6 +100,35 @@ fn parse_whisper_command(cmd: &str) -> Option<(String, String)> {
Some((name, message))
}
/// Parse a mod command and return (subcommand, args) if valid.
///
/// Supports `/mod summon [nick|*]` etc.
#[cfg(feature = "hydrate")]
fn parse_mod_command(cmd: &str) -> Option<(String, Vec<String>)> {
let cmd = cmd.trim();
// Strip the leading slash if present
let cmd = cmd.strip_prefix('/').unwrap_or(cmd);
// Check for `mod <subcommand> [args...]`
let rest = cmd.strip_prefix("mod ").map(str::trim)?;
if rest.is_empty() {
return None;
}
// Split into parts
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.is_empty() {
return None;
}
let subcommand = parts[0].to_lowercase();
let args: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
Some((subcommand, args))
}
/// Chat input component with emote command support.
///
/// Props:
@ -140,6 +171,12 @@ pub fn ChatInput(
/// Callback when a teleport is requested.
#[prop(optional)]
on_teleport: Option<Callback<Uuid>>,
/// Whether the current user is a moderator.
#[prop(default = Signal::derive(|| false))]
is_moderator: Signal<bool>,
/// Callback to send a mod command.
#[prop(optional)]
on_mod_command: Option<Callback<(String, Vec<String>)>>,
) -> impl IntoView {
let (message, set_message) = signal(String::new());
let (command_mode, set_command_mode) = signal(CommandMode::None);
@ -324,9 +361,16 @@ pub fn ChatInput(
}
};
// Check if mod command (only for moderators)
let is_mod_command = is_moderator.get_untracked()
&& (cmd.starts_with("mod") || "mod".starts_with(&cmd));
if is_complete_whisper || is_complete_teleport {
// User is typing the argument part, no hint needed
set_command_mode.set(CommandMode::None);
} else if is_mod_command {
// Show mod command hint
set_command_mode.set(CommandMode::ShowingModHint);
} else if cmd.is_empty()
|| "setting".starts_with(&cmd)
|| "inventory".starts_with(&cmd)
@ -625,6 +669,22 @@ pub fn ChatInput(
return;
}
// /mod <subcommand> [args...] - execute mod command
if is_moderator.get_untracked() {
if let Some((subcommand, args)) = parse_mod_command(&msg) {
if let Some(ref callback) = on_mod_command {
callback.run((subcommand, args));
}
set_message.set(String::new());
set_command_mode.set(CommandMode::None);
if let Some(input) = input_ref.get() {
input.set_value("");
}
ev.prevent_default();
return;
}
}
// Invalid slash command - just ignore, don't send
ev.prevent_default();
return;
@ -769,6 +829,17 @@ pub fn ChatInput(
</div>
</Show>
// Mod command hint bar (/mod summon [nick|*])
<Show when=move || command_mode.get() == CommandMode::ShowingModHint>
<div class="absolute bottom-full left-0 mb-1 px-3 py-1 bg-purple-800/90 backdrop-blur-sm rounded text-sm">
<span class="text-purple-300 font-medium">"[MOD] "</span>
<span class="text-gray-400">"/"</span>
<span class="text-purple-400">"mod"</span>
<span class="text-gray-300">" summon"</span>
<span class="text-gray-500">" [nick|*]"</span>
</div>
</Show>
// Emotion list popup
<Show when=move || command_mode.get() == CommandMode::ShowingList>
<EmoteListPopup