add :emote and :list to chat

This commit is contained in:
Evan Carroll 2026-01-12 17:23:41 -06:00
parent 1ca300098f
commit bd28e201a2
7 changed files with 741 additions and 22 deletions

View file

@ -1,6 +1,7 @@
//! Realm landing page after login.
use leptos::prelude::*;
use leptos::reactive::owner::LocalStorage;
#[cfg(feature = "hydrate")]
use leptos::task::spawn_local;
#[cfg(feature = "hydrate")]
@ -10,10 +11,14 @@ use leptos_router::hooks::use_params_map;
use crate::components::{Card, ChatInput, RealmHeader, RealmSceneViewer};
#[cfg(feature = "hydrate")]
use crate::components::use_channel_websocket;
use chattyness_db::models::{ChannelMemberWithAvatar, RealmRole, RealmWithUserRole, Scene};
use chattyness_db::models::{
ChannelMemberWithAvatar, EmotionAvailability, RealmRole, RealmWithUserRole, Scene,
};
#[cfg(feature = "hydrate")]
use chattyness_db::ws_messages::ClientMessage;
use crate::components::ws_client::WsSender;
/// Realm landing page component.
#[component]
pub fn RealmPage() -> impl IntoView {
@ -27,6 +32,16 @@ pub fn RealmPage() -> impl IntoView {
let (members, set_members) = signal(Vec::<ChannelMemberWithAvatar>::new());
let (channel_id, set_channel_id) = signal(Option::<uuid::Uuid>::None);
// Chat focus coordination
let (chat_focused, set_chat_focused) = signal(false);
let (focus_chat_trigger, set_focus_chat_trigger) = signal(false);
// Emotion availability for emote picker
let (emotion_availability, set_emotion_availability) =
signal(Option::<EmotionAvailability>::None);
// Skin preview path for emote picker (position 4 of skin layer)
let (skin_preview_path, set_skin_preview_path) = signal(Option::<String>::None);
let realm_data = LocalResource::new(move || {
let slug = slug.get();
async move {
@ -78,6 +93,52 @@ pub fn RealmPage() -> impl IntoView {
}
});
// Fetch emotion availability and avatar render data for emote picker
#[cfg(feature = "hydrate")]
{
let slug_for_emotions = slug.clone();
Effect::new(move |_| {
use gloo_net::http::Request;
let current_slug = slug_for_emotions.get();
if current_slug.is_empty() {
return;
}
// Fetch emotion availability
let slug_clone = current_slug.clone();
spawn_local(async move {
let response = Request::get(&format!("/api/realms/{}/avatar/emotions", slug_clone))
.send()
.await;
if let Ok(resp) = response {
if resp.ok() {
if let Ok(avail) = resp.json::<EmotionAvailability>().await {
set_emotion_availability.set(Some(avail));
}
}
}
});
// Fetch avatar render data for skin preview
let slug_clone2 = current_slug.clone();
spawn_local(async move {
use chattyness_db::models::AvatarRenderData;
let response = Request::get(&format!("/api/realms/{}/avatar/current", slug_clone2))
.send()
.await;
if let Ok(resp) = response {
if resp.ok() {
if let Ok(render_data) = resp.json::<AvatarRenderData>().await {
// Get skin layer position 4 (center)
set_skin_preview_path.set(render_data.skin_layer[4].clone());
}
}
}
});
});
}
// WebSocket connection for real-time updates
#[cfg(feature = "hydrate")]
let on_members_update = Callback::new(move |new_members: Vec<ChannelMemberWithAvatar>| {
@ -115,7 +176,7 @@ pub fn RealmPage() -> impl IntoView {
#[cfg(not(feature = "hydrate"))]
let on_move = Callback::new(move |(_x, _y): (f64, f64)| {});
// Handle emotion change via keyboard (e then 0-9)
// Handle global keyboard shortcuts (e+0-9 for emotions, : for chat focus)
#[cfg(feature = "hydrate")]
{
use std::cell::RefCell;
@ -149,6 +210,25 @@ pub fn RealmPage() -> impl IntoView {
let closure = Closure::new(move |ev: web_sys::KeyboardEvent| {
let key = ev.key();
// If chat is focused, let it handle all keys
if chat_focused.get() {
*e_pressed_clone.borrow_mut() = false;
return;
}
// Handle ':' to focus chat input
if key == ":" {
set_focus_chat_trigger.set(true);
// Reset trigger after a short delay so it can be triggered again
use gloo_timers::callback::Timeout;
Timeout::new(100, move || {
set_focus_chat_trigger.set(false);
})
.forget();
ev.prevent_default();
return;
}
// Check if 'e' key was pressed
if key == "e" || key == "E" {
*e_pressed_clone.borrow_mut() = true;
@ -189,6 +269,11 @@ pub fn RealmPage() -> impl IntoView {
});
}
// Callback for chat focus changes
let on_chat_focus_change = Callback::new(move |focused: bool| {
set_chat_focused.set(focused);
});
// Create logout callback (WebSocket disconnects automatically)
let on_logout = Callback::new(move |_: ()| {
#[cfg(feature = "hydrate")]
@ -272,13 +357,23 @@ pub fn RealmPage() -> impl IntoView {
}>
{move || {
let on_move = on_move.clone();
let on_chat_focus_change = on_chat_focus_change.clone();
let realm_slug_for_viewer = realm_slug_val.clone();
#[cfg(feature = "hydrate")]
let ws_sender_clone = ws_sender.clone();
entry_scene
.get()
.map(|maybe_scene| {
match maybe_scene {
Some(scene) => {
let members_signal = Signal::derive(move || members.get());
let emotion_avail_signal = Signal::derive(move || emotion_availability.get());
let skin_path_signal = Signal::derive(move || skin_preview_path.get());
let focus_trigger_signal = Signal::derive(move || focus_chat_trigger.get());
#[cfg(feature = "hydrate")]
let ws_for_chat = ws_sender_clone.clone();
#[cfg(not(feature = "hydrate"))]
let ws_for_chat: StoredValue<Option<WsSender>, LocalStorage> = StoredValue::new_local(None);
view! {
<div class="relative w-full">
<RealmSceneViewer
@ -287,8 +382,14 @@ pub fn RealmPage() -> impl IntoView {
members=members_signal
on_move=on_move.clone()
/>
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4">
<ChatInput />
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
<ChatInput
ws_sender=ws_for_chat
emotion_availability=emotion_avail_signal
skin_preview_path=skin_path_signal
focus_trigger=focus_trigger_signal
on_focus_change=on_chat_focus_change.clone()
/>
</div>
</div>
}