diff --git a/crates/chattyness-db/src/models.rs b/crates/chattyness-db/src/models.rs index 919709b..3dee106 100644 --- a/crates/chattyness-db/src/models.rs +++ b/crates/chattyness-db/src/models.rs @@ -227,6 +227,7 @@ pub enum ActionType { MessageDeletion, Summon, SummonAll, + Teleport, } impl std::fmt::Display for ActionType { @@ -241,6 +242,7 @@ impl std::fmt::Display for ActionType { ActionType::MessageDeletion => write!(f, "message_deletion"), ActionType::Summon => write!(f, "summon"), ActionType::SummonAll => write!(f, "summon_all"), + ActionType::Teleport => write!(f, "teleport"), } } } diff --git a/crates/chattyness-user-ui/src/api/websocket.rs b/crates/chattyness-user-ui/src/api/websocket.rs index 1f6f54c..1473f24 100644 --- a/crates/chattyness-user-ui/src/api/websocket.rs +++ b/crates/chattyness-user-ui/src/api/websocket.rs @@ -983,6 +983,89 @@ async fn handle_socket( } } } + "teleport" => { + if args.len() < 2 { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: "Usage: /mod teleport [nick] [slug]".to_string(), + }).await; + continue; + } + + let target_nick = &args[0]; + let target_slug = &args[1]; + + // Look up the target scene by slug + let target_scene = match scenes::get_scene_by_slug(&pool, realm_id, target_slug).await { + Ok(Some(s)) => s, + Ok(None) => { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: format!("Scene '{}' not found", target_slug), + }).await; + continue; + } + Err(_) => { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: "Failed to look up scene".to_string(), + }).await; + continue; + } + }; + + // Find target user by display name + if let Some((target_user_id, target_conn)) = ws_state + .find_user_by_display_name(realm_id, target_nick) + { + if target_user_id == user_id { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: "You cannot teleport yourself".to_string(), + }).await; + continue; + } + + // Send Summoned message to target user with the specified scene + let teleport_msg = ServerMessage::Summoned { + scene_id: target_scene.id, + scene_slug: target_scene.slug.clone(), + summoned_by: mod_member.display_name.clone(), + }; + if target_conn.direct_tx.send(teleport_msg).await.is_ok() { + // Log the action + let metadata = serde_json::json!({ + "scene_id": target_scene.id, + "scene_slug": target_scene.slug, + "target_display_name": target_nick, + }); + let _ = moderation::log_moderation_action( + &pool, + realm_id, + user_id, + ActionType::Teleport, + Some(target_user_id), + &format!("Teleported {} to scene {}", target_nick, target_scene.name), + metadata, + ).await; + + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: true, + message: format!("Teleported {} to {}", target_nick, target_scene.name), + }).await; + } else { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: format!("Failed to send teleport to {}", target_nick), + }).await; + } + } else { + let _ = direct_tx.send(ServerMessage::ModCommandResult { + success: false, + message: format!("User '{}' is not online in this realm", target_nick), + }).await; + } + } _ => { let _ = direct_tx.send(ServerMessage::ModCommandResult { success: false, diff --git a/crates/chattyness-user-ui/src/components/chat.rs b/crates/chattyness-user-ui/src/components/chat.rs index ec0a6d5..3f3dfe7 100644 --- a/crates/chattyness-user-ui/src/components/chat.rs +++ b/crates/chattyness-user-ui/src/components/chat.rs @@ -17,9 +17,9 @@ enum CommandMode { None, /// Showing command hint for colon commands (`:e[mote], :l[ist]`). ShowingColonHint, - /// Showing command hint for slash commands (`/setting`). + /// Showing command hint for slash commands (`/setting`, `/mod` for mods). ShowingSlashHint, - /// Showing mod command hint (`/mod summon [nick|*]`). + /// Showing mod command hints only (`/mod summon [nick|*]`). ShowingModHint, /// Showing emotion list popup. ShowingList, @@ -361,15 +361,21 @@ 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)); + // Check if typing mod command (only for moderators) + // Show mod hint when typing "/mod" or "/mod ..." + let is_typing_mod = is_moderator.get_untracked() + && (cmd == "mod" || cmd.starts_with("mod ")); + // Show /mod in slash hints when just starting to type it + let is_partial_mod = is_moderator.get_untracked() + && !cmd.is_empty() + && "mod".starts_with(&cmd) + && cmd != "mod"; 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 + } else if is_typing_mod { + // Show mod-specific hint bar set_command_mode.set(CommandMode::ShowingModHint); } else if cmd.is_empty() || "setting".starts_with(&cmd) @@ -385,6 +391,7 @@ pub fn ChatInput( || cmd.starts_with("whisper ") || cmd.starts_with("t ") || cmd.starts_with("teleport ") + || is_partial_mod { set_command_mode.set(CommandMode::ShowingSlashHint); } else { @@ -826,17 +833,25 @@ pub fn ChatInput( "t" "[eleport]" + // Show /mod hint for moderators (details shown when typing /mod) + + "|" + "/" + "mod" + - // Mod command hint bar (/mod summon [nick|*]) + // Mod command hint bar (shown when typing /mod) - - "[MOD] " - "/" + + "/" "mod" - " summon" - " [nick|*]" + " summon" + " [nick|*]" + " | " + "teleport" + " [nick] [slug]"