From e1c1b43186c902bdd43c1e9a53ce1b412df117dd Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Fri, 23 Feb 2024 14:56:19 -0800 Subject: [PATCH 01/17] Initial topic banner implementation. --- config.yaml | 11 +++ data/src/buffer.rs | 12 +++ data/src/config/buffer.rs | 14 ++-- data/src/history.rs | 5 ++ data/src/history/manager.rs | 141 +++++++++++++++++++++++++--------- data/src/message.rs | 47 +++++++++++- data/src/message/broadcast.rs | 2 + data/src/message/source.rs | 40 +++++++++- src/buffer.rs | 1 + src/buffer/banner_view.rs | 92 ++++++++++++++++++++++ src/buffer/channel.rs | 110 +++++++++++++++++++++----- src/buffer/scroll_view.rs | 20 ++--- src/theme.rs | 38 ++++++++- 13 files changed, 456 insertions(+), 77 deletions(-) create mode 100644 src/buffer/banner_view.rs diff --git a/config.yaml b/config.yaml index 3369f8618..4ddd59ffc 100644 --- a/config.yaml +++ b/config.yaml @@ -86,6 +86,17 @@ buffer: # - Focused: Only show input when the buffer is focused input_visibility: Always + # Topic banner settings: + topic_banner: + # Show topic messages in a topic banner at the topic of a pane instead + # of server messages. Topic changes will still show up as a server + # message. + # - Default is false. + enabled: false + # Maximum height of the topic banner. + # - Default is 42 + max_height: 42 + # Control different server messages. # - exclude [All, None, !Smart seconds]: # - Smart will show a server message if the user has sent a message diff --git a/data/src/buffer.rs b/data/src/buffer.rs index 55e1a8b50..be327b63c 100644 --- a/data/src/buffer.rs +++ b/data/src/buffer.rs @@ -66,6 +66,18 @@ pub enum InputVisibility { Always, } +#[derive(Debug, Clone, Copy, Default, Deserialize)] +pub struct TopicBanner { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_topic_banner_max_height")] + pub max_height: u16, +} + +fn default_topic_banner_max_height() -> u16 { + 42 +} + #[derive(Debug, Clone, Deserialize)] pub struct Timestamp { pub format: String, diff --git a/data/src/config/buffer.rs b/data/src/config/buffer.rs index 523cccb8b..406030332 100644 --- a/data/src/config/buffer.rs +++ b/data/src/config/buffer.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use super::Channel; use crate::{ - buffer::{Color, InputVisibility, Nickname, Timestamp}, + buffer::{Color, InputVisibility, Nickname, Timestamp, TopicBanner}, message::source, }; @@ -19,6 +19,8 @@ pub struct Buffer { pub channel: Channel, #[serde(default)] pub server_messages: ServerMessages, + #[serde(default)] + pub topic_banner: TopicBanner, } #[derive(Debug, Copy, Clone, Default, Deserialize)] @@ -40,11 +42,12 @@ pub struct ServerMessages { } impl ServerMessages { - pub fn get(&self, server: &source::Server) -> ServerMessage { + pub fn get(&self, server: &source::Server) -> Option { match server.kind() { - source::server::Kind::Join => self.join, - source::server::Kind::Part => self.part, - source::server::Kind::Quit => self.quit, + source::server::Kind::Join => Some(self.join), + source::server::Kind::Part => Some(self.part), + source::server::Kind::Quit => Some(self.quit), + _ => None, } } } @@ -78,6 +81,7 @@ impl Default for Buffer { input_visibility: InputVisibility::default(), channel: Channel::default(), server_messages: Default::default(), + topic_banner: TopicBanner::default(), } } } diff --git a/data/src/history.rs b/data/src/history.rs index 8b0fee180..a57b0320c 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -274,6 +274,11 @@ pub struct View<'a> { pub new_messages: Vec<&'a Message>, } +#[derive(Debug)] +pub struct BannerView<'a> { + pub messages: Vec<&'a Message>, +} + #[derive(Debug, Clone, Default)] struct Input(HashMap>); diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index a6f97e4f9..3e589f5d5 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -6,8 +6,10 @@ use futures::{future, Future, FutureExt}; use itertools::Itertools; use tokio::time::Instant; -use crate::config::buffer::{Exclude, ServerMessages}; +use crate::config; +use crate::config::buffer::Exclude; use crate::history::{self, History}; +use crate::message::source; use crate::message::{self, Limit}; use crate::time::Posix; use crate::user::{Nick, NickRef}; @@ -189,24 +191,33 @@ impl Manager { server: &Server, channel: &str, limit: Option, - server_messages: &ServerMessages, + buffer_config: &config::buffer::Buffer, ) -> Option> { - self.data.history_view( + self.data.history_scroll_view( server, &history::Kind::Channel(channel.to_string()), limit, - server_messages, + buffer_config, ) } + pub fn get_channel_topic( + &self, + server: &Server, + channel: &str, + ) -> Option> { + self.data + .history_banner_view(server, &history::Kind::Channel(channel.to_string())) + } + pub fn get_server_messages( &self, server: &Server, limit: Option, - server_messages: &ServerMessages, + buffer_config: &config::buffer::Buffer, ) -> Option> { self.data - .history_view(server, &history::Kind::Server, limit, server_messages) + .history_scroll_view(server, &history::Kind::Server, limit, buffer_config) } pub fn get_query_messages( @@ -214,13 +225,13 @@ impl Manager { server: &Server, nick: &Nick, limit: Option, - server_messages: &ServerMessages, + buffer_config: &config::buffer::Buffer, ) -> Option> { - self.data.history_view( + self.data.history_scroll_view( server, &history::Kind::Query(nick.clone()), limit, - server_messages, + buffer_config, ) } @@ -422,12 +433,12 @@ impl Data { } } - fn history_view( + fn history_scroll_view( &self, server: &server::Server, kind: &history::Kind, limit: Option, - server_messages: &ServerMessages, + buffer_config: &config::buffer::Buffer, ) -> Option { let History::Full { messages, @@ -444,32 +455,36 @@ impl Data { .iter() .filter(|message| match message.target.source() { message::Source::Server(Some(source)) => { - let source_config = server_messages.get(source); - - match source_config.exclude { - Exclude::All => false, - Exclude::None => true, - Exclude::Smart(seconds) => { - if let Some(nick) = source.nick() { - !smart_filter_message( - message, - &seconds, - most_recent_messages.get(nick), - ) - } else if let Some(nickname) = - message.text.split(' ').collect::>().get(1) - { - let nick = Nick::from(*nickname); - - !smart_filter_message( - message, - &seconds, - most_recent_messages.get(&nick), - ) - } else { - true + if let Some(source_config) = buffer_config.server_messages.get(source) { + match source_config.exclude { + Exclude::All => false, + Exclude::None => true, + Exclude::Smart(seconds) => { + if let Some(nick) = source.nick() { + !smart_filter_message( + message, + &seconds, + most_recent_messages.get(nick), + ) + } else if let Some(nickname) = + message.text.split(' ').collect::>().get(1) + { + let nick = Nick::from(*nickname); + + !smart_filter_message( + message, + &seconds, + most_recent_messages.get(&nick), + ) + } else { + true + } } } + } else if buffer_config.topic_banner.enabled { + matches!(source.kind(), source::server::Kind::Topic) + } else { + true } } crate::message::Source::User(message_user) => { @@ -499,6 +514,62 @@ impl Data { }) } + fn history_banner_view( + &self, + server: &server::Server, + kind: &history::Kind, + ) -> Option { + let History::Full { messages, .. } = self.map.get(server)?.get(kind)? else { + return None; + }; + + if let Some(topic_messages) = + messages + .iter() + .rev() + .find_map(|message| match message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::Topic => Some(vec![message]), + source::server::Kind::ReplyTopicWhoTime => { + messages.iter().rev().find_map(|topic_message| { + match topic_message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::ReplyTopic => { + Some(vec![topic_message, message]) + } + _ => None, + }, + _ => None, + } + }) + } + source::server::Kind::ReplyTopic => { + messages + .iter() + .rev() + .find_map(|whotime_message| match whotime_message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::ReplyTopicWhoTime => { + Some(vec![message, whotime_message]) + } + _ => None, + }, + _ => None, + }) + } + _ => None, + }, + _ => None, + }) + { + Some(history::BannerView { + messages: topic_messages, + }) + } else { + Some(history::BannerView { messages: vec![] }) + } + } + fn add_message( &mut self, server: server::Server, diff --git a/data/src/message.rs b/data/src/message.rs index 7bfcd3362..52baed888 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -121,7 +121,16 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { channel: target, source: source::Source::Server(None), }), - Command::TOPIC(channel, _) | Command::KICK(channel, _, _) => Some(Target::Channel { + Command::TOPIC(channel, topic) => Some(Target::Channel { + channel, + source: source::Source::Server(Some(source::Server::new( + source::server::Kind::Topic, + Some(user?.nickname().to_owned()), + topic, + None, + ))), + }), + Command::KICK(channel, _, _) => Some(Target::Channel { channel, source: source::Source::Server(None), }), @@ -130,6 +139,8 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { source: source::Source::Server(Some(source::Server::new( source::server::Kind::Part, Some(user?.nickname().to_owned()), + None, + None, ))), }), Command::JOIN(channel, _) => Some(Target::Channel { @@ -137,9 +148,41 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { source: source::Source::Server(Some(source::Server::new( source::server::Kind::Join, Some(user?.nickname().to_owned()), + None, + None, ))), }), - Command::Numeric(RPL_TOPIC | RPL_TOPICWHOTIME | RPL_CHANNELMODEIS, params) => { + Command::Numeric(RPL_TOPIC, params) => { + let channel = params.get(1)?.clone(); + Some(Target::Channel { + channel, + source: source::Source::Server(Some(source::Server::new( + source::server::Kind::ReplyTopic, + None, + Some(params.get(2)?.to_owned()), + None, + ))), + }) + } + Command::Numeric(RPL_TOPICWHOTIME, params) => { + let channel = params.get(1)?.clone(); + Some(Target::Channel { + channel, + source: source::Source::Server(Some(source::Server::new( + source::server::Kind::ReplyTopicWhoTime, + Some(Nick::from(params.get(2)?.to_owned())), + None, + Some( + params + .get(3)? + .parse::() + .ok() + .map(Posix::from_seconds)?, + ), + ))), + }) + } + Command::Numeric(RPL_CHANNELMODEIS, params) => { let channel = params.get(1)?.clone(); Some(Target::Channel { channel, diff --git a/data/src/message/broadcast.rs b/data/src/message/broadcast.rs index 058bb9338..700b09cf2 100644 --- a/data/src/message/broadcast.rs +++ b/data/src/message/broadcast.rs @@ -132,6 +132,8 @@ pub fn quit( Cause::Server(Some(source::Server::new( source::server::Kind::Quit, Some(user.nickname().to_owned()), + None, + None, ))), text, ) diff --git a/data/src/message/source.rs b/data/src/message/source.rs index e32371fc7..7cbbde0f7 100644 --- a/data/src/message/source.rs +++ b/data/src/message/source.rs @@ -27,6 +27,7 @@ pub mod server { #![allow(deprecated)] use serde::{Deserialize, Serialize}; + use crate::time::Posix; use crate::user::Nick; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -38,8 +39,18 @@ pub mod server { } impl Server { - pub fn new(kind: Kind, nick: Option) -> Self { - Self::Details(Details { kind, nick }) + pub fn new( + kind: Kind, + nick: Option, + text: Option, + time: Option, + ) -> Self { + Self::Details(Details { + kind, + nick, + text, + time, + }) } pub fn kind(&self) -> Kind { @@ -55,6 +66,26 @@ pub mod server { Server::Details(details) => details.nick.as_ref(), } } + + pub fn text(&self) -> Option<&str> { + match self { + Server::Kind(_) => None, + Server::Details(details) => { + if let Some(text) = &details.text { + Some(text.as_str()) + } else { + None + } + } + } + } + + pub fn time(&self) -> Option<&Posix> { + match self { + Server::Kind(_) => None, + Server::Details(details) => details.time.as_ref(), + } + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -63,11 +94,16 @@ pub mod server { Join, Part, Quit, + ReplyTopic, + ReplyTopicWhoTime, + Topic, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Details { pub kind: Kind, pub nick: Option, + pub text: Option, + pub time: Option, } } diff --git a/src/buffer.rs b/src/buffer.rs index ee47fd83a..5bf8a0743 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -7,6 +7,7 @@ use self::query::Query; use self::server::Server; use crate::widget::Element; +mod banner_view; pub mod channel; pub mod empty; mod input_view; diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs new file mode 100644 index 000000000..3d26250bf --- /dev/null +++ b/src/buffer/banner_view.rs @@ -0,0 +1,92 @@ +use data::server::Server; +use data::history; +use iced::widget::{column, container, scrollable}; +use iced::{Command, Length}; + +use super::user_context; +use crate::widget::Element; +use crate::theme; + +#[derive(Debug, Clone)] +pub enum Message { + Scrolled { viewport: scrollable::Viewport }, + UserContext(user_context::Message), +} + +#[derive(Debug, Clone)] +pub enum Event { + UserContext(user_context::Event), +} + +#[derive(Debug, Clone, Copy)] +pub enum Kind<'a> { + ChannelTopic(&'a Server, &'a str), +} + +pub fn view<'a>( + state: &State, + kind: Kind, + history: &'a history::Manager, + format: impl Fn(&'a data::Message) -> Option> + 'a, +) -> Element<'a, Message> { + let Some(history::BannerView { messages }) = (match kind { + Kind::ChannelTopic(server, channel) => history.get_channel_topic(server, channel), + }) else { + return column![].into(); + }; + + let messages = messages.into_iter().filter_map(format).collect::>(); + + let padding = if messages.is_empty() { + [0, 0] + } else { + [4, 8] + }; + + let content = column![column(messages)]; + + scrollable( + container(content) + .width(Length::Fill) + .padding(padding), + ) + .style(theme::Scrollable::Banner) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::default() + .alignment(scrollable::Alignment::Start) + .width(5) + .scroller_width(5), + )) + .id(state.scrollable.clone()) + .into() +} + +#[derive(Debug, Clone)] +pub struct State { + pub scrollable: scrollable::Id, +} + +impl Default for State { + fn default() -> Self { + Self { + scrollable: scrollable::Id::unique(), + } + } +} + +impl State { + pub fn new() -> Self { + Self::default() + } + + pub fn update(&mut self, message: Message) -> (Command, Option) { + if let Message::UserContext(message) = message { + return ( + Command::none(), + Some(Event::UserContext(user_context::update(message))), + ); + } + + (Command::none(), None) + } +} diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 09e392cc8..3ae8eee2b 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,17 +1,19 @@ +use data::message::source; use data::server::Server; use data::{channel, client, history, message, Config}; -use iced::widget::{column, container, row, vertical_space}; +use iced::widget::{column, container, row}; use iced::{Command, Length}; -use super::{input_view, scroll_view, user_context}; -use crate::theme; +use super::{banner_view, input_view, scroll_view, user_context}; use crate::widget::{selectable_text, Collection, Element}; +use crate::{font, theme}; #[derive(Debug, Clone)] pub enum Message { ScrollView(scroll_view::Message), InputView(input_view::Message), UserContext(user_context::Message), + BannerView(banner_view::Message), } #[derive(Debug, Clone)] @@ -117,19 +119,76 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let text_input = show_text_input.then(|| { - column![ - vertical_space(4), - input_view::view( - &state.input_view, - buffer, - users, - channels, - input_history, - is_focused + let topic_banner = config.buffer.topic_banner.enabled.then(|| { + column![container( + banner_view::view( + &state.banner_view, + banner_view::Kind::ChannelTopic(&state.server, &state.channel), + history, + move |message| { + match message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::Topic => { + let topic = selectable_text(source.text()?) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner); + + let nick = selectable_text(format!( + "set by {} at {}", + source.nick()?, + message.server_time.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner); + + Some( + container( + column![].push(row![].push(topic)).push(row![].push(nick)), + ) + .into(), + ) + } + source::server::Kind::ReplyTopic => { + let topic = selectable_text(source.text()?) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner); + + Some(container(row![].push(topic)).into()) + } + source::server::Kind::ReplyTopicWhoTime => { + let nick = selectable_text(format!( + "set by {} at {}", + source.nick()?, + source.time()?.datetime()?.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner); + + Some(container(row![].push(nick)).into()) + } + _ => None, + }, + _ => None, + } + }, ) - .map(Message::InputView) - ] + .map(Message::BannerView), + ) + .style(theme::Container::Banner) + .max_height(config.buffer.topic_banner.max_height),] + .width(Length::Fill) + }); + + let text_input = show_text_input.then(|| { + column![input_view::view( + &state.input_view, + buffer, + users, + channels, + input_history, + is_focused + ) + .map(Message::InputView)] .width(Length::Fill) }); @@ -143,9 +202,12 @@ pub fn view<'a>( (false, _) => { row![messages] }.height(Length::Fill), }; - let scrollable = column![container(content).height(Length::Fill)] + let scrollable = column![] + .push_maybe(topic_banner) + .push(container(content).height(Length::Fill)) .push_maybe(text_input) - .height(Length::Fill); + .height(Length::Fill) + .spacing(4); container(scrollable) .width(Length::Fill) @@ -158,7 +220,8 @@ pub fn view<'a>( pub struct Channel { pub server: Server, pub channel: String, - pub topic: Option, + + pub banner_view: banner_view::State, pub scroll_view: scroll_view::State, pub input_view: input_view::State, } @@ -168,7 +231,7 @@ impl Channel { Self { server, channel, - topic: None, + banner_view: banner_view::State::new(), scroll_view: scroll_view::State::new(), input_view: input_view::State::new(), } @@ -185,6 +248,15 @@ impl Channel { history: &mut history::Manager, ) -> (Command, Option) { match message { + Message::BannerView(message) => { + let (command, event) = self.banner_view.update(message); + + let event = event.map(|event| match event { + banner_view::Event::UserContext(event) => Event::UserContext(event), + }); + + (command.map(Message::BannerView), event) + } Message::ScrollView(message) => { let (command, event) = self.scroll_view.update(message); diff --git a/src/buffer/scroll_view.rs b/src/buffer/scroll_view.rs index f4d149561..c7a419aba 100644 --- a/src/buffer/scroll_view.rs +++ b/src/buffer/scroll_view.rs @@ -46,20 +46,14 @@ pub fn view<'a>( new_messages, }) = (match kind { Kind::Server(server) => { - history.get_server_messages(server, Some(state.limit), &config.buffer.server_messages) + history.get_server_messages(server, Some(state.limit), &config.buffer) + } + Kind::Channel(server, channel) => { + history.get_channel_messages(server, channel, Some(state.limit), &config.buffer) + } + Kind::Query(server, user) => { + history.get_query_messages(server, user, Some(state.limit), &config.buffer) } - Kind::Channel(server, channel) => history.get_channel_messages( - server, - channel, - Some(state.limit), - &config.buffer.server_messages, - ), - Kind::Query(server, user) => history.get_query_messages( - server, - user, - Some(state.limit), - &config.buffer.server_messages, - ), }) else { return column![].into(); diff --git a/src/theme.rs b/src/theme.rs index 65e4c320e..16bc89791 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -109,6 +109,7 @@ pub enum Text { Transparent, Status(message::source::Status), Nickname(Option, bool), + Banner, } impl text::StyleSheet for Theme { @@ -159,6 +160,9 @@ impl text::StyleSheet for Theme { Text::Transparent => text::Appearance { color: Some(self.colors().text.low_alpha), }, + Text::Banner => text::Appearance { + color: Some(self.colors().background.base), + }, } } } @@ -178,6 +182,7 @@ pub enum Container { Context, Highlight, SemiTransparent, + Banner, } impl container::StyleSheet for Theme { @@ -259,6 +264,15 @@ impl container::StyleSheet for Theme { ), ..Default::default() }, + Container::Banner => container::Appearance { + background: Some(Background::Color(self.colors().accent.darkest)), + border: Border { + radius: [4.0, 4.0, 4.0, 4.0].into(), + width: 1.0, + color: Color::TRANSPARENT, + }, + ..Default::default() + }, } } } @@ -441,6 +455,7 @@ pub enum Scrollable { #[default] Default, Hidden, + Banner, } impl scrollable::StyleSheet for Theme { @@ -480,6 +495,22 @@ impl scrollable::StyleSheet for Theme { }, }, }, + Scrollable::Banner => scrollable::Scrollbar { + background: Some(Background::Color(self.colors().accent.darker)), + border: Border { + radius: 8.0.into(), + width: 1.0, + color: Color::TRANSPARENT, + }, + scroller: scrollable::Scroller { + color: self.colors().accent.base, + border: Border { + radius: 8.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, + }, + }, } } @@ -492,6 +523,7 @@ impl scrollable::StyleSheet for Theme { match style { Scrollable::Default => scrollable::Scrollbar { ..active }, Scrollable::Hidden => scrollable::Scrollbar { ..active }, + Scrollable::Banner => scrollable::Scrollbar { ..active }, } } } @@ -623,7 +655,11 @@ impl selectable_text::StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> selectable_text::Appearance { let color = ::appearance(self, style.clone()).color; - let selection_color = self.colors().accent.high_alpha; + + let selection_color = match style { + Text::Banner => self.colors().background.low_alpha, + _ => self.colors().accent.high_alpha, + }; selectable_text::Appearance { color, From d68cd9fefa1c9ed72d08a7bbc30a19483d6cbb2a Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sat, 24 Feb 2024 01:37:54 -0800 Subject: [PATCH 02/17] Add user context to topic banner. --- Cargo.lock | 1 + Cargo.toml | 1 + data/src/user.rs | 6 ++ src/buffer/banner_view.rs | 34 ++++------- src/buffer/channel.rs | 122 +++++++++++++++++++++++++++++++------- 5 files changed, 121 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edebdccb0..94dfd4d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,6 +1461,7 @@ dependencies = [ "iced", "image", "log", + "nom", "notify-rust", "once_cell", "open", diff --git a/Cargo.toml b/Cargo.toml index 3ed8bfb9a..423b4abd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ chrono = { version = "0.4", features = ['serde'] } fern = "0.6.1" iced = { version = "0.12.0", features = ["tokio", "lazy", "advanced", "image"] } log = "0.4.16" +nom = "7.1" once_cell = "1.18" palette = "0.7.4" thiserror = "1.0.30" diff --git a/data/src/user.rs b/data/src/user.rs index 8c6d25b29..a2cf02001 100644 --- a/data/src/user.rs +++ b/data/src/user.rs @@ -216,6 +216,12 @@ impl fmt::Display for Nick { } } +impl AsRef for Nick { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} + impl From for Nick { fn from(nick: String) -> Self { Nick(nick) diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs index 3d26250bf..1feec4c29 100644 --- a/src/buffer/banner_view.rs +++ b/src/buffer/banner_view.rs @@ -1,11 +1,11 @@ -use data::server::Server; use data::history; +use data::server::Server; use iced::widget::{column, container, scrollable}; use iced::{Command, Length}; use super::user_context; -use crate::widget::Element; use crate::theme; +use crate::widget::Element; #[derive(Debug, Clone)] pub enum Message { @@ -37,28 +37,20 @@ pub fn view<'a>( let messages = messages.into_iter().filter_map(format).collect::>(); - let padding = if messages.is_empty() { - [0, 0] - } else { - [4, 8] - }; + let padding = if messages.is_empty() { [0, 0] } else { [4, 8] }; let content = column![column(messages)]; - scrollable( - container(content) - .width(Length::Fill) - .padding(padding), - ) - .style(theme::Scrollable::Banner) - .direction(scrollable::Direction::Vertical( - scrollable::Properties::default() - .alignment(scrollable::Alignment::Start) - .width(5) - .scroller_width(5), - )) - .id(state.scrollable.clone()) - .into() + scrollable(container(content).width(Length::Fill).padding(padding)) + .style(theme::Scrollable::Banner) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::default() + .alignment(scrollable::Alignment::Start) + .width(5) + .scroller_width(5), + )) + .id(state.scrollable.clone()) + .into() } #[derive(Debug, Clone)] diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 3ae8eee2b..308a370d9 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,8 +1,11 @@ use data::message::source; use data::server::Server; +use data::user::Nick; use data::{channel, client, history, message, Config}; use iced::widget::{column, container, row}; use iced::{Command, Length}; +use nom::character::complete::satisfy; +use nom::multi::many1; use super::{banner_view, input_view, scroll_view, user_context}; use crate::widget::{selectable_text, Collection, Element}; @@ -132,21 +135,55 @@ pub fn view<'a>( let topic = selectable_text(source.text()?) .font(font::MONO_BOLD.clone()) .style(theme::Text::Banner); - - let nick = selectable_text(format!( - "set by {} at {}", - source.nick()?, - message.server_time.to_rfc2822() - )) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner); - - Some( - container( - column![].push(row![].push(topic)).push(row![].push(nick)), - ) - .into(), - ) + let topic = row![].push(topic); + + let nick = if let Some(nick) = source.nick() { + if let Some(user) = + users.iter().find(|user| user.nickname() == *nick) + { + Some( + row![] + .push( + selectable_text("set by ") + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ) + .push( + user_context::view( + selectable_text(nick) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + user.clone(), + ) + .map(banner_view::Message::UserContext), + ) + .push( + selectable_text(format!( + " at {}", + message.server_time.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ), + ) + } else { + Some( + row![].push( + selectable_text(format!( + "set by {} at {}", + nick, + message.server_time.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ), + ) + } + } else { + None + }; + + Some(container(column![].push(topic).push_maybe(nick)).into()) } source::server::Kind::ReplyTopic => { let topic = selectable_text(source.text()?) @@ -156,13 +193,54 @@ pub fn view<'a>( Some(container(row![].push(topic)).into()) } source::server::Kind::ReplyTopicWhoTime => { - let nick = selectable_text(format!( - "set by {} at {}", - source.nick()?, - source.time()?.datetime()?.to_rfc2822() - )) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner); + let Ok((_, nick)) = + many1::<&str, char, nom::error::Error<_>, _>(satisfy(|c| { + c.is_ascii_alphanumeric() || c == '-' + }))(source.nick()?.as_ref()) + else { + return None; + }; + + let nick: String = nick.into_iter().collect(); + let nick = Nick::from(nick); + + let nick = if let Some(user) = + users.iter().find(|user| user.nickname() == nick) + { + row![] + .push( + selectable_text("set by ") + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ) + .push( + user_context::view( + selectable_text(source.nick()?) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + user.clone(), + ) + .map(banner_view::Message::UserContext), + ) + .push( + selectable_text(format!( + " at {}", + message.server_time.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ) + } else { + row![].push( + selectable_text(format!( + "set by {} at {}", + source.nick()?, + message.server_time.to_rfc2822() + )) + .font(font::MONO_BOLD.clone()) + .style(theme::Text::Banner), + ) + }; Some(container(row![].push(nick)).into()) } From 2ab072fd5dbe59504af7d69c42fcea2e7f54c929 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Fri, 1 Mar 2024 16:36:08 -0800 Subject: [PATCH 03/17] Tone down visual prominence of topic banner. Allows set by nickname to be colored. Restructure configuration. --- config.yaml | 15 +-- data/src/buffer.rs | 12 +- data/src/config/buffer.rs | 6 +- data/src/history/manager.rs | 3 +- src/buffer/channel.rs | 261 ++++++++++++++++++++---------------- src/theme.rs | 25 ++-- 6 files changed, 184 insertions(+), 138 deletions(-) diff --git a/config.yaml b/config.yaml index 4ddd59ffc..4bd0cd3cd 100644 --- a/config.yaml +++ b/config.yaml @@ -86,14 +86,13 @@ buffer: # - Focused: Only show input when the buffer is focused input_visibility: Always - # Topic banner settings: - topic_banner: - # Show topic messages in a topic banner at the topic of a pane instead - # of server messages. Topic changes will still show up as a server - # message. - # - Default is false. - enabled: false - # Maximum height of the topic banner. + # Topic settings: + # - Inline: Show topic as server messages in each channel. [default] + # - !Banner: Show topic as a banner at the topic of each channel. + # Topic changes will still show up as a server + # message in channel. + topic: !Banner + # Maximum height of the topic banner, when topic is set to !Banner. # - Default is 42 max_height: 42 diff --git a/data/src/buffer.rs b/data/src/buffer.rs index be327b63c..637e4ad44 100644 --- a/data/src/buffer.rs +++ b/data/src/buffer.rs @@ -67,11 +67,13 @@ pub enum InputVisibility { } #[derive(Debug, Clone, Copy, Default, Deserialize)] -pub struct TopicBanner { - #[serde(default)] - pub enabled: bool, - #[serde(default = "default_topic_banner_max_height")] - pub max_height: u16, +pub enum Topic { + #[default] + Inline, + Banner { + #[serde(default = "default_topic_banner_max_height")] + max_height: u16, + }, } fn default_topic_banner_max_height() -> u16 { diff --git a/data/src/config/buffer.rs b/data/src/config/buffer.rs index 406030332..9c4af6d26 100644 --- a/data/src/config/buffer.rs +++ b/data/src/config/buffer.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use super::Channel; use crate::{ - buffer::{Color, InputVisibility, Nickname, Timestamp, TopicBanner}, + buffer::{Color, InputVisibility, Nickname, Timestamp, Topic}, message::source, }; @@ -20,7 +20,7 @@ pub struct Buffer { #[serde(default)] pub server_messages: ServerMessages, #[serde(default)] - pub topic_banner: TopicBanner, + pub topic: Topic, } #[derive(Debug, Copy, Clone, Default, Deserialize)] @@ -81,7 +81,7 @@ impl Default for Buffer { input_visibility: InputVisibility::default(), channel: Channel::default(), server_messages: Default::default(), - topic_banner: TopicBanner::default(), + topic: Topic::default(), } } } diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 3e589f5d5..34cdcde16 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -6,6 +6,7 @@ use futures::{future, Future, FutureExt}; use itertools::Itertools; use tokio::time::Instant; +use crate::buffer::Topic; use crate::config; use crate::config::buffer::Exclude; use crate::history::{self, History}; @@ -481,7 +482,7 @@ impl Data { } } } - } else if buffer_config.topic_banner.enabled { + } else if matches!(buffer_config.topic, Topic::Banner { .. }) { matches!(source.kind(), source::server::Kind::Topic) } else { true diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 308a370d9..2572a25c1 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,3 +1,4 @@ +use data::buffer::Topic; use data::message::source; use data::server::Server; use data::user::Nick; @@ -8,8 +9,8 @@ use nom::character::complete::satisfy; use nom::multi::many1; use super::{banner_view, input_view, scroll_view, user_context}; +use crate::theme; use crate::widget::{selectable_text, Collection, Element}; -use crate::{font, theme}; #[derive(Debug, Clone)] pub enum Message { @@ -122,37 +123,127 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic_banner = config.buffer.topic_banner.enabled.then(|| { - column![container( - banner_view::view( - &state.banner_view, - banner_view::Kind::ChannelTopic(&state.server, &state.channel), - history, - move |message| { - match message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::Topic => { - let topic = selectable_text(source.text()?) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner); - let topic = row![].push(topic); - - let nick = if let Some(nick) = source.nick() { - if let Some(user) = - users.iter().find(|user| user.nickname() == *nick) - { + let topic_banner = + if let Topic::Banner { + max_height: topic_banner_max_height, + } = config.buffer.topic + { + Some( + column![container( + banner_view::view( + &state.banner_view, + banner_view::Kind::ChannelTopic(&state.server, &state.channel), + history, + move |message| { + match message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::Topic => { + let topic = selectable_text(source.text()?) + .style(theme::Text::Banner); + let topic = row![].push(topic); + + let nick = if let Some(nick) = source.nick() { + if let Some(user) = + users.iter().find(|user| user.nickname() == *nick) + { + Some( + row![] + .push( + selectable_text("set by ") + .style(theme::Text::Banner), + ) + .push( + user_context::view( + selectable_text(nick).style( + theme::Text::Nickname( + user.color_seed( + &config + .buffer + .nickname + .color, + ), + false, + ), + ), + user.clone(), + ) + .map(banner_view::Message::UserContext), + ) + .push( + selectable_text(format!( + " at {}", + message.server_time.to_rfc2822() + )) + .style(theme::Text::Banner), + ), + ) + } else { + Some( + row![] + .push( + selectable_text("set by ") + .style(theme::Text::Banner), + ) + .push( + selectable_text(nick) + .style(theme::Text::Server), + ) + .push( + selectable_text(format!( + " at {}", + message.server_time.to_rfc2822() + )) + .style(theme::Text::Banner), + ), + ) + } + } else { + None + }; + Some( + container(column![].push(topic).push_maybe(nick)) + .into(), + ) + } + source::server::Kind::ReplyTopic => { + let topic = selectable_text(source.text()?) + .style(theme::Text::Banner); + + Some(container(row![].push(topic)).into()) + } + source::server::Kind::ReplyTopicWhoTime => { + let Ok((_, nick)) = + many1::<&str, char, nom::error::Error<_>, _>(satisfy( + |c| c.is_ascii_alphanumeric() || c == '-', + ))( + source.nick()?.as_ref() + ) + else { + return None; + }; + + let nick: String = nick.into_iter().collect(); + let nick = Nick::from(nick); + + let nick = if let Some(user) = + users.iter().find(|user| user.nickname() == nick) + { row![] .push( selectable_text("set by ") - .font(font::MONO_BOLD.clone()) .style(theme::Text::Banner), ) .push( user_context::view( - selectable_text(nick) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner), + selectable_text(source.nick()?).style( + theme::Text::Nickname( + user.color_seed( + &config.buffer.nickname.color, + ), + false, + ), + ), user.clone(), ) .map(banner_view::Message::UserContext), @@ -162,100 +253,44 @@ pub fn view<'a>( " at {}", message.server_time.to_rfc2822() )) - .font(font::MONO_BOLD.clone()) .style(theme::Text::Banner), - ), - ) - } else { - Some( - row![].push( - selectable_text(format!( - "set by {} at {}", - nick, - message.server_time.to_rfc2822() - )) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner), - ), - ) - } - } else { - None - }; - - Some(container(column![].push(topic).push_maybe(nick)).into()) - } - source::server::Kind::ReplyTopic => { - let topic = selectable_text(source.text()?) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner); - - Some(container(row![].push(topic)).into()) - } - source::server::Kind::ReplyTopicWhoTime => { - let Ok((_, nick)) = - many1::<&str, char, nom::error::Error<_>, _>(satisfy(|c| { - c.is_ascii_alphanumeric() || c == '-' - }))(source.nick()?.as_ref()) - else { - return None; - }; - - let nick: String = nick.into_iter().collect(); - let nick = Nick::from(nick); - - let nick = if let Some(user) = - users.iter().find(|user| user.nickname() == nick) - { - row![] - .push( - selectable_text("set by ") - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner), - ) - .push( - user_context::view( - selectable_text(source.nick()?) - .font(font::MONO_BOLD.clone()) + ) + } else { + row![] + .push( + selectable_text("set by ") + .style(theme::Text::Banner), + ) + .push( + selectable_text(source.nick()?) + .style(theme::Text::Server), + ) + .push( + selectable_text(format!( + " at {}", + message.server_time.to_rfc2822() + )) .style(theme::Text::Banner), - user.clone(), - ) - .map(banner_view::Message::UserContext), - ) - .push( - selectable_text(format!( - " at {}", - message.server_time.to_rfc2822() - )) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner), - ) - } else { - row![].push( - selectable_text(format!( - "set by {} at {}", - source.nick()?, - message.server_time.to_rfc2822() - )) - .font(font::MONO_BOLD.clone()) - .style(theme::Text::Banner), - ) - }; - - Some(container(row![].push(nick)).into()) + ) + }; + + Some(container(row![].push(nick)).into()) + } + _ => None, + }, + _ => None, } - _ => None, }, - _ => None, - } - }, + ) + .map(Message::BannerView), + ) + .style(theme::Container::Banner) + .max_height(topic_banner_max_height),] + .width(Length::Fill), ) - .map(Message::BannerView), - ) - .style(theme::Container::Banner) - .max_height(config.buffer.topic_banner.max_height),] - .width(Length::Fill) - }); + } else { + None + }; let text_input = show_text_input.then(|| { column![input_view::view( diff --git a/src/theme.rs b/src/theme.rs index 16bc89791..59f28c67d 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -161,7 +161,7 @@ impl text::StyleSheet for Theme { color: Some(self.colors().text.low_alpha), }, Text::Banner => text::Appearance { - color: Some(self.colors().background.base), + color: Some(self.colors().text.base), }, } } @@ -265,7 +265,11 @@ impl container::StyleSheet for Theme { ..Default::default() }, Container::Banner => container::Appearance { - background: Some(Background::Color(self.colors().accent.darkest)), + background: Some(Background::Color(if self.colors().is_dark_theme() { + self.colors().background.light + } else { + self.colors().background.dark + })), border: Border { radius: [4.0, 4.0, 4.0, 4.0].into(), width: 1.0, @@ -496,14 +500,22 @@ impl scrollable::StyleSheet for Theme { }, }, Scrollable::Banner => scrollable::Scrollbar { - background: Some(Background::Color(self.colors().accent.darker)), + background: Some(Background::Color(if self.colors().is_dark_theme() { + self.colors().background.lighter + } else { + self.colors().background.darker + })), border: Border { radius: 8.0.into(), width: 1.0, color: Color::TRANSPARENT, }, scroller: scrollable::Scroller { - color: self.colors().accent.base, + color: if self.colors().is_dark_theme() { + self.colors().background.lightest + } else { + self.colors().background.darkest + }, border: Border { radius: 8.0.into(), width: 0.0, @@ -656,10 +668,7 @@ impl selectable_text::StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> selectable_text::Appearance { let color = ::appearance(self, style.clone()).color; - let selection_color = match style { - Text::Banner => self.colors().background.low_alpha, - _ => self.colors().accent.high_alpha, - }; + let selection_color = self.colors().accent.high_alpha; selectable_text::Appearance { color, From 3b1b7e6f000bee5d3279c87230fe7ea13ac92aea Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Fri, 1 Mar 2024 20:29:48 -0800 Subject: [PATCH 04/17] Clean up topic banner styling code. --- Cargo.lock | 1 - Cargo.toml | 1 - src/buffer/banner_view.rs | 38 ++++++- src/buffer/channel.rs | 228 ++++++++++---------------------------- 4 files changed, 95 insertions(+), 173 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 94dfd4d40..edebdccb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,7 +1461,6 @@ dependencies = [ "iced", "image", "log", - "nom", "notify-rust", "once_cell", "open", diff --git a/Cargo.toml b/Cargo.toml index 423b4abd2..3ed8bfb9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ chrono = { version = "0.4", features = ['serde'] } fern = "0.6.1" iced = { version = "0.12.0", features = ["tokio", "lazy", "advanced", "image"] } log = "0.4.16" -nom = "7.1" once_cell = "1.18" palette = "0.7.4" thiserror = "1.0.30" diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs index 1feec4c29..819606715 100644 --- a/src/buffer/banner_view.rs +++ b/src/buffer/banner_view.rs @@ -1,11 +1,14 @@ +use chrono::{DateTime, Utc}; use data::history; use data::server::Server; -use iced::widget::{column, container, scrollable}; +use data::user::Nick; +use data::{Config, User}; +use iced::widget::{column, container, row, scrollable, Row}; use iced::{Command, Length}; use super::user_context; -use crate::theme; -use crate::widget::Element; +use crate::widget::{selectable_text, Element}; +use crate::{theme, Theme}; #[derive(Debug, Clone)] pub enum Message { @@ -53,6 +56,35 @@ pub fn view<'a>( .into() } +pub fn style_topic_who_time<'a>( + who: &str, + time: DateTime, + long_who: Option<&str>, + users: &[User], + config: &'a Config, +) -> Row<'a, Message, Theme> { + if let Some(user) = users.iter().find(|user| user.nickname() == Nick::from(who)) { + row![] + .push(selectable_text("set by ").style(theme::Text::Banner)) + .push( + user_context::view( + selectable_text(long_who.unwrap_or(who)).style(theme::Text::Nickname( + user.color_seed(&config.buffer.nickname.color), + false, + )), + user.clone(), + ) + .map(Message::UserContext), + ) + .push(selectable_text(format!(" at {}", time.to_rfc2822())).style(theme::Text::Banner)) + } else { + row![] + .push(selectable_text("set by ").style(theme::Text::Banner)) + .push(selectable_text(long_who.unwrap_or(who)).style(theme::Text::Server)) + .push(selectable_text(format!(" at {}", time.to_rfc2822())).style(theme::Text::Banner)) + } +} + #[derive(Debug, Clone)] pub struct State { pub scrollable: scrollable::Id, diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 2572a25c1..e81faa845 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,12 +1,9 @@ use data::buffer::Topic; use data::message::source; use data::server::Server; -use data::user::Nick; use data::{channel, client, history, message, Config}; use iced::widget::{column, container, row}; use iced::{Command, Length}; -use nom::character::complete::satisfy; -use nom::multi::many1; use super::{banner_view, input_view, scroll_view, user_context}; use crate::theme; @@ -123,174 +120,69 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic_banner = - if let Topic::Banner { - max_height: topic_banner_max_height, - } = config.buffer.topic - { - Some( - column![container( - banner_view::view( - &state.banner_view, - banner_view::Kind::ChannelTopic(&state.server, &state.channel), - history, - move |message| { - match message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::Topic => { - let topic = selectable_text(source.text()?) - .style(theme::Text::Banner); - let topic = row![].push(topic); - - let nick = if let Some(nick) = source.nick() { - if let Some(user) = - users.iter().find(|user| user.nickname() == *nick) - { - Some( - row![] - .push( - selectable_text("set by ") - .style(theme::Text::Banner), - ) - .push( - user_context::view( - selectable_text(nick).style( - theme::Text::Nickname( - user.color_seed( - &config - .buffer - .nickname - .color, - ), - false, - ), - ), - user.clone(), - ) - .map(banner_view::Message::UserContext), - ) - .push( - selectable_text(format!( - " at {}", - message.server_time.to_rfc2822() - )) - .style(theme::Text::Banner), - ), - ) - } else { - Some( - row![] - .push( - selectable_text("set by ") - .style(theme::Text::Banner), - ) - .push( - selectable_text(nick) - .style(theme::Text::Server), - ) - .push( - selectable_text(format!( - " at {}", - message.server_time.to_rfc2822() - )) - .style(theme::Text::Banner), - ), - ) - } - } else { - None - }; - - Some( - container(column![].push(topic).push_maybe(nick)) - .into(), - ) - } - source::server::Kind::ReplyTopic => { - let topic = selectable_text(source.text()?) - .style(theme::Text::Banner); - - Some(container(row![].push(topic)).into()) - } - source::server::Kind::ReplyTopicWhoTime => { - let Ok((_, nick)) = - many1::<&str, char, nom::error::Error<_>, _>(satisfy( - |c| c.is_ascii_alphanumeric() || c == '-', - ))( - source.nick()?.as_ref() - ) - else { - return None; - }; - - let nick: String = nick.into_iter().collect(); - let nick = Nick::from(nick); - - let nick = if let Some(user) = - users.iter().find(|user| user.nickname() == nick) - { - row![] - .push( - selectable_text("set by ") - .style(theme::Text::Banner), - ) - .push( - user_context::view( - selectable_text(source.nick()?).style( - theme::Text::Nickname( - user.color_seed( - &config.buffer.nickname.color, - ), - false, - ), - ), - user.clone(), - ) - .map(banner_view::Message::UserContext), - ) - .push( - selectable_text(format!( - " at {}", - message.server_time.to_rfc2822() - )) - .style(theme::Text::Banner), - ) - } else { - row![] - .push( - selectable_text("set by ") - .style(theme::Text::Banner), - ) - .push( - selectable_text(source.nick()?) - .style(theme::Text::Server), - ) - .push( - selectable_text(format!( - " at {}", - message.server_time.to_rfc2822() - )) - .style(theme::Text::Banner), - ) - }; - - Some(container(row![].push(nick)).into()) - } - _ => None, - }, + let topic_banner = if let Topic::Banner { + max_height: topic_banner_max_height, + } = config.buffer.topic + { + Some( + column![container( + banner_view::view( + &state.banner_view, + banner_view::Kind::ChannelTopic(&state.server, &state.channel), + history, + move |message| { + match message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::Topic => { + let topic = row![].push( + selectable_text(source.text()?).style(theme::Text::Banner), + ); + + let nick = banner_view::style_topic_who_time( + source.nick()?.as_ref(), + message.server_time, + None, + users, + config, + ); + + Some(container(column![].push(topic).push(nick)).into()) + } + source::server::Kind::ReplyTopic => { + let topic = row![].push( + selectable_text(source.text()?).style(theme::Text::Banner), + ); + + Some(container(topic).into()) + } + source::server::Kind::ReplyTopicWhoTime => { + let nick = source.nick()?.as_ref().split('!').next()?; + + let nick = banner_view::style_topic_who_time( + nick, + source.time()?.datetime()?, + Some(source.nick()?.as_ref()), + users, + config, + ); + + Some(container(nick).into()) + } _ => None, - } - }, - ) - .map(Message::BannerView), + }, + _ => None, + } + }, ) - .style(theme::Container::Banner) - .max_height(topic_banner_max_height),] - .width(Length::Fill), + .map(Message::BannerView), ) - } else { - None - }; + .style(theme::Container::Banner) + .max_height(topic_banner_max_height),] + .width(Length::Fill), + ) + } else { + None + }; let text_input = show_text_input.then(|| { column![input_view::view( From e7979abe70b83d953f3859ec37efe1c6cd76f804 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sun, 3 Mar 2024 01:32:04 -0800 Subject: [PATCH 05/17] Have banner_view::view return an Option<_> such that no logic for handling an empty container is needed. --- src/buffer/banner_view.rs | 30 ++++++----- src/buffer/channel.rs | 104 ++++++++++++++++++-------------------- 2 files changed, 66 insertions(+), 68 deletions(-) diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs index 819606715..58e403021 100644 --- a/src/buffer/banner_view.rs +++ b/src/buffer/banner_view.rs @@ -31,29 +31,33 @@ pub fn view<'a>( kind: Kind, history: &'a history::Manager, format: impl Fn(&'a data::Message) -> Option> + 'a, -) -> Element<'a, Message> { +) -> Option> { let Some(history::BannerView { messages }) = (match kind { Kind::ChannelTopic(server, channel) => history.get_channel_topic(server, channel), }) else { - return column![].into(); + return None; }; let messages = messages.into_iter().filter_map(format).collect::>(); - let padding = if messages.is_empty() { [0, 0] } else { [4, 8] }; + if messages.is_empty() { + return None; + } let content = column![column(messages)]; - scrollable(container(content).width(Length::Fill).padding(padding)) - .style(theme::Scrollable::Banner) - .direction(scrollable::Direction::Vertical( - scrollable::Properties::default() - .alignment(scrollable::Alignment::Start) - .width(5) - .scroller_width(5), - )) - .id(state.scrollable.clone()) - .into() + Some( + scrollable(container(content).width(Length::Fill).padding([4, 8])) + .style(theme::Scrollable::Banner) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::default() + .alignment(scrollable::Alignment::Start) + .width(5) + .scroller_width(5), + )) + .id(state.scrollable.clone()) + .into(), + ) } pub fn style_topic_who_time<'a>( diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index e81faa845..a65cf4001 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -124,62 +124,56 @@ pub fn view<'a>( max_height: topic_banner_max_height, } = config.buffer.topic { - Some( - column![container( - banner_view::view( - &state.banner_view, - banner_view::Kind::ChannelTopic(&state.server, &state.channel), - history, - move |message| { - match message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::Topic => { - let topic = row![].push( - selectable_text(source.text()?).style(theme::Text::Banner), - ); - - let nick = banner_view::style_topic_who_time( - source.nick()?.as_ref(), - message.server_time, - None, - users, - config, - ); - - Some(container(column![].push(topic).push(nick)).into()) - } - source::server::Kind::ReplyTopic => { - let topic = row![].push( - selectable_text(source.text()?).style(theme::Text::Banner), - ); - - Some(container(topic).into()) - } - source::server::Kind::ReplyTopicWhoTime => { - let nick = source.nick()?.as_ref().split('!').next()?; - - let nick = banner_view::style_topic_who_time( - nick, - source.time()?.datetime()?, - Some(source.nick()?.as_ref()), - users, - config, - ); - - Some(container(nick).into()) - } - _ => None, - }, - _ => None, - } - }, - ) - .map(Message::BannerView), - ) - .style(theme::Container::Banner) - .max_height(topic_banner_max_height),] - .width(Length::Fill), + banner_view::view( + &state.banner_view, + banner_view::Kind::ChannelTopic(&state.server, &state.channel), + history, + move |message| match message.target.source() { + message::Source::Server(Some(source)) => match source.kind() { + source::server::Kind::Topic => { + let topic = + row![].push(selectable_text(source.text()?).style(theme::Text::Banner)); + + let nick = banner_view::style_topic_who_time( + source.nick()?.as_ref(), + message.server_time, + None, + users, + config, + ); + + Some(container(column![].push(topic).push(nick)).into()) + } + source::server::Kind::ReplyTopic => { + let topic = + row![].push(selectable_text(source.text()?).style(theme::Text::Banner)); + + Some(container(topic).into()) + } + source::server::Kind::ReplyTopicWhoTime => { + let nick = source.nick()?.as_ref().split('!').next()?; + + let nick = banner_view::style_topic_who_time( + nick, + source.time()?.datetime()?, + Some(source.nick()?.as_ref()), + users, + config, + ); + + Some(container(nick).into()) + } + _ => None, + }, + _ => None, + }, ) + .map(|banner| { + column![container(banner.map(Message::BannerView)) + .style(theme::Container::Banner) + .max_height(topic_banner_max_height)] + .width(Length::Fill) + }) } else { None }; From c6e53275384818b729687a6b704f9894c0cffb34 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sun, 3 Mar 2024 23:33:44 -0800 Subject: [PATCH 06/17] Convert max height topic banner configuration to max lines. --- config.yaml | 7 ++++--- data/src/buffer.rs | 8 ++++---- src/buffer/banner_view.rs | 6 +++++- src/buffer/channel.rs | 23 ++++++++++++++--------- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/config.yaml b/config.yaml index 4bd0cd3cd..42867649d 100644 --- a/config.yaml +++ b/config.yaml @@ -92,9 +92,10 @@ buffer: # Topic changes will still show up as a server # message in channel. topic: !Banner - # Maximum height of the topic banner, when topic is set to !Banner. - # - Default is 42 - max_height: 42 + # Maximum visible lines of the topic banner before scrolling, when + # topic is set to !Banner. + # - Default is 2 + max_lines: 2 # Control different server messages. # - exclude [All, None, !Smart seconds]: diff --git a/data/src/buffer.rs b/data/src/buffer.rs index 637e4ad44..49d4e3934 100644 --- a/data/src/buffer.rs +++ b/data/src/buffer.rs @@ -71,13 +71,13 @@ pub enum Topic { #[default] Inline, Banner { - #[serde(default = "default_topic_banner_max_height")] - max_height: u16, + #[serde(default = "default_topic_banner_max_lines")] + max_lines: u16, }, } -fn default_topic_banner_max_height() -> u16 { - 42 +fn default_topic_banner_max_lines() -> u16 { + 2 } #[derive(Debug, Clone, Deserialize)] diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs index 58e403021..a12d31542 100644 --- a/src/buffer/banner_view.rs +++ b/src/buffer/banner_view.rs @@ -47,7 +47,7 @@ pub fn view<'a>( let content = column![column(messages)]; Some( - scrollable(container(content).width(Length::Fill).padding([4, 8])) + scrollable(container(content).width(Length::Fill).padding(padding())) .style(theme::Scrollable::Banner) .direction(scrollable::Direction::Vertical( scrollable::Properties::default() @@ -60,6 +60,10 @@ pub fn view<'a>( ) } +pub fn padding() -> [u16; 2] { + [4, 8] +} + pub fn style_topic_who_time<'a>( who: &str, time: DateTime, diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index a65cf4001..fe6f1e010 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -7,7 +7,7 @@ use iced::{Command, Length}; use super::{banner_view, input_view, scroll_view, user_context}; use crate::theme; -use crate::widget::{selectable_text, Collection, Element}; +use crate::widget::{double_pass, selectable_text, Collection, Element}; #[derive(Debug, Clone)] pub enum Message { @@ -120,10 +120,7 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic_banner = if let Topic::Banner { - max_height: topic_banner_max_height, - } = config.buffer.topic - { + let topic_banner = if let Topic::Banner { max_lines } = config.buffer.topic { banner_view::view( &state.banner_view, banner_view::Kind::ChannelTopic(&state.server, &state.channel), @@ -169,10 +166,18 @@ pub fn view<'a>( }, ) .map(|banner| { - column![container(banner.map(Message::BannerView)) - .style(theme::Container::Banner) - .max_height(topic_banner_max_height)] - .width(Length::Fill) + let mut layout_column = column![]; + for _ in 0..max_lines { + layout_column = layout_column.push(row![].push(selectable_text(" "))); + } + + double_pass( + container(layout_column) + .width(Length::Fill) + .padding(banner_view::padding()), + column![container(banner.map(Message::BannerView)).style(theme::Container::Banner)] + .width(Length::Fill), + ) }) } else { None From 5852354fcc40b0c0f76f8d7998970c065790dc07 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Wed, 6 Mar 2024 19:52:29 -0800 Subject: [PATCH 07/17] =?UTF-8?q?Expect=20topic=20information=20from=20RPL?= =?UTF-8?q?=5FTOPIC(WHOTIME)=20=E2=86=92=20do=20not=20extract=20it=20from?= =?UTF-8?q?=20history.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/client.rs | 47 +++++++++++++++++ data/src/history/manager.rs | 75 ++------------------------ data/src/message.rs | 39 ++------------ data/src/message/broadcast.rs | 2 - data/src/message/source.rs | 29 ---------- src/buffer/banner_view.rs | 99 ++++++++++++++++------------------- src/buffer/channel.rs | 79 +++++++--------------------- 7 files changed, 120 insertions(+), 250 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 1c480ece3..1c2680fb0 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use futures::channel::mpsc; use irc::proto::{self, command, Command}; use itertools::Itertools; @@ -5,6 +6,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt; use std::time::{Duration, Instant}; +use crate::message::server_time; use crate::time::Posix; use crate::user::{Nick, NickRef}; use crate::{config, message, mode, Buffer, Server, User}; @@ -638,6 +640,33 @@ impl Client { } } } + Command::TOPIC(channel, topic) => { + if let Some(channel) = self.chanmap.get_mut(channel) { + channel.topic.text = topic.to_owned(); + + channel.topic.who = message + .user() + .map(|user| user.username().unwrap().to_string()); + channel.topic.time = Some(server_time(&message)); + } + } + Command::Numeric(RPL_TOPIC, args) => { + if let Some(channel) = self.chanmap.get_mut(&args[1]) { + channel.topic.text = Some(args.get(2)?.to_owned()); + } + } + Command::Numeric(RPL_TOPICWHOTIME, args) => { + if let Some(channel) = self.chanmap.get_mut(&args[1]) { + channel.topic.who = Some(args.get(2)?.to_string()); + channel.topic.time = Some( + args.get(3)? + .parse::() + .ok() + .map(Posix::from_seconds)? + .datetime()?, + ); + } + } #[cfg(feature = "dev")] // Suppress topic during development to prevent history spam Command::Numeric(RPL_TOPIC | RPL_TOPICWHOTIME, _) => return None, @@ -665,6 +694,10 @@ impl Client { &self.channels } + fn topic<'a>(&'a self, channel: &str) -> Option<&'a Topic> { + self.chanmap.get(channel).map(|channel| &channel.topic) + } + fn users<'a>(&'a self, channel: &str) -> &'a [User] { self.users .get(channel) @@ -823,6 +856,12 @@ impl Map { .unwrap_or_default() } + pub fn get_channel_topic<'a>(&'a self, server: &Server, channel: &str) -> Option<&'a Topic> { + self.client(server) + .map(|client| client.topic(channel)) + .unwrap_or_default() + } + pub fn get_channels<'a>(&'a self, server: &Server) -> &'a [String] { self.client(server) .map(|client| client.channels()) @@ -950,6 +989,14 @@ enum RegistrationStep { pub struct Channel { pub users: HashSet, pub last_who: Option, + pub topic: Topic, +} + +#[derive(Default, Debug, Clone)] +pub struct Topic { + pub text: Option, + pub who: Option, + pub time: Option>, } #[derive(Debug, Clone, Copy)] diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 34cdcde16..486ef1fc4 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -194,7 +194,7 @@ impl Manager { limit: Option, buffer_config: &config::buffer::Buffer, ) -> Option> { - self.data.history_scroll_view( + self.data.history_view( server, &history::Kind::Channel(channel.to_string()), limit, @@ -202,15 +202,6 @@ impl Manager { ) } - pub fn get_channel_topic( - &self, - server: &Server, - channel: &str, - ) -> Option> { - self.data - .history_banner_view(server, &history::Kind::Channel(channel.to_string())) - } - pub fn get_server_messages( &self, server: &Server, @@ -218,7 +209,7 @@ impl Manager { buffer_config: &config::buffer::Buffer, ) -> Option> { self.data - .history_scroll_view(server, &history::Kind::Server, limit, buffer_config) + .history_view(server, &history::Kind::Server, limit, buffer_config) } pub fn get_query_messages( @@ -228,7 +219,7 @@ impl Manager { limit: Option, buffer_config: &config::buffer::Buffer, ) -> Option> { - self.data.history_scroll_view( + self.data.history_view( server, &history::Kind::Query(nick.clone()), limit, @@ -434,7 +425,7 @@ impl Data { } } - fn history_scroll_view( + fn history_view( &self, server: &server::Server, kind: &history::Kind, @@ -483,7 +474,7 @@ impl Data { } } } else if matches!(buffer_config.topic, Topic::Banner { .. }) { - matches!(source.kind(), source::server::Kind::Topic) + !matches!(source.kind(), source::server::Kind::ReplyTopic) } else { true } @@ -515,62 +506,6 @@ impl Data { }) } - fn history_banner_view( - &self, - server: &server::Server, - kind: &history::Kind, - ) -> Option { - let History::Full { messages, .. } = self.map.get(server)?.get(kind)? else { - return None; - }; - - if let Some(topic_messages) = - messages - .iter() - .rev() - .find_map(|message| match message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::Topic => Some(vec![message]), - source::server::Kind::ReplyTopicWhoTime => { - messages.iter().rev().find_map(|topic_message| { - match topic_message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::ReplyTopic => { - Some(vec![topic_message, message]) - } - _ => None, - }, - _ => None, - } - }) - } - source::server::Kind::ReplyTopic => { - messages - .iter() - .rev() - .find_map(|whotime_message| match whotime_message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::ReplyTopicWhoTime => { - Some(vec![message, whotime_message]) - } - _ => None, - }, - _ => None, - }) - } - _ => None, - }, - _ => None, - }) - { - Some(history::BannerView { - messages: topic_messages, - }) - } else { - Some(history::BannerView { messages: vec![] }) - } - } - fn add_message( &mut self, server: server::Server, diff --git a/data/src/message.rs b/data/src/message.rs index 52baed888..bd88681ff 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -121,16 +121,7 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { channel: target, source: source::Source::Server(None), }), - Command::TOPIC(channel, topic) => Some(Target::Channel { - channel, - source: source::Source::Server(Some(source::Server::new( - source::server::Kind::Topic, - Some(user?.nickname().to_owned()), - topic, - None, - ))), - }), - Command::KICK(channel, _, _) => Some(Target::Channel { + Command::TOPIC(channel, _) | Command::KICK(channel, _, _) => Some(Target::Channel { channel, source: source::Source::Server(None), }), @@ -139,8 +130,6 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { source: source::Source::Server(Some(source::Server::new( source::server::Kind::Part, Some(user?.nickname().to_owned()), - None, - None, ))), }), Command::JOIN(channel, _) => Some(Target::Channel { @@ -148,37 +137,15 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { source: source::Source::Server(Some(source::Server::new( source::server::Kind::Join, Some(user?.nickname().to_owned()), - None, - None, ))), }), - Command::Numeric(RPL_TOPIC, params) => { + Command::Numeric(RPL_TOPIC | RPL_TOPICWHOTIME, params) => { let channel = params.get(1)?.clone(); Some(Target::Channel { channel, source: source::Source::Server(Some(source::Server::new( source::server::Kind::ReplyTopic, None, - Some(params.get(2)?.to_owned()), - None, - ))), - }) - } - Command::Numeric(RPL_TOPICWHOTIME, params) => { - let channel = params.get(1)?.clone(); - Some(Target::Channel { - channel, - source: source::Source::Server(Some(source::Server::new( - source::server::Kind::ReplyTopicWhoTime, - Some(Nick::from(params.get(2)?.to_owned())), - None, - Some( - params - .get(3)? - .parse::() - .ok() - .map(Posix::from_seconds)?, - ), ))), }) } @@ -296,7 +263,7 @@ fn target(message: Encoded, our_nick: &Nick) -> Option { } } -fn server_time(message: &Encoded) -> DateTime { +pub fn server_time(message: &Encoded) -> DateTime { message .tags .iter() diff --git a/data/src/message/broadcast.rs b/data/src/message/broadcast.rs index 700b09cf2..058bb9338 100644 --- a/data/src/message/broadcast.rs +++ b/data/src/message/broadcast.rs @@ -132,8 +132,6 @@ pub fn quit( Cause::Server(Some(source::Server::new( source::server::Kind::Quit, Some(user.nickname().to_owned()), - None, - None, ))), text, ) diff --git a/data/src/message/source.rs b/data/src/message/source.rs index 7cbbde0f7..5a708c033 100644 --- a/data/src/message/source.rs +++ b/data/src/message/source.rs @@ -27,7 +27,6 @@ pub mod server { #![allow(deprecated)] use serde::{Deserialize, Serialize}; - use crate::time::Posix; use crate::user::Nick; #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -42,14 +41,10 @@ pub mod server { pub fn new( kind: Kind, nick: Option, - text: Option, - time: Option, ) -> Self { Self::Details(Details { kind, nick, - text, - time, }) } @@ -66,26 +61,6 @@ pub mod server { Server::Details(details) => details.nick.as_ref(), } } - - pub fn text(&self) -> Option<&str> { - match self { - Server::Kind(_) => None, - Server::Details(details) => { - if let Some(text) = &details.text { - Some(text.as_str()) - } else { - None - } - } - } - } - - pub fn time(&self) -> Option<&Posix> { - match self { - Server::Kind(_) => None, - Server::Details(details) => details.time.as_ref(), - } - } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -95,15 +70,11 @@ pub mod server { Part, Quit, ReplyTopic, - ReplyTopicWhoTime, - Topic, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Details { pub kind: Kind, pub nick: Option, - pub text: Option, - pub time: Option, } } diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs index a12d31542..d79d47624 100644 --- a/src/buffer/banner_view.rs +++ b/src/buffer/banner_view.rs @@ -1,14 +1,12 @@ -use chrono::{DateTime, Utc}; -use data::history; -use data::server::Server; +use data::client::Topic; use data::user::Nick; use data::{Config, User}; -use iced::widget::{column, container, row, scrollable, Row}; +use iced::widget::{column, container, row, scrollable}; use iced::{Command, Length}; use super::user_context; -use crate::widget::{selectable_text, Element}; -use crate::{theme, Theme}; +use crate::theme; +use crate::widget::{selectable_text, Collection, Element}; #[derive(Debug, Clone)] pub enum Message { @@ -21,30 +19,52 @@ pub enum Event { UserContext(user_context::Event), } -#[derive(Debug, Clone, Copy)] -pub enum Kind<'a> { - ChannelTopic(&'a Server, &'a str), -} - pub fn view<'a>( state: &State, - kind: Kind, - history: &'a history::Manager, - format: impl Fn(&'a data::Message) -> Option> + 'a, + topic: &Topic, + users: &[User], + config: &'a Config, ) -> Option> { - let Some(history::BannerView { messages }) = (match kind { - Kind::ChannelTopic(server, channel) => history.get_channel_topic(server, channel), - }) else { - return None; + let set_by = if let Some(who) = topic.who.clone() { + let nick = Nick::from(who.split('!').next()?); + + if let Some(user) = users.iter().find(|user| user.nickname() == nick) { + Some( + row![] + .push(selectable_text("set by ").style(theme::Text::Banner)) + .push( + user_context::view( + selectable_text(who).style(theme::Text::Nickname( + user.color_seed(&config.buffer.nickname.color), + false, + )), + user.clone(), + ) + .map(Message::UserContext), + ) + .push( + selectable_text(format!(" at {}", topic.time?.to_rfc2822())) + .style(theme::Text::Banner), + ), + ) + } else { + Some( + row![] + .push(selectable_text("set by ").style(theme::Text::Banner)) + .push(selectable_text(who).style(theme::Text::Server)) + .push( + selectable_text(format!(" at {}", topic.time?.to_rfc2822())) + .style(theme::Text::Banner), + ), + ) + } + } else { + None }; - let messages = messages.into_iter().filter_map(format).collect::>(); - - if messages.is_empty() { - return None; - } - - let content = column![column(messages)]; + let content = column![] + .push(row![].push(selectable_text(topic.text.clone()?).style(theme::Text::Banner))) + .push_maybe(set_by); Some( scrollable(container(content).width(Length::Fill).padding(padding())) @@ -64,35 +84,6 @@ pub fn padding() -> [u16; 2] { [4, 8] } -pub fn style_topic_who_time<'a>( - who: &str, - time: DateTime, - long_who: Option<&str>, - users: &[User], - config: &'a Config, -) -> Row<'a, Message, Theme> { - if let Some(user) = users.iter().find(|user| user.nickname() == Nick::from(who)) { - row![] - .push(selectable_text("set by ").style(theme::Text::Banner)) - .push( - user_context::view( - selectable_text(long_who.unwrap_or(who)).style(theme::Text::Nickname( - user.color_seed(&config.buffer.nickname.color), - false, - )), - user.clone(), - ) - .map(Message::UserContext), - ) - .push(selectable_text(format!(" at {}", time.to_rfc2822())).style(theme::Text::Banner)) - } else { - row![] - .push(selectable_text("set by ").style(theme::Text::Banner)) - .push(selectable_text(long_who.unwrap_or(who)).style(theme::Text::Server)) - .push(selectable_text(format!(" at {}", time.to_rfc2822())).style(theme::Text::Banner)) - } -} - #[derive(Debug, Clone)] pub struct State { pub scrollable: scrollable::Id, diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index fe6f1e010..cf06c2848 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,5 +1,4 @@ -use data::buffer::Topic; -use data::message::source; +use data::buffer; use data::server::Server; use data::{channel, client, history, message, Config}; use iced::widget::{column, container, row}; @@ -120,65 +119,27 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic_banner = if let Topic::Banner { max_lines } = config.buffer.topic { - banner_view::view( - &state.banner_view, - banner_view::Kind::ChannelTopic(&state.server, &state.channel), - history, - move |message| match message.target.source() { - message::Source::Server(Some(source)) => match source.kind() { - source::server::Kind::Topic => { - let topic = - row![].push(selectable_text(source.text()?).style(theme::Text::Banner)); - - let nick = banner_view::style_topic_who_time( - source.nick()?.as_ref(), - message.server_time, - None, - users, - config, - ); - - Some(container(column![].push(topic).push(nick)).into()) - } - source::server::Kind::ReplyTopic => { - let topic = - row![].push(selectable_text(source.text()?).style(theme::Text::Banner)); - - Some(container(topic).into()) - } - source::server::Kind::ReplyTopicWhoTime => { - let nick = source.nick()?.as_ref().split('!').next()?; - - let nick = banner_view::style_topic_who_time( - nick, - source.time()?.datetime()?, - Some(source.nick()?.as_ref()), - users, - config, - ); - - Some(container(nick).into()) - } - _ => None, - }, - _ => None, - }, - ) - .map(|banner| { - let mut layout_column = column![]; - for _ in 0..max_lines { - layout_column = layout_column.push(row![].push(selectable_text(" "))); - } + let topic_banner = if let buffer::Topic::Banner { max_lines } = config.buffer.topic { + if let Some(topic) = clients.get_channel_topic(&state.server, &state.channel) { + banner_view::view(&state.banner_view, topic, users, config).map(|banner| { + let mut layout_column = column![]; + for _ in 0..max_lines { + layout_column = layout_column.push(row![].push(selectable_text(" "))); + } - double_pass( - container(layout_column) - .width(Length::Fill) - .padding(banner_view::padding()), - column![container(banner.map(Message::BannerView)).style(theme::Container::Banner)] + double_pass( + container(layout_column) + .width(Length::Fill) + .padding(banner_view::padding()), + column![ + container(banner.map(Message::BannerView)).style(theme::Container::Banner) + ] .width(Length::Fill), - ) - }) + ) + }) + } else { + None + } } else { None }; From 840e8f525cc6d96fcd19a2a84552d84c8d66d661 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 7 Mar 2024 10:15:47 -0800 Subject: [PATCH 08/17] Move unreachable code --- data/src/client.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/src/client.rs b/data/src/client.rs index 1c2680fb0..9c271d3ec 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -654,6 +654,9 @@ impl Client { if let Some(channel) = self.chanmap.get_mut(&args[1]) { channel.topic.text = Some(args.get(2)?.to_owned()); } + // Exclude topic message from history to prevent spam during dev + #[cfg(feature = "dev")] + return None; } Command::Numeric(RPL_TOPICWHOTIME, args) => { if let Some(channel) = self.chanmap.get_mut(&args[1]) { @@ -666,10 +669,10 @@ impl Client { .datetime()?, ); } + // Exclude topic message from history to prevent spam during dev + #[cfg(feature = "dev")] + return None; } - #[cfg(feature = "dev")] - // Suppress topic during development to prevent history spam - Command::Numeric(RPL_TOPIC | RPL_TOPICWHOTIME, _) => return None, _ => {} } From 907192245be748c099a3fa336198ac9bc9ee9002 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 7 Mar 2024 11:33:39 -0800 Subject: [PATCH 09/17] Remove dead code --- data/src/history.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/data/src/history.rs b/data/src/history.rs index a57b0320c..8b0fee180 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -274,11 +274,6 @@ pub struct View<'a> { pub new_messages: Vec<&'a Message>, } -#[derive(Debug)] -pub struct BannerView<'a> { - pub messages: Vec<&'a Message>, -} - #[derive(Debug, Clone, Default)] struct Input(HashMap>); From 7400f4c2229ee8fd99b61a02b8449d5f600da899 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 7 Mar 2024 11:33:52 -0800 Subject: [PATCH 10/17] Remove redundant module path --- data/src/history/manager.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 486ef1fc4..9e9b481cd 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -192,7 +192,7 @@ impl Manager { server: &Server, channel: &str, limit: Option, - buffer_config: &config::buffer::Buffer, + buffer_config: &config::Buffer, ) -> Option> { self.data.history_view( server, @@ -206,7 +206,7 @@ impl Manager { &self, server: &Server, limit: Option, - buffer_config: &config::buffer::Buffer, + buffer_config: &config::Buffer, ) -> Option> { self.data .history_view(server, &history::Kind::Server, limit, buffer_config) @@ -217,7 +217,7 @@ impl Manager { server: &Server, nick: &Nick, limit: Option, - buffer_config: &config::buffer::Buffer, + buffer_config: &config::Buffer, ) -> Option> { self.data.history_view( server, @@ -430,7 +430,7 @@ impl Data { server: &server::Server, kind: &history::Kind, limit: Option, - buffer_config: &config::buffer::Buffer, + buffer_config: &config::Buffer, ) -> Option { let History::Full { messages, From afa2cd71b65ad920705a30f602f87e36af9e4c19 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 7 Mar 2024 11:34:06 -0800 Subject: [PATCH 11/17] Use message source as logic branch Since the first if branch is on message source as well --- data/src/history/manager.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 9e9b481cd..0c41d5361 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -473,8 +473,10 @@ impl Data { } } } - } else if matches!(buffer_config.topic, Topic::Banner { .. }) { - !matches!(source.kind(), source::server::Kind::ReplyTopic) + } + // ReplyTopic messages are not shown when topic banner is visible + else if matches!(source.kind(), source::server::Kind::ReplyTopic) { + !matches!(buffer_config.topic, Topic::Banner { .. }) } else { true } From 9b1507dc9f095054403cdadd97357823c6f96479 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 7 Mar 2024 12:11:58 -0800 Subject: [PATCH 12/17] Clean up view code --- src/buffer.rs | 1 - src/buffer/banner_view.rs | 115 ------------------------------------ src/buffer/channel.rs | 83 ++++++++++++-------------- src/buffer/channel/topic.rs | 63 ++++++++++++++++++++ 4 files changed, 102 insertions(+), 160 deletions(-) delete mode 100644 src/buffer/banner_view.rs create mode 100644 src/buffer/channel/topic.rs diff --git a/src/buffer.rs b/src/buffer.rs index 5bf8a0743..ee47fd83a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -7,7 +7,6 @@ use self::query::Query; use self::server::Server; use crate::widget::Element; -mod banner_view; pub mod channel; pub mod empty; mod input_view; diff --git a/src/buffer/banner_view.rs b/src/buffer/banner_view.rs deleted file mode 100644 index d79d47624..000000000 --- a/src/buffer/banner_view.rs +++ /dev/null @@ -1,115 +0,0 @@ -use data::client::Topic; -use data::user::Nick; -use data::{Config, User}; -use iced::widget::{column, container, row, scrollable}; -use iced::{Command, Length}; - -use super::user_context; -use crate::theme; -use crate::widget::{selectable_text, Collection, Element}; - -#[derive(Debug, Clone)] -pub enum Message { - Scrolled { viewport: scrollable::Viewport }, - UserContext(user_context::Message), -} - -#[derive(Debug, Clone)] -pub enum Event { - UserContext(user_context::Event), -} - -pub fn view<'a>( - state: &State, - topic: &Topic, - users: &[User], - config: &'a Config, -) -> Option> { - let set_by = if let Some(who) = topic.who.clone() { - let nick = Nick::from(who.split('!').next()?); - - if let Some(user) = users.iter().find(|user| user.nickname() == nick) { - Some( - row![] - .push(selectable_text("set by ").style(theme::Text::Banner)) - .push( - user_context::view( - selectable_text(who).style(theme::Text::Nickname( - user.color_seed(&config.buffer.nickname.color), - false, - )), - user.clone(), - ) - .map(Message::UserContext), - ) - .push( - selectable_text(format!(" at {}", topic.time?.to_rfc2822())) - .style(theme::Text::Banner), - ), - ) - } else { - Some( - row![] - .push(selectable_text("set by ").style(theme::Text::Banner)) - .push(selectable_text(who).style(theme::Text::Server)) - .push( - selectable_text(format!(" at {}", topic.time?.to_rfc2822())) - .style(theme::Text::Banner), - ), - ) - } - } else { - None - }; - - let content = column![] - .push(row![].push(selectable_text(topic.text.clone()?).style(theme::Text::Banner))) - .push_maybe(set_by); - - Some( - scrollable(container(content).width(Length::Fill).padding(padding())) - .style(theme::Scrollable::Banner) - .direction(scrollable::Direction::Vertical( - scrollable::Properties::default() - .alignment(scrollable::Alignment::Start) - .width(5) - .scroller_width(5), - )) - .id(state.scrollable.clone()) - .into(), - ) -} - -pub fn padding() -> [u16; 2] { - [4, 8] -} - -#[derive(Debug, Clone)] -pub struct State { - pub scrollable: scrollable::Id, -} - -impl Default for State { - fn default() -> Self { - Self { - scrollable: scrollable::Id::unique(), - } - } -} - -impl State { - pub fn new() -> Self { - Self::default() - } - - pub fn update(&mut self, message: Message) -> (Command, Option) { - if let Message::UserContext(message) = message { - return ( - Command::none(), - Some(Event::UserContext(user_context::update(message))), - ); - } - - (Command::none(), None) - } -} diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index cf06c2848..da851206c 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,19 +1,20 @@ -use data::buffer; use data::server::Server; +use data::{buffer, User}; use data::{channel, client, history, message, Config}; use iced::widget::{column, container, row}; use iced::{Command, Length}; -use super::{banner_view, input_view, scroll_view, user_context}; +use super::{input_view, scroll_view, user_context}; use crate::theme; -use crate::widget::{double_pass, selectable_text, Collection, Element}; +use crate::widget::{selectable_text, Collection, Element}; + +mod topic; #[derive(Debug, Clone)] pub enum Message { ScrollView(scroll_view::Message), InputView(input_view::Message), UserContext(user_context::Message), - BannerView(banner_view::Message), } #[derive(Debug, Clone)] @@ -119,30 +120,7 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic_banner = if let buffer::Topic::Banner { max_lines } = config.buffer.topic { - if let Some(topic) = clients.get_channel_topic(&state.server, &state.channel) { - banner_view::view(&state.banner_view, topic, users, config).map(|banner| { - let mut layout_column = column![]; - for _ in 0..max_lines { - layout_column = layout_column.push(row![].push(selectable_text(" "))); - } - - double_pass( - container(layout_column) - .width(Length::Fill) - .padding(banner_view::padding()), - column![ - container(banner.map(Message::BannerView)).style(theme::Container::Banner) - ] - .width(Length::Fill), - ) - }) - } else { - None - } - } else { - None - }; + let topic = topic(state, clients, users, config); let text_input = show_text_input.then(|| { column![input_view::view( @@ -167,14 +145,13 @@ pub fn view<'a>( (false, _) => { row![messages] }.height(Length::Fill), }; - let scrollable = column![] - .push_maybe(topic_banner) + let body = column![] + .push_maybe(topic) .push(container(content).height(Length::Fill)) .push_maybe(text_input) - .height(Length::Fill) - .spacing(4); + .height(Length::Fill); - container(scrollable) + container(body) .width(Length::Fill) .height(Length::Fill) .padding(8) @@ -186,7 +163,6 @@ pub struct Channel { pub server: Server, pub channel: String, - pub banner_view: banner_view::State, pub scroll_view: scroll_view::State, pub input_view: input_view::State, } @@ -196,7 +172,6 @@ impl Channel { Self { server, channel, - banner_view: banner_view::State::new(), scroll_view: scroll_view::State::new(), input_view: input_view::State::new(), } @@ -213,15 +188,6 @@ impl Channel { history: &mut history::Manager, ) -> (Command, Option) { match message { - Message::BannerView(message) => { - let (command, event) = self.banner_view.update(message); - - let event = event.map(|event| match event { - banner_view::Event::UserContext(event) => Event::UserContext(event), - }); - - (command.map(Message::BannerView), event) - } Message::ScrollView(message) => { let (command, event) = self.scroll_view.update(message); @@ -263,6 +229,35 @@ impl Channel { } } +fn topic<'a>( + state: &'a Channel, + clients: &'a data::client::Map, + users: &'a [User], + config: &'a Config, +) -> Option> { + let buffer::Topic::Banner { max_lines } = config.buffer.topic else { + return None; + }; + + let topic = clients.get_channel_topic(&state.server, &state.channel)?; + + Some( + container( + topic::view( + topic.text.as_deref()?, + topic.who.as_deref(), + topic.time.as_ref(), + max_lines, + users, + config, + ) + .map(Message::UserContext), + ) + .padding([0, 0, 4, 0]) + .into(), + ) +} + mod nick_list { use data::{Config, User}; use iced::widget::{column, container, scrollable, text}; diff --git a/src/buffer/channel/topic.rs b/src/buffer/channel/topic.rs new file mode 100644 index 000000000..3b6bef8c0 --- /dev/null +++ b/src/buffer/channel/topic.rs @@ -0,0 +1,63 @@ +use chrono::{DateTime, Utc}; +use data::user::Nick; +use data::{Config, User}; +use iced::widget::{column, container, row, scrollable}; +use iced::Length; + +use super::user_context; +use crate::theme; +use crate::widget::{double_pass, selectable_text, Collection, Element}; + +pub fn view<'a>( + text: &'a str, + who: Option<&'a str>, + time: Option<&'a DateTime>, + max_lines: u16, + users: &[User], + config: &'a Config, +) -> Element<'a, user_context::Message> { + let set_by = who.and_then(|who| { + let nick = Nick::from(who.split('!').next()?); + + let user = if let Some(user) = users.iter().find(|user| user.nickname() == nick) { + user_context::view( + selectable_text(who).style(theme::Text::Nickname( + user.color_seed(&config.buffer.nickname.color), + false, + )), + user.clone(), + ) + } else { + selectable_text(who).style(theme::Text::Server).into() + }; + + Some(row![ + selectable_text("set by ").style(theme::Text::Banner), + user, + selectable_text(format!(" at {}", time?.to_rfc2822())).style(theme::Text::Banner), + ]) + }); + + let content = column![selectable_text(text).style(theme::Text::Banner)].push_maybe(set_by); + + let scrollable = scrollable(container(content).width(Length::Fill).padding(padding())) + .style(theme::Scrollable::Banner) + .direction(scrollable::Direction::Vertical( + scrollable::Properties::default() + .alignment(scrollable::Alignment::Start) + .width(5) + .scroller_width(5), + )); + + // Use double pass to limit layout to `max_lines` of text + double_pass( + container(column((0..max_lines).map(|_| "".into()))) + .width(Length::Fill) + .padding(padding()), + column![container(scrollable).style(theme::Container::Banner)].width(Length::Fill), + ) +} + +fn padding() -> [u16; 2] { + [4, 8] +} From 4bbca7e1723b6dbd2fa4d4d83ead54cccca193cb Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Thu, 7 Mar 2024 12:49:34 -0800 Subject: [PATCH 13/17] Revert vertical_space removal from 4ee37fa1186b3f9144ac66f6e8ce53d32cda15b5. --- src/buffer/channel.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index da851206c..1f1c0f3aa 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,7 +1,7 @@ use data::server::Server; use data::{buffer, User}; use data::{channel, client, history, message, Config}; -use iced::widget::{column, container, row}; +use iced::widget::{column, container, row, vertical_space}; use iced::{Command, Length}; use super::{input_view, scroll_view, user_context}; @@ -123,15 +123,18 @@ pub fn view<'a>( let topic = topic(state, clients, users, config); let text_input = show_text_input.then(|| { - column![input_view::view( - &state.input_view, - buffer, - users, - channels, - input_history, - is_focused - ) - .map(Message::InputView)] + column![ + vertical_space(4), + input_view::view( + &state.input_view, + buffer, + users, + channels, + input_history, + is_focused + ) + .map(Message::InputView) + ] .width(Length::Fill) }); From 49732a77b088e0dd305c0150b1d3e173f2505788 Mon Sep 17 00:00:00 2001 From: Andrew Baldwin Date: Sat, 9 Mar 2024 04:00:47 -0800 Subject: [PATCH 14/17] =?UTF-8?q?Make=20topic=20(banner)=20a=20buffer?= =?UTF-8?q?=E2=86=92channel=20configuration=20(instead=20of=20a=20buffer?= =?UTF-8?q?=20configuration).=20Add=20topic=20toggle=20button=20to=20pane?= =?UTF-8?q?=20(functionality=20mimicking=20the=20existing=20users=20toggle?= =?UTF-8?q?=20button).=20Tweak=20padding=20under=20topic=20banner=20/=20ab?= =?UTF-8?q?ove=20message=20history.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/src/buffer.rs | 14 ------------ data/src/channel.rs | 21 +++++++++++++++++ data/src/config/buffer.rs | 11 ++++----- data/src/config/channel.rs | 14 ++++++++++++ data/src/history/manager.rs | 6 ----- src/buffer/channel.rs | 26 ++++++++++++--------- src/buffer/channel/topic.rs | 29 ++++++++++++++---------- src/icon.rs | 4 ++++ src/screen/dashboard.rs | 6 +++++ src/screen/dashboard/pane.rs | 26 ++++++++++++++++++++- src/theme.rs | 44 ------------------------------------ 11 files changed, 107 insertions(+), 94 deletions(-) diff --git a/data/src/buffer.rs b/data/src/buffer.rs index 49d4e3934..55e1a8b50 100644 --- a/data/src/buffer.rs +++ b/data/src/buffer.rs @@ -66,20 +66,6 @@ pub enum InputVisibility { Always, } -#[derive(Debug, Clone, Copy, Default, Deserialize)] -pub enum Topic { - #[default] - Inline, - Banner { - #[serde(default = "default_topic_banner_max_lines")] - max_lines: u16, - }, -} - -fn default_topic_banner_max_lines() -> u16 { - 2 -} - #[derive(Debug, Clone, Deserialize)] pub struct Timestamp { pub format: String, diff --git a/data/src/channel.rs b/data/src/channel.rs index ffc67f4aa..3d64923b6 100644 --- a/data/src/channel.rs +++ b/data/src/channel.rs @@ -5,12 +5,14 @@ use crate::config; #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct Settings { pub users: Users, + pub topic: Topic, } impl From for Settings { fn from(config: config::Channel) -> Self { Self { users: Users::from(config.users), + topic: Topic::from(config.topic), } } } @@ -46,3 +48,22 @@ impl Users { self.visible = !self.visible } } + +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)] +pub struct Topic { + pub visible: bool, +} + +impl From for Topic { + fn from(config: config::channel::Topic) -> Self { + Topic { + visible: config.visible, + } + } +} + +impl Topic { + pub fn toggle_visibility(&mut self) { + self.visible = !self.visible + } +} diff --git a/data/src/config/buffer.rs b/data/src/config/buffer.rs index 9c4af6d26..e6bc87bdc 100644 --- a/data/src/config/buffer.rs +++ b/data/src/config/buffer.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use super::Channel; use crate::{ - buffer::{Color, InputVisibility, Nickname, Timestamp, Topic}, + buffer::{Color, InputVisibility, Nickname, Timestamp}, message::source, }; @@ -19,8 +19,6 @@ pub struct Buffer { pub channel: Channel, #[serde(default)] pub server_messages: ServerMessages, - #[serde(default)] - pub topic: Topic, } #[derive(Debug, Copy, Clone, Default, Deserialize)] @@ -33,6 +31,8 @@ pub enum Exclude { #[derive(Debug, Clone, Default, Deserialize)] pub struct ServerMessages { + #[serde(default)] + pub topic: ServerMessage, #[serde(default)] pub join: ServerMessage, #[serde(default)] @@ -44,10 +44,10 @@ pub struct ServerMessages { impl ServerMessages { pub fn get(&self, server: &source::Server) -> Option { match server.kind() { - source::server::Kind::Join => Some(self.join), + source::server::Kind::ReplyTopic => Some(self.topic), source::server::Kind::Part => Some(self.part), source::server::Kind::Quit => Some(self.quit), - _ => None, + source::server::Kind::Join => Some(self.join), } } } @@ -81,7 +81,6 @@ impl Default for Buffer { input_visibility: InputVisibility::default(), channel: Channel::default(), server_messages: Default::default(), - topic: Topic::default(), } } } diff --git a/data/src/config/channel.rs b/data/src/config/channel.rs index fb0e0bb4e..60e4ebc87 100644 --- a/data/src/config/channel.rs +++ b/data/src/config/channel.rs @@ -6,7 +6,9 @@ use crate::channel::Position; #[derive(Debug, Clone, Default, Deserialize)] pub struct Channel { pub users: Users, + pub topic: Topic, } + #[derive(Debug, Clone, Copy, Deserialize)] pub struct Users { pub(crate) visible: bool, @@ -25,3 +27,15 @@ impl Default for Users { } } } + +#[derive(Debug, Clone, Copy, Default, Deserialize)] +pub struct Topic { + #[serde(default)] + pub(crate) visible: bool, + #[serde(default = "default_topic_banner_max_lines")] + pub max_lines: u16, +} + +fn default_topic_banner_max_lines() -> u16 { + 2 +} diff --git a/data/src/history/manager.rs b/data/src/history/manager.rs index 0c41d5361..71afcae4a 100644 --- a/data/src/history/manager.rs +++ b/data/src/history/manager.rs @@ -6,11 +6,9 @@ use futures::{future, Future, FutureExt}; use itertools::Itertools; use tokio::time::Instant; -use crate::buffer::Topic; use crate::config; use crate::config::buffer::Exclude; use crate::history::{self, History}; -use crate::message::source; use crate::message::{self, Limit}; use crate::time::Posix; use crate::user::{Nick, NickRef}; @@ -473,10 +471,6 @@ impl Data { } } } - } - // ReplyTopic messages are not shown when topic banner is visible - else if matches!(source.kind(), source::server::Kind::ReplyTopic) { - !matches!(buffer_config.topic, Topic::Banner { .. }) } else { true } diff --git a/src/buffer/channel.rs b/src/buffer/channel.rs index 1f1c0f3aa..a72c213cf 100644 --- a/src/buffer/channel.rs +++ b/src/buffer/channel.rs @@ -1,5 +1,5 @@ use data::server::Server; -use data::{buffer, User}; +use data::User; use data::{channel, client, history, message, Config}; use iced::widget::{column, container, row, vertical_space}; use iced::{Command, Length}; @@ -120,7 +120,9 @@ pub fn view<'a>( data::buffer::InputVisibility::Always => status.connected(), }; - let topic = topic(state, clients, users, config); + // If topic toggles from None to Some then it messes with messages' scroll state, + // so produce a zero-height placeholder when topic is None. + let topic = topic(state, clients, users, settings, config).unwrap_or_else(|| column![].into()); let text_input = show_text_input.then(|| { column![ @@ -138,18 +140,19 @@ pub fn view<'a>( .width(Length::Fill) }); + let content = column![].push(topic).push(messages); + let content = match (settings.users.visible, config.buffer.channel.users.position) { (true, data::channel::Position::Left) => { - row![nick_list, messages] + row![nick_list, content] } (true, data::channel::Position::Right) => { - row![messages, nick_list] + row![content, nick_list] } - (false, _) => { row![messages] }.height(Length::Fill), + (false, _) => { row![content] }.height(Length::Fill), }; let body = column![] - .push_maybe(topic) .push(container(content).height(Length::Fill)) .push_maybe(text_input) .height(Length::Fill); @@ -157,7 +160,7 @@ pub fn view<'a>( container(body) .width(Length::Fill) .height(Length::Fill) - .padding(8) + .padding([4, 8, 8, 8]) .into() } @@ -236,11 +239,12 @@ fn topic<'a>( state: &'a Channel, clients: &'a data::client::Map, users: &'a [User], + settings: &'a channel::Settings, config: &'a Config, ) -> Option> { - let buffer::Topic::Banner { max_lines } = config.buffer.topic else { + if !settings.topic.visible { return None; - }; + } let topic = clients.get_channel_topic(&state.server, &state.channel)?; @@ -250,13 +254,13 @@ fn topic<'a>( topic.text.as_deref()?, topic.who.as_deref(), topic.time.as_ref(), - max_lines, + config.buffer.channel.topic.max_lines, users, config, ) .map(Message::UserContext), ) - .padding([0, 0, 4, 0]) + .padding([0, 0, 3, 0]) .into(), ) } diff --git a/src/buffer/channel/topic.rs b/src/buffer/channel/topic.rs index 3b6bef8c0..f31042303 100644 --- a/src/buffer/channel/topic.rs +++ b/src/buffer/channel/topic.rs @@ -1,7 +1,7 @@ use chrono::{DateTime, Utc}; use data::user::Nick; use data::{Config, User}; -use iced::widget::{column, container, row, scrollable}; +use iced::widget::{column, container, horizontal_rule, row, scrollable, vertical_space}; use iced::Length; use super::user_context; @@ -32,16 +32,16 @@ pub fn view<'a>( }; Some(row![ - selectable_text("set by ").style(theme::Text::Banner), + selectable_text("set by ").style(theme::Text::Transparent), user, - selectable_text(format!(" at {}", time?.to_rfc2822())).style(theme::Text::Banner), + selectable_text(format!(" at {}", time?.to_rfc2822())).style(theme::Text::Transparent), ]) }); - let content = column![selectable_text(text).style(theme::Text::Banner)].push_maybe(set_by); + let content = column![selectable_text(text).style(theme::Text::Transparent)].push_maybe(set_by); let scrollable = scrollable(container(content).width(Length::Fill).padding(padding())) - .style(theme::Scrollable::Banner) + .style(theme::Scrollable::Hidden) .direction(scrollable::Direction::Vertical( scrollable::Properties::default() .alignment(scrollable::Alignment::Start) @@ -50,14 +50,19 @@ pub fn view<'a>( )); // Use double pass to limit layout to `max_lines` of text - double_pass( - container(column((0..max_lines).map(|_| "".into()))) - .width(Length::Fill) - .padding(padding()), - column![container(scrollable).style(theme::Container::Banner)].width(Length::Fill), - ) + column![ + double_pass( + container(column((0..max_lines).map(|_| "".into()))) + .width(Length::Fill) + .padding(padding()), + column![container(scrollable)].width(Length::Fill), + ), + vertical_space(1), + horizontal_rule(1).style(theme::Rule::Default) + ] + .into() } fn padding() -> [u16; 2] { - [4, 8] + [0, 8] } diff --git a/src/icon.rs b/src/icon.rs index 97fad4616..2b95988bd 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -38,6 +38,10 @@ pub fn people<'a>() -> Text<'a> { to_text('\u{f4db}') } +pub fn topic<'a>() -> Text<'a> { + to_text('\u{f5af}') +} + fn to_text<'a>(unicode: char) -> Text<'a> { text(unicode.to_string()) .style(theme::Text::Primary) diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index 33a4d40a4..d43369f1a 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -162,6 +162,12 @@ impl Dashboard { self.last_changed = Some(Instant::now()); } } + pane::Message::ToggleShowTopic => { + if let Some((_, pane)) = self.get_focused_mut() { + pane.update_settings(|settings| settings.channel.topic.toggle_visibility()); + self.last_changed = Some(Instant::now()); + } + } pane::Message::MaximizePane => self.maximize_pane(), }, Message::SideMenu(message) => { diff --git a/src/screen/dashboard/pane.rs b/src/screen/dashboard/pane.rs index 894b6e4c5..5264ce99e 100644 --- a/src/screen/dashboard/pane.rs +++ b/src/screen/dashboard/pane.rs @@ -16,6 +16,7 @@ pub enum Message { SplitPane(pane_grid::Axis), MaximizePane, ToggleShowUserList, + ToggleShowTopic, } #[derive(Clone)] @@ -80,6 +81,7 @@ impl Pane { panes, is_focused, maximized, + clients, &self.settings, ); @@ -127,12 +129,34 @@ impl TitleBar { panes: usize, _is_focused: bool, maximized: bool, + clients: &'a data::client::Map, settings: &'a buffer::Settings, ) -> widget::TitleBar<'a, Message> { // Pane controls. let mut controls = row![].spacing(2); - if let Buffer::Channel(_) = &buffer { + if let Buffer::Channel(state) = &buffer { + // Show topic button only if there is a topic to show + if let Some(topic) = clients.get_channel_topic(&state.server, &state.channel) { + if topic.text.is_some() { + let topic = button( + container(icon::topic()) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(), + ) + .width(22) + .height(22) + .on_press(Message::ToggleShowTopic) + .style(theme::Button::Pane { + selected: settings.channel.topic.visible, + }); + + controls = controls.push(topic); + } + } + let users = button( container(icon::people()) .width(Length::Fill) diff --git a/src/theme.rs b/src/theme.rs index 59f28c67d..8e0e48c63 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -109,7 +109,6 @@ pub enum Text { Transparent, Status(message::source::Status), Nickname(Option, bool), - Banner, } impl text::StyleSheet for Theme { @@ -160,9 +159,6 @@ impl text::StyleSheet for Theme { Text::Transparent => text::Appearance { color: Some(self.colors().text.low_alpha), }, - Text::Banner => text::Appearance { - color: Some(self.colors().text.base), - }, } } } @@ -182,7 +178,6 @@ pub enum Container { Context, Highlight, SemiTransparent, - Banner, } impl container::StyleSheet for Theme { @@ -264,19 +259,6 @@ impl container::StyleSheet for Theme { ), ..Default::default() }, - Container::Banner => container::Appearance { - background: Some(Background::Color(if self.colors().is_dark_theme() { - self.colors().background.light - } else { - self.colors().background.dark - })), - border: Border { - radius: [4.0, 4.0, 4.0, 4.0].into(), - width: 1.0, - color: Color::TRANSPARENT, - }, - ..Default::default() - }, } } } @@ -459,7 +441,6 @@ pub enum Scrollable { #[default] Default, Hidden, - Banner, } impl scrollable::StyleSheet for Theme { @@ -499,30 +480,6 @@ impl scrollable::StyleSheet for Theme { }, }, }, - Scrollable::Banner => scrollable::Scrollbar { - background: Some(Background::Color(if self.colors().is_dark_theme() { - self.colors().background.lighter - } else { - self.colors().background.darker - })), - border: Border { - radius: 8.0.into(), - width: 1.0, - color: Color::TRANSPARENT, - }, - scroller: scrollable::Scroller { - color: if self.colors().is_dark_theme() { - self.colors().background.lightest - } else { - self.colors().background.darkest - }, - border: Border { - radius: 8.0.into(), - width: 0.0, - color: Color::TRANSPARENT, - }, - }, - }, } } @@ -535,7 +492,6 @@ impl scrollable::StyleSheet for Theme { match style { Scrollable::Default => scrollable::Scrollbar { ..active }, Scrollable::Hidden => scrollable::Scrollbar { ..active }, - Scrollable::Banner => scrollable::Scrollbar { ..active }, } } } From bb1e08d8e94564a83eb43d6b9141c5b0be7c2de9 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Sat, 9 Mar 2024 15:55:14 +0100 Subject: [PATCH 15/17] updated config.yaml and spacing --- config.yaml | 20 ++++++++------------ src/buffer/channel/topic.rs | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/config.yaml b/config.yaml index 42867649d..878b33d9d 100644 --- a/config.yaml +++ b/config.yaml @@ -86,17 +86,6 @@ buffer: # - Focused: Only show input when the buffer is focused input_visibility: Always - # Topic settings: - # - Inline: Show topic as server messages in each channel. [default] - # - !Banner: Show topic as a banner at the topic of each channel. - # Topic changes will still show up as a server - # message in channel. - topic: !Banner - # Maximum visible lines of the topic banner before scrolling, when - # topic is set to !Banner. - # - Default is 2 - max_lines: 2 - # Control different server messages. # - exclude [All, None, !Smart seconds]: # - Smart will show a server message if the user has sent a message @@ -126,7 +115,14 @@ buffer: # - Unique: Unique user colors [default] # - Solid: Solid user colors color: Unique - + # Topic banner settings: + topic: + # Visible by default + # - Default is false + visible: true + # Maximum visible lines of the topic banner before scrolling + # - Default is 2 + max_lines: 3 # Dashboard settings dashboard: diff --git a/src/buffer/channel/topic.rs b/src/buffer/channel/topic.rs index f31042303..4205c1cfc 100644 --- a/src/buffer/channel/topic.rs +++ b/src/buffer/channel/topic.rs @@ -57,7 +57,7 @@ pub fn view<'a>( .padding(padding()), column![container(scrollable)].width(Length::Fill), ), - vertical_space(1), + vertical_space(4), horizontal_rule(1).style(theme::Rule::Default) ] .into() From 57727c41cab305c5338b252d6d90000b451b2ad9 Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Sat, 9 Mar 2024 15:58:16 +0100 Subject: [PATCH 16/17] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f3f659ab..ef5c52a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Unreleased +Added: + +- Configuration option to enable a topic banner in channels. This can be enabled under `buffer.channel.topic` + Fix: - Context menus now shows buttons as expected @@ -22,6 +26,7 @@ Fixed: - Clipped buttons in context menu Changed: + - Improved user experience in text input when auto-completing a nickname. - Configuration option `server_messages` changed `exclude` from a boolean value to [`All`, `None` or `!Smart seconds`]. - `All` excludes all messages for the specific server message. From ac20dde4bbeff3cee10dbd7a076401d28b5c1c7c Mon Sep 17 00:00:00 2001 From: Casper Rogild Storm Date: Sat, 9 Mar 2024 16:01:04 +0100 Subject: [PATCH 17/17] Update config.yaml --- config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 878b33d9d..f76c0ed9a 100644 --- a/config.yaml +++ b/config.yaml @@ -117,7 +117,7 @@ buffer: color: Unique # Topic banner settings: topic: - # Visible by default + # Visibility # - Default is false visible: true # Maximum visible lines of the topic banner before scrolling