From 6943577ec5169b04d8a24726fd87e85fc5b261a9 Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Thu, 1 May 2025 02:17:47 +0100 Subject: feat: new message composer --- src/lib.rs | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 298 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/lib.rs b/src/lib.rs index 2b64970..f2c6e97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,17 +12,17 @@ use std::{ }; use filamento::{ - chat::{Chat, Message}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::Contact, user::User, UpdateMessage + chat::{Body, Chat, ChatStoreFields, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage }; use futures::stream::StreamExt; use indexmap::IndexMap; use jid::JID; use leptos::{ - prelude::*, - task::{spawn, spawn_local}, + html::{self, Div, Input, Textarea}, prelude::*, tachys::dom::document, task::{spawn, spawn_local} }; use leptos_meta::Stylesheet; -use reactive_stores::Store; +use leptos_use::{use_textarea_autosize, UseTextareaAutosizeReturn}; +use reactive_stores::{Store, StoreField}; use stylance::import_style; use thiserror::Error; use tokio::sync::{mpsc::{self, Receiver}, Mutex}; @@ -293,6 +293,80 @@ impl MessageSubscriptions { } } +#[derive(Store)] +pub struct Roster { + #[store(key: JID = |(jid, _)| jid.clone())] + contacts: HashMap, +} + +impl Roster { + pub fn new() -> Self { + Self { + contacts: HashMap::new(), + } + } +} + +// TODO: multiple panels +// pub struct OpenChats { +// panels: +// } + +#[derive(Store, Default)] +pub struct OpenChatsPanel { + // jid must be a chat in the chats map + chat_view: Option, + #[store(key: JID = |(jid, _)| jid.clone())] + chats: IndexMap, +} + +pub fn open_chat(open_chats: Store, chat: MacawChat) { + if let Some(jid) = &*open_chats.chat_view().read() { + if let Some((index, _jid, entry)) = open_chats.chats().write().shift_remove_full(jid) { + let new_jid = chat.chat.correspondent().read().clone(); + open_chats.chats().write().insert_before(index, new_jid.clone(), chat); + *open_chats.chat_view().write() = Some(new_jid); + } else { + let new_jid = chat.chat.correspondent().read().clone(); + open_chats.chats().write().insert(new_jid.clone(), chat); + *open_chats.chat_view().write() = Some(new_jid); + } + } else { + let new_jid = chat.chat.correspondent().read().clone(); + open_chats.chats().write().insert(new_jid.clone(), chat); + *open_chats.chat_view().write() = Some(new_jid); + } +} + +impl OpenChatsPanel { + pub fn open(&mut self, chat: MacawChat) { + if let Some(jid) = &mut self.chat_view { + if let Some((index, _jid, entry)) = self.chats.shift_remove_full(jid) { + let new_jid = chat.chat.correspondent().read().clone(); + self.chats.insert_before(index, new_jid.clone(), chat); + *&mut self.chat_view = Some(new_jid); + } else { + let new_jid = chat.chat.correspondent().read().clone(); + self.chats.insert(new_jid.clone(), chat); + *&mut self.chat_view = Some(new_jid); + } + } else { + let new_jid = chat.chat.correspondent().read().clone(); + self.chats.insert(new_jid.clone(), chat); + *&mut self.chat_view = Some(new_jid); + } + } + + // TODO: + // pub fn open_in_new_tab_unfocused(&mut self) { + + // } + + // pub fn open_in_new_tab_focus(&mut self) { + + // } +} + #[component] fn Macaw( // TODO: logout @@ -302,8 +376,7 @@ fn Macaw( ) -> impl IntoView { provide_context(client); - // TODO: roster as store - let (roster, set_roster) = signal(HashMap::::new()); + let roster = Store::new(Roster::new()); provide_context(roster); let message_subscriptions = RwSignal::new(MessageSubscriptions::new()); @@ -316,17 +389,37 @@ fn Macaw( let users_store: StateStore> = StateStore::new(); provide_context(users_store); + let open_chats = Store::new(OpenChatsPanel::default()); + provide_context(open_chats); + // TODO: get cached contacts on login before getting the updated contacts OnceResource::new(async move { while let Some(update) = updates.recv().await { match update { UpdateMessage::Online(online, items) => { - + let contacts = items.into_iter().map(|(contact, user)| { + (contact.user_jid.clone(), MacawContact::got_contact_and_user(contact, user)) + }).collect(); + roster.contacts().set(contacts); }, UpdateMessage::Offline(offline) => {}, - UpdateMessage::RosterUpdate(contact, user) => {}, - UpdateMessage::RosterDelete(jid) => {}, + UpdateMessage::RosterUpdate(contact, user) => { + roster.contacts().update(|roster| { + if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) { + macaw_contact.set(contact); + } else { + let jid = contact.user_jid.clone(); + let contact = MacawContact::got_contact_and_user(contact, user); + roster.insert(jid, contact); + } + }); + }, + UpdateMessage::RosterDelete(jid) => { + roster.contacts().update(|roster| { + roster.remove(&jid); + }); + }, UpdateMessage::Presence { from, presence } => {}, UpdateMessage::Message { to, from, message } => { // debug!("before got message"); @@ -335,7 +428,9 @@ fn Macaw( spawn_local(async move { message_subscriptions.write_untracked().broadcast(to, new_message).await }); // debug!("after set message"); }, - UpdateMessage::MessageDelivery { id, chat, delivery } => {}, + UpdateMessage::MessageDelivery { id, chat, delivery } => { + messages_store.modify(&id, |message| message.delivery().set(Some(delivery))); + }, UpdateMessage::SubscriptionRequest(jid) => {}, UpdateMessage::NickChanged { jid, nick } => { users_store.modify(&jid, |user| user.update(|user| *&mut user.nick = nick.clone())); @@ -347,7 +442,190 @@ fn Macaw( } }); - view! { } + view! { + + + } +} + +#[component] +pub fn OpenChatsPanelView() -> impl IntoView { + let open_chats: Store = use_context().expect("no open chats panel in context"); + + // TODO: tabs + // view! { + // {move || { + // if open_chats.chats().read().len() > 1 { + // Some( + // view! { + // + // }, + // ) + // } else { + // None + // } + // }} + // } + view! { +
+ {move || { + if let Some(open_chat) = open_chats.chat_view().get() { + if let Some(open_chat) = open_chats.chats().read().get(&open_chat) { + view! { }.into_any() + } else { + view! {}.into_any() + } + } else { + view! {}.into_any() + } + }} +
+ } +} + +#[component] +pub fn OpenChatView(chat: MacawChat) -> impl IntoView { + let chat_chat = *chat.chat; + let chat_jid = move || chat_chat.correspondent().get(); + + view! { +
+ + + +
+ } +} + +#[component] +pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView { + let chat_user = *chat.user; + let avatar = move || get_avatar(chat_user); + let name = move || get_name(chat_user); + let jid = move || chat_user.jid().read().to_string(); + + view! { +
+ + +
+ } +} + +#[component] +pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView { + view! {} +} + +#[component] +pub fn Message(message: MacawMessage, major: bool, r#final: bool) -> impl IntoView { + let message_message = *message.message; + let message_user = *message.user; + let avatar = move || get_avatar(message_user); + let name = move || get_name(message_user); + + // TODO: chrono-humanize? + // TODO: if final, show delivery not only on hover. + if major { + view! { +
+ +
+
+
{name}
+
{move || message_message.timestamp().read().to_string()}
+
+
+ {move || message_message.body().read().body.clone()} +
+
+
+
+ }.into_any() + } else { + view! { +
+
+ {move || message_message.timestamp().read().to_string()} +
+
{move || message_message.body().read().body.clone()}
+
+
+ }.into_any() + } +} + +#[component] +pub fn ChatViewMessageComposer(chat: JID) -> impl IntoView { + let message_input: NodeRef