summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-05-01 02:17:47 +0100
committerLibravatar cel 🌸 <cel@bunny.garden>2025-05-01 02:17:47 +0100
commit6943577ec5169b04d8a24726fd87e85fc5b261a9 (patch)
tree0c2cf4d422bd62c4e4d1f9697766108f8a38b266
parent8012b20aefa1b2e52cccbc132e8e96ce6bf2b81f (diff)
downloadmacaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.tar.gz
macaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.tar.bz2
macaw-web-6943577ec5169b04d8a24726fd87e85fc5b261a9.zip
feat: new message composer
-rw-r--r--.helix/languages.toml2
-rw-r--r--Cargo.lock186
-rw-r--r--Cargo.toml3
-rw-r--r--assets/style.scss107
-rw-r--r--index.html2
-rw-r--r--src/lib.rs311
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"] }
diff --git a/Cargo.lock b/Cargo.lock
index 62a6def..c7e44d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 06662d9..4b80be7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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 */
diff --git a/index.html b/index.html
index a3ab66f..7255857 100644
--- a/index.html
+++ b/index.html
@@ -21,4 +21,4 @@
<body></body>
-</html> \ No newline at end of file
+</html>
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<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>