diff options
author | 2025-05-01 02:17:47 +0100 | |
---|---|---|
committer | 2025-05-01 02:17:47 +0100 | |
commit | 6943577ec5169b04d8a24726fd87e85fc5b261a9 (patch) | |
tree | 0c2cf4d422bd62c4e4d1f9697766108f8a38b266 | |
parent | 8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f (diff) | |
download | macaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.tar.gz macaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.tar.bz2 macaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.zip |
feat: new message composer
-rw-r--r-- | .helix/languages.toml | 2 | ||||
-rw-r--r-- | Cargo.lock | 186 | ||||
-rw-r--r-- | Cargo.toml | 3 | ||||
-rw-r--r-- | assets/style.scss | 107 | ||||
-rw-r--r-- | index.html | 2 | ||||
-rw-r--r-- | src/lib.rs | 311 |
6 files changed, 588 insertions, 23 deletions
diff --git a/.helix/languages.toml b/.helix/languages.toml index 3be859c..75f513c 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,5 +1,5 @@ [language-server.rust-analyzer] command = "rust-analyzer" -config = { rustfmt.overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"] } +config = { rustfmt.overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"], cargo.features = ["filamento/reactive_stores"] } # environment = { "DATABASE_URL" = "sqlite://filamento.db" } # config = { cargo.features = ["stanza/rfc_6121", "stanza/xep_0203", "stanza/xep_0030", "stanza/xep_0060", "stanza/xep_0172", "stanza/xep_0390", "stanza/xep_0128", "stanza/xep_0115", "stanza/xep_0084", "sqlx/sqlite", "sqlx/runtime-tokio", "sqlx/uuid", "sqlx/chrono", "jid/sqlx", "uuid/v4", "tokio/full", "rsasl/provider_base64", "rsasl/plain", "rsasl/config_builder", "rsasl/scram-sha-1"] } @@ -431,6 +431,17 @@ dependencies = [ ] [[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] name = "core-foundation" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -515,6 +526,41 @@ dependencies = [ ] [[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.101", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.101", +] + +[[package]] name = "dashmap" version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -535,6 +581,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] +name = "default-struct-builder" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] name = "delegate-display" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -549,6 +607,15 @@ dependencies = [ ] [[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] name = "derive-where" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -744,6 +811,7 @@ dependencies = [ "image", "jid", "lampada", + "reactive_stores", "rusqlite", "serde", "sha1", @@ -974,6 +1042,18 @@ dependencies = [ ] [[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "gloo-utils" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1247,6 +1327,12 @@ dependencies = [ ] [[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] name = "idna" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1538,6 +1624,31 @@ dependencies = [ ] [[package]] +name = "leptos-use" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e612389629007497d1e90deecf5bddd790e59e32358546fa7beaf88a68d2067b" +dependencies = [ + "cfg-if", + "chrono", + "codee", + "cookie", + "default-struct-builder", + "futures-util", + "gloo-timers", + "js-sys", + "lazy_static", + "leptos", + "paste", + "send_wrapper", + "thiserror 2.0.12", + "unic-langid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] name = "leptos_config" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1776,6 +1887,7 @@ dependencies = [ "try_map", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] @@ -1789,6 +1901,7 @@ dependencies = [ "indexmap", "jid", "leptos", + "leptos-use", "leptos_meta", "leptos_reactive", "reactive_stores", @@ -1977,6 +2090,12 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] name = "num-derive" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2164,7 +2283,7 @@ dependencies = [ [[package]] name = "peanuts" version = "0.1.0" -source = "git+https://bunny.garden/peanuts#9aa337bd703426f737c5d4f94fe84c4a646b7836" +source = "git+https://bunny.garden/peanuts#94afe363ea88a6fd036d1681542d650e7cd3c2e7" dependencies = [ "async-recursion", "circular", @@ -2238,6 +2357,12 @@ dependencies = [ ] [[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2879,9 +3004,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3015,6 +3140,12 @@ dependencies = [ ] [[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] name = "stylance" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3243,6 +3374,37 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] name = "tinystr" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3500,6 +3662,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] +name = "unic-langid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +dependencies = [ + "tinystr", +] + +[[package]] name = "unicode-bidi" version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5,11 +5,12 @@ edition = "2024" [dependencies] console_error_panic_hook = "0.1.7" -filamento = { path = "../luz/filamento", features = ["serde"] } +filamento = { path = "../luz/filamento", features = ["serde", "reactive_stores"] } futures = "0.3.31" indexmap = "2.9.0" jid = { path = "../luz/jid" } leptos = { version = "0.7.5", features = ["csr"] } +leptos-use = "0.15.7" leptos_meta = "0.7.8" leptos_reactive = { version = "0.6.15", features = ["csr"] } reactive_stores = "0.1.8" diff --git a/assets/style.scss b/assets/style.scss index fdc6e61..d4b4e91 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -14,6 +14,10 @@ body { word-wrap: break-word; text-wrap: wrap; overflow-wrap: break-word; + display: flex; + justify-content: stretch; + align-items: stretch; + flex: 1 1 auto; } html { @@ -54,12 +58,15 @@ html { .panel { border: 2px solid black; + border-color: black; + border-style: solid; background: linear-gradient(180deg, #364B3B 0%, #19311F 100%); } #login-form { padding: 32px; margin: 4px; + max-width: 850px; } #login-form form { @@ -131,8 +138,13 @@ p { } .chats-list { - border: 2px solid black; - height: 100%; + border-width: 0 2px 0 0; + border-color: black; + border-style: solid; + height: auto; + width: 400px; + flex: 0 0 auto; + align-self: stretch; } .chats-list>* { @@ -162,11 +174,98 @@ p { /* background: linear-gradient(0deg, #364B3B 0%, #19311F 100%); */ } +.avatar { + object-fit: contain; + border-radius: 50%; + width: 48px; + height: 48px; +} + .chats-list-item .avatar { width: 48px; height: 48px; - object-fit: contain; - border-radius: 50%; +} + +.chats-list-chats { + height: auto; +} + +.open-chat-views { + flex: 1 1 100%; +} + +.open-chat-view { + height: auto; +} + +.chat-view-header .avatar { + width: 64px; + height: 64px; +} + +.chat-view-header { + border-width: 0 0 2px 0; + display: flex; + padding: 16px; + gap: 16px; +} + +.chat-view-header h2 { + font-family: 'K2D'; + font-weight: bold; +} + +.new-message-composer { + border-width: 2px 0 0 0; + width: auto; + padding: 16px; +} + +.new-message-composer textarea { + font-family: 'K2D'; + border: 2px solid black; + font-size: 16px; + padding: 8px; + /* width: auto; */ + margin: 0; + height: auto; + /* resize: none; */ + /* box-sizing: border-box; */ + /* overflow: scroll; */ + /* display: block; */ +} + +.grow-wrap { + /* easy way to plop the elements on top of each other and have them both sized based on the tallest one's height */ + display: grid; +} + +.grow-wrap::after { + /* Note the weird space! Needed to preventy jumpy behavior */ + content: attr(data-replicated-value) " "; + /* This is how textarea text behaves */ + white-space: pre-wrap; + /* Hidden from view, clicks, and screen readers */ + visibility: hidden; + max-height: 5em; +} + +.grow-wrap>textarea { + /* You could leave this, but after a user resizes, then it ruins the auto sizing */ + resize: none; + /* Firefox shows scrollbar on growth, you can hide like this. */ + overflow: scroll; +} + +.grow-wrap>textarea, +.grow-wrap::after { + /* Identical styling required!! */ + border: 2px solid black; + padding: 8px; + font: inherit; + + /* Place on top of each other */ + grid-area: 1 / 1 / 2 / 2; } /* font-families */ @@ -21,4 +21,4 @@ <body></body> -</html>
\ No newline at end of file +</html> @@ -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<JID, MacawContact>, +} + +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<JID>, + #[store(key: JID = |(jid, _)| jid.clone())] + chats: IndexMap<JID, MacawChat>, +} + +pub fn open_chat(open_chats: Store<OpenChatsPanel>, 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::<JID, MacawContact>::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<JID, Store<User>> = 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! { <ChatsList /> } + view! { + <ChatsList /> + <OpenChatsPanelView /> + } +} + +#[component] +pub fn OpenChatsPanelView() -> impl IntoView { + let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel in context"); + + // TODO: tabs + // view! { + // {move || { + // if open_chats.chats().read().len() > 1 { + // Some( + // view! { + // <For + // each=move || open_chats.chats().get() + // key=|(jid, _)| jid.clone() + // let(chat) + // ></For> + // }, + // ) + // } else { + // None + // } + // }} + // } + view! { + <div class="open-chat-views"> + {move || { + if let Some(open_chat) = open_chats.chat_view().get() { + if let Some(open_chat) = open_chats.chats().read().get(&open_chat) { + view! { <OpenChatView chat=open_chat.clone() /> }.into_any() + } else { + view! {}.into_any() + } + } else { + view! {}.into_any() + } + }} + </div> + } +} + +#[component] +pub fn OpenChatView(chat: MacawChat) -> impl IntoView { + let chat_chat = *chat.chat; + let chat_jid = move || chat_chat.correspondent().get(); + + view! { + <div class="open-chat-view"> + <ChatViewHeader chat=chat.clone() /> + <MessageHistoryBuffer chat=chat.clone() /> + <ChatViewMessageComposer chat=chat_jid() /> + </div> + } +} + +#[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! { + <div class="chat-view-header panel"> + <img class="avatar" src=avatar /> + <div class="user-info"> + <h2 class="name">{name}</h2> + <h3>{jid}</h3> + </div> + </div> + } +} + +#[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! { + <div class="chat-message major"> + <img class="avatar" src=avatar /> + <div> + <div class="message-info"> + <div>{name}</div> + <div>{move || message_message.timestamp().read().to_string()}</div> + </div> + <div class="message-text"> + {move || message_message.body().read().body.clone()} + </div> + </div> + <div class="message-delivery"></div> + </div> + }.into_any() + } else { + view! { + <div class="chat-message minor"> + <div class="message-timestamp"> + {move || message_message.timestamp().read().to_string()} + </div> + <div class="message-text">{move || message_message.body().read().body.clone()}</div> + <div class="message-delivery"></div> + </div> + }.into_any() + } +} + +#[component] +pub fn ChatViewMessageComposer(chat: JID) -> impl IntoView { + let message_input: NodeRef<Textarea> = NodeRef::new(); + + // TODO: load last message draft + let new_message = RwSignal::new("".to_string()); + let client: Client = use_context().expect("no client in context"); + let client = RwSignal::new(client); + + let send_message = Action::new(move |_| { + let value = chat.clone(); + async move { + spawn_local(async move { match client.read_untracked().send_message(value, Body { body: new_message.get_untracked() }).await { + Ok(_) => { + new_message.set("".to_string()); + }, + Err(e) => tracing::error!("message send error: {}", e), + }}) + } + }); + + let _focus = Effect::new(move |_| { + if let Some(input) = message_input.get() { + let _ = input.focus(); + // input.style("height: 0"); + // let height = input.scroll_height(); + // input.style(format!("height: {}px", height)); + } + }); + + view! { + <script> + const growers = document.querySelectorAll(".grow-wrap"); + + growers.forEach((grower) => { + const textarea = grower.querySelector("textarea"); + textarea.addEventListener("input", () => { + grower.dataset.replicatedValue = textarea.value; + }); + }); + + </script> + <form + class="new-message-composer panel" + // on:input=resize + // on:input=|_| "this.parentNode.dataset.replicatedValue = this.value" + on:submit=move |ev| { + ev.prevent_default(); + send_message.dispatch(()); + } + > + <div class="grow-wrap"> + <textarea + placeholder="New Message" + prop:value=new_message + on:input=move |ev| new_message.set(event_target_value(&ev)) + name="new_message" + node_ref=message_input + autofocus="true" + /> + </div> + <input hidden type="submit" /> + </form> + } } // V has to be an arc signal @@ -724,8 +1002,9 @@ pub fn get_avatar(user: Store<User>) -> String { } pub fn get_name(user: Store<User>) -> String { - let roster: ReadSignal<HashMap<JID, MacawContact>> = use_context().expect("no roster in context"); + let roster: Store<Roster> = use_context().expect("no roster in context"); if let Some(name) = roster + .contacts() .read() .get(&user.read().jid) .map(|contact| contact.read().name.clone()) @@ -747,9 +1026,15 @@ fn ChatsListItem(chat: MacawChat, message: MacawMessage) -> impl IntoView { // TODO: store fine-grained reactivity let latest_message_body = move || message.get().body.body; + let open_chats: Store<OpenChatsPanel> = use_context().expect("no open chats panel store in context"); + + let open_chat = move |_| { + debug!("opening chat"); + open_chats.update(|open_chats| open_chats.open(chat.clone())); + }; view! { - <div class="chats-list-item"> + <div class="chats-list-item" on:click=open_chat> <img class="avatar" src=avatar /> <div class="item-info"> <h3>{name}</h3> |