Implement chat history and multiline messages
This commit is contained in:
parent
63b04208d0
commit
9006f53e4d
6 changed files with 361 additions and 79 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -477,6 +477,7 @@ dependencies = [
|
||||||
name = "cri"
|
name = "cri"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"iced",
|
"iced",
|
||||||
|
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = "0.4.31"
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
futures = "0.3.29"
|
futures = "0.3.29"
|
||||||
iced = { version = "0.10.0", features = ["tokio"] }
|
iced = { version = "0.10.0", features = ["tokio"] }
|
||||||
|
|
|
@ -8,7 +8,16 @@ use tokio::{
|
||||||
|
|
||||||
pub async fn connect() -> Result<irc::client::Client> {
|
pub async fn connect() -> Result<irc::client::Client> {
|
||||||
let client = irc::client::Client::new("config.toml").await?;
|
let client = irc::client::Client::new("config.toml").await?;
|
||||||
client.send_cap_req(&[Capability::EchoMessage, Capability::ServerTime])?;
|
client.send_cap_req(&[
|
||||||
|
Capability::AccountTag,
|
||||||
|
Capability::Batch,
|
||||||
|
Capability::EchoMessage,
|
||||||
|
Capability::ServerTime,
|
||||||
|
Capability::Custom("message-tags"),
|
||||||
|
Capability::Custom("draft/chathistory"),
|
||||||
|
Capability::Custom("draft/event-playback"),
|
||||||
|
Capability::Custom("draft/multiline"),
|
||||||
|
])?;
|
||||||
Ok(client)
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
pub enum IrcMessage {
|
use chrono::{DateTime, Local};
|
||||||
|
|
||||||
|
pub struct IrcMessage {
|
||||||
|
pub detail: MessageDetail,
|
||||||
|
pub message_id: Option<String>,
|
||||||
|
pub timestamp: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MessageDetail {
|
||||||
Join {
|
Join {
|
||||||
nickname: String,
|
nickname: String,
|
||||||
},
|
},
|
||||||
|
|
103
src/main.rs
103
src/main.rs
|
@ -2,7 +2,7 @@ mod irc_handler;
|
||||||
mod irc_message;
|
mod irc_message;
|
||||||
mod message_log;
|
mod message_log;
|
||||||
|
|
||||||
use crate::{irc_message::IrcMessage, message_log::MessageLog};
|
use chrono::{DateTime, Local};
|
||||||
use color_eyre::eyre::Result;
|
use color_eyre::eyre::Result;
|
||||||
use iced::{
|
use iced::{
|
||||||
alignment::{Horizontal, Vertical},
|
alignment::{Horizontal, Vertical},
|
||||||
|
@ -11,9 +11,11 @@ use iced::{
|
||||||
widget::{column, container, mouse_area, row, text, text_input},
|
widget::{column, container, mouse_area, row, text, text_input},
|
||||||
Application, Background, Color, Element, Length, Settings,
|
Application, Background, Color, Element, Length, Settings,
|
||||||
};
|
};
|
||||||
use irc::proto::{Command as IrcCommand, Response};
|
use irc::proto::{message::Tag, Command as IrcCommand, Response};
|
||||||
|
use irc_message::MessageDetail;
|
||||||
|
use message_log::MessageLog;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::cell::RefCell;
|
use std::{cell::RefCell, collections::HashMap};
|
||||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique);
|
static INPUT_ID: Lazy<text_input::Id> = Lazy::new(text_input::Id::unique);
|
||||||
|
@ -49,6 +51,7 @@ pub enum UiMessage {
|
||||||
InputChanged(String),
|
InputChanged(String),
|
||||||
InputSubmitted,
|
InputSubmitted,
|
||||||
HandleChannelPress(Option<String>),
|
HandleChannelPress(Option<String>),
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Cri {
|
struct Cri {
|
||||||
|
@ -170,23 +173,80 @@ impl Application for Cri {
|
||||||
.unwrap_or(&self.nickname)
|
.unwrap_or(&self.nickname)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
let tags_map = message
|
||||||
|
.tags
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.map(|Tag(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
let timestamp = tags_map
|
||||||
|
.get(&"time".to_string())
|
||||||
|
.cloned()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|time| DateTime::parse_from_rfc3339(&time).ok())
|
||||||
|
.map(DateTime::into)
|
||||||
|
.unwrap_or_else(Local::now);
|
||||||
|
|
||||||
|
let message_id = tags_map.get(&"msgid".to_string()).cloned().flatten();
|
||||||
|
let message_id = message_id.as_deref();
|
||||||
|
|
||||||
match &message.command {
|
match &message.command {
|
||||||
IrcCommand::JOIN(chanlist, _, _) => {
|
IrcCommand::JOIN(chanlist, _, _) => {
|
||||||
self.message_log.on_join(chanlist, &source_nickname);
|
let already_joined = self.message_log.has_channel(chanlist);
|
||||||
|
self.message_log.on_join(
|
||||||
|
chanlist,
|
||||||
|
&source_nickname,
|
||||||
|
×tamp,
|
||||||
|
message_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !already_joined && source_nickname == self.nickname {
|
||||||
|
self.input_tx
|
||||||
|
.borrow()
|
||||||
|
.send(
|
||||||
|
IrcCommand::Raw(
|
||||||
|
"CHATHISTORY".to_string(),
|
||||||
|
vec![
|
||||||
|
"LATEST".to_string(),
|
||||||
|
chanlist.clone(),
|
||||||
|
"*".to_string(),
|
||||||
|
100.to_string(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IrcCommand::PART(chanlist, comment) => {
|
IrcCommand::PART(chanlist, comment) => {
|
||||||
self.message_log
|
self.message_log.on_part(
|
||||||
.on_part(chanlist, &source_nickname, comment.as_deref());
|
chanlist,
|
||||||
|
&source_nickname,
|
||||||
|
comment.as_deref(),
|
||||||
|
×tamp,
|
||||||
|
message_id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IrcCommand::NICK(new) => {
|
IrcCommand::NICK(new) => {
|
||||||
self.message_log.on_nick(&source_nickname, new);
|
self.message_log
|
||||||
|
.on_nick(&source_nickname, new, ×tamp, message_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
IrcCommand::QUIT(comment) => {
|
IrcCommand::QUIT(comment) => {
|
||||||
self.message_log
|
self.message_log.on_quit(
|
||||||
.on_quit(&source_nickname, comment.as_deref());
|
&source_nickname,
|
||||||
|
comment.as_deref(),
|
||||||
|
×tamp,
|
||||||
|
message_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IrcCommand::TOPIC(channel, topic) => {
|
||||||
|
self.message_log.on_topic(channel, topic.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
IrcCommand::PRIVMSG(msgtarget, content)
|
IrcCommand::PRIVMSG(msgtarget, content)
|
||||||
|
@ -197,11 +257,22 @@ impl Application for Cri {
|
||||||
&channel,
|
&channel,
|
||||||
&source_nickname,
|
&source_nickname,
|
||||||
content,
|
content,
|
||||||
|
message_id,
|
||||||
|
×tamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
IrcCommand::Response(Response::RPL_WELCOME, args) => {
|
IrcCommand::Response(Response::RPL_WELCOME, args) => {
|
||||||
self.nickname = args[0].clone()
|
self.nickname = args[0].clone()
|
||||||
}
|
}
|
||||||
|
IrcCommand::BATCH(tag, subcommand, params) => {
|
||||||
|
self.message_log.on_batch(
|
||||||
|
&self.nickname,
|
||||||
|
tag,
|
||||||
|
subcommand.as_ref(),
|
||||||
|
params.as_ref(),
|
||||||
|
×tamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => self.message_log.on_other(&message.to_string()),
|
_ => self.message_log.on_other(&message.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,6 +289,7 @@ impl Application for Cri {
|
||||||
self.input_value.clear();
|
self.input_value.clear();
|
||||||
}
|
}
|
||||||
UiMessage::HandleChannelPress(channel) => self.message_log.set_active(channel),
|
UiMessage::HandleChannelPress(channel) => self.message_log.set_active(channel),
|
||||||
|
UiMessage::None => (),
|
||||||
}
|
}
|
||||||
iced::Command::none()
|
iced::Command::none()
|
||||||
}
|
}
|
||||||
|
@ -227,8 +299,11 @@ impl Application for Cri {
|
||||||
"irc message",
|
"irc message",
|
||||||
self.message_rx.take(),
|
self.message_rx.take(),
|
||||||
move |mut receiver| async move {
|
move |mut receiver| async move {
|
||||||
let message = receiver.as_mut().unwrap().recv().await.unwrap();
|
if let Some(message) = receiver.as_mut().unwrap().recv().await {
|
||||||
(UiMessage::IrcMessageReceived(Box::new(message)), receiver)
|
(UiMessage::IrcMessageReceived(Box::new(message)), receiver)
|
||||||
|
} else {
|
||||||
|
(UiMessage::None, receiver)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -267,8 +342,8 @@ impl Application for Cri {
|
||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.find_map(|m| -> Option<Element<_, _>> {
|
.find_map(|m| -> Option<Element<_, _>> {
|
||||||
match m {
|
match &m.detail {
|
||||||
IrcMessage::Privmsg { nickname, message } => Some(
|
MessageDetail::Privmsg { nickname, message } => Some(
|
||||||
row![
|
row![
|
||||||
text(format!("{nickname}: "))
|
text(format!("{nickname}: "))
|
||||||
.style(nickname_color)
|
.style(nickname_color)
|
||||||
|
@ -277,7 +352,7 @@ impl Application for Cri {
|
||||||
]
|
]
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
IrcMessage::Join { nickname } => Some(
|
MessageDetail::Join { nickname } => Some(
|
||||||
row![
|
row![
|
||||||
text(nickname).style(nickname_color).size(text_size),
|
text(nickname).style(nickname_color).size(text_size),
|
||||||
text(" joined").style(no_message_color).size(text_size)
|
text(" joined").style(no_message_color).size(text_size)
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
use crate::irc_message::IrcMessage;
|
use crate::irc_message::{IrcMessage, MessageDetail};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
use iced::{
|
use iced::{
|
||||||
alignment::Horizontal,
|
alignment::Horizontal,
|
||||||
widget::{column, container, scrollable, text, Container},
|
widget::{column, container, scrollable, text, Container},
|
||||||
Background, Color, Length,
|
Background, Color, Length,
|
||||||
};
|
};
|
||||||
|
use irc::proto::BatchSubCommand;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub messages: Vec<IrcMessage>,
|
pub messages: Vec<IrcMessage>,
|
||||||
|
pub message_ids: HashSet<String>,
|
||||||
|
|
||||||
|
pub topic: Option<String>,
|
||||||
pub unread_messages: i32,
|
pub unread_messages: i32,
|
||||||
pub unread_highlights: i32,
|
pub unread_highlights: i32,
|
||||||
pub unread_events: i32,
|
pub unread_events: i32,
|
||||||
|
|
||||||
|
pub is_multiline: bool,
|
||||||
|
pub multiline_privmsgs: Option<Vec<String>>,
|
||||||
|
pub multiline_timestamp: Option<chrono::DateTime<Local>>,
|
||||||
|
pub multiline_nickname: Option<String>,
|
||||||
|
pub multiline_message_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MessageLog {
|
pub struct MessageLog {
|
||||||
channels: HashMap<Option<String>, Channel>,
|
channels: HashMap<Option<String>, Channel>,
|
||||||
pub active_channel: Option<String>,
|
pub active_channel: Option<String>,
|
||||||
|
|
||||||
|
/// Maps multiline batch tags to channels
|
||||||
|
pub batch_channels: HashMap<String, (BatchSubCommand, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageLog {
|
impl<'a> MessageLog {
|
||||||
|
@ -28,9 +42,14 @@ impl<'a> MessageLog {
|
||||||
Self {
|
Self {
|
||||||
channels,
|
channels,
|
||||||
active_channel: None,
|
active_channel: None,
|
||||||
|
batch_channels: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_channel(&self, channel: &str) -> bool {
|
||||||
|
self.channels.contains_key(&Some(channel.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_all(&'a self) -> Vec<(&'a Option<String>, &'a Channel)> {
|
pub fn get_all(&'a self) -> Vec<(&'a Option<String>, &'a Channel)> {
|
||||||
let mut log: Vec<_> = self.channels.iter().collect();
|
let mut log: Vec<_> = self.channels.iter().collect();
|
||||||
log.sort_unstable_by_key(|(name, _)| name.as_deref());
|
log.sort_unstable_by_key(|(name, _)| name.as_deref());
|
||||||
|
@ -45,12 +64,26 @@ impl<'a> MessageLog {
|
||||||
self.channels.entry(channel_name).or_default()
|
self.channels.entry(channel_name).or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_join(&mut self, channel_name: &str, nickname: &str) {
|
pub fn on_join(
|
||||||
|
&mut self,
|
||||||
|
channel_name: &str,
|
||||||
|
nickname: &str,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
|
message_id: Option<&str>,
|
||||||
|
) {
|
||||||
let is_active = self.active_channel.as_deref() != Some(channel_name);
|
let is_active = self.active_channel.as_deref() != Some(channel_name);
|
||||||
|
|
||||||
let channel = self.get_mut(Some(channel_name.to_string()));
|
let channel = self.get_mut(Some(channel_name.to_string()));
|
||||||
channel.messages.push(IrcMessage::Join {
|
|
||||||
nickname: nickname.to_string(),
|
if message_id.is_some() && !channel.message_ids.insert(message_id.unwrap().to_owned()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.messages.push(IrcMessage {
|
||||||
|
detail: MessageDetail::Join {
|
||||||
|
nickname: nickname.to_string(),
|
||||||
|
},
|
||||||
|
message_id: message_id.map(str::to_string),
|
||||||
|
timestamp: *timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if is_active {
|
if is_active {
|
||||||
|
@ -58,13 +91,28 @@ impl<'a> MessageLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_part(&mut self, channel_name: &str, nickname: &str, comment: Option<&str>) {
|
pub fn on_part(
|
||||||
|
&mut self,
|
||||||
|
channel_name: &str,
|
||||||
|
nickname: &str,
|
||||||
|
comment: Option<&str>,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
|
message_id: Option<&str>,
|
||||||
|
) {
|
||||||
let is_active = self.active_channel.as_deref() != Some(channel_name);
|
let is_active = self.active_channel.as_deref() != Some(channel_name);
|
||||||
|
|
||||||
let channel = self.get_mut(Some(channel_name.to_string()));
|
let channel = self.get_mut(Some(channel_name.to_string()));
|
||||||
channel.messages.push(IrcMessage::Part {
|
|
||||||
nickname: nickname.to_string(),
|
if message_id.is_some() && !channel.message_ids.insert(message_id.unwrap().to_owned()) {
|
||||||
reason: comment.map(str::to_string),
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.messages.push(IrcMessage {
|
||||||
|
detail: MessageDetail::Part {
|
||||||
|
nickname: nickname.to_string(),
|
||||||
|
reason: comment.map(str::to_string),
|
||||||
|
},
|
||||||
|
message_id: message_id.map(str::to_string),
|
||||||
|
timestamp: *timestamp,
|
||||||
});
|
});
|
||||||
|
|
||||||
if is_active {
|
if is_active {
|
||||||
|
@ -72,42 +120,147 @@ impl<'a> MessageLog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_nick(&mut self, old: &str, new: &str) {
|
pub fn on_nick(
|
||||||
|
&mut self,
|
||||||
|
old: &str,
|
||||||
|
new: &str,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
|
message_id: Option<&str>,
|
||||||
|
) {
|
||||||
// TODO increment event counter for each relevant channel
|
// TODO increment event counter for each relevant channel
|
||||||
for log in self.channels.values_mut() {
|
for channel in self.channels.values_mut() {
|
||||||
|
if message_id.is_some() && !channel.message_ids.insert(message_id.unwrap().to_owned()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO only show in relevant channels
|
// TODO only show in relevant channels
|
||||||
log.messages.push(IrcMessage::Nick {
|
channel.messages.push(IrcMessage {
|
||||||
old: old.to_string(),
|
detail: MessageDetail::Nick {
|
||||||
new: new.to_string(),
|
old: old.to_string(),
|
||||||
|
new: new.to_string(),
|
||||||
|
},
|
||||||
|
message_id: message_id.map(str::to_string),
|
||||||
|
timestamp: *timestamp,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_quit(&mut self, nickname: &str, reason: Option<&str>) {
|
pub fn on_quit(
|
||||||
|
&mut self,
|
||||||
|
nickname: &str,
|
||||||
|
reason: Option<&str>,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
|
message_id: Option<&str>,
|
||||||
|
) {
|
||||||
// TODO increment event counter for each relevant channel
|
// TODO increment event counter for each relevant channel
|
||||||
for log in self.channels.values_mut() {
|
for channel in self.channels.values_mut() {
|
||||||
|
if message_id.is_some() && !channel.message_ids.insert(message_id.unwrap().to_owned()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO only show in relevant channels
|
// TODO only show in relevant channels
|
||||||
log.messages.push(IrcMessage::Quit {
|
channel.messages.push(IrcMessage {
|
||||||
nickname: nickname.to_string(),
|
detail: MessageDetail::Quit {
|
||||||
reason: reason.map(str::to_string),
|
nickname: nickname.to_string(),
|
||||||
|
reason: reason.map(str::to_string),
|
||||||
|
},
|
||||||
|
message_id: message_id.map(str::to_string),
|
||||||
|
timestamp: *timestamp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_batch(
|
||||||
|
&mut self,
|
||||||
|
current_nickname: &str,
|
||||||
|
tag: &str,
|
||||||
|
subcommand: Option<&BatchSubCommand>,
|
||||||
|
params: Option<&Vec<String>>,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
|
) {
|
||||||
|
if let Some(tag) = tag.strip_prefix('+') {
|
||||||
|
if subcommand == Some(&BatchSubCommand::CUSTOM("DRAFT/MULTILINE".to_string())) {
|
||||||
|
let channel_name = ¶ms.unwrap()[0];
|
||||||
|
self.batch_channels.insert(
|
||||||
|
tag.to_string(),
|
||||||
|
(subcommand.unwrap().clone(), channel_name.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let channel = self.get_mut(Some(channel_name.clone()));
|
||||||
|
|
||||||
|
channel.is_multiline = true;
|
||||||
|
channel.multiline_privmsgs = Some(Vec::new());
|
||||||
|
channel.multiline_timestamp = Some(*timestamp);
|
||||||
|
} else if subcommand == Some(&BatchSubCommand::CUSTOM("CHATHISTORY".to_string())) {
|
||||||
|
let channel_name = ¶ms.unwrap()[0];
|
||||||
|
self.batch_channels.insert(
|
||||||
|
tag.to_string(),
|
||||||
|
(subcommand.unwrap().clone(), channel_name.clone()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if let Some(tag) = tag.strip_prefix('-') {
|
||||||
|
if let Some((subcommand, channel_name)) = self.batch_channels.remove(tag) {
|
||||||
|
let channel = self.get_mut(Some(channel_name.clone()));
|
||||||
|
|
||||||
|
if subcommand == BatchSubCommand::CUSTOM("DRAFT/MULTILINE".to_string()) {
|
||||||
|
channel.is_multiline = false;
|
||||||
|
|
||||||
|
let nickname = channel.multiline_nickname.clone().unwrap();
|
||||||
|
let message = channel.multiline_privmsgs.as_ref().unwrap().join("\n");
|
||||||
|
let timestamp = channel.multiline_timestamp.unwrap();
|
||||||
|
|
||||||
|
self.on_privmsg(
|
||||||
|
current_nickname,
|
||||||
|
&channel_name,
|
||||||
|
&nickname,
|
||||||
|
&message,
|
||||||
|
None,
|
||||||
|
×tamp,
|
||||||
|
);
|
||||||
|
} else if subcommand == BatchSubCommand::CUSTOM("CHATHISTORY".to_string()) {
|
||||||
|
channel.messages.sort_by_key(|m| m.timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_privmsg(
|
pub fn on_privmsg(
|
||||||
&mut self,
|
&mut self,
|
||||||
current_nickname: &str,
|
current_nickname: &str,
|
||||||
channel_name: &str,
|
channel_name: &str,
|
||||||
nickname: &str,
|
nickname: &str,
|
||||||
message: &str,
|
message: &str,
|
||||||
|
message_id: Option<&str>,
|
||||||
|
timestamp: &DateTime<Local>,
|
||||||
) {
|
) {
|
||||||
let is_active = self.active_channel.as_deref() == Some(channel_name);
|
let is_active = self.active_channel.as_deref() == Some(channel_name);
|
||||||
|
|
||||||
let channel = self.get_mut(Some(channel_name.to_string()));
|
let channel = self.get_mut(Some(channel_name.to_string()));
|
||||||
channel.messages.push(IrcMessage::Privmsg {
|
|
||||||
nickname: nickname.to_string(),
|
if channel.is_multiline {
|
||||||
message: message.to_string(),
|
channel.multiline_nickname = Some(nickname.to_string());
|
||||||
});
|
channel
|
||||||
|
.multiline_privmsgs
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(message.to_string());
|
||||||
|
|
||||||
|
if let Some(message_id) = message_id {
|
||||||
|
channel.multiline_message_id = Some(message_id.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if message_id.is_none() || channel.message_ids.insert(message_id.unwrap().to_owned()) {
|
||||||
|
channel.messages.push(IrcMessage {
|
||||||
|
detail: MessageDetail::Privmsg {
|
||||||
|
nickname: nickname.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
},
|
||||||
|
message_id: message_id.map(str::to_string),
|
||||||
|
timestamp: *timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if !is_active {
|
if !is_active {
|
||||||
let highlight_regex =
|
let highlight_regex =
|
||||||
|
@ -121,8 +274,12 @@ impl<'a> MessageLog {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_other(&mut self, message: &str) {
|
pub fn on_other(&mut self, message: &str) {
|
||||||
self.get_mut(None).messages.push(IrcMessage::Other {
|
self.get_mut(None).messages.push(IrcMessage {
|
||||||
message: message.trim().to_string(),
|
detail: MessageDetail::Other {
|
||||||
|
message: message.trim().to_string(),
|
||||||
|
},
|
||||||
|
message_id: None,
|
||||||
|
timestamp: chrono::Local::now(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,25 +316,39 @@ impl<'a> MessageLog {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let messages = self
|
let channel = self.get(&self.active_channel).unwrap();
|
||||||
.get(&self.active_channel)
|
|
||||||
.unwrap()
|
let header = container(column![
|
||||||
|
text(
|
||||||
|
self.active_channel
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("Server messages".to_string())
|
||||||
|
),
|
||||||
|
text(channel.topic.clone().unwrap_or_default())
|
||||||
|
]);
|
||||||
|
|
||||||
|
let messages = channel
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|message| -> Option<iced::Element<_>> {
|
.flat_map(|irc_message| -> Option<iced::Element<_>> {
|
||||||
match message {
|
let timestamp = irc_message.timestamp.format("%H:%M:%S");
|
||||||
IrcMessage::Join { nickname } => Some(
|
|
||||||
|
match &irc_message.detail {
|
||||||
|
MessageDetail::Join { nickname } => Some(
|
||||||
container(
|
container(
|
||||||
container(text(format!("{nickname} joined the channel")))
|
container(
|
||||||
.style(move |_: &_| event_appearance)
|
text(format!("{nickname} joined the channel"))
|
||||||
.padding([3, 10]),
|
.horizontal_alignment(Horizontal::Center),
|
||||||
|
)
|
||||||
|
.style(move |_: &_| event_appearance)
|
||||||
|
.padding([3, 10]),
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.center_x()
|
.center_x()
|
||||||
.padding([3, 0])
|
.padding([3, 0])
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
IrcMessage::Part { nickname, reason } => {
|
MessageDetail::Part { nickname, reason } => {
|
||||||
let reason = match reason {
|
let reason = match reason {
|
||||||
Some(reason) => format!(" ({reason})"),
|
Some(reason) => format!(" ({reason})"),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
|
@ -194,7 +365,7 @@ impl<'a> MessageLog {
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IrcMessage::Nick { old, new } => Some(
|
MessageDetail::Nick { old, new } => Some(
|
||||||
container(
|
container(
|
||||||
container(text(format!("{old} changed their nickname to {new}")))
|
container(text(format!("{old} changed their nickname to {new}")))
|
||||||
.style(move |_: &_| event_appearance)
|
.style(move |_: &_| event_appearance)
|
||||||
|
@ -205,7 +376,7 @@ impl<'a> MessageLog {
|
||||||
.padding([3, 0])
|
.padding([3, 0])
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
IrcMessage::Quit { nickname, reason } => {
|
MessageDetail::Quit { nickname, reason } => {
|
||||||
let reason = match reason {
|
let reason = match reason {
|
||||||
Some(reason) => format!(" ({reason})"),
|
Some(reason) => format!(" ({reason})"),
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
|
@ -223,7 +394,7 @@ impl<'a> MessageLog {
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IrcMessage::Privmsg { nickname, message } => {
|
MessageDetail::Privmsg { nickname, message } => {
|
||||||
let is_self = nickname == current_nickname;
|
let is_self = nickname == current_nickname;
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
|
@ -231,44 +402,61 @@ impl<'a> MessageLog {
|
||||||
elements.push(text(nickname).style(dark_red).into())
|
elements.push(text(nickname).style(dark_red).into())
|
||||||
}
|
}
|
||||||
elements.push(text(message).into());
|
elements.push(text(message).into());
|
||||||
|
elements.push(
|
||||||
|
text(timestamp)
|
||||||
|
.style(dark_grey)
|
||||||
|
.horizontal_alignment(Horizontal::Right)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let appearance = if is_self {
|
||||||
|
own_message_appearance
|
||||||
|
} else {
|
||||||
|
message_appearance
|
||||||
|
};
|
||||||
|
|
||||||
|
let alignment = if is_self {
|
||||||
|
Horizontal::Right
|
||||||
|
} else {
|
||||||
|
Horizontal::Left
|
||||||
|
};
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
container(
|
container(
|
||||||
container(column(elements))
|
container(column(elements))
|
||||||
.style(move |_: &_| {
|
.style(move |_: &_| appearance)
|
||||||
if is_self {
|
|
||||||
own_message_appearance
|
|
||||||
} else {
|
|
||||||
message_appearance
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.padding([4, 10]),
|
.padding([4, 10]),
|
||||||
)
|
)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.align_x(if is_self {
|
.align_x(alignment)
|
||||||
Horizontal::Right
|
|
||||||
} else {
|
|
||||||
Horizontal::Left
|
|
||||||
})
|
|
||||||
.padding([4, 8])
|
.padding([4, 8])
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IrcMessage::Other { message } => Some(text(message).into()),
|
MessageDetail::Other { message } => Some(text(message).into()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
container(
|
container(column![
|
||||||
scrollable(column(messages))
|
header,
|
||||||
.height(Length::Fill)
|
container(
|
||||||
.width(Length::Fill),
|
scrollable(column(messages))
|
||||||
)
|
.height(Length::Fill)
|
||||||
|
.width(Length::Fill),
|
||||||
|
)
|
||||||
|
.height(Length::Fill)
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(move |_: &_| container::Appearance {
|
||||||
|
background: Some(Background::Color(lighter_grey)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
])
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.style(move |_: &_| container::Appearance {
|
}
|
||||||
background: Some(Background::Color(lighter_grey)),
|
|
||||||
..Default::default()
|
pub fn on_topic(&mut self, channel: &str, topic: Option<String>) {
|
||||||
})
|
self.get_mut(Some(channel.to_string())).topic = topic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue