diff options
author | 2025-03-06 10:42:44 +0000 | |
---|---|---|
committer | 2025-03-06 10:42:44 +0000 | |
commit | 9bfb143c323b86d07f25c9aa22859f0223df79eb (patch) | |
tree | fedbb2f453d32a658aa2db6d5bf6db420eece03e /src | |
parent | d0e122655926504cc2a29e7239cb88fddaed9c76 (diff) | |
download | macaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.tar.gz macaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.tar.bz2 macaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.zip |
feat: message view
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 291 | ||||
-rw-r--r-- | src/message_view.rs | 169 |
2 files changed, 383 insertions, 77 deletions
diff --git a/src/main.rs b/src/main.rs index 27314ae..ed6ce1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,10 @@ use std::str::FromStr; use std::sync::Arc; use iced::futures::{SinkExt, Stream, StreamExt}; +use iced::theme::palette::{ + Background, Danger, Extended, Pair, Primary, Secondary, Success, Warning, +}; +use iced::theme::{Custom, Palette}; use iced::widget::button::Status; use iced::widget::text::{Fragment, IntoFragment}; use iced::widget::{ @@ -14,15 +18,17 @@ use iced::widget::{ text_input, Column, Text, Toggler, }; use iced::Length::Fill; -use iced::{stream, Color, Element, Subscription, Task, Theme}; +use iced::{color, stream, Color, Element, Subscription, Task, Theme}; use indexmap::{indexmap, IndexMap}; use jid::JID; use keyring::Entry; use login_modal::{Creds, LoginModal}; use luz::chat::{Chat, Message as ChatMessage}; +use luz::error::CommandError; use luz::presence::{Offline, Presence}; use luz::CommandMessage; use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage}; +use message_view::MessageView; use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; @@ -31,6 +37,7 @@ use tracing::{error, info}; use uuid::Uuid; mod login_modal; +mod message_view; #[derive(Serialize, Deserialize, Clone)] pub struct Config { @@ -55,17 +62,12 @@ pub struct Macaw { roster: HashMap<JID, Contact>, users: HashMap<JID, User>, presences: HashMap<JID, Presence>, - chats: IndexMap<JID, (Chat, IndexMap<Uuid, ChatMessage>)>, + chats: IndexMap<JID, (Chat, ChatMessage)>, subscription_requests: HashSet<JID>, - open_chat: Option<OpenChat>, + open_chat: Option<MessageView>, new_chat: Option<NewChat>, } -pub struct OpenChat { - jid: JID, - new_message: String, -} - pub struct NewChat; impl Macaw { @@ -157,7 +159,7 @@ async fn luz(jid: &JID, creds: &Creds, cfg: &Config) -> (LuzHandle, mpsc::Receiv async fn main() -> iced::Result { tracing_subscriber::fmt::init(); - let cfg: Config = confy::load("macaw", None).unwrap(); + let cfg: Config = confy::load("macaw", None).unwrap_or_default(); let entry = Entry::new("macaw", "macaw"); let mut client_creation_error: Option<Error> = None; let mut creds: Option<Creds> = None; @@ -213,21 +215,23 @@ async fn main() -> iced::Result { let luz_handle2 = luz_handle.clone(); if cfg.auto_connect { Task::batch([ - Task::perform(async move { luz_handle1.get_roster().await }, |result| { - let roster = result.unwrap(); - let mut macaw_roster = HashMap::new(); - for contact in roster { - macaw_roster.insert(contact.user_jid.clone(), contact); - } - Message::Roster(macaw_roster) - }), - Task::perform(async move { luz_handle2.get_chats().await }, |chats| { - let chats = chats.unwrap(); - info!("got chats: {:?}", chats); - Message::GotChats(chats) - }), + Task::batch([ + Task::perform(async move { luz_handle1.get_roster().await }, |result| { + let roster = result.unwrap(); + let mut macaw_roster = HashMap::new(); + for contact in roster { + macaw_roster.insert(contact.user_jid.clone(), contact); + } + Message::Roster(macaw_roster) + }), + Task::perform(async move { luz_handle2.get_chats().await }, |chats| { + let chats = chats.unwrap(); + info!("got chats: {:?}", chats); + Message::GotChats(chats) + }), + ]) + .chain(Task::done(Message::Connect)), Task::stream(stream), - Task::done(Message::Connect), ]) } else { Task::batch([ @@ -248,20 +252,22 @@ async fn main() -> iced::Result { ]) } }; - iced::application("Macaw", Macaw::update, Macaw::view).run_with(|| { - ( - Macaw::new( - Some(Client { - client: luz_handle, - // TODO: - jid, - connection_status: Presence::Offline(Offline::default()), - }), - cfg, - ), - task, - ) - }) + iced::application("Macaw", Macaw::update, Macaw::view) + .theme(Macaw::theme) + .run_with(|| { + ( + Macaw::new( + Some(Client { + client: luz_handle, + // TODO: + jid, + connection_status: Presence::Offline(Offline::default()), + }), + cfg, + ), + task, + ) + }) } else { if let Some(e) = client_creation_error { iced::application("Macaw", Macaw::update, Macaw::view) @@ -288,6 +294,7 @@ pub enum Message { MessageCompose(String), SendMessage(JID, String), Error(Error), + MessageView(message_view::Message), } #[derive(Debug, Error, Clone)] @@ -298,6 +305,8 @@ pub enum Error { CredentialsSave(CredentialsSaveError), #[error("failed to load credentials: {0}")] CredentialsLoad(CredentialsLoadError), + #[error("failed to retreive messages for chat {0}")] + MessageHistory(JID, CommandError<luz::error::DatabaseError>), } #[derive(Debug, Error, Clone)] @@ -381,14 +390,22 @@ impl Macaw { Task::none() } UpdateMessage::Message { to, message } => { - if let Some((_chat, message_history)) = self.chats.get_mut(&to) { - message_history.insert(message.id, message); + if let Some((chat_jid, (chat, old_message))) = + self.chats.shift_remove_entry(&to) + { + self.chats + .insert_before(0, chat_jid, (chat, message.clone())); + if let Some(open_chat) = &mut self.open_chat { + if open_chat.jid == to { + open_chat.update(message_view::Message::Message(message)); + } + } } else { let chat = Chat { correspondent: to.clone(), }; - let message_history = indexmap! {message.id => message}; - self.chats.insert(to, (chat, message_history)); + let message_history = indexmap! {message.id => message.clone()}; + self.chats.insert_before(0, to, (chat, message)); } Task::none() } @@ -421,8 +438,8 @@ impl Macaw { info!("got chats: {:?}", chats); Message::GotChats(chats) }), - Task::done(Message::Connect), ]) + .chain(Task::done(Message::Connect)) } else { Task::batch([ Task::perform(async move { client1.client.get_roster().await }, |result| { @@ -472,11 +489,23 @@ impl Macaw { Account::LoggedOut(login_modal) => Task::none(), }, Message::OpenChat(jid) => { - self.open_chat = Some(OpenChat { - jid, - new_message: String::new(), - }); - Task::none() + self.open_chat = Some(MessageView::new(jid.clone())); + let jid1 = jid.clone(); + match &self.client { + Account::LoggedIn(client) => { + let client = client.clone(); + Task::perform( + async move { client.get_messages(jid1).await }, + move |result| match result { + Ok(h) => { + Message::MessageView(message_view::Message::MessageHistory(h)) + } + Err(e) => Message::Error(Error::MessageHistory(jid.clone(), e)), + }, + ) + } + Account::LoggedOut(login_modal) => Task::none(), + } } Message::LoginModal(login_modal_message) => match &mut self.client { Account::LoggedIn(_client) => Task::none(), @@ -567,6 +596,7 @@ impl Macaw { let client = client.clone(); let correspondent = chat.correspondent.clone(); tasks.push(Task::perform( + // TODO: don't get the entire message history LOL async move { (chat, client.get_messages(correspondent).await) }, |result| { let messages: IndexMap<Uuid, ChatMessage> = result @@ -591,9 +621,12 @@ impl Macaw { // Task::batch(tasks) // }), } - Message::GotMessageHistory(chat, message_history) => { - self.chats - .insert(chat.correspondent.clone(), (chat, message_history)); + Message::GotMessageHistory(chat, mut message_history) => { + // TODO: don't get the entire message history LOL + if let Some((_id, message)) = message_history.pop() { + self.chats + .insert(chat.correspondent.clone(), (chat, message)); + } Task::none() } Message::CloseChat(jid) => { @@ -619,7 +652,23 @@ impl Macaw { ) .discard() } - Message::Error(error) => todo!("error notification toasts, logging?"), + Message::Error(error) => { + error!("{}", error); + Task::none() + } + Message::MessageView(message) => { + if let Some(message_view) = &mut self.open_chat { + let action = message_view.update(message); + match action { + message_view::Action::None => Task::none(), + message_view::Action::SendMessage(m) => { + Task::done(Message::SendMessage(message_view.jid.clone(), m)) + } + } + } else { + Task::none() + } + } } } @@ -670,34 +719,12 @@ impl Macaw { let message_view; if let Some(open_chat) = &self.open_chat { - let (chat, messages) = self.chats.get(&open_chat.jid).unwrap(); - let mut messages_view = column![]; - for (_id, message) in messages { - let from: Cow<'_, str> = (&message.from).into(); - let message: Column<Message> = - column![text(from).size(12), text(&message.body.body)].into(); - messages_view = messages_view.push(message); - } - let message_send_input = row![ - text_input("new message", &open_chat.new_message) - .on_input(Message::MessageCompose), - button("send").on_press(Message::SendMessage( - chat.correspondent.clone(), - open_chat.new_message.clone() - )) - ]; - message_view = column![ - scrollable(messages_view) - .height(Fill) - .width(Fill) - .anchor_bottom(), - message_send_input - ]; + message_view = open_chat.view().map(Message::MessageView) } else { - message_view = column![]; + message_view = column![].into(); } - row![sidebar, message_view.width(Fill)] + row![sidebar, container(message_view).width(Fill)] // old @@ -731,7 +758,117 @@ impl Macaw { } fn theme(&self) -> Theme { - Theme::Dark + let extended = Extended { + background: Background { + base: Pair { + color: color!(0x392c25), + text: color!(0xdcdcdc), + }, + weakest: Pair { + color: color!(0xdcdcdc), + text: color!(0x392c25), + }, + weak: Pair { + color: color!(0xdcdcdc), + text: color!(0x392c25), + }, + strong: Pair { + color: color!(0x364b3b), + text: color!(0xdcdcdc), + }, + strongest: Pair { + color: color!(0x364b3b), + text: color!(0xdcdcdc), + }, + }, + primary: Primary { + base: Pair { + color: color!(0x2b33b4), + text: color!(0xdcdcdc), + }, + weak: Pair { + color: color!(0x4D4A5E), + text: color!(0xdcdcdc), + }, + strong: Pair { + color: color!(0x2b33b4), + text: color!(0xdcdcdc), + }, + }, + secondary: Secondary { + base: Pair { + color: color!(0xffce07), + text: color!(0x000000), + }, + weak: Pair { + color: color!(0xffce07), + text: color!(0x000000), + }, + strong: Pair { + color: color!(0xffce07), + text: color!(0x000000), + }, + }, + success: Success { + base: Pair { + color: color!(0x14802E), + text: color!(0xdcdcdc), + }, + weak: Pair { + color: color!(0x14802E), + text: color!(0xdcdcdc), + }, + strong: Pair { + color: color!(0x14802E), + text: color!(0xdcdcdc), + }, + }, + warning: Warning { + base: Pair { + color: color!(0xFF9D00), + text: color!(0x000000), + }, + weak: Pair { + color: color!(0xFF9D00), + text: color!(0x000000), + }, + strong: Pair { + color: color!(0xFF9D00), + text: color!(0x000000), + }, + }, + danger: Danger { + base: Pair { + color: color!(0xC1173C), + text: color!(0xdcdcdc), + }, + weak: Pair { + color: color!(0xC1173C), + text: color!(0xdcdcdc), + }, + strong: Pair { + color: color!(0xC1173C), + text: color!(0xdcdcdc), + }, + }, + is_dark: true, + }; + Theme::Custom(Arc::new(Custom::with_fn( + "macaw".to_string(), + Palette::DARK, + |_| extended, + ))) + // Theme::Custom(Arc::new(Custom::new( + // "macaw".to_string(), + // Palette { + // background: color!(0x392c25), + // text: color!(0xdcdcdc), + // primary: color!(0x2b33b4), + // success: color!(0x14802e), + // warning: color!(0xffce07), + // danger: color!(0xc1173c), + // }, + // ))) } } diff --git a/src/message_view.rs b/src/message_view.rs new file mode 100644 index 0000000..4709dd6 --- /dev/null +++ b/src/message_view.rs @@ -0,0 +1,169 @@ +use std::borrow::Cow; + +use chrono::NaiveDate; +use iced::{ + alignment::Horizontal::{self, Right}, + border::Radius, + color, + theme::Palette, + widget::{button, column, container, row, scrollable, text, text_input, Column}, + Border, Color, Element, + Length::{Fill, Shrink}, + Theme, +}; +use indexmap::IndexMap; +use jid::JID; +use luz::chat::Message as ChatMessage; +use uuid::Uuid; + +pub struct MessageView { + pub jid: JID, + pub message_history: IndexMap<Uuid, ChatMessage>, + pub new_message: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + MessageHistory(Vec<ChatMessage>), + Message(ChatMessage), + MessageCompose(String), + SendMessage(String), +} + +pub enum Action { + None, + SendMessage(String), +} + +impl MessageView { + pub fn new(jid: JID) -> Self { + Self { + jid, + // TODO: save position in message history + message_history: IndexMap::new(), + // TODO: save draft (as part of chat struct?) + new_message: String::new(), + } + } + pub fn update(&mut self, message: Message) -> Action { + match message { + Message::MessageHistory(messages) => { + if self.message_history.is_empty() { + self.message_history = messages + .into_iter() + .map(|message| (message.id.clone(), message)) + .collect(); + } + Action::None + } + Message::Message(message) => { + let i = self + .message_history + .iter() + .position(|(_id, m)| m.timestamp > message.timestamp); + if let Some(i) = i { + self.message_history.insert_before(i, message.id, message); + } else { + self.message_history.insert(message.id, message); + } + Action::None + } + Message::MessageCompose(s) => { + self.new_message = s; + Action::None + } + Message::SendMessage(m) => { + self.new_message = String::new(); + Action::SendMessage(m) + } + } + } + pub fn view(&self) -> Element<Message> { + let mut messages_view = column![].spacing(8); + let mut latest_date = NaiveDate::MIN; + for (_id, message) in &self.message_history { + let message_date = message.timestamp.naive_local().date(); + if message_date > latest_date { + latest_date = message_date; + messages_view = messages_view.push(date(latest_date)); + } + messages_view = messages_view.push(self.message(message)); + } + let message_send_input = row![ + text_input("new message", &self.new_message).on_input(Message::MessageCompose), + button("send").on_press(Message::SendMessage(self.new_message.clone())) + ]; + column![ + scrollable(messages_view) + .height(Fill) + .width(Fill) + .spacing(1) + .anchor_bottom(), + message_send_input + ] + .into() + } + + pub fn message<'a>(&'a self, message: &'a ChatMessage) -> Element<'a, Message> { + let timestamp = message.timestamp.naive_local(); + let timestamp = timestamp.time().format("%H:%M").to_string(); + + if self.jid == message.from.as_bare() { + container( + container( + column![ + text(message.body.body.as_str()), + container(text(timestamp).wrapping(text::Wrapping::None).size(12)) + .align_right(Fill) + ] + .width(Shrink) + .max_width(500), + ) + .padding(16) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style::default() + .background(palette.primary.weak.color) + .border(Border { + color: Color::BLACK, + width: 4., + radius: Radius::new(16), + }) + }), + ) + .align_left(Fill) + .into() + } else { + let element: Element<Message> = container( + container( + column![ + text(message.body.body.as_str()), + container(text(timestamp).wrapping(text::Wrapping::None).size(12)) + .align_right(Fill) + ] + .width(Shrink) + .max_width(500), + ) + .padding(16) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + container::Style::default() + .background(palette.primary.base.color) + .border(Border { + color: Color::BLACK, + width: 4., + radius: Radius::new(16), + }) + }), + ) + .align_right(Fill) + .into(); + // element.explain(Color::BLACK) + element + } + } +} + +pub fn date(date: NaiveDate) -> Element<'static, Message> { + container(text(date.to_string())).center_x(Fill).into() +} |