From 7859947fcc643a96d20b7c56df912d8e3230429d Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Tue, 6 May 2025 20:46:16 +0100 Subject: feat: message buffer --- Cargo.lock | 11 ++++++ Cargo.toml | 2 + assets/style.scss | 84 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 197 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7e44d3..3bfef73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + [[package]] name = "circular" version = "0.3.0" @@ -1895,6 +1904,8 @@ dependencies = [ name = "macaw-web" version = "0.1.0" dependencies = [ + "chrono", + "chrono-humanize", "console_error_panic_hook", "filamento", "futures", diff --git a/Cargo.toml b/Cargo.toml index 4b80be7..8ba81a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4.41" +chrono-humanize = "0.2.3" console_error_panic_hook = "0.1.7" filamento = { path = "../luz/filamento", features = ["serde", "reactive_stores"] } futures = "0.3.31" diff --git a/assets/style.scss b/assets/style.scss index deccd7c..b829501 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -289,6 +289,90 @@ p { grid-area: 1 / 1 / 2 / 2; } +.open-chat-view { + flex-grow: 1; + max-height: 100%; +} + +.messages-buffer { + display: flex; + flex-direction: column-reverse; + flex-grow: 1; + overflow: scroll; +} + +.chat-message { + display: flex; + padding: 4px 0; +} + +.chat-message:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.chat-message .left { + width: 48px; + flex: none; + padding: 0 8px; +} + +.chat-message .middle { + width: auto; + flex-grow: 1; +} + +.chat-message .right { + width: 16px; + padding: 0 8px; + flex: none; +} + +.message-info { + display: flex; + align-items: baseline; + gap: 8px; +} + +.message-text { + white-space: pre-wrap; +} + +.chat-message.major { + margin-top: 8px; +} + +.chat-message.major .left { + height: 48px; +} + +.chat-message.major .middle { + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.message-info .message-user-name { + font-weight: bold; + font-size: 1.1em; + margin-bottom: 4px; +} + +.chat-message.major .message-timestamp { + font-weight: light; + font-size: 0.8em; +} + +.chat-message.minor .message-timestamp { + font-weight: light; + font-size: 0.7em; + /* TODO: better alignment */ + margin-top: 0.3em; +} + +.chat-message.minor:not(:hover) .message-timestamp { + opacity: 0; +} + /* font-families */ /* thai */ diff --git a/src/lib.rs b/src/lib.rs index a05f511..7ff95d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ use std::{ time::{self, Duration}, }; +use chrono::{NaiveDateTime, TimeDelta}; use filamento::{ chat::{Body, Chat, ChatStoreFields, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage }; @@ -521,7 +522,94 @@ pub fn ChatViewHeader(chat: MacawChat) -> impl IntoView { #[component] pub fn MessageHistoryBuffer(chat: MacawChat) -> impl IntoView { - view! {} + let (messages, set_messages) = signal(IndexMap::new()); + let chat_chat = *chat.chat; + let chat_user = *chat.user; + + let load_messages = LocalResource::new(move || async move { + let client = use_context::().expect("client not in context"); + let messages = client.get_messages_with_users(chat_chat.correspondent().get_untracked()).await.map_err(|e| e.to_string()); + match messages { + Ok(m) => { + let messages = m.into_iter().map(|(message, message_user)| { + (message.id, MacawMessage::got_message_and_user(message, message_user)) + }).rev().collect::>(); + set_messages.set(messages); + }, + Err(_) => { + // TODO: show error message at top of chats list + }, + } + }); + + // TODO: filter new messages signal + let new_messages_signal: RwSignal = use_context().unwrap(); + let _load_new_messages = LocalResource::new(move || async move { + load_messages.await; + let mut new_messages = new_messages_signal.write().subscribe_chat(chat_chat.correspondent().get_untracked()); + while let Some(new_message) = new_messages.recv().await { + debug!("got new message in let message buffer"); + let mut messages= set_messages.write(); + if let Some((_, last)) = messages.last() { + if *last.message.timestamp().read_untracked() < *new_message.timestamp().read_untracked() { + messages + .insert(new_message.message.id().get_untracked(), new_message); + debug!("set the new message in message buffer"); + } else { + let index = match messages.binary_search_by(|_, value| { + value.message.timestamp().read_untracked().cmp(&new_message.message.timestamp().read_untracked()) + }) { + Ok(i) => i, + Err(i) => i, + }; + messages.insert_before( + // TODO: check if this logic is correct + index, + new_message.message.id().get_untracked(), + new_message, + ); + debug!("set the new message in message buffer"); + } + } else { + messages + .insert(new_message.message.id().get_untracked(), new_message); + debug!("set the new message in message buffer"); + } + } + }); + + let each = move || { + let mut last_timestamp = NaiveDateTime::MIN; + let mut last_user: Option = None; + let mut messages = messages.get().into_iter().map(|(id, message)| { + let message_timestamp = message.message.timestamp().read().naive_local(); + // if message_timestamp.date() > last_timestamp.date() { + // messages_view = messages_view.push(date(message_timestamp.date())); + // } + let major = if last_user.as_ref() != Some(&message.message.read().from) + || message_timestamp - last_timestamp > TimeDelta::minutes(3) + { + true + } else { + false + }; + last_user = Some(message.message.from().get()); + last_timestamp = message_timestamp; + (id, (message, major, false)) + }).collect::>(); + if let Some((_id, (_, _, last))) = messages.last_mut() { + *last = true + } + messages.into_iter().rev() + }; + + view! { +
+ + + +
+ } } #[component] @@ -533,30 +621,31 @@ pub fn Message(message: MacawMessage, major: bool, r#final: bool) -> impl IntoVi // TODO: chrono-humanize? // TODO: if final, show delivery not only on hover. + // {move || message_message.delivery().read().map(|delivery| delivery.to_string()).unwrap_or_default()} if major { view! {
- -
+
+
-
{name}
-
{move || message_message.timestamp().read().to_string()}
+
{name}
+
{move || message_message.timestamp().read().format("%H:%M").to_string()}
{move || message_message.body().read().body.clone()}
-
+
}.into_any() } else { view! {
-
- {move || message_message.timestamp().read().to_string()} +
+ {move || message_message.timestamp().read().format("%H:%M").to_string()}
-
{move || message_message.body().read().body.clone()}
-
+
{move || message_message.body().read().body.clone()}
+
}.into_any() } @@ -967,6 +1056,7 @@ fn ChatsList() -> impl IntoView { debug!("got new message in let"); let mut chats = set_chats.write(); if let Some((chat, _latest_message)) = chats.shift_remove(&to) { + // TODO: check if new message is actually latest message debug!("chat existed"); debug!("new message: {}", new_message.read().body.body); chats.insert_before(0, to, (chat.clone(), new_message)); -- cgit