summaryrefslogtreecommitdiffstats
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs1062
1 files changed, 9 insertions, 1053 deletions
diff --git a/src/main.rs b/src/main.rs
index 37551c8..3db4c25 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,1057 +1,13 @@
-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;
+// SPDX-FileCopyrightText: 2025 cel <cel@bunny.garden>
+//
+// SPDX-License-Identifier: AGPL-3.0-or-later
-use chrono::{Local, Utc};
-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 luz::chat::{Chat, Message as ChatMessage};
-use luz::error::CommandError;
-use luz::presence::{Offline, Presence, PresenceType};
-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};
-use tokio_stream::wrappers::ReceiverStream;
-use tracing::{error, info};
-use uuid::Uuid;
+use leptos::prelude::*;
+use macaw_web::App;
-mod login_modal;
-mod message_view;
+fn main() {
+ tracing_wasm::set_as_global_default();
+ console_error_panic_hook::set_once();
-#[derive(Serialize, Deserialize, Clone)]
-pub struct Config {
- auto_connect: bool,
- storage_dir: Option<String>,
- dburl: Option<String>,
- 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<JID, Contact>,
- users: HashMap<JID, User>,
- presences: HashMap<JID, Presence>,
- chats: IndexMap<JID, (Chat, Option<ChatMessage>)>,
- subscription_requests: HashSet<JID>,
- open_chat: Option<MessageView>,
- new_chat: Option<NewChat>,
-}
-
-pub struct NewChat;
-
-impl Macaw {
- pub fn new(client: Option<Client>, 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: LuzHandle,
- 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 = LuzHandle;
-
- fn deref(&self) -> &Self::Target {
- &self.client
- }
-}
-
-async fn luz(jid: &JID, creds: &Creds, cfg: &Config) -> (LuzHandle, mpsc::Receiver<UpdateMessage>) {
- let luz;
- 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 = luz::db::Db::create_connect_and_migrate(db_path)
- .await
- .unwrap();
- luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- luz = LuzHandle::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 = luz::db::Db::create_connect_and_migrate(data_dir)
- .await
- .unwrap();
- luz = LuzHandle::new(jid.clone(), creds.password.to_string(), db);
- }
- luz
-}
-
-#[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<Error> = None;
- let mut creds: Option<Creds> = 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, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
- if let Some(creds) = creds {
- let jid = creds.jid.parse::<JID>();
- match jid {
- Ok(jid) => {
- let (handle, updates) = luz(&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<Message> {
- Subscription::batch([press_subscription(state), release_subscription(state)])
-}
-
-fn press_subscription(_state: &Macaw) -> Subscription<Message> {
- on_key_press(handle_key_press)
-}
-
-fn handle_key_press(key: Key, r#mod: Modifiers) -> Option<Message> {
- match key {
- Key::Named(iced::keyboard::key::Named::Shift) => Some(Message::ShiftPressed),
- _ => None,
- }
-}
-
-fn release_subscription(_state: &Macaw) -> Subscription<Message> {
- on_key_release(handle_key_release)
-}
-
-fn handle_key_release(key: Key, r#mod: Modifiers) -> Option<Message> {
- 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<JID, Contact>),
- Connect,
- Disconnect,
- GotChats(Vec<(Chat, ChatMessage)>),
- GotMessageHistory(Chat, IndexMap<Uuid, ChatMessage>),
- 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] luz::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<luz::error::DatabaseError>),
-}
-
-#[derive(Debug, Error, Clone)]
-pub enum CredentialsSaveError {
- #[error("keyring: {0}")]
- Keyring(Arc<keyring::Error>),
- #[error("toml serialisation: {0}")]
- Toml(#[from] toml::ser::Error),
-}
-
-impl From<keyring::Error> 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<keyring::Error>),
- #[error("toml serialisation: {0}")]
- Toml(#[from] toml::de::Error),
- #[error("invalid jid: {0}")]
- JID(#[from] jid::ParseError),
-}
-
-impl From<keyring::Error> for CredentialsLoadError {
- fn from(e: keyring::Error) -> Self {
- Self::Keyring(Arc::new(e))
- }
-}
-
-impl Macaw {
- fn update(&mut self, message: Message) -> Task<Message> {
- 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<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = 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<JID, (Chat, IndexMap<Uuid, ChatMessage>)> = 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.send(CommandMessage::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
- .send(CommandMessage::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::<JID>();
- let config = self.config.clone();
- match jid {
- Ok(jid) => {
- Task::perform(async move {
- let (jid, creds, config) = (jid, creds, config);
- let (handle, recv) = luz(&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<Uuid, ChatMessage> = 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, luz::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<Message> {
- let mut ui: Element<Message> = {
- let mut chats_list: Column<Message> = 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<Element<'a, Message>>,
- content: impl Into<Element<'a, Message>>,
- on_blur: Option<Message>,
-) -> 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<ChatMessage>,
- open: bool,
-) -> Element<'a, Message> {
- let mut content: Column<Message> = 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()
+ leptos::mount::mount_to_body(App)
}