Add the right-click ability on avatars

This commit is contained in:
Evan Carroll 2026-01-18 10:05:38 -06:00
parent d1cbb3ba34
commit 0492043625
7 changed files with 438 additions and 2 deletions

View file

@ -15,8 +15,11 @@ use uuid::Uuid;
use chattyness_db::models::{ChannelMemberWithAvatar, LooseProp, Scene};
#[cfg(feature = "hydrate")]
use super::avatar_canvas::hit_test_canvas;
use super::avatar_canvas::{member_key, AvatarCanvas};
use super::chat_types::ActiveBubble;
use super::context_menu::{ContextMenu, ContextMenuItem};
use super::settings::{
calculate_min_zoom, ViewerSettings, BASE_PROP_SIZE, REFERENCE_HEIGHT, REFERENCE_WIDTH,
};
@ -53,6 +56,15 @@ pub fn RealmSceneViewer(
/// Members that are fading out after timeout disconnect.
#[prop(optional, into)]
fading_members: Option<Signal<Vec<FadingMember>>>,
/// Current user's user_id (for context menu filtering).
#[prop(optional, into)]
current_user_id: Option<Signal<Option<Uuid>>>,
/// Current user's guest_session_id (for context menu filtering).
#[prop(optional, into)]
current_guest_session_id: Option<Signal<Option<Uuid>>>,
/// Callback when whisper is requested on a member.
#[prop(optional, into)]
on_whisper_request: Option<Callback<String>>,
) -> impl IntoView {
// Use default settings if none provided
let settings = settings.unwrap_or_else(|| Signal::derive(ViewerSettings::default));
@ -115,6 +127,11 @@ pub fn RealmSceneViewer(
// Signal to track when scale factors have been properly calculated
let (scales_ready, set_scales_ready) = signal(false);
// Context menu state
let (context_menu_open, set_context_menu_open) = signal(false);
let (context_menu_position, set_context_menu_position) = signal((0.0_f64, 0.0_f64));
let (context_menu_target, set_context_menu_target) = signal(Option::<String>::None);
// Handle overlay click for movement or prop pickup
// TODO: Add hit-testing for avatar clicks
#[cfg(feature = "hydrate")]
@ -166,6 +183,75 @@ pub fn RealmSceneViewer(
}
};
// Handle right-click for context menu on avatars
#[cfg(feature = "hydrate")]
let on_overlay_contextmenu = {
let current_user_id = current_user_id.clone();
let current_guest_session_id = current_guest_session_id.clone();
move |ev: web_sys::MouseEvent| {
use wasm_bindgen::JsCast;
// Get click position
let client_x = ev.client_x() as f64;
let client_y = ev.client_y() as f64;
// Get current user identity for filtering
let my_user_id = current_user_id.map(|s| s.get()).flatten();
let my_guest_session_id = current_guest_session_id.map(|s| s.get()).flatten();
// Query all avatar canvases and check for hit
let document = web_sys::window().unwrap().document().unwrap();
// Get the avatars container and find all canvas elements within it
if let Some(container) = document.query_selector(".avatars-container").ok().flatten() {
let canvases = container.get_elements_by_tag_name("canvas");
let canvas_count = canvases.length();
for i in 0..canvas_count {
if let Some(element) = canvases.item(i) {
if let Ok(canvas) = element.dyn_into::<web_sys::HtmlCanvasElement>() {
// Check for data-member-id attribute
if let Some(member_id_str) = canvas.get_attribute("data-member-id") {
// Check if click hits a non-transparent pixel
if hit_test_canvas(&canvas, client_x, client_y) {
// Parse the member ID to determine if it's a user_id or guest_session_id
if let Ok(member_id) = member_id_str.parse::<Uuid>() {
// Check if this is the current user's avatar
let is_current_user = my_user_id == Some(member_id) ||
my_guest_session_id == Some(member_id);
if !is_current_user {
// Find the display name for this member
let display_name = members.get().iter()
.find(|m| {
m.member.user_id == Some(member_id) ||
m.member.guest_session_id == Some(member_id)
})
.map(|m| m.member.display_name.clone());
if let Some(name) = display_name {
// Prevent default browser context menu
ev.prevent_default();
// Show context menu at click position
set_context_menu_position.set((client_x, client_y));
set_context_menu_target.set(Some(name));
set_context_menu_open.set(true);
return;
}
}
}
}
}
}
}
}
}
// No avatar hit - allow default context menu
}
};
#[cfg(feature = "hydrate")]
{
use std::cell::RefCell;
@ -918,6 +1004,41 @@ pub fn RealmSceneViewer(
#[cfg(not(feature = "hydrate"))]
let _ = ev;
}
on:contextmenu=move |ev| {
#[cfg(feature = "hydrate")]
on_overlay_contextmenu(ev);
#[cfg(not(feature = "hydrate"))]
let _ = ev;
}
/>
// Context menu for avatar interactions
<ContextMenu
open=Signal::derive(move || context_menu_open.get())
position=Signal::derive(move || context_menu_position.get())
items=Signal::derive(move || {
vec![
ContextMenuItem {
label: "Whisper".to_string(),
action: "whisper".to_string(),
},
]
})
on_select=Callback::new({
let on_whisper_request = on_whisper_request.clone();
move |action: String| {
if action == "whisper" {
if let Some(target) = context_menu_target.get() {
if let Some(ref callback) = on_whisper_request {
callback.run(target);
}
}
}
}
})
on_close=Callback::new(move |_: ()| {
set_context_menu_open.set(false);
set_context_menu_target.set(None);
})
/>
</div>
</div>