aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-03-06 10:42:44 +0000
committerLibravatar cel 🌸 <cel@bunny.garden>2025-03-06 10:42:44 +0000
commit9bfb143c323b86d07f25c9aa22859f0223df79eb (patch)
treefedbb2f453d32a658aa2db6d5bf6db420eece03e /src
parentd0e122655926504cc2a29e7239cb88fddaed9c76 (diff)
downloadmacaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.tar.gz
macaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.tar.bz2
macaw-9bfb143c323b86d07f25c9aa22859f0223df79eb.zip
feat: message view
Diffstat (limited to 'src')
-rw-r--r--src/main.rs291
-rw-r--r--src/message_view.rs169
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()
+}