use std::borrow::Cow; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::ops::{Deref, DerefMut}; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use chrono::{Local, Utc}; use filamento::chat::{Chat, Message as ChatMessage}; use filamento::error::CommandError; use filamento::presence::{Offline, Presence, PresenceType}; use filamento::{roster::Contact, user::User, UpdateMessage}; use iced::alignment::Horizontal::Right; use iced::futures::{SinkExt, Stream, StreamExt}; use iced::keyboard::{on_key_press, on_key_release, Key, Modifiers}; 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, Wrapping}; use iced::widget::{ button, center, checkbox, column, container, horizontal_space, mouse_area, opaque, row, scrollable, stack, text, text_input, toggler, Column, Text, Toggler, }; use iced::Length::{self, Fill, Shrink}; 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 message_view::MessageView; use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; use tracing::{error, info}; use uuid::Uuid; mod login_modal; mod message_view; #[derive(Serialize, Deserialize, Clone)] pub struct Config { auto_connect: bool, storage_dir: Option, dburl: Option, message_view_config: message_view::Config, } impl Default for Config { fn default() -> Self { Self { auto_connect: true, storage_dir: None, dburl: None, message_view_config: message_view::Config::default(), } } } pub struct Macaw { client: Account, config: Config, roster: HashMap, users: HashMap, presences: HashMap, chats: IndexMap)>, subscription_requests: HashSet, open_chat: Option, new_chat: Option, } pub struct NewChat; impl Macaw { pub fn new(client: Option, config: Config) -> Self { let account; if let Some(client) = client { account = Account::LoggedIn(client); } else { account = Account::LoggedOut(LoginModal::default()); } Self { client: account, config, roster: HashMap::new(), users: HashMap::new(), presences: HashMap::new(), chats: IndexMap::new(), subscription_requests: HashSet::new(), open_chat: None, new_chat: None, } } } pub enum Account { LoggedIn(Client), LoggedOut(LoginModal), } impl Account { pub fn is_connected(&self) -> bool { match self { Account::LoggedIn(client) => client.connection_state.is_connected(), Account::LoggedOut(login_modal) => false, } } pub fn connection_status(&self) -> String { match self { Account::LoggedIn(client) => match client.connection_state { ConnectionState::Online => "online".to_string(), ConnectionState::Connecting => "connecting".to_string(), ConnectionState::Offline => "offline".to_string(), }, Account::LoggedOut(login_modal) => "no account".to_string(), } } } #[derive(Clone, Debug)] pub struct Client { client: filamento::Client, jid: JID, status: Presence, connection_state: ConnectionState, } impl Client { pub fn is_connected(&self) -> bool { self.connection_state.is_connected() } } #[derive(Clone, Debug)] pub enum ConnectionState { Online, Connecting, Offline, } impl ConnectionState { pub fn is_connected(&self) -> bool { match self { ConnectionState::Online => true, ConnectionState::Connecting => false, ConnectionState::Offline => false, } } } impl DerefMut for Client { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.client } } impl Deref for Client { type Target = filamento::Client; fn deref(&self) -> &Self::Target { &self.client } } async fn filamento( jid: &JID, creds: &Creds, cfg: &Config, ) -> (filamento::Client, mpsc::Receiver) { let filamento; if let Some(ref dburl) = cfg.dburl { // TODO: have some sort of crash popup for this stuff let db_path = dburl.strip_prefix("sqlite://").unwrap_or(&dburl); let db_path = PathBuf::from_str(db_path).expect("invalid database path"); let db = filamento::db::Db::create_connect_and_migrate(db_path) .await .unwrap(); filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); } else if let Some(ref dir) = cfg.storage_dir { let mut data_dir = PathBuf::from_str(&dir).expect("invalid storage directory path"); data_dir.push(creds.jid.clone()); data_dir.push(creds.jid.clone()); data_dir.set_extension("db"); let db = filamento::db::Db::create_connect_and_migrate(data_dir) .await .unwrap(); filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); } else { let mut data_dir = dirs::data_dir() .expect("operating system does not support retreiving determining default data dir"); data_dir.push("macaw"); data_dir.push(creds.jid.clone()); data_dir.push(creds.jid.clone()); // TODO: better lol data_dir.set_extension("db"); info!("db_path: {:?}", data_dir); let db = filamento::db::Db::create_connect_and_migrate(data_dir) .await .unwrap(); filamento = filamento::Client::new(jid.clone(), creds.password.to_string(), db); } filamento } #[tokio::main] async fn main() -> iced::Result { tracing_subscriber::fmt::init(); let cfg: Config = confy::load("macaw", None).unwrap_or_default(); let entry = Entry::new("macaw", "macaw"); let mut client_creation_error: Option = None; let mut creds: Option = None; match entry { Ok(e) => { let result = e.get_password(); match result { Ok(c) => { let result = toml::from_str(&c); match result { Ok(c) => creds = Some(c), Err(e) => { client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into()))) } } } Err(e) => match e { keyring::Error::NoEntry => {} _ => { client_creation_error = Some(Error::CredentialsLoad( CredentialsLoadError::Keyring(e.into()), )) } }, } } Err(e) => { client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring( e.into(), ))) } } let mut client: Option<(JID, filamento::Client, mpsc::Receiver)> = None; if let Some(creds) = creds { let jid = creds.jid.parse::(); match jid { Ok(jid) => { let (handle, updates) = filamento(&jid, &creds, &cfg).await; client = Some((jid, handle, updates)); } Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())), } } if let Some((jid, luz_handle, update_recv)) = client { let stream = ReceiverStream::new(update_recv); let stream = stream.map(|message| Message::Luz(message)); let task = { let luz_handle1 = luz_handle.clone(); let luz_handle2 = luz_handle.clone(); if cfg.auto_connect { Task::batch( [ 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_ordered_with_latest_messages().await }, |chats| { let chats = chats.unwrap(); info!("got chats: {:?}", chats); Message::GotChats(chats) }, ), ]) .chain(Task::done(Message::Connect)), Task::stream(stream), ], ) } else { 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_ordered_with_latest_messages().await }, |chats| { let chats = chats.unwrap(); info!("got chats: {:?}", chats); Message::GotChats(chats) }, ), Task::stream(stream), ]) } }; iced::application("Macaw", Macaw::update, Macaw::view) .subscription(subscription) .theme(Macaw::theme) .run_with(|| { ( Macaw::new( Some(Client { client: luz_handle, // TODO: jid, // TODO: store cached status status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()), }, connection_state: ConnectionState::Offline, }), cfg, ), task, ) }) } else { if let Some(e) = client_creation_error { iced::application("Macaw", Macaw::update, Macaw::view) .run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e)))) } else { iced::application("Macaw", Macaw::update, Macaw::view) .run_with(|| (Macaw::new(None, cfg), Task::none())) } } } fn subscription(state: &Macaw) -> Subscription { Subscription::batch([press_subscription(state), release_subscription(state)]) } fn press_subscription(_state: &Macaw) -> Subscription { on_key_press(handle_key_press) } fn handle_key_press(key: Key, r#mod: Modifiers) -> Option { match key { Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftPressed), _ => None, } } fn release_subscription(_state: &Macaw) -> Subscription { on_key_release(handle_key_release) } fn handle_key_release(key: Key, r#mod: Modifiers) -> Option { match key { Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftReleased), _ => None, } } #[derive(Debug, Clone)] pub enum Message { ShiftPressed, ShiftReleased, LoginModal(login_modal::Message), ClientCreated(Client), Luz(UpdateMessage), Roster(HashMap), Connect, Disconnect, GotChats(Vec<(Chat, ChatMessage)>), GotMessageHistory(Chat, IndexMap), ToggleChat(JID), SendMessage(JID, String), Error(Error), MessageView(message_view::Message), } #[derive(Debug, Error, Clone)] pub enum Error { #[error("failed to create Luz client: {0}")] ClientCreation(#[from] filamento::error::DatabaseError), #[error("failed to save credentials: {0}")] CredentialsSave(CredentialsSaveError), #[error("failed to load credentials: {0}")] CredentialsLoad(CredentialsLoadError), #[error("failed to retreive messages for chat {0}")] MessageHistory(JID, CommandError), } #[derive(Debug, Error, Clone)] pub enum CredentialsSaveError { #[error("keyring: {0}")] Keyring(Arc), #[error("toml serialisation: {0}")] Toml(#[from] toml::ser::Error), } impl From for CredentialsSaveError { fn from(e: keyring::Error) -> Self { Self::Keyring(Arc::new(e)) } } #[derive(Debug, Error, Clone)] pub enum CredentialsLoadError { #[error("keyring: {0}")] Keyring(Arc), #[error("toml serialisation: {0}")] Toml(#[from] toml::de::Error), #[error("invalid jid: {0}")] JID(#[from] jid::ParseError), } impl From for CredentialsLoadError { fn from(e: keyring::Error) -> Self { Self::Keyring(Arc::new(e)) } } impl Macaw { fn update(&mut self, message: Message) -> Task { match message { Message::Luz(update_message) => match update_message { UpdateMessage::Error(error) => { tracing::error!("Luz error: {:?}", error); Task::none() } UpdateMessage::Online(online, vec) => match &mut self.client { Account::LoggedIn(client) => { client.status = Presence { timestamp: Utc::now(), presence: PresenceType::Online(online), }; client.connection_state = ConnectionState::Online; let mut roster = HashMap::new(); for contact in vec { roster.insert(contact.user_jid.clone(), contact); } self.roster = roster; Task::none() } Account::LoggedOut(login_modal) => Task::none(), }, UpdateMessage::Offline(offline) => { // TODO: update all contacts' presences to unknown (offline) match &mut self.client { Account::LoggedIn(client) => { client.status = Presence { timestamp: Utc::now(), presence: PresenceType::Offline(offline), }; client.connection_state = ConnectionState::Offline; Task::none() } Account::LoggedOut(login_modal) => Task::none(), } } UpdateMessage::FullRoster(vec) => { let mut macaw_roster = HashMap::new(); for contact in vec { macaw_roster.insert(contact.user_jid.clone(), contact); } self.roster = macaw_roster; Task::none() } UpdateMessage::RosterUpdate(contact) => { self.roster.insert(contact.user_jid.clone(), contact); Task::none() } UpdateMessage::RosterDelete(jid) => { self.roster.remove(&jid); Task::none() } UpdateMessage::Presence { from, presence } => { self.presences.insert(from, presence); Task::none() } UpdateMessage::Message { to, message } => { if let Some((chat_jid, (chat, old_message))) = self.chats.shift_remove_entry(&to) { self.chats .insert_before(0, chat_jid, (chat, Some(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.clone()}; self.chats.insert_before(0, to, (chat, Some(message))); } Task::none() } UpdateMessage::SubscriptionRequest(jid) => { // TODO: subscription requests Task::none() } }, // TODO: NEXT Message::ClientCreated(client) => { self.client = Account::LoggedIn(client.clone()); let client1 = client.clone(); let client2 = client.clone(); if self.config.auto_connect { Task::batch([ Task::perform(async move { client1.client.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 { client2 .client .get_chats_ordered_with_latest_messages() .await }, |chats| { let chats = chats.unwrap(); // let chats: HashMap)> = chats // .into_iter() // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new()))) // .collect(); info!("got chats: {:?}", chats); Message::GotChats(chats) }, ), ]) .chain(Task::done(Message::Connect)) } else { Task::batch([ Task::perform(async move { client1.client.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 { client2 .client .get_chats_ordered_with_latest_messages() .await }, |chats| { let chats = chats.unwrap(); // let chats: HashMap)> = chats // .into_iter() // .map(|chat| (chat.correspondent.clone(), (chat, IndexMap::new()))) // .collect(); info!("got chats: {:?}", chats); Message::GotChats(chats) }, ), ]) } } Message::Roster(hash_map) => { self.roster = hash_map; Task::none() } Message::Connect => match &mut self.client { Account::LoggedIn(client) => { client.connection_state = ConnectionState::Connecting; let client = client.client.clone(); Task::future(async move { client.connect().await; }) .discard() } Account::LoggedOut(login_modal) => Task::none(), }, Message::Disconnect => match &self.client { Account::LoggedIn(client) => { let client = client.client.clone(); Task::future(async move { client.disconnect(Offline::default()).await; }) .discard() } Account::LoggedOut(login_modal) => Task::none(), }, Message::ToggleChat(jid) => { match &self.open_chat { Some(message_view) => { if message_view.jid == jid { self.open_chat = None; return Task::none(); } } None => {} } self.open_chat = Some(MessageView::new(jid.clone(), &self.config)); 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(), Account::LoggedOut(login_modal) => { let action = login_modal.update(login_modal_message); match action { login_modal::Action::None => Task::none(), login_modal::Action::CreateClient(jid, password, remember_me) => { let creds = Creds { jid, password }; let jid = creds.jid.parse::(); let config = self.config.clone(); match jid { Ok(jid) => { Task::perform(async move { let (jid, creds, config) = (jid, creds, config); let (handle, recv) = filamento(&jid, &creds, &config).await; (handle, recv, jid, creds, config) }, move |(handle, recv, jid, creds, config)| { let creds = creds; let mut tasks = Vec::new(); tasks.push(Task::done(crate::Message::ClientCreated( Client { client: handle, jid, status: Presence { timestamp: Utc::now(), presence: PresenceType::Offline(Offline::default()) }, connection_state: ConnectionState::Offline, }, ))); let stream = ReceiverStream::new(recv); let stream = stream.map(|message| crate::Message::Luz(message)); tasks.push(Task::stream(stream)); if remember_me { let entry = Entry::new("macaw", "macaw"); match entry { Ok(e) => { let creds = toml::to_string(&creds); match creds { Ok(c) => { let result = e.set_password(&c); if let Err(e) = result { tasks.push(Task::done(crate::Message::Error( crate::Error::CredentialsSave(e.into()), ))); } } Err(e) => tasks.push(Task::done( crate::Message::Error( crate::Error::CredentialsSave( e.into(), ), ), )), } } Err(e) => { tasks.push(Task::done(crate::Message::Error( crate::Error::CredentialsSave(e.into()), ))) } } } tasks }).then(|tasks| Task::batch(tasks)) } Err(e) => Task::done(Message::LoginModal( login_modal::Message::Error(login_modal::Error::InvalidJID), )), } } login_modal::Action::ClientCreated(task) => task, } } }, Message::GotChats(chats) => { let mut tasks = Vec::new(); let client = match &self.client { Account::LoggedIn(client) => client, Account::LoggedOut(_) => { // TODO: error into event tracing subscriber error!("no client, cannot retreive chat history for chats"); return Task::none(); } }; for chat in chats { self.chats // TODO: could have a chat with no messages, bad database state .insert(chat.0.correspondent.clone(), (chat.0.clone(), Some(chat.1))); // 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 = result // .1 // .unwrap() // .into_iter() // .map(|message| (message.id.clone(), message)) // .collect(); // Message::GotMessageHistory(result.0, messages) // }, // )) } Task::batch(tasks) // .then(|chats| { // let tasks = Vec::new(); // for key in chats.keys() { // let client = client.client.clone(); // tasks.push(Task::future(async { // client.get_messages(key.clone()).await; // })); // } // Task::batch(tasks) // }), } 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, Some(message))); } Task::none() } Message::SendMessage(jid, body) => { let client = match &self.client { Account::LoggedIn(client) => client.clone(), Account::LoggedOut(_) => { error!("cannot send message when no client set up"); return Task::none(); } }; Task::future(async move { client .send_message(jid, filamento::chat::Body { body }) .await }) .discard() } 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() } } Message::ShiftPressed => { info!("shift pressed"); if let Some(open_chat) = &mut self.open_chat { open_chat.shift_pressed = true; } Task::none() } Message::ShiftReleased => { info!("shift released"); if let Some(open_chat) = &mut self.open_chat { open_chat.shift_pressed = false; } Task::none() } } } fn view(&self) -> Element { let mut ui: Element = { let mut chats_list: Column = column![]; for (jid, (chat, latest_message)) in &self.chats { let mut open = false; if let Some(open_chat) = &self.open_chat { if open_chat.jid == *jid { open = true; } } let chat_list_item = chat_list_item(chat, latest_message, open); chats_list = chats_list.push(chat_list_item); } let chats_list = scrollable(chats_list.spacing(8).padding(8)) .spacing(1) .height(Fill); let connection_status = self.client.connection_status(); let client_jid: Cow<'_, str> = match &self.client { Account::LoggedIn(client) => (&client.jid).into(), Account::LoggedOut(_) => Cow::from("no account"), // map(|client| (&client.jid).into()); }; let connected = self.client.is_connected(); let account_view = container(row![ text(client_jid), horizontal_space(), text(connection_status), horizontal_space().width(8), toggler(connected).on_toggle(|connect| { if connect { Message::Connect } else { Message::Disconnect } }) ]) .padding(8); // TODO: config width/resizing let sidebar = column![chats_list, account_view].height(Fill).width(300); let message_view; if let Some(open_chat) = &self.open_chat { message_view = open_chat.view().map(Message::MessageView) } else { message_view = column![].into(); } row![sidebar, container(message_view).width(Fill)] } .into(); if let Some(new_chat) = &self.new_chat { // TODO: close new chat window ui = modal(ui, text("new chat"), None); } // temporarily center to fill space // let ui = center(ui).into(); let ui = container(ui).center_x(Fill).center_y(Fill); match &self.client { Account::LoggedIn(_client) => ui.into(), Account::LoggedOut(login_modal) => { let signup = login_modal.view().map(Message::LoginModal); modal(ui, signup, None) } } } fn theme(&self) -> Theme { 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), // }, // ))) } } fn modal<'a, Message>( base: impl Into>, content: impl Into>, on_blur: Option, ) -> Element<'a, Message> where Message: Clone + 'a, { let mut mouse_area = mouse_area(center(opaque(content)).style(|_theme| { container::Style { background: Some( Color { a: 0.8, ..Color::BLACK } .into(), ), ..container::Style::default() } })); // .on_press(on_blur) if let Some(on_blur) = on_blur { mouse_area = mouse_area.on_press(on_blur) } stack![base.into(), opaque(mouse_area)].into() } fn chat_list_item<'a>( chat: &'a Chat, latest_message: &'a Option, open: bool, ) -> Element<'a, Message> { let mut content: Column = column![text(chat.correspondent.to_string())]; if let Some(latest_message) = latest_message { let message = latest_message.body.body.replace("\n", " "); let date = latest_message.timestamp.naive_local(); let now = Local::now().naive_local(); let timeinfo; if date.date() == now.date() { // TODO: localisation/config timeinfo = text(date.time().format("%H:%M").to_string()) } else { timeinfo = text(date.date().format("%d/%m").to_string()) } content = content.push( row![ container(text(message).wrapping(Wrapping::None)) .clip(true) .width(Fill), timeinfo ] .spacing(8) .width(Fill), ); } let mut button = button(content).on_press(Message::ToggleChat(chat.correspondent.clone())); if open { button = button.style(|theme: &Theme, status| { let palette = theme.extended_palette(); button::Style::default().with_background(palette.primary.weak.color) }); } button.width(Fill).into() }