174 lines
6.5 KiB
Rust
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>
|
|
}
|
|
}
|