chattyness/crates/chattyness-user-ui/src/components/conversation_modal.rs

174 lines
6.5 KiB
Rust

//! Conversation modal for viewing whisper history with a specific user.
use leptos::prelude::*;
use leptos::reactive::owner::LocalStorage;
#[cfg(feature = "hydrate")]
use wasm_bindgen::JsCast;
use chattyness_db::ws_messages::ClientMessage;
use super::chat_types::ChatMessage;
use super::modals::Modal;
use super::ws_client::WsSender;
/// Conversation modal component for viewing/replying to whispers.
#[component]
pub fn ConversationModal(
#[prop(into)] open: Signal<bool>,
on_close: Callback<()>,
/// The display name of the conversation partner.
#[prop(into)]
partner_name: Signal<String>,
/// All whisper messages involving this partner.
#[prop(into)]
messages: Signal<Vec<ChatMessage>>,
/// Current user's display name for determining message direction.
#[prop(into)]
current_user_name: Signal<String>,
/// WebSocket sender for sending replies.
ws_sender: StoredValue<Option<WsSender>, LocalStorage>,
) -> impl IntoView {
let (reply_text, set_reply_text) = signal(String::new());
let input_ref = NodeRef::<leptos::html::Input>::new();
// Focus input when modal opens
#[cfg(feature = "hydrate")]
{
Effect::new(move |_| {
if open.get() {
// Small delay to ensure modal is rendered
let input_ref = input_ref;
let closure = wasm_bindgen::closure::Closure::once(Box::new(move || {
if let Some(input) = input_ref.get() {
let _ = input.focus();
}
})
as Box<dyn FnOnce()>);
if let Some(window) = web_sys::window() {
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
100,
);
}
closure.forget();
}
});
}
let send_reply = {
let on_close = on_close.clone();
move || {
let text = reply_text.get();
let target = partner_name.get();
if !text.trim().is_empty() && !target.is_empty() {
ws_sender.with_value(|sender| {
if let Some(send_fn) = sender {
send_fn(ClientMessage::SendChatMessage {
content: text.trim().to_string(),
target_display_name: Some(target.clone()),
});
}
});
set_reply_text.set(String::new());
on_close.run(());
}
}
};
#[cfg(feature = "hydrate")]
let on_keydown = {
let send_reply = send_reply.clone();
move |ev: web_sys::KeyboardEvent| {
if ev.key() == "Enter" && !ev.shift_key() {
ev.prevent_default();
send_reply();
}
}
};
#[cfg(not(feature = "hydrate"))]
let on_keydown = move |_ev| {};
view! {
<Modal
open=open
on_close=on_close.clone()
title="Conversation"
title_id="conversation-modal-title"
max_width="max-w-lg"
>
// Partner name header
<div class="mb-4 pb-2 border-b border-gray-600">
<h3 class="text-lg text-purple-300">
"with " {move || partner_name.get()}
</h3>
</div>
// Messages list
<div class="max-h-64 overflow-y-auto mb-4 space-y-2">
<Show
when=move || !messages.get().is_empty()
fallback=|| view! {
<p class="text-gray-400 text-center py-4">
"No messages yet"
</p>
}
>
<For
each=move || messages.get()
key=|msg| msg.message_id
children=move |msg: ChatMessage| {
let is_from_me = msg.display_name == current_user_name.get();
let align_class = if is_from_me { "text-right" } else { "text-left" };
let bubble_class = if is_from_me {
"bg-purple-600/50 ml-8"
} else {
"bg-gray-700 mr-8"
};
let sender = if is_from_me { "you" } else { &msg.display_name };
view! {
<div class=align_class>
<div class=format!("inline-block p-2 rounded-lg {}", bubble_class)>
<span class="text-xs text-gray-400">
{sender.to_string()} ": "
</span>
<span class="text-gray-200 italic">
{msg.content.clone()}
</span>
</div>
</div>
}
}
/>
</Show>
</div>
// Reply input
<div class="border-t border-gray-600 pt-4">
<div class="flex gap-2">
<input
type="text"
node_ref=input_ref
class="flex-1 bg-gray-700 text-white rounded px-3 py-2 outline-none focus:ring-2 focus:ring-purple-500"
placeholder=move || format!("/whisper {} ", partner_name.get())
prop:value=move || reply_text.get()
on:input=move |ev| set_reply_text.set(event_target_value(&ev))
on:keydown=on_keydown
/>
<button
class="px-4 py-2 bg-purple-600 hover:bg-purple-500 rounded text-white"
on:click=move |_| send_reply()
>
"Send"
</button>
</div>
<p class="text-xs text-gray-500 mt-2">
"Press " <kbd class="px-1 py-0.5 bg-gray-700 rounded">"Enter"</kbd> " to send, "
<kbd class="px-1 py-0.5 bg-gray-700 rounded">"Esc"</kbd> " to close"
</p>
</div>
</Modal>
}
}