mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
320 lines
10 KiB
Rust
320 lines
10 KiB
Rust
//! # Message summary for chatlist.
|
||
|
||
use std::borrow::Cow;
|
||
use std::fmt;
|
||
|
||
use crate::chat::Chat;
|
||
use crate::constants::Chattype;
|
||
use crate::contact::{Contact, ContactId};
|
||
use crate::context::Context;
|
||
use crate::message::{Message, MessageState, Viewtype};
|
||
use crate::mimeparser::SystemMessage;
|
||
use crate::stock_str;
|
||
use crate::tools::truncate;
|
||
|
||
/// Prefix displayed before message and separated by ":" in the chatlist.
|
||
#[derive(Debug)]
|
||
pub enum SummaryPrefix {
|
||
/// Username.
|
||
Username(String),
|
||
|
||
/// Stock string saying "Draft".
|
||
Draft(String),
|
||
|
||
/// Stock string saying "Me".
|
||
Me(String),
|
||
}
|
||
|
||
impl fmt::Display for SummaryPrefix {
|
||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
match self {
|
||
SummaryPrefix::Username(username) => write!(f, "{username}"),
|
||
SummaryPrefix::Draft(text) => write!(f, "{text}"),
|
||
SummaryPrefix::Me(text) => write!(f, "{text}"),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Message summary.
|
||
#[derive(Debug, Default)]
|
||
pub struct Summary {
|
||
/// Part displayed before ":", such as an username or a string "Draft".
|
||
pub prefix: Option<SummaryPrefix>,
|
||
|
||
/// Summary text, always present.
|
||
pub text: String,
|
||
|
||
/// Message timestamp.
|
||
pub timestamp: i64,
|
||
|
||
/// Message state.
|
||
pub state: MessageState,
|
||
|
||
/// Message preview image path
|
||
pub thumbnail_path: Option<String>,
|
||
}
|
||
|
||
impl Summary {
|
||
/// Constructs chatlist summary
|
||
/// from the provided message, chat and message author contact snapshots.
|
||
pub async fn new(
|
||
context: &Context,
|
||
msg: &Message,
|
||
chat: &Chat,
|
||
contact: Option<&Contact>,
|
||
) -> Self {
|
||
let prefix = if msg.state == MessageState::OutDraft {
|
||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||
} else if msg.from_id == ContactId::SELF {
|
||
if msg.is_info() || chat.is_self_talk() {
|
||
None
|
||
} else {
|
||
Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
|
||
}
|
||
} else {
|
||
match chat.typ {
|
||
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
|
||
if msg.is_info() || contact.is_none() {
|
||
None
|
||
} else {
|
||
msg.get_override_sender_name()
|
||
.or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
|
||
.map(SummaryPrefix::Username)
|
||
}
|
||
}
|
||
Chattype::Single => None,
|
||
}
|
||
};
|
||
|
||
let mut text = msg.get_summary_text(context).await;
|
||
|
||
if text.is_empty() && msg.quoted_text().is_some() {
|
||
text = stock_str::reply_noun(context).await
|
||
}
|
||
|
||
let thumbnail_path = if msg.viewtype == Viewtype::Image
|
||
|| msg.viewtype == Viewtype::Gif
|
||
|| msg.viewtype == Viewtype::Sticker
|
||
{
|
||
msg.get_file(context)
|
||
.and_then(|path| path.to_str().map(|p| p.to_owned()))
|
||
} else {
|
||
None
|
||
};
|
||
|
||
Self {
|
||
prefix,
|
||
text,
|
||
timestamp: msg.get_timestamp(),
|
||
state: msg.state,
|
||
thumbnail_path,
|
||
}
|
||
}
|
||
|
||
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
|
||
pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
|
||
truncate(&self.text, approx_chars)
|
||
}
|
||
}
|
||
|
||
impl Message {
|
||
/// Returns a summary text.
|
||
async fn get_summary_text(&self, context: &Context) -> String {
|
||
let mut append_text = true;
|
||
let prefix = match self.viewtype {
|
||
Viewtype::Image => stock_str::image(context).await,
|
||
Viewtype::Gif => stock_str::gif(context).await,
|
||
Viewtype::Sticker => stock_str::sticker(context).await,
|
||
Viewtype::Video => stock_str::video(context).await,
|
||
Viewtype::Voice => stock_str::voice_message(context).await,
|
||
Viewtype::Audio | Viewtype::File => {
|
||
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||
append_text = false;
|
||
stock_str::ac_setup_msg_subject(context).await
|
||
} else {
|
||
let file_name = self
|
||
.get_filename()
|
||
.unwrap_or_else(|| String::from("ErrFileName"));
|
||
let label = if self.viewtype == Viewtype::Audio {
|
||
stock_str::audio(context).await
|
||
} else {
|
||
stock_str::file(context).await
|
||
};
|
||
format!("{label} – {file_name}")
|
||
}
|
||
}
|
||
Viewtype::VideochatInvitation => {
|
||
append_text = false;
|
||
stock_str::videochat_invitation(context).await
|
||
}
|
||
Viewtype::Webxdc => {
|
||
append_text = true;
|
||
self.get_webxdc_info(context)
|
||
.await
|
||
.map(|info| info.name)
|
||
.unwrap_or_else(|_| "ErrWebxdcName".to_string())
|
||
}
|
||
Viewtype::Text | Viewtype::Unknown => {
|
||
if self.param.get_cmd() != SystemMessage::LocationOnly {
|
||
"".to_string()
|
||
} else {
|
||
append_text = false;
|
||
stock_str::location(context).await
|
||
}
|
||
}
|
||
};
|
||
|
||
if !append_text {
|
||
return prefix;
|
||
}
|
||
|
||
let summary_content = if self.text.is_empty() {
|
||
prefix
|
||
} else if prefix.is_empty() {
|
||
self.text.to_string()
|
||
} else {
|
||
format!("{prefix} – {}", self.text)
|
||
};
|
||
|
||
let summary = if self.is_forwarded() {
|
||
format!(
|
||
"{}: {}",
|
||
stock_str::forwarded(context).await,
|
||
summary_content
|
||
)
|
||
} else {
|
||
summary_content
|
||
};
|
||
|
||
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::param::Param;
|
||
use crate::test_utils as test;
|
||
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||
async fn test_get_summary_text() {
|
||
let d = test::TestContext::new().await;
|
||
let ctx = &d.ctx;
|
||
|
||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||
|
||
let mut msg = Message::new(Viewtype::Text);
|
||
msg.set_text(some_text.to_string());
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"bla bla" // for simple text, the type is not added to the summary
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Image);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Image" // file names are not added for images
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Video);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Video" // file names are not added for videos
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Gif);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"GIF" // file names are not added for GIFs
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Sticker);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Sticker" // file names are not added for stickers
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Voice);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Voice message" // file names are not added for voice messages, empty text is skipped
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Voice);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Voice message" // file names are not added for voice messages
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Voice);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH"
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Audio);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Audio \u{2013} foo.bar" // file name is added for audio
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Audio);
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::Audio);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files
|
||
);
|
||
|
||
// Forwarded
|
||
let mut msg = Message::new(Viewtype::Text);
|
||
msg.set_text(some_text.clone());
|
||
msg.param.set_int(Param::Forwarded, 1);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Forwarded: bla bla" // for simple text, the type is not added to the summary
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
msg.param.set_int(Param::Forwarded, 1);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Forwarded: File \u{2013} foo.bar \u{2013} bla bla"
|
||
);
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.param.set(Param::File, "foo.bar");
|
||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Autocrypt Setup Message" // file name is not added for autocrypt setup messages
|
||
);
|
||
}
|
||
}
|