diff options
author | 2025-04-30 02:51:02 +0100 | |
---|---|---|
committer | 2025-04-30 02:51:02 +0100 | |
commit | 8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f (patch) | |
tree | 09797c1d2da7f4840141639e154cae2b6901b314 | |
parent | 4ea506818e85de5453e661552a0fd7dffda38d6e (diff) | |
download | macaw-web-8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f.tar.gz macaw-web-8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f.tar.bz2 macaw-web-8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f.zip |
feat: chat list is more pretty
-rw-r--r-- | src/lib.rs | 111 |
1 files changed, 99 insertions, 12 deletions
@@ -12,12 +12,7 @@ use std::{ }; use filamento::{ - chat::{Chat, Message}, - db::Db, - error::{CommandError, ConnectionError, DatabaseError}, - files::FilesMem, - user::User, - UpdateMessage, + chat::{Chat, Message}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::Contact, user::User, UpdateMessage }; use futures::stream::StreamExt; use indexmap::IndexMap; @@ -34,6 +29,8 @@ use tokio::sync::{mpsc::{self, Receiver}, Mutex}; use tracing::debug; use uuid::Uuid; +const NO_AVATAR: &str = "/assets/no-avatar.png"; + pub enum AppState { LoggedOut, LoggedIn, @@ -305,6 +302,10 @@ fn Macaw( ) -> impl IntoView { provide_context(client); + // TODO: roster as store + let (roster, set_roster) = signal(HashMap::<JID, MacawContact>::new()); + provide_context(roster); + let message_subscriptions = RwSignal::new(MessageSubscriptions::new()); provide_context(message_subscriptions); @@ -315,10 +316,14 @@ fn Macaw( let users_store: StateStore<JID, Store<User>> = StateStore::new(); provide_context(users_store); + // 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) => {}, + UpdateMessage::Online(online, items) => { + + }, UpdateMessage::Offline(offline) => {}, UpdateMessage::RosterUpdate(contact, user) => {}, UpdateMessage::RosterDelete(jid) => {}, @@ -332,8 +337,12 @@ fn Macaw( }, UpdateMessage::MessageDelivery { id, chat, delivery } => {}, UpdateMessage::SubscriptionRequest(jid) => {}, - UpdateMessage::NickChanged { jid, nick } => {}, - UpdateMessage::AvatarChanged { jid, id } => {}, + UpdateMessage::NickChanged { jid, nick } => { + users_store.modify(&jid, |user| user.update(|user| *&mut user.nick = nick.clone())); + }, + UpdateMessage::AvatarChanged { jid, id } => { + users_store.modify(&jid, |user| *&mut user.write().avatar = id.clone()); + }, } } }); @@ -613,6 +622,37 @@ impl DerefMut for MacawUser { } } +struct MacawContact { + contact: Store<Contact>, + user: StateListener<JID, Store<User>>, +} + +impl MacawContact { + fn got_contact_and_user(contact: Contact, user: User) -> Self { + let contact = Store::new(contact); + let user_state_store: StateStore<JID, Store<User>> = use_context().expect("no user state store"); + let user = user_state_store.store(user.jid.clone(), Store::new(user)); + Self { + contact, + user, + } + } +} + +impl Deref for MacawContact { + type Target = Store<Contact>; + + fn deref(&self) -> &Self::Target { + &self.contact + } +} + +impl DerefMut for MacawContact { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.contact + } +} + #[component] fn ChatsList() -> impl IntoView { let (chats, set_chats) = signal(IndexMap::new()); @@ -643,6 +683,7 @@ fn ChatsList() -> impl IntoView { let mut chats = set_chats.write(); if let Some((chat, _latest_message)) = chats.shift_remove(&to) { debug!("chat existed"); + debug!("new message: {}", new_message.read().body.body); chats.insert_before(0, to, (chat.clone(), new_message)); debug!("done setting"); } else { @@ -663,11 +704,57 @@ fn ChatsList() -> impl IntoView { view! { <div class="chats-list panel"> <h2>Chats</h2> - <div> - <For each=move || chats.get() key=|chat| chat.0.clone() let(chat)> - <p>{chat.0.to_string()}</p> + <div class="chats-list-chats"> + <For each=move || chats.get() key=|chat| chat.1.1.message.read().id let(chat)> + <ChatsListItem chat=chat.1.0 message=chat.1.1 /> </For> </div> </div> } } + +pub fn get_avatar(user: Store<User>) -> String { + if let Some(avatar) = &user.read().avatar { + NO_AVATAR.to_string() + // TODO: enable avatar fetching + // format!("/files/{}", avatar) + } else { + NO_AVATAR.to_string() + } +} + +pub fn get_name(user: Store<User>) -> String { + let roster: ReadSignal<HashMap<JID, MacawContact>> = use_context().expect("no roster in context"); + if let Some(name) = roster + .read() + .get(&user.read().jid) + .map(|contact| contact.read().name.clone()) + .unwrap_or_default() + { + name.to_string() + } else if let Some(nick) = &user.read().nick { + nick.to_string() + } else { + user.read().jid.to_string() + } +} + +#[component] +fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView { + let chat_user = *chat.user; + let avatar = move || get_avatar(chat_user); + let name = move || get_name(chat_user); + + // TODO: store fine-grained reactivity + let latest_message_body = move || message.get().body.body; + + view! { + <div class="chats-list-item"> + <img class="avatar" src=avatar /> + <div class="item-info"> + <h3>{name}</h3> + <p>{latest_message_body}</p> + </div> + </div> + } +} |