aboutsummaryrefslogblamecommitdiffstats
path: root/src/message_view.rs
blob: 742aac00a4b1d83cf6be83c20fe337e1e80ae920 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                                  



                                                                                         
           
                   


                                                                                            


                           
                                            
                       
             
                                    

               
                 
                                                  
 
                        
                           


                                 
                                                       
                       
                             
                            












                                        



                       

                                      


                              
                                        








                        
                                                                                     
              
                 
                                                     
                                      
                                                         

                                                       

                                                              
                      

         
 

                                                          



                                                                   












                                                                          





                                            


                                        
                                                  

                                      



                                                        
                                                                      




                                                   
                                                                                                   



                                        

                                                                                





                                                               


                                                                              

                                                                                     
                                                                           



                                        




                                                    

                        







                                                                          


                            






                                                                     

         
 
                                            
                                          

                                                    
                                              
                                                                      


                                                                                   
                                                              

                                                                             
                                                                                              
                    
                                                                                               
             
                                                         
                                               
         


                                                        












                                                       
                                      
                        
                                                                                                

                    
                
                          









                                     































                                                                                     






                                  


                                                                     
                                                      




                                                                       

                                                  





                                                                
                                                                                     
                                                                     


                                                           






















                                                                                 
             


                                                                                          
                





























































                                                                                       





                                                           
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<Uuid, (MacawMessage, bool)>,
    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<MacawMessage>),
    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<Message> {
        let mut messages_view = column![];
        let mut last_timestamp = NaiveDateTime::MIN;
        let mut last_user: Option<JID> = 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<Message> = 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<Message> = 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<Message> = 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()
}