diff options
| author | 2025-05-06 20:46:16 +0100 | |
|---|---|---|
| committer | 2025-05-06 21:16:58 +0100 | |
| commit | 7859947fcc643a96d20b7c56df912d8e3230429d (patch) | |
| tree | 0dede9bac67e1fd2182a7ffd975f3450ae510fa9 | |
| parent | e3b5d43978f06f4a8c06d49467e4bb1d1f740375 (diff) | |
| download | macaw-web-7859947fcc643a96d20b7c56df912d8e3230429d.tar.gz macaw-web-7859947fcc643a96d20b7c56df912d8e3230429d.tar.bz2 macaw-web-7859947fcc643a96d20b7c56df912d8e3230429d.zip  | |
feat: message buffer
| -rw-r--r-- | Cargo.lock | 11 | ||||
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | assets/style.scss | 84 | ||||
| -rw-r--r-- | src/lib.rs | 110 | 
4 files changed, 197 insertions, 10 deletions
@@ -324,6 +324,15 @@ dependencies = [  ]  [[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"  source = "git+https://bunny.garden/forks/circular#0a9c9ab1e2f4e3eb912ad9fe91e3ea953e066453" @@ -1895,6 +1904,8 @@ dependencies = [  name = "macaw-web"  version = "0.1.0"  dependencies = [ + "chrono", + "chrono-humanize",   "console_error_panic_hook",   "filamento",   "futures", @@ -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 */ @@ -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::<Client>().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::<IndexMap<Uuid, _>>(); +                set_messages.set(messages); +            }, +            Err(_) => { +                // TODO: show error message at top of chats list +            }, +        } +    }); + +    // TODO: filter new messages signal +    let new_messages_signal: RwSignal<MessageSubscriptions> = 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<JID> = 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::<Vec<_>>(); +        if let Some((_id, (_, _, last))) = messages.last_mut() { +            *last = true +        } +        messages.into_iter().rev() +    }; + +    view! { +        <div class="messages-buffer"> +            <For each=each key=|message| message.0 let(message)> +                <Message message=message.1.0 major=message.1.1 r#final=message.1.2 /> +            </For> +        </div> +    }  }  #[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! {              <div class="chat-message major"> -                <img class="avatar" src=avatar /> -                <div> +                <div class="left"><img class="avatar" src=avatar /></div> +                <div class="middle">                      <div class="message-info"> -                        <div>{name}</div> -                        <div>{move || message_message.timestamp().read().to_string()}</div> +                        <div class="message-user-name">{name}</div> +                        <div class="message-timestamp">{move || message_message.timestamp().read().format("%H:%M").to_string()}</div>                      </div>                      <div class="message-text">                          {move || message_message.body().read().body.clone()}                      </div>                  </div> -                <div class="message-delivery"></div> +                <div class="right 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 class="left message-timestamp"> +                    {move || message_message.timestamp().read().format("%H:%M").to_string()}                  </div> -                <div class="message-text">{move || message_message.body().read().body.clone()}</div> -                <div class="message-delivery"></div> +                <div class="middle message-text">{move || message_message.body().read().body.clone()}</div> +                <div class="right message-delivery"></div>              </div>          }.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));  | 
