use std::{path::PathBuf, time::Duration}; use chrono::{NaiveDate, NaiveDateTime, TimeDelta}; use filamento::chat::Delivery; use iced::advanced::Overlay; use iced::alignment::Vertical; use iced::widget::{horizontal_space, mouse_area, text_editor, vertical_space, Container}; use iced::{ border::Radius, font::{Style, Weight}, widget::{button, column, container, image, row, scrollable, text, text_editor::Content}, Border, Color, Element, Font, Length::{Fill, Shrink}, Theme, }; use iced::{color, overlay, Length, Padding}; use indexmap::IndexMap; use jid::JID; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::icons; use crate::{icons::Icon, MacawChat, MacawMessage}; pub struct MessageView { pub file_root: PathBuf, // references chats, users pub chat: MacawChat, // references users, messages pub messages: IndexMap, pub config: Config, pub new_message: Content, pub shift_pressed: bool, } #[derive(Serialize, Deserialize, Clone)] pub struct Config { pub send_on_enter: bool, } impl Default for Config { fn default() -> Self { Self { send_on_enter: true, } } } #[derive(Debug, Clone)] pub enum Message { MessageHistory(Vec), Message(MacawMessage), MessageHovered(Uuid), MessageUnhovered(Uuid), MessageRightClicked(Uuid), MessageCompose(text_editor::Action), SendMessage(String), } pub enum Action { None, SendMessage(String), } impl MessageView { pub fn new(chat: MacawChat, config: &super::Config, file_root: PathBuf) -> Self { Self { chat, // TODO: save position in message history messages: IndexMap::new(), // TODO: save draft (as part of chat struct?) new_message: Content::new(), config: config.message_view_config.clone(), // TODO: have centralised modifier state location? shift_pressed: false, file_root, } } pub fn update(&mut self, message: Message) -> Action { match message { Message::MessageCompose(a) => { match &a { text_editor::Action::Edit(edit) => match edit { text_editor::Edit::Enter => { if self.config.send_on_enter { if !self.shift_pressed { let message = self.new_message.text(); self.new_message = Content::new(); return Action::SendMessage(message); } } else { if self.shift_pressed { let message = self.new_message.text(); self.new_message = Content::new(); return Action::SendMessage(message); } } } _ => {} }, _ => {} } self.new_message.perform(a); Action::None } Message::SendMessage(m) => { self.new_message = Content::new(); Action::SendMessage(m) } Message::MessageHistory(macaw_messages) => { if self.messages.is_empty() { self.messages = macaw_messages .into_iter() .map(|message| (message.id, (message, false))) .collect() } else { for message in macaw_messages { let index = match self .messages .binary_search_by(|_, value| value.0.timestamp.cmp(&message.timestamp)) { Ok(i) => i, Err(i) => i, }; self.messages .insert_before(index, message.id, (message, false)); } } Action::None } Message::Message(macaw_message) => { if let Some((_, last)) = self.messages.last() { if last.0.timestamp < macaw_message.timestamp { self.messages .insert(macaw_message.id, (macaw_message, false)); } else { let index = match self.messages.binary_search_by(|_, value| { value.0.timestamp.cmp(&macaw_message.timestamp) }) { Ok(i) => i, Err(i) => i, }; self.messages.insert_before( index, macaw_message.id, (macaw_message, false), ); } } else { self.messages .insert(macaw_message.id, (macaw_message, false)); } Action::None } Message::MessageHovered(uuid) => { if let Some(message) = self.messages.get_mut(&uuid) { message.1 = true; } Action::None } Message::MessageUnhovered(uuid) => { if let Some(message) = self.messages.get_mut(&uuid) { message.1 = false; } Action::None } Message::MessageRightClicked(uuid) => todo!(), } } pub fn view(&self) -> Element { let mut messages_view = column![]; let mut last_timestamp = NaiveDateTime::MIN; let mut last_user: Option = None; for (_id, message) in &self.messages { let message_timestamp = message.0.timestamp.naive_local(); if message_timestamp.date() > last_timestamp.date() { messages_view = messages_view.push(date(message_timestamp.date())); } if last_user.as_ref() != Some(&message.0.from.jid) || message_timestamp - last_timestamp > TimeDelta::minutes(3) { messages_view = messages_view.push(self.message(&message.0, message.1, true)); } else { messages_view = messages_view.push(self.message(&message.0, message.1, false)); } last_user = Some(message.0.from.jid.clone()); last_timestamp = message_timestamp; } let text_editor = text_editor(&self.new_message) .placeholder("new message") .on_action(Message::MessageCompose) .wrapping(text::Wrapping::WordOrGlyph) .style(|theme, status| text_editor::Style { background: color!(0xdcdcdc).into(), border: Border { color: Color::BLACK, width: 0.0, radius: 0.into(), }, icon: color!(0x00000000), placeholder: color!(0xacacac), value: color!(0x000000), selection: color!(0xffce07), }); let message_send_input = row![ text_editor, // button(Icon::NewBubble24).on_press(Message::SendMessage(self.new_message.text())) ] .padding(8); column![ self.header(), scrollable(messages_view) .height(Fill) .width(Fill) .spacing(1) .anchor_bottom(), message_send_input ] .into() } pub fn header(&self) -> Element<'_, Message> { // TODO: contact stored here for name let mut bold = Font::with_name("K2D"); bold.weight = Weight::Bold; let mut sweet = Font::with_name("Diolce"); sweet.style = Style::Italic; let mut name_and_jid = column![]; if let Some(nick) = &self.chat.user.nick { name_and_jid = name_and_jid.push(text(nick).font(bold).size(20)); } let jid = self.chat.user.jid.as_bare().to_string(); name_and_jid = name_and_jid.push(text(jid).font(sweet)); let mut header = row![]; if let Some(avatar) = &self.chat.user.avatar { let mut path = self.file_root.join(avatar); path.set_extension("jpg"); header = header.push(container(image(path).width(48).height(48))); } header = header.push(name_and_jid); container( container(header.spacing(8).padding(8)) .style(|theme: &Theme| { container::Style::default() .background(theme.extended_palette().background.strong.color) }) .width(Fill), ) .padding(8) .width(Fill) .into() } pub fn message<'a>( &'a self, message: &'a MacawMessage, hovered: bool, major: bool, // next_read: bool, ) -> Element<'a, Message> { let timestamp = message.timestamp.naive_local(); let timestamp = timestamp.time().format("%H:%M").to_string(); let container: Container = if major { let nick: String = if let Some(nick) = &message.from.nick { nick.to_string() } else { message.from.jid.as_bare().to_string() }; let mut bold = Font::with_name("K2D"); bold.weight = Weight::Bold; let mut thin = Font::with_name("K2D"); thin.weight = Weight::Thin; let timestamp = text(timestamp).size(12).font(thin); let header = row![text(nick).font(bold), timestamp] .align_y(Vertical::Bottom) .spacing(8); let message_right = column![header, text(&message.body.body)].spacing(8); let avatar = if let Some(avatar) = &message.from.avatar { let mut path = self.file_root.join(avatar); path.set_extension("jpg"); // info!("got avatar: {:?}", path); container(image(path).width(48).height(48)) } else { container("").width(48) }; let show_delivery = match message.delivery { Some(Delivery::Sending) => true, Some(Delivery::Failed) => true, // TODO: queued icon Some(Delivery::Queued) => true, _ => hovered, }; let delivery = if show_delivery { message .delivery .map(|delivery| icons::delivery_to_icon_svg(delivery)) .unwrap_or_default() } else { None }; let delivery: Container = if let Some(delivery) = delivery { container(delivery) } else { container("") } .width(16); let major_message = row![avatar, message_right, horizontal_space(), delivery]; container(major_message.spacing(8)) } else { let timestamp = if hovered { let mut thin = Font::with_name("K2D"); thin.weight = Weight::Thin; let timestamp = text(timestamp).size(10).font(thin); container(timestamp).align_right(48) } else { container("").width(48) }; let show_delivery = match message.delivery { Some(Delivery::Sending) => true, Some(Delivery::Failed) => true, // TODO: queued icon Some(Delivery::Queued) => true, _ => hovered, }; let delivery = if show_delivery { message .delivery .map(|delivery| icons::delivery_to_icon_svg(delivery)) .unwrap_or_default() } else { None }; let delivery: Container = if let Some(delivery) = delivery { container(delivery) } else { container("") } .width(16); container( row![ timestamp, text(&message.body.body), horizontal_space(), delivery ] .align_y(Vertical::Top) .spacing(8), ) }; // let overlay = overlay::Element::new(Box::new(Overlay {})); mouse_area( container .width(Length::Fill) .style(move |theme| { if hovered { container::Style::default() .background(theme.extended_palette().background.weak.color) } else { container::Style::default() } }) .padding(Padding { top: 4., right: 8., bottom: 4., left: 8., }), ) .on_enter(Message::MessageHovered(message.id)) .on_exit(Message::MessageUnhovered(message.id)) .into() } } pub fn date(date: NaiveDate) -> Element<'static, Message> { container(text(date.to_string())).center_x(Fill).into() }