feat: add /mod summon
This commit is contained in:
parent
864cfaec54
commit
45a7e44b3a
11 changed files with 598 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -88,6 +88,26 @@ pub struct TeleportInfo {
|
|||
pub scene_slug: String,
|
||||
}
|
||||
|
||||
/// Summon information received from server (moderator summoned this user).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SummonInfo {
|
||||
/// Scene ID to teleport to.
|
||||
pub scene_id: uuid::Uuid,
|
||||
/// Scene slug for URL.
|
||||
pub scene_slug: String,
|
||||
/// Display name of the moderator who summoned.
|
||||
pub summoned_by: String,
|
||||
}
|
||||
|
||||
/// Result of a moderator command.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ModCommandResultInfo {
|
||||
/// Whether the command succeeded.
|
||||
pub success: bool,
|
||||
/// Human-readable result message.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// Hook to manage WebSocket connection for a channel.
|
||||
///
|
||||
/// Returns a tuple of:
|
||||
|
|
@ -107,6 +127,8 @@ pub fn use_channel_websocket(
|
|||
on_welcome: Option<Callback<ChannelMemberInfo>>,
|
||||
on_error: Option<Callback<WsError>>,
|
||||
on_teleport_approved: Option<Callback<TeleportInfo>>,
|
||||
on_summoned: Option<Callback<SummonInfo>>,
|
||||
on_mod_command_result: Option<Callback<ModCommandResultInfo>>,
|
||||
) -> (Signal<WsState>, WsSenderStorage) {
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
|
@ -220,6 +242,8 @@ pub fn use_channel_websocket(
|
|||
let on_welcome_clone = on_welcome.clone();
|
||||
let on_error_clone = on_error.clone();
|
||||
let on_teleport_approved_clone = on_teleport_approved.clone();
|
||||
let on_summoned_clone = on_summoned.clone();
|
||||
let on_mod_command_result_clone = on_mod_command_result.clone();
|
||||
// For starting heartbeat on Welcome
|
||||
let ws_ref_for_heartbeat = ws_ref.clone();
|
||||
let heartbeat_started: Rc<RefCell<bool>> = Rc::new(RefCell::new(false));
|
||||
|
|
@ -293,6 +317,8 @@ pub fn use_channel_websocket(
|
|||
&on_member_fading_clone,
|
||||
&on_error_clone,
|
||||
&on_teleport_approved_clone,
|
||||
&on_summoned_clone,
|
||||
&on_mod_command_result_clone,
|
||||
¤t_user_id_for_msg,
|
||||
);
|
||||
}
|
||||
|
|
@ -402,6 +428,8 @@ fn handle_server_message(
|
|||
on_member_fading: &Callback<FadingMember>,
|
||||
on_error: &Option<Callback<WsError>>,
|
||||
on_teleport_approved: &Option<Callback<TeleportInfo>>,
|
||||
on_summoned: &Option<Callback<SummonInfo>>,
|
||||
on_mod_command_result: &Option<Callback<ModCommandResultInfo>>,
|
||||
current_user_id: &std::rc::Rc<std::cell::RefCell<Option<uuid::Uuid>>>,
|
||||
) {
|
||||
let mut members_vec = members.borrow_mut();
|
||||
|
|
@ -578,6 +606,24 @@ fn handle_server_message(
|
|||
});
|
||||
}
|
||||
}
|
||||
ServerMessage::Summoned {
|
||||
scene_id,
|
||||
scene_slug,
|
||||
summoned_by,
|
||||
} => {
|
||||
if let Some(callback) = on_summoned {
|
||||
callback.run(SummonInfo {
|
||||
scene_id,
|
||||
scene_slug,
|
||||
summoned_by,
|
||||
});
|
||||
}
|
||||
}
|
||||
ServerMessage::ModCommandResult { success, message } => {
|
||||
if let Some(callback) = on_mod_command_result {
|
||||
callback.run(ModCommandResultInfo { success, message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -596,6 +642,8 @@ pub fn use_channel_websocket(
|
|||
_on_welcome: Option<Callback<ChannelMemberInfo>>,
|
||||
_on_error: Option<Callback<WsError>>,
|
||||
_on_teleport_approved: Option<Callback<TeleportInfo>>,
|
||||
_on_summoned: Option<Callback<SummonInfo>>,
|
||||
_on_mod_command_result: Option<Callback<ModCommandResultInfo>>,
|
||||
) -> (Signal<WsState>, WsSenderStorage) {
|
||||
let (ws_state, _) = signal(WsState::Disconnected);
|
||||
let sender: WsSenderStorage = StoredValue::new_local(None);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue