From acd5bb125346980bc47643431f6a999cb0c62285 Mon Sep 17 00:00:00 2001 From: TicClick Date: Thu, 10 Oct 2024 23:48:13 +0200 Subject: [PATCH] Log system events and improve their handling (#138) --- crates/steel_core/src/chat/mod.rs | 11 +- crates/steel_core/src/ipc/client.rs | 16 +- crates/steel_core/src/ipc/server.rs | 5 +- crates/steel_core/src/ipc/ui.rs | 15 +- crates/steel_core/src/settings/journal.rs | 15 +- src/app/date_announcer.rs | 40 +++++ src/app/mod.rs | 210 ++++++++++++++-------- src/core/irc/event_handler.rs | 19 +- src/core/logging.rs | 68 ++++++- src/gui/chat.rs | 13 +- src/gui/chat_tabs.rs | 17 +- src/gui/command.rs | 11 +- src/gui/menu.rs | 6 +- src/gui/settings/journal.rs | 52 ++++-- src/gui/state/mod.rs | 74 +++----- src/gui/window.rs | 103 +---------- visual-tests/src/main.rs | 11 +- 17 files changed, 363 insertions(+), 323 deletions(-) create mode 100644 src/app/date_announcer.rs diff --git a/crates/steel_core/src/chat/mod.rs b/crates/steel_core/src/chat/mod.rs index d725db7..d83fa62 100644 --- a/crates/steel_core/src/chat/mod.rs +++ b/crates/steel_core/src/chat/mod.rs @@ -143,7 +143,7 @@ impl Message { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub enum ChatState { #[default] Left, @@ -159,9 +159,9 @@ pub struct Chat { } impl Chat { - pub fn new(name: String) -> Self { + pub fn new(name: &str) -> Self { Self { - name, + name: name.to_owned(), messages: Vec::new(), state: ChatState::Left, } @@ -175,11 +175,8 @@ impl Chat { self.name.chat_type() } - pub fn set_state(&mut self, state: ChatState, reason: Option<&str>) { + pub fn set_state(&mut self, state: ChatState) { self.state = state; - if let Some(reason) = reason { - self.push(Message::new_system(reason)); - } } } diff --git a/crates/steel_core/src/ipc/client.rs b/crates/steel_core/src/ipc/client.rs index 02dd491..c3dd0a8 100644 --- a/crates/steel_core/src/ipc/client.rs +++ b/crates/steel_core/src/ipc/client.rs @@ -16,21 +16,9 @@ impl CoreClient { } impl CoreClient { - pub fn channel_opened(&self, channel: &str) { + pub fn chat_opened(&self, chat: &str) { self.server - .send(AppMessageIn::UIChannelOpened(channel.to_owned())) - .unwrap(); - } - - pub fn private_chat_opened(&self, chat: &str) { - self.server - .send(AppMessageIn::UIPrivateChatOpened(chat.to_owned())) - .unwrap(); - } - - pub fn channel_join_requested(&self, channel: &str) { - self.server - .send(AppMessageIn::UIChannelJoinRequested(channel.to_owned())) + .send(AppMessageIn::UIChatOpened(chat.to_owned())) .unwrap(); } diff --git a/crates/steel_core/src/ipc/server.rs b/crates/steel_core/src/ipc/server.rs index d14490e..8fc0eca 100644 --- a/crates/steel_core/src/ipc/server.rs +++ b/crates/steel_core/src/ipc/server.rs @@ -11,13 +11,12 @@ pub enum AppMessageIn { ChatMessageReceived { target: String, message: Message }, ServerMessageReceived { content: String }, ChannelJoined(String), + DateChanged(chrono::DateTime, String), UIConnectRequested, UIDisconnectRequested, UIExitRequested, - UIChannelOpened(String), - UIChannelJoinRequested(String), - UIPrivateChatOpened(String), + UIChatOpened(String), UIChatClosed(String), UIChatCleared(String), UIChatSwitchRequested(String, Option), diff --git a/crates/steel_core/src/ipc/ui.rs b/crates/steel_core/src/ipc/ui.rs index 1f2b849..81d1d91 100644 --- a/crates/steel_core/src/ipc/ui.rs +++ b/crates/steel_core/src/ipc/ui.rs @@ -7,19 +7,12 @@ use crate::settings::Settings; pub enum UIMessageIn { SettingsChanged(Settings), ConnectionStatusChanged(ConnectionStatus), - NewMessageReceived { - target: String, - message: Message, - }, + NewSystemMessage { target: String, message: Message }, + NewMessageReceived { target: String, message: Message }, NewServerMessageReceived(String), - NewChatStatusReceived { - target: String, - state: ChatState, - details: String, - }, - NewChatRequested(String, ChatState, bool), + NewChatStateReceived { target: String, state: ChatState }, + NewChatRequested { target: String, switch: bool }, ChatSwitchRequested(String, Option), - ChannelJoined(String), ChatClosed(String), ChatCleared(String), ChatModeratorAdded(String), diff --git a/crates/steel_core/src/settings/journal.rs b/crates/steel_core/src/settings/journal.rs index d86b1d3..a4cca65 100644 --- a/crates/steel_core/src/settings/journal.rs +++ b/crates/steel_core/src/settings/journal.rs @@ -1,5 +1,9 @@ use serde::{Deserialize, Serialize}; +use crate::DEFAULT_DATETIME_FORMAT; + +pub const DEFAULT_LOG_DIRECTORY: &str = "./chat-logs"; + #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(default)] pub struct Journal { @@ -41,16 +45,19 @@ pub struct ChatEvents { pub enabled: bool, pub directory: String, pub format: String, - pub with_system_events: bool, + pub log_system_events: bool, } impl Default for ChatEvents { fn default() -> Self { Self { enabled: true, - directory: "./chat-logs".to_owned(), - format: "[{date:%Y-%m-%d %H:%M:%S}] <{username}> {text}".to_owned(), - with_system_events: true, + directory: DEFAULT_LOG_DIRECTORY.to_owned(), + format: format!( + "{{date:{}}} <{{username}}> {{text}}", + DEFAULT_DATETIME_FORMAT + ), + log_system_events: true, } } } diff --git a/src/app/date_announcer.rs b/src/app/date_announcer.rs new file mode 100644 index 0000000..db0dc91 --- /dev/null +++ b/src/app/date_announcer.rs @@ -0,0 +1,40 @@ +use std::time::Duration; + +use chrono::DurationRound; +use steel_core::{ipc::server::AppMessageIn, DEFAULT_DATE_FORMAT}; +use tokio::sync::mpsc::UnboundedSender; + +use crate::actor::ActorHandle; + +pub(super) struct DateAnnouncer { + _thread: std::thread::JoinHandle<()>, +} + +impl ActorHandle for DateAnnouncer {} + +impl DateAnnouncer { + pub(super) fn new(app_queue: UnboundedSender) -> Self { + Self { + _thread: std::thread::spawn(|| announcer(app_queue)), + } + } +} + +fn announcer(app_queue: UnboundedSender) { + let mut then = chrono::Local::now(); + loop { + let now = chrono::Local::now(); + if then.date_naive() < now.date_naive() { + then = now; + let round_date = now.duration_trunc(chrono::Duration::days(1)).unwrap(); + let message = format!( + "A new day is born ({})", + round_date.format(DEFAULT_DATE_FORMAT), + ); + app_queue + .send(AppMessageIn::DateChanged(round_date, message)) + .unwrap(); + } + std::thread::sleep(Duration::from_millis(100)); + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 050fbf5..0015ab7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,5 +1,6 @@ use std::collections::BTreeSet; +use date_announcer::DateAnnouncer; use steel_core::ipc::updater::UpdateState; use steel_core::settings::application::AutoUpdate; use steel_core::settings::Loadable; @@ -14,6 +15,8 @@ use crate::core::updater::Updater; use crate::core::{settings, updater}; use steel_core::ipc::{server::AppMessageIn, ui::UIMessageIn}; +pub mod date_announcer; + const DEFAULT_SETTINGS_PATH: &str = "settings.yaml"; #[derive(Clone, Default)] @@ -30,6 +33,7 @@ pub struct Application { irc: IRCActorHandle, chat_logger: Option, updater: Option, + _date_announcer: DateAnnouncer, ui_queue: UnboundedSender, pub app_queue: UnboundedSender, } @@ -41,6 +45,7 @@ impl Application { state: ApplicationState::default(), events, updater: None, + _date_announcer: DateAnnouncer::new(app_queue.clone()), irc: IRCActorHandle::new(app_queue.clone()), chat_logger: None, ui_queue, @@ -51,11 +56,16 @@ impl Application { pub fn run(&mut self) { while let Some(event) = self.events.blocking_recv() { match event { + AppMessageIn::DateChanged(_date, message) => { + for chat in self.state.chats.clone() { + self.send_system_message(&chat, &message); + } + } AppMessageIn::ConnectionChanged(status) => { self.handle_connection_status(status); } AppMessageIn::ChatMessageReceived { target, message } => { - self.handle_chat_message(target, message, false); + self.handle_chat_message(&target, message); } AppMessageIn::ServerMessageReceived { content } => { self.handle_server_message(content); @@ -79,15 +89,13 @@ impl Application { AppMessageIn::UIExitRequested => { break; } - AppMessageIn::UIChannelOpened(target) - | AppMessageIn::UIPrivateChatOpened(target) => { - self.maybe_remember_chat(&target, true); + + AppMessageIn::UIChatOpened(target) => { + self.ui_handle_chat_opened(&target); } + AppMessageIn::UIChatSwitchRequested(target, id) => { - self.ui_handle_chat_switch_requested(target, id); - } - AppMessageIn::UIChannelJoinRequested(channel) => { - self.handle_ui_channel_join_requested(channel); + self.ui_handle_chat_switch_requested(&target, id); } AppMessageIn::UIChatClosed(target) => { self.ui_handle_close_chat(&target); @@ -132,14 +140,12 @@ impl Application { } impl Application { - pub fn handle_ui_channel_join_requested(&mut self, channel: String) { - self.maybe_remember_chat(&channel, true); - self.join_channel(&channel); - } - - pub fn ui_handle_chat_switch_requested(&self, chat: String, message_id: Option) { + pub fn ui_handle_chat_switch_requested(&self, chat: &str, message_id: Option) { self.ui_queue - .send(UIMessageIn::ChatSwitchRequested(chat, message_id)) + .send(UIMessageIn::ChatSwitchRequested( + chat.to_owned(), + message_id, + )) .unwrap(); } @@ -223,7 +229,7 @@ impl Application { } } - if let Some(chat_logger) = &self.chat_logger { + if let Some(chat_logger) = &mut self.chat_logger { if old_settings.chat_events.directory != new_settings.chat_events.directory { chat_logger.change_logging_directory(new_settings.chat_events.directory.clone()); } @@ -231,6 +237,12 @@ impl Application { if old_settings.chat_events.format != new_settings.chat_events.format { chat_logger.change_log_format(new_settings.chat_events.format.clone()); } + + if old_settings.chat_events.log_system_events + != new_settings.chat_events.log_system_events + { + chat_logger.log_system_messages(new_settings.chat_events.log_system_events); + } } } @@ -267,21 +279,28 @@ impl Application { log::debug!("IRC connection status changed to {:?}", status); match status { ConnectionStatus::Connected => { - let chats = self.state.settings.chat.autojoin.clone(); - let connected_to: Vec = self.state.chats.iter().cloned().collect(); - for cs in [chats, connected_to] { - for chat in cs { - self.maybe_remember_chat(&chat, false); - if chat.is_channel() { - self.join_channel(&chat); - } else { - self.push_chat_to_ui(&chat, false); - } - } + for chat in self.state.chats.clone() { + self.rejoin_chat(&chat); + } + + let wanted_chats = self + .state + .settings + .chat + .autojoin + .iter() + .filter(|ch| !self.state.chats.contains(&ch.to_lowercase())); + for chat in wanted_chats.cloned().collect::>() { + self.save_chat(&chat); + self.ui_add_chat(&chat, false); + self.rejoin_chat(&chat); } } ConnectionStatus::InProgress | ConnectionStatus::Scheduled(_) => (), ConnectionStatus::Disconnected { by_user } => { + for chat in self.state.chats.iter().cloned().collect::>() { + self.change_chat_state(&chat, ChatState::Left); + } if self.state.settings.chat.reconnect && !by_user { self.queue_reconnect(); } @@ -289,6 +308,18 @@ impl Application { } } + fn rejoin_chat(&mut self, chat: &str) { + match chat.is_channel() { + true => { + self.change_chat_state(chat, ChatState::JoinInProgress); + self.join_channel(chat); + } + false => { + self.change_chat_state(chat, ChatState::Joined); + } + } + } + fn queue_reconnect(&self) { let queue = self.app_queue.clone(); let delta = chrono::Duration::seconds(15); @@ -307,45 +338,56 @@ impl Application { }); } - fn push_chat_to_ui(&self, target: &str, switch: bool) { - let chat_state = if target.is_channel() { - ChatState::JoinInProgress - } else { - ChatState::Joined - }; - self.ui_queue - .send(UIMessageIn::NewChatRequested( - target.to_owned(), - chat_state, - switch, - )) - .unwrap(); - } + pub fn handle_chat_message(&mut self, target: &str, message: Message) { + if !self.state.chats.contains(&target.to_lowercase()) { + self.save_chat(target); + self.ui_add_chat(target, false); + } - pub fn handle_chat_message( - &mut self, - target: String, - message: Message, - switch_if_missing: bool, - ) { if let Some(chat_logger) = &self.chat_logger { - chat_logger.log(target.clone(), message.clone()); + chat_logger.log(target, &message); } - self.maybe_remember_chat(&target, switch_if_missing); self.ui_queue - .send(UIMessageIn::NewMessageReceived { target, message }) + .send(UIMessageIn::NewMessageReceived { + target: target.to_owned(), + message, + }) .unwrap(); } - fn maybe_remember_chat(&mut self, target: &str, switch_if_missing: bool) { - let normalized = target.to_lowercase(); - if !self.state.chats.contains(&normalized) { - self.state.chats.insert(normalized); - self.push_chat_to_ui(target, switch_if_missing); + fn ui_handle_chat_opened(&mut self, target: &str) { + if !self.state.chats.contains(&target.to_lowercase()) { + self.save_chat(target); + self.ui_add_chat(target, true); + } + self.ui_handle_chat_switch_requested(target, None); + + match target.is_channel() { + true => { + self.change_chat_state(target, ChatState::JoinInProgress); + self.join_channel(target); + } + false => { + self.change_chat_state(target, ChatState::Joined); + } } } + fn save_chat(&mut self, target: &str) { + let normalized = target.to_lowercase(); + self.state.chats.insert(normalized); + } + + fn ui_add_chat(&self, target: &str, switch: bool) { + self.ui_queue + .send(UIMessageIn::NewChatRequested { + target: target.to_owned(), + switch, + }) + .unwrap(); + } + pub fn handle_server_message(&mut self, content: String) { log::debug!("IRC server message: {}", content); self.ui_queue @@ -368,22 +410,48 @@ impl Application { { let normalized = chat.to_lowercase(); self.state.chats.remove(&normalized); - self.ui_queue - .send(UIMessageIn::NewChatStatusReceived { - target: chat, - state: ChatState::Left, - details: content, - }) - .unwrap(); + self.change_chat_state(&chat, ChatState::Left); + self.send_system_message(&chat, &content); } self.ui_queue .send(UIMessageIn::NewServerMessageReceived(error_text)) .unwrap(); } - pub fn handle_channel_join(&mut self, channel: String) { + fn change_chat_state(&mut self, chat: &str, state: ChatState) { + self.ui_queue + .send(UIMessageIn::NewChatStateReceived { + target: chat.to_owned(), + state: state.clone(), + }) + .unwrap(); + + match state { + ChatState::Left => { + self.send_system_message(chat, "You have left the chat (disconnected)") + } + ChatState::JoinInProgress => self.send_system_message(chat, "Joining the chat..."), + ChatState::Joined => match chat.is_channel() { + true => self.send_system_message(chat, "You have joined the chat"), + false => self.send_system_message(chat, "You have opened the chat"), + }, + } + } + + fn handle_channel_join(&mut self, channel: String) { + self.change_chat_state(&channel, ChatState::Joined); + } + + fn send_system_message(&mut self, target: &str, text: &str) { + let message = Message::new_system(text); + if let Some(chat_logger) = &self.chat_logger { + chat_logger.log(target, &message); + } self.ui_queue - .send(UIMessageIn::ChannelJoined(channel)) + .send(UIMessageIn::NewSystemMessage { + target: target.to_owned(), + message, + }) .unwrap(); } @@ -436,23 +504,13 @@ impl Application { pub fn send_text_message(&mut self, target: &str, text: &str) { self.irc.send_message(target, text); let message = Message::new_text(&self.state.settings.chat.irc.username, text); - self.ui_queue - .send(UIMessageIn::NewMessageReceived { - target: target.to_owned(), - message, - }) - .unwrap(); + self.handle_chat_message(target, message); } pub fn send_action(&mut self, target: &str, text: &str) { self.irc.send_action(target, text); let message = Message::new_action(&self.state.settings.chat.irc.username, text); - self.ui_queue - .send(UIMessageIn::NewMessageReceived { - target: target.to_owned(), - message, - }) - .unwrap(); + self.handle_chat_message(target, message); } pub fn join_channel(&self, channel: &str) { diff --git a/src/core/irc/event_handler.rs b/src/core/irc/event_handler.rs index 98a9f0b..f8b2913 100644 --- a/src/core/irc/event_handler.rs +++ b/src/core/irc/event_handler.rs @@ -8,19 +8,22 @@ use steel_core::chat::{Message, MessageType}; use steel_core::ipc::server::AppMessageIn; static ACTION_PREFIX: &str = "\x01ACTION"; +static ACTION_SUFFIX: &str = "\x01"; pub fn empty_handler(_sender: &UnboundedSender, _msg: irc::proto::Message) {} pub fn privmsg_handler(sender: &UnboundedSender, msg: irc::proto::Message) { if let irc::proto::Command::PRIVMSG(_, ref text) = msg.command { - let (message_type, text) = if text.starts_with(ACTION_PREFIX) { - ( - MessageType::Action, - text.strip_prefix(ACTION_PREFIX).unwrap().trim(), - ) - } else { - (MessageType::Text, text.as_str()) - }; + let (message_type, text) = + if let Some(text_without_prefix) = text.strip_prefix(ACTION_PREFIX) { + let action_text = match text_without_prefix.strip_suffix(ACTION_SUFFIX) { + Some(clean_action) => clean_action, + None => text_without_prefix, + }; + (MessageType::Action, action_text.trim()) + } else { + (MessageType::Text, text.as_str()) + }; let message_target = match msg.response_target() { Some(target) => target.to_owned(), diff --git a/src/core/logging.rs b/src/core/logging.rs index 94a6029..141445e 100644 --- a/src/core/logging.rs +++ b/src/core/logging.rs @@ -4,7 +4,8 @@ use std::fs::File; use std::io::Write as IOWrite; use std::path::{Path, PathBuf}; -use steel_core::chat::Message; +use steel_core::chat::{Message, MessageType}; +use steel_core::DEFAULT_DATETIME_FORMAT; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use crate::actor::ActorHandle; @@ -19,6 +20,7 @@ pub enum LoggingRequest { pub struct ChatLoggerHandle { channel: UnboundedSender, + log_system_messages: bool, } impl ActorHandle for ChatLoggerHandle {} @@ -30,13 +32,25 @@ impl ChatLoggerHandle { std::thread::spawn(move || { actor.run(); }); - Self { channel: tx } + Self { + channel: tx, + log_system_messages: true, + } } - pub fn log(&self, chat_name: String, message: Message) { - let _ = self - .channel - .send(LoggingRequest::LogMessage { chat_name, message }); + pub fn log_system_messages(&mut self, log: bool) { + self.log_system_messages = log; + } + + pub fn log(&self, chat_name: &str, message: &Message) { + if matches!(message.r#type, MessageType::System) && !self.log_system_messages { + return; + } + + let _ = self.channel.send(LoggingRequest::LogMessage { + chat_name: chat_name.to_owned(), + message: message.to_owned(), + }); } pub fn close_log(&self, chat_name: String) { @@ -62,7 +76,11 @@ impl ChatLoggerHandle { struct ChatLoggerBackend { log_directory: PathBuf, + log_line_format: String, + log_system_line_format: String, + log_action_line_format: String, + channel: UnboundedReceiver, files: HashMap, } @@ -74,8 +92,12 @@ impl ChatLoggerBackend { channel: UnboundedReceiver, ) -> Self { Self { - log_directory: Path::new(&log_directory).to_path_buf(), + log_directory: Path::new(log_directory).to_path_buf(), + log_line_format: log_line_format.to_owned(), + log_system_line_format: to_log_system_line_format(log_line_format), + log_action_line_format: to_log_action_line_format(log_line_format), + channel, files: HashMap::new(), } @@ -90,6 +112,8 @@ impl ChatLoggerBackend { } } LoggingRequest::ChangeLogFormat { log_line_format } => { + self.log_system_line_format = to_log_system_line_format(&log_line_format); + self.log_action_line_format = to_log_action_line_format(&log_line_format); self.log_line_format = log_line_format; } LoggingRequest::ChangeLoggingDirectory { logging_directory } => { @@ -158,7 +182,13 @@ impl ChatLoggerBackend { } } - let formatted_message = format_message_for_logging(&self.log_line_format, &message); + let log_line_format = match message.r#type { + MessageType::System => &self.log_system_line_format, + MessageType::Text => &self.log_line_format, + MessageType::Action => &self.log_action_line_format, + }; + + let formatted_message = format_message_for_logging(log_line_format, &message); if let Err(e) = writeln!(&mut f, "{}", formatted_message) { log::error!("Failed to append a chat log line for {}: {}", chat_name, e); return Err(e); @@ -222,3 +252,25 @@ fn resolve_placeholder(placeholder: &str, message: &Message) -> String { } } } + +fn get_or_default_date_format(log_line_format: &str) -> String { + if let Some(start_pos) = log_line_format.find("{date:") { + if let Some(pos) = &log_line_format[start_pos..].find('}') { + let end_pos = start_pos + *pos; + let date_format = log_line_format[start_pos..end_pos + 1].to_owned(); + return date_format; + } + } + format!("{{date:{}}}", DEFAULT_DATETIME_FORMAT) +} + +pub fn to_log_system_line_format(log_line_format: &str) -> String { + format!("{} * {{text}}", get_or_default_date_format(log_line_format)) +} + +pub fn to_log_action_line_format(log_line_format: &str) -> String { + format!( + "{} * {{username}} {{text}}", + get_or_default_date_format(log_line_format) + ) +} diff --git a/src/gui/chat.rs b/src/gui/chat.rs index 3197b5c..198a91c 100644 --- a/src/gui/chat.rs +++ b/src/gui/chat.rs @@ -503,7 +503,7 @@ fn show_datetime( #[allow(unused_variables)] // glass fn show_username_menu(ui: &mut egui::Ui, state: &UIState, chat_name: &str, message: &Message) { if state.is_connected() && ui.button("💬 Open chat").clicked() { - state.core.private_chat_opened(&message.username); + state.core.chat_opened(&message.username); ui.close_menu(); } @@ -649,14 +649,7 @@ fn format_chat_message_text( .core .chat_switch_requested(chat, None); } else { - match chat.is_channel() { - true => state - .core - .channel_join_requested(chat), - false => { - state.core.private_chat_opened(chat) - } - } + state.core.chat_opened(chat) } } } @@ -746,7 +739,7 @@ fn format_chat_message_text( if state.has_chat(location) { state.core.chat_switch_requested(location, None); } else { - state.core.channel_join_requested(location); + state.core.chat_opened(location); } } } diff --git a/src/gui/chat_tabs.rs b/src/gui/chat_tabs.rs index d885535..e7b647b 100644 --- a/src/gui/chat_tabs.rs +++ b/src/gui/chat_tabs.rs @@ -203,17 +203,12 @@ impl ChatTabs { if add_chat && validation_result.is_ok() { // Whether the channel is valid or not is determined by the server (will send us a message), // but for now let's add it to the interface. - match mode { - ChatType::Channel => { - let channel_name = match input.is_channel() { - true => input.clone(), - false => format!("#{}", input), - }; - state.core.channel_opened(&channel_name); - state.core.channel_join_requested(&channel_name); - } - ChatType::Person => state.core.private_chat_opened(input), - } + let target = if matches!(mode, ChatType::Channel) && !input.is_channel() { + format!("#{}", input) + } else { + input.to_owned() + }; + state.core.chat_opened(&target); input.clear(); response.request_focus(); } diff --git a/src/gui/command.rs b/src/gui/command.rs index e5a5456..517cab5 100644 --- a/src/gui/command.rs +++ b/src/gui/command.rs @@ -110,7 +110,7 @@ impl Command for OpenChat { egui::RichText::new("/chat ") } fn action(&self, state: &UIState, args: Vec) { - state.core.private_chat_opened(&args[0]); + state.core.chat_opened(&args[0]); } } @@ -140,11 +140,12 @@ impl Command for JoinChannel { egui::RichText::new("/join <#channel>") } fn action(&self, state: &UIState, args: Vec) { - if args[0].is_channel() { - state.core.channel_join_requested(&args[0]); + let target = if args[0].is_channel() { + args[0].to_owned() } else { - state.core.channel_join_requested(&format!("#{}", &args[0])); - } + format!("#{}", &args[0]) + }; + state.core.chat_opened(&target); } } diff --git a/src/gui/menu.rs b/src/gui/menu.rs index 412dd3a..2ed986a 100644 --- a/src/gui/menu.rs +++ b/src/gui/menu.rs @@ -162,14 +162,14 @@ impl Menu { ui.separator(); ui.menu_button("open", |ui| { - if ui.button("app location").on_hover_text_at_pointer( - "open the directory where the app is located" + if ui.button("runtime directory").on_hover_text_at_pointer( + "open the directory where the application is located" ).clicked() { crate::core::os::open_own_directory(); ui.close_menu(); } - if ui.button("log file").on_hover_text_at_pointer( + if ui.button("runtime log").on_hover_text_at_pointer( "open text journal with debug messages and errors -- may or may not help with debugging" ).clicked() { crate::core::os::open_runtime_log(); diff --git a/src/gui/settings/journal.rs b/src/gui/settings/journal.rs index 5db327b..00b2cf9 100644 --- a/src/gui/settings/journal.rs +++ b/src/gui/settings/journal.rs @@ -1,9 +1,14 @@ -use eframe::egui::RichText; +use eframe::egui::{self, RichText}; use steel_core::chat; use super::SettingsWindow; use crate::{ - core::{self, logging::format_message_for_logging}, + core::{ + self, + logging::{ + format_message_for_logging, to_log_action_line_format, to_log_system_line_format, + }, + }, gui::state::UIState, }; @@ -15,6 +20,10 @@ impl SettingsWindow { &mut state.settings.journal.chat_events.enabled, "enable chat logging", ); + ui.checkbox( + &mut state.settings.journal.chat_events.log_system_events, + "log system events", + ); ui.horizontal(|ui| { ui.label("directory with logs"); @@ -25,7 +34,7 @@ impl SettingsWindow { && std::path::Path::new(&state.settings.journal.chat_events.directory).exists() { core::os::open_external_directory( - &mut state.settings.journal.chat_events.directory, + &state.settings.journal.chat_events.directory, ); } }); @@ -34,15 +43,10 @@ impl SettingsWindow { ui.text_edit_multiline(&mut state.settings.journal.chat_events.format); ui.horizontal_wrapped(|ui| { - ui.label(RichText::new("preview: →").color(ui.visuals().warn_fg_color)); - let message = - chat::Message::new_text("WilliamGibson", "I think I left my cyberdeck on"); - let formatted_message = format_message_for_logging( - &state.settings.journal.chat_events.format, - &message, - ); - ui.label(formatted_message); - ui.label(RichText::new("←").color(ui.visuals().warn_fg_color)); + let mut example_chat_log = + make_example_chat_log(&state.settings.journal.chat_events.format); + ui.add_enabled(false, egui::TextEdit::multiline(&mut example_chat_log)); + // ui.label(RichText::new("←").color(ui.visuals().warn_fg_color)); }); ui.collapsing("click to show help", |ui| { @@ -82,3 +86,27 @@ impl SettingsWindow { // TODO(logging): Add a setting for logging system events. } } + +fn make_example_chat_log(message_format: &str) -> String { + let action_message_format = to_log_action_line_format(message_format); + let system_message_format = to_log_system_line_format(message_format); + let chat_log = vec![ + ( + chat::Message::new_system("You have joined #sprawl"), + system_message_format.as_str(), + ), + ( + chat::Message::new_text("WilliamGibson", "I think I left my cyberdeck on"), + message_format, + ), + ( + chat::Message::new_action("WilliamGibson", "runs away"), + action_message_format.as_str(), + ), + ]; + chat_log + .iter() + .map(|(message, line_format)| format_message_for_logging(line_format, message)) + .collect::>() + .join("\n") +} diff --git a/src/gui/state/mod.rs b/src/gui/state/mod.rs index 552ec74..c78b68e 100644 --- a/src/gui/state/mod.rs +++ b/src/gui/state/mod.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use steel_core::chat::{Chat, ChatLike, ChatState, ConnectionStatus, Message}; +use steel_core::chat::{Chat, ChatLike, ChatState, ConnectionStatus, Message, MessageType}; use steel_core::ipc::updater::UpdateState; use steel_core::ipc::{client::CoreClient, server::AppMessageIn}; @@ -116,16 +116,13 @@ impl UIState { matches!(self.connection, ConnectionStatus::Connected) } - pub fn add_new_chat(&mut self, name: String, state: ChatState, switch_to_chat: bool) { - let mut chat = Chat::new(name); - chat.state = state; - + pub fn add_new_chat(&mut self, name: String, switch_to_chat: bool) { + let chat = Chat::new(&name); let normalized = chat.name.to_lowercase(); - self.name_to_chat - .insert(normalized.to_owned(), self.chats.len()); + self.name_to_chat.insert(normalized, self.chats.len()); self.chats.push(chat); if switch_to_chat { - self.active_chat_tab_name = normalized; + self.active_chat_tab_name = name; } } @@ -145,11 +142,13 @@ impl UIState { self.active_chat_tab_name == target } - pub fn set_chat_state(&mut self, target: &str, state: ChatState, reason: Option<&str>) { + pub fn set_chat_state(&mut self, target: &str, state: ChatState) { let normalized = target.to_lowercase(); if let Some(pos) = self.name_to_chat.get(&normalized) { if let Some(ch) = self.chats.get_mut(*pos) { - ch.set_state(state, reason); + if ch.state != state { + ch.set_state(state); + } } } } @@ -161,18 +160,12 @@ impl UIState { ctx: &egui::Context, ) -> bool { let normalized = target.to_lowercase(); - let tab_inactive = !self.is_active_tab(&normalized); - + let is_tab_inactive = !self.is_active_tab(&normalized); + let is_system_message = matches!(message.r#type, MessageType::System); let mut name_updated = false; if let Some(pos) = self.name_to_chat.get(&normalized) { if let Some(ch) = self.chats.get_mut(*pos) { - // If the chat was open with an improper case, fix it! - if ch.name != target { - ch.name = target; - name_updated = true; - } - message.id = Some(ch.messages.len()); message.parse_for_links(); @@ -185,10 +178,19 @@ impl UIState { { current_username = None; } + + // If the chat was open with an improper case, fix it! + if ch.name != target && !is_system_message { + ch.name = target; + name_updated = true; + } + message.detect_highlights(self.highlights.keywords(), current_username); - let highlight = message.highlight; - if highlight { + let contains_highlight = message.highlight; + let requires_attention = contains_highlight || !normalized.is_channel(); + + if contains_highlight { self.highlights.add(&normalized, &message); if self.active_chat_tab_name != HIGHLIGHTS_TAB_NAME { self.highlights.mark_as_unread(HIGHLIGHTS_TAB_NAME); @@ -196,8 +198,7 @@ impl UIState { } ch.push(message); - let requires_attention = highlight || !normalized.is_channel(); - if tab_inactive { + if is_tab_inactive && !is_system_message { if requires_attention { self.highlights.mark_as_highlighted(&normalized); } else { @@ -278,33 +279,4 @@ impl UIState { pub fn has_chat(&self, target: &str) -> bool { self.name_to_chat.contains_key(&target.to_lowercase()) } - - pub fn push_to_all_chats(&mut self, message: Message) { - for chat in self.chats.iter_mut() { - chat.push(message.clone()); - } - } - - pub fn mark_all_as_disconnected(&mut self) { - let open_chats: Vec = self.chats.iter().map(|ch| ch.name.clone()).collect(); - for chat_name in open_chats { - self.set_chat_state( - &chat_name, - ChatState::Left, - Some("You have left the chat (disconnected)"), - ); - } - } - - pub fn mark_all_as_connected(&mut self) { - let open_chats: Vec = self.chats.iter().map(|ch| ch.name.clone()).collect(); - for chat_name in open_chats { - let (new_state, reason) = match chat_name.is_channel() { - // Joins are handled by the app server - true => (ChatState::JoinInProgress, None), - false => (ChatState::Joined, Some("You are online")), - }; - self.set_chat_state(&chat_name, new_state, reason); - } - } } diff --git a/src/gui/window.rs b/src/gui/window.rs index 38bc3de..1fdaf41 100644 --- a/src/gui/window.rs +++ b/src/gui/window.rs @@ -1,12 +1,9 @@ -use chrono::{DurationRound, Timelike}; use eframe::egui; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -use crate::core::chat::ChatState; use crate::gui; use crate::gui::state::UIState; -use steel_core::chat::{ConnectionStatus, Message}; use steel_core::ipc::{server::AppMessageIn, ui::UIMessageIn}; use crate::core::settings; @@ -89,43 +86,6 @@ fn setup_custom_fonts(ctx: &egui::Context) { ctx.set_fonts(fonts); } -struct DateAnnouncer { - pub prev_event: Option>, - pub current_event: chrono::DateTime, -} - -impl Default for DateAnnouncer { - fn default() -> Self { - let now = chrono::Local::now(); - Self { - prev_event: None, - current_event: now, - } - } -} - -impl DateAnnouncer { - fn should_announce(&self) -> bool { - match self.prev_event { - None => { - self.current_event.hour() == 0 - && self.current_event.minute() == 0 - && self.current_event.second() == 0 - } - Some(dt) => { - dt.date_naive() < self.current_event.date_naive() - && self.current_event.hour() == 0 - && self.current_event.minute() == 0 - } - } - } - - fn refresh(&mut self) { - self.prev_event = Some(self.current_event); - self.current_event = chrono::Local::now(); - } -} - pub struct ApplicationWindow { menu: gui::menu::Menu, chat: gui::chat::ChatWindow, @@ -137,7 +97,6 @@ pub struct ApplicationWindow { ui_queue: UnboundedReceiver, s: UIState, - date_announcer: DateAnnouncer, filter_ui: gui::filter::FilterWindow, } @@ -159,7 +118,6 @@ impl ApplicationWindow { usage_window: gui::usage::UsageWindow::default(), ui_queue, s: UIState::new(app_queue_handle), - date_announcer: DateAnnouncer::default(), filter_ui: gui::filter::FilterWindow::default(), } } @@ -179,26 +137,6 @@ impl ApplicationWindow { } pub fn process_pending_events(&mut self, ctx: &egui::Context) { - if self.date_announcer.should_announce() { - let text = format!( - "A new day is born ({})", - self.date_announcer - .current_event - .date_naive() - .format(crate::core::DEFAULT_DATE_FORMAT) - ); - self.s.push_to_all_chats( - Message::new_system(&text).with_time( - self.date_announcer - .current_event - .duration_trunc(chrono::Duration::days(1)) - .unwrap(), - ), - ); - ctx.request_repaint(); - } - self.date_announcer.refresh(); - // If the main window is restored after having being minimized for some time, it still needs to be responsive // enough. let mut i = 0; @@ -221,42 +159,27 @@ impl ApplicationWindow { fn dispatch_event(&mut self, event: UIMessageIn, ctx: &egui::Context) { match event { + UIMessageIn::NewSystemMessage { target, message } => { + self.s.push_chat_message(target, message, ctx); + } + UIMessageIn::SettingsChanged(settings) => { self.s.set_settings(ctx, settings); } UIMessageIn::ConnectionStatusChanged(conn) => { self.s.connection = conn; - match conn { - ConnectionStatus::Disconnected { .. } => { - self.s.mark_all_as_disconnected(); - } - ConnectionStatus::InProgress | ConnectionStatus::Scheduled(_) => (), - ConnectionStatus::Connected => { - self.s.mark_all_as_connected(); - } - } } - UIMessageIn::NewChatRequested(name, state, switch_to_chat) => { - if self.s.has_chat(&name) { - self.s.set_chat_state(&name, state, None); - } else { - self.s.add_new_chat(name, state, switch_to_chat); - } - if switch_to_chat { + UIMessageIn::NewChatRequested { target, switch } => { + self.s.add_new_chat(target, switch); + if switch { self.refresh_window_title(ctx); } } - UIMessageIn::NewChatStatusReceived { - target, - state, - details, - } => { - if self.s.has_chat(&target) { - self.s.set_chat_state(&target, state, Some(&details)); - } + UIMessageIn::NewChatStateReceived { target, state } => { + self.s.set_chat_state(&target, state); } UIMessageIn::ChatSwitchRequested(name, message_id) => { @@ -271,14 +194,6 @@ impl ApplicationWindow { self.refresh_window_title(ctx); } - UIMessageIn::ChannelJoined(name) => { - self.s.set_chat_state( - &name, - ChatState::Joined, - Some("You have joined the channel"), - ); - } - UIMessageIn::NewMessageReceived { target, message } => { let name_updated = self .s diff --git a/visual-tests/src/main.rs b/visual-tests/src/main.rs index cf9725c..b1d3fef 100644 --- a/visual-tests/src/main.rs +++ b/visual-tests/src/main.rs @@ -1,7 +1,7 @@ use rand::{thread_rng, Rng}; use tokio::sync::mpsc::unbounded_channel; -use steel::core::chat::{ChatState, Message}; +use steel::core::chat::Message; use steel::core::ipc::ui::UIMessageIn; use steel::run_app; @@ -15,11 +15,10 @@ pub fn main() { .unwrap(); ui_queue_in - .send(UIMessageIn::NewChatRequested( - "#test".to_owned(), - ChatState::Joined, - true, - )) + .send(UIMessageIn::NewChatRequested { + target: "#test".to_owned(), + switch: true, + }) .unwrap(); for i in 0..25 {