use std::borrow::Cow;
use std::fmt;
use std::str;
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::param::Param;
use crate::stock_str;
use crate::stock_str::msg_reacted;
use crate::tools::truncate;
use anyhow::Result;
#[derive(Debug)]
pub enum SummaryPrefix {
Username(String),
Draft(String),
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}"),
}
}
}
#[derive(Debug, Default)]
pub struct Summary {
pub prefix: Option<SummaryPrefix>,
pub text: String,
pub timestamp: i64,
pub state: MessageState,
pub thumbnail_path: Option<String>,
}
impl Summary {
pub async fn new_with_reaction_details(
context: &Context,
msg: &Message,
chat: &Chat,
contact: Option<&Contact>,
) -> Result<Summary> {
if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
.await?
{
let summary = reaction_msg.get_summary_text_without_prefix(context).await;
return Ok(Summary {
prefix: None,
text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
timestamp: msg.get_timestamp(), state: msg.state, thumbnail_path: None,
});
}
Self::new(context, msg, chat, contact).await
}
pub async fn new(
context: &Context,
msg: &Message,
chat: &Chat,
contact: Option<&Contact>,
) -> Result<Summary> {
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 if msg.viewtype == Viewtype::Webxdc {
Some("webxdc-icon://last-msg-id".to_string())
} else {
None
};
Ok(Summary {
prefix,
text,
timestamp: msg.get_timestamp(),
state: msg.state,
thumbnail_path,
})
}
pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
truncate(&self.text, approx_chars)
}
}
impl Message {
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
let summary = self.get_summary_text_without_prefix(context).await;
if self.is_forwarded() {
format!("{}: {}", stock_str::forwarded(context).await, summary)
} else {
summary
}
}
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
let (emoji, type_name, type_file, append_text);
match self.viewtype {
Viewtype::Image => {
emoji = Some("📷");
type_name = Some(stock_str::image(context).await);
type_file = None;
append_text = true;
}
Viewtype::Gif => {
emoji = None;
type_name = Some(stock_str::gif(context).await);
type_file = None;
append_text = true;
}
Viewtype::Sticker => {
emoji = None;
type_name = Some(stock_str::sticker(context).await);
type_file = None;
append_text = true;
}
Viewtype::Video => {
emoji = Some("🎥");
type_name = Some(stock_str::video(context).await);
type_file = None;
append_text = true;
}
Viewtype::Voice => {
emoji = Some("🎤");
type_name = Some(stock_str::voice_message(context).await);
type_file = None;
append_text = true;
}
Viewtype::Audio => {
emoji = Some("🎵");
type_name = Some(stock_str::audio(context).await);
type_file = self.get_filename();
append_text = true
}
Viewtype::File => {
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
emoji = None;
type_name = Some(stock_str::ac_setup_msg_subject(context).await);
type_file = None;
append_text = false;
} else {
emoji = Some("📎");
type_name = Some(stock_str::file(context).await);
type_file = self.get_filename();
append_text = true
}
}
Viewtype::VideochatInvitation => {
emoji = None;
type_name = Some(stock_str::videochat_invitation(context).await);
type_file = None;
append_text = false;
}
Viewtype::Webxdc => {
emoji = None;
type_name = None;
type_file = Some(
self.get_webxdc_info(context)
.await
.map(|info| info.name)
.unwrap_or_else(|_| "ErrWebxdcName".to_string()),
);
append_text = true;
}
Viewtype::Vcard => {
emoji = Some("👤");
type_name = None;
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
append_text = true;
}
Viewtype::Text | Viewtype::Unknown => {
emoji = None;
if self.param.get_cmd() == SystemMessage::LocationOnly {
type_name = Some(stock_str::location(context).await);
type_file = None;
append_text = false;
} else {
type_name = None;
type_file = None;
append_text = true;
}
}
};
let text = self.text.clone();
let summary = if let Some(type_file) = type_file {
if append_text && !text.is_empty() {
format!("{type_file} – {text}")
} else {
type_file
}
} else if append_text && !text.is_empty() {
if emoji.is_some() {
text
} else if let Some(type_name) = type_name {
format!("{type_name} – {text}")
} else {
text
}
} else if let Some(type_name) = type_name {
type_name
} else {
"".to_string()
};
let summary = if let Some(emoji) = emoji {
format!("{emoji} {summary}")
} else {
summary
};
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::ChatId;
use crate::param::Param;
use crate::test_utils::TestContext;
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
assert_eq!(msg.get_summary_text(ctx).await, expected);
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_get_summary_text() {
let d = TestContext::new_alice().await;
let ctx = &d.ctx;
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
.await
.unwrap();
let some_text = " bla \t\n\tbla\n\t".to_string();
let msg = Message::new_text(some_text.to_string());
assert_summary_texts(&msg, ctx, "bla bla").await; let mut msg = Message::new(Viewtype::Image);
msg.set_file("foo.jpg", None);
assert_summary_texts(&msg, ctx, "📷 Image").await; let mut msg = Message::new(Viewtype::Image);
msg.set_text(some_text.to_string());
msg.set_file("foo.jpg", None);
assert_summary_texts(&msg, ctx, "📷 bla bla").await; let mut msg = Message::new(Viewtype::Video);
msg.set_file("foo.mp4", None);
assert_summary_texts(&msg, ctx, "🎥 Video").await; let mut msg = Message::new(Viewtype::Video);
msg.set_text(some_text.to_string());
msg.set_file("foo.mp4", None);
assert_summary_texts(&msg, ctx, "🎥 bla bla").await; let mut msg = Message::new(Viewtype::Gif);
msg.set_file("foo.gif", None);
assert_summary_texts(&msg, ctx, "GIF").await; let mut msg = Message::new(Viewtype::Gif);
msg.set_text(some_text.to_string());
msg.set_file("foo.gif", None);
assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::Sticker);
msg.set_file("foo.png", None);
assert_summary_texts(&msg, ctx, "Sticker").await; let mut msg = Message::new(Viewtype::Voice);
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎤 Voice message").await; let mut msg = Message::new(Viewtype::Voice);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
let mut msg = Message::new(Viewtype::Audio);
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; let mut msg = Message::new(Viewtype::Audio);
msg.set_text(some_text.clone());
msg.set_file("foo.mp3", None);
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::File);
let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
.await
.unwrap();
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_eq!(msg.viewtype, Viewtype::Webxdc);
assert_summary_texts(&msg, ctx, "nice app!").await;
msg.set_text(some_text.clone());
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
let mut msg = Message::new(Viewtype::File);
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::VideochatInvitation);
msg.set_text(some_text.clone());
msg.set_file("foo.bar", None);
assert_summary_texts(&msg, ctx, "Video chat invitation").await; let mut msg = Message::new(Viewtype::Vcard);
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None)
.await
.unwrap();
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_eq!(msg.viewtype, Viewtype::File);
assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
msg.set_text(some_text.clone());
assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
for vt in [Viewtype::Vcard, Viewtype::File] {
let mut msg = Message::new(vt);
msg.set_file_from_bytes(
ctx,
"alice.vcf",
b"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
END:VCARD",
None,
)
.await
.unwrap();
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
assert_eq!(msg.viewtype, Viewtype::Vcard);
assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
}
let mut msg = Message::new_text(some_text.clone());
msg.param.set_int(Param::Forwarded, 1);
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); 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: 📎 foo.bar \u{2013} bla bla"
);
assert_eq!(
msg.get_summary_text_without_prefix(ctx).await,
"📎 foo.bar \u{2013} bla bla"
); let mut msg = Message::new(Viewtype::File);
msg.set_text(some_text.clone());
msg.param.set(Param::File, "foo.bar");
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; }
}