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()
}