diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index d60ae5329..f31f8824b 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -2404,13 +2404,13 @@ pub unsafe extern "C" fn dc_chatlist_get_summary( let ctx = &*ffi_list.context; block_on(async move { - let lot = ffi_list + let summary = ffi_list .list .get_summary(ctx, index as usize, maybe_chat) .await .log_err(ctx, "get_summary failed") .unwrap_or_default(); - Box::into_raw(Box::new(lot)) + Box::into_raw(Box::new(summary.into())) }) } @@ -2430,13 +2430,15 @@ pub unsafe extern "C" fn dc_chatlist_get_summary2( } else { Some(MsgId::new(msg_id)) }; - block_on(async move { - let lot = Chatlist::get_summary2(ctx, ChatId::new(chat_id), msg_id, None) - .await - .log_err(ctx, "get_summary2 failed") - .unwrap_or_default(); - Box::into_raw(Box::new(lot)) - }) + let summary = block_on(Chatlist::get_summary2( + ctx, + ChatId::new(chat_id), + msg_id, + None, + )) + .log_err(ctx, "get_summary2 failed") + .unwrap_or_default(); + Box::into_raw(Box::new(summary.into())) } #[no_mangle] @@ -2978,10 +2980,10 @@ pub unsafe extern "C" fn dc_msg_get_summary( let ffi_msg = &mut *msg; let ctx = &*ffi_msg.context; - block_on(async move { - let lot = ffi_msg.message.get_summary(ctx, maybe_chat).await; - Box::into_raw(Box::new(lot)) - }) + let summary = block_on(async move { ffi_msg.message.get_summary(ctx, maybe_chat).await }) + .log_err(ctx, "dc_msg_get_summary failed") + .unwrap_or_default(); + Box::into_raw(Box::new(summary.into())) } #[no_mangle] diff --git a/examples/repl/cmdline.rs b/examples/repl/cmdline.rs index bec5937c5..b84272408 100644 --- a/examples/repl/cmdline.rs +++ b/examples/repl/cmdline.rs @@ -16,7 +16,6 @@ use deltachat::dc_tools::*; use deltachat::imex::*; use deltachat::location; use deltachat::log::LogExt; -use deltachat::lot::LotState; use deltachat::message::{self, Message, MessageState, MsgId}; use deltachat::peerstate::*; use deltachat::qr::*; @@ -569,26 +568,25 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu "" }, ); - let lot = chatlist.get_summary(&context, i, Some(&chat)).await?; + let summary = chatlist.get_summary(&context, i, Some(&chat)).await?; let statestr = if chat.visibility == ChatVisibility::Archived { " [Archived]" } else { - match lot.get_state() { - LotState::MsgOutPending => " o", - LotState::MsgOutDelivered => " √", - LotState::MsgOutMdnRcvd => " √√", - LotState::MsgOutFailed => " !!", + match summary.state { + MessageState::OutPending => " o", + MessageState::OutDelivered => " √", + MessageState::OutMdnRcvd => " √√", + MessageState::OutFailed => " !!", _ => "", } }; - let timestr = dc_timestamp_to_str(lot.get_timestamp()); - let text1 = lot.get_text1(); - let text2 = lot.get_text2(); + let timestr = dc_timestamp_to_str(summary.timestamp); println!( - "{}{}{}{} [{}]{}", - text1.unwrap_or(""), - if text1.is_some() { ": " } else { "" }, - text2.unwrap_or(""), + "{}{}{} [{}]{}", + summary + .prefix + .map_or_else(String::new, |prefix| format!("{}: ", prefix)), + summary.text, statestr, ×tr, if chat.is_sending_locations() { diff --git a/src/chatlist.rs b/src/chatlist.rs index c43b5b7ea..c33ff8df7 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -11,9 +11,9 @@ use crate::constants::{ use crate::contact::Contact; use crate::context::Context; use crate::ephemeral::delete_expired_messages; -use crate::lot::Lot; use crate::message::{Message, MessageState, MsgId}; use crate::stock_str; +use crate::summary::Summary; /// An object representing a single chatlist in memory. /// @@ -288,26 +288,13 @@ impl Chatlist { } } - /// Get a summary for a chatlist index. - /// - /// The summary is returned by a dc_lot_t object with the following fields: - /// - /// - dc_lot_t::text1: contains the username or the strings "Me", "Draft" and so on. - /// The string may be colored by having a look at text1_meaning. - /// If there is no such name or it should not be displayed, the element is NULL. - /// - dc_lot_t::text1_meaning: one of DC_TEXT1_USERNAME, DC_TEXT1_SELF or DC_TEXT1_DRAFT. - /// Typically used to show dc_lot_t::text1 with different colors. 0 if not applicable. - /// - dc_lot_t::text2: contains an excerpt of the message text or strings as - /// "No messages". May be NULL of there is no such text (eg. for the archive link) - /// - dc_lot_t::timestamp: the timestamp of the message. 0 if not applicable. - /// - dc_lot_t::state: The state of the message as one of the DC_STATE_* constants (see #dc_msg_get_state()). - // 0 if not applicable. + /// Returns a summary for a given chatlist index. pub async fn get_summary( &self, context: &Context, index: usize, chat: Option<&Chat>, - ) -> Result { + ) -> Result { // The summary is created by the chat, not by the last message. // This is because we may want to display drafts here or stuff as // "is typing". @@ -320,14 +307,13 @@ impl Chatlist { Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await } + /// Returns a summary for a given chatlist item. pub async fn get_summary2( context: &Context, chat_id: ChatId, lastmsg_id: Option, chat: Option<&Chat>, - ) -> Result { - let mut ret = Lot::new(); - + ) -> Result { let chat_loaded: Chat; let chat = if let Some(chat) = chat { chat @@ -344,9 +330,8 @@ impl Chatlist { } else { match chat.typ { Chattype::Group | Chattype::Mailinglist => { - let lastcontact = - Contact::load_from_db(context, lastmsg.from_id).await.ok(); - (Some(lastmsg), lastcontact) + let lastcontact = Contact::load_from_db(context, lastmsg.from_id).await?; + (Some(lastmsg), Some(lastcontact)) } Chattype::Single | Chattype::Undefined => (Some(lastmsg), None), } @@ -356,17 +341,15 @@ impl Chatlist { }; if chat.id.is_archived_link() { - ret.text2 = None; - } else if let Some(mut lastmsg) = - lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) - { - ret.fill(&mut lastmsg, chat, lastcontact.as_ref(), context) - .await; + Ok(Default::default()) + } else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != DC_CONTACT_ID_UNDEFINED) { + Ok(Summary::new(context, &lastmsg, chat, lastcontact.as_ref()).await) } else { - ret.text2 = Some(stock_str::no_messages(context).await); + Ok(Summary { + text: stock_str::no_messages(context).await, + ..Default::default() + }) } - - Ok(ret) } pub fn get_index_for_id(&self, id: ChatId) -> Option { @@ -637,6 +620,6 @@ mod tests { let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); let summary = chats.get_summary(&t, 0, None).await.unwrap(); - assert_eq!(summary.get_text2().unwrap(), "foo: bar test"); // the linebreak should be removed from summary + assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary } } diff --git a/src/lib.rs b/src/lib.rs index 8a8f1ffb0..b3372dd3e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ mod dehtml; mod color; pub mod html; pub mod plaintext; +pub mod summary; pub mod dc_receive_imf; pub mod dc_tools; diff --git a/src/lot.rs b/src/lot.rs index 02bf4df13..cac5cc6bf 100644 --- a/src/lot.rs +++ b/src/lot.rs @@ -3,6 +3,8 @@ use deltachat_derive::{FromSql, ToSql}; use crate::key::Fingerprint; +use crate::message::MessageState; +use crate::summary::{Summary, SummaryPrefix}; /// An object containing a set of values. /// The meaning of the values is defined by the function returning the object. @@ -139,3 +141,40 @@ impl Default for LotState { LotState::Undefined } } + +impl From for LotState { + fn from(s: MessageState) -> Self { + use MessageState::*; + match s { + Undefined => LotState::Undefined, + InFresh => LotState::MsgInFresh, + InNoticed => LotState::MsgInNoticed, + InSeen => LotState::MsgInSeen, + OutPreparing => LotState::MsgOutPreparing, + OutDraft => LotState::MsgOutDraft, + OutPending => LotState::MsgOutPending, + OutFailed => LotState::MsgOutFailed, + OutDelivered => LotState::MsgOutDelivered, + OutMdnRcvd => LotState::MsgOutMdnRcvd, + } + } +} + +impl From for Lot { + fn from(summary: Summary) -> Self { + let (text1, text1_meaning) = match summary.prefix { + None => (None, Meaning::None), + Some(SummaryPrefix::Draft(text)) => (Some(text), Meaning::Text1Draft), + Some(SummaryPrefix::Username(username)) => (Some(username), Meaning::Text1Username), + Some(SummaryPrefix::Me(text)) => (Some(text), Meaning::Text1Self), + }; + Self { + text1_meaning, + text1, + text2: Some(summary.text), + timestamp: summary.timestamp, + state: summary.state.into(), + ..Default::default() + } + } +} diff --git a/src/message.rs b/src/message.rs index 1c9cd0345..acf2b9281 100644 --- a/src/message.rs +++ b/src/message.rs @@ -6,7 +6,6 @@ use std::convert::TryInto; use anyhow::{ensure, format_err, Context as _, Result}; use async_std::path::{Path, PathBuf}; use deltachat_derive::{FromSql, ToSql}; -use itertools::Itertools; use rusqlite::types::ValueRef; use serde::{Deserialize, Serialize}; @@ -26,15 +25,11 @@ use crate::ephemeral::Timer as EphemeralTimer; use crate::events::EventType; use crate::job::{self, Action}; use crate::log::LogExt; -use crate::lot::{Lot, LotState, Meaning}; use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::split_armored_data; use crate::stock_str; - -// In practice, the user additionally cuts the string themselves -// pixel-accurate. -const SUMMARY_CHARACTERS: usize = 160; +use crate::summary::{get_summarytext_by_raw, Summary}; /// Message ID, including reserved IDs. /// @@ -592,23 +587,21 @@ impl Message { self.ephemeral_timestamp } - pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Lot { - let mut ret = Lot::new(); - + /// Returns message summary for display in the search results. + pub async fn get_summary(&mut self, context: &Context, chat: Option<&Chat>) -> Result { let chat_loaded: Chat; let chat = if let Some(chat) = chat { chat - } else if let Ok(chat) = Chat::load_from_db(context, self.chat_id).await { + } else { + let chat = Chat::load_from_db(context, self.chat_id).await?; chat_loaded = chat; &chat_loaded - } else { - return ret; }; let contact = if self.from_id != DC_CONTACT_ID_SELF { match chat.typ { Chattype::Group | Chattype::Mailinglist => { - Contact::get_by_id(context, self.from_id).await.ok() + Some(Contact::get_by_id(context, self.from_id).await?) } Chattype::Single | Chattype::Undefined => None, } @@ -616,9 +609,7 @@ impl Message { None }; - ret.fill(self, chat, contact.as_ref(), context).await; - - ret + Ok(Summary::new(context, self, chat, contact.as_ref()).await) } pub async fn get_summarytext(&self, context: &Context, approx_characters: usize) -> String { @@ -1029,24 +1020,6 @@ impl std::fmt::Display for MessageState { } } -impl From for LotState { - fn from(s: MessageState) -> Self { - use MessageState::*; - match s { - Undefined => LotState::Undefined, - InFresh => LotState::MsgInFresh, - InNoticed => LotState::MsgInNoticed, - InSeen => LotState::MsgInSeen, - OutPreparing => LotState::MsgOutPreparing, - OutDraft => LotState::MsgOutDraft, - OutPending => LotState::MsgOutPending, - OutFailed => LotState::MsgOutFailed, - OutDelivered => LotState::MsgOutDelivered, - OutMdnRcvd => LotState::MsgOutMdnRcvd, - } - } -} - impl MessageState { pub fn can_fail(self) -> bool { use MessageState::*; @@ -1064,68 +1037,6 @@ impl MessageState { } } -impl Lot { - /* library-internal */ - /* in practice, the user additionally cuts the string himself pixel-accurate */ - pub async fn fill( - &mut self, - msg: &mut Message, - chat: &Chat, - contact: Option<&Contact>, - context: &Context, - ) { - if msg.state == MessageState::OutDraft { - self.text1 = Some(stock_str::draft(context).await); - self.text1_meaning = Meaning::Text1Draft; - } else if msg.from_id == DC_CONTACT_ID_SELF { - if msg.is_info() || chat.is_self_talk() { - self.text1 = None; - self.text1_meaning = Meaning::None; - } else { - self.text1 = Some(stock_str::self_msg(context).await); - self.text1_meaning = Meaning::Text1Self; - } - } else { - match chat.typ { - Chattype::Group | Chattype::Mailinglist => { - if msg.is_info() || contact.is_none() { - self.text1 = None; - self.text1_meaning = Meaning::None; - } else { - self.text1 = msg - .get_override_sender_name() - .or_else(|| contact.map(|contact| msg.get_sender_name(contact))); - self.text1_meaning = Meaning::Text1Username; - } - } - Chattype::Single | Chattype::Undefined => { - self.text1 = None; - self.text1_meaning = Meaning::None; - } - } - } - - let mut text2 = get_summarytext_by_raw( - msg.viewtype, - msg.text.as_ref(), - msg.is_forwarded(), - &msg.param, - SUMMARY_CHARACTERS, - context, - ) - .await; - - if text2.is_empty() && msg.quoted_text().is_some() { - text2 = stock_str::reply_noun(context).await - } - - self.text2 = Some(text2); - - self.timestamp = msg.get_timestamp(); - self.state = msg.state.into(); - } -} - pub async fn get_msg_info(context: &Context, msg_id: MsgId) -> Result { let msg = Message::load_from_db(context, msg_id).await?; let rawtxt: Option = context @@ -1491,88 +1402,6 @@ pub async fn update_msg_state(context: &Context, msg_id: MsgId, state: MessageSt .is_ok() } -/// Returns a summary text. -pub async fn get_summarytext_by_raw( - viewtype: Viewtype, - text: Option>, - was_forwarded: bool, - param: &Params, - approx_characters: usize, - context: &Context, -) -> String { - let mut append_text = true; - let prefix = match 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 param.get_cmd() == SystemMessage::AutocryptSetupMessage { - append_text = false; - stock_str::ac_setup_msg_subject(context).await - } else { - let file_name: String = param - .get_path(Param::File, context) - .unwrap_or(None) - .and_then(|path| { - path.file_name() - .map(|fname| fname.to_string_lossy().into_owned()) - }) - .unwrap_or_else(|| String::from("ErrFileName")); - let label = if 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 - } - _ => { - if 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 let Some(text) = text { - if text.as_ref().is_empty() { - prefix - } else if prefix.is_empty() { - dc_truncate(text.as_ref(), approx_characters).to_string() - } else { - let tmp = format!("{} – {}", prefix, text.as_ref()); - dc_truncate(&tmp, approx_characters).to_string() - } - } else { - prefix - }; - - let summary = if was_forwarded { - let tmp = format!( - "{}: {}", - stock_str::forwarded(context).await, - summary_content - ); - dc_truncate(&tmp, approx_characters).to_string() - } else { - summary_content - }; - - summary.split_whitespace().join(" ") -} - // as we do not cut inside words, this results in about 32-42 characters. // Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise. // It should also be very clear, the subject is _not_ the whole message. @@ -2239,203 +2068,6 @@ mod tests { assert!(chat::prepare_msg(ctx, chat.id, &mut msg).await.is_err()); } - #[async_std::test] - async fn test_get_summarytext_by_raw() { - let d = test::TestContext::new().await; - let ctx = &d.ctx; - - let some_text = Some(" bla \t\n\tbla\n\t".to_string()); - let empty_text = Some("".to_string()); - let no_text: Option = None; - - let mut some_file = Params::new(); - some_file.set(Param::File, "foo.bar"); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Text, - some_text.as_ref(), - false, - &Params::new(), - 50, - ctx - ) - .await, - "bla bla" // for simple text, the type is not added to the summary - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Image, - no_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Image" // file names are not added for images - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Video, - no_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Video" // file names are not added for videos - ); - - assert_eq!( - get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,) - .await, - "GIF" // file names are not added for GIFs - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Sticker, - no_text.as_ref(), - false, - &some_file, - 50, - ctx, - ) - .await, - "Sticker" // file names are not added for stickers - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Voice, - empty_text.as_ref(), - false, - &some_file, - 50, - ctx, - ) - .await, - "Voice message" // file names are not added for voice messages, empty text is skipped - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Voice, - no_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Voice message" // file names are not added for voice messages - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Voice, - some_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH" - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Audio, - no_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Audio \u{2013} foo.bar" // file name is added for audio - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Audio, - empty_text.as_ref(), - false, - &some_file, - 50, - ctx, - ) - .await, - "Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::Audio, - some_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio - ); - - assert_eq!( - get_summarytext_by_raw( - Viewtype::File, - some_text.as_ref(), - false, - &some_file, - 50, - ctx - ) - .await, - "File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files - ); - - // Forwarded - assert_eq!( - get_summarytext_by_raw( - Viewtype::Text, - some_text.as_ref(), - true, - &Params::new(), - 50, - ctx - ) - .await, - "Forwarded: bla bla" // for simple text, the type is not added to the summary - ); - assert_eq!( - get_summarytext_by_raw( - Viewtype::File, - some_text.as_ref(), - true, - &some_file, - 50, - ctx - ) - .await, - "Forwarded: File \u{2013} foo.bar \u{2013} bla bla" - ); - - let mut asm_file = Params::new(); - asm_file.set(Param::File, "foo.bar"); - asm_file.set_cmd(SystemMessage::AutocryptSetupMessage); - assert_eq!( - get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx) - .await, - "Autocrypt Setup Message" // file name is not added for autocrypt setup messages - ); - } - #[async_std::test] async fn test_parse_webrtc_instance() { let (webrtc_type, url) = Message::parse_webrtc_instance("basicwebrtc:https://foo/bar"); diff --git a/src/summary.rs b/src/summary.rs new file mode 100644 index 000000000..e6fe559d3 --- /dev/null +++ b/src/summary.rs @@ -0,0 +1,394 @@ +//! # Message summary for chatlist. + +use crate::chat::Chat; +use crate::constants::{Chattype, Viewtype, DC_CONTACT_ID_SELF}; +use crate::contact::Contact; +use crate::context::Context; +use crate::dc_tools::dc_truncate; +use crate::message::{Message, MessageState}; +use crate::mimeparser::SystemMessage; +use crate::param::{Param, Params}; +use crate::stock_str; +use itertools::Itertools; +use std::fmt; + +// In practice, the user additionally cuts the string themselves +// pixel-accurate. +const SUMMARY_CHARACTERS: usize = 160; + +/// 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, + + /// Summary text, always present. + pub text: String, + + /// Message timestamp. + pub timestamp: i64, + + /// Message state. + pub state: MessageState, +} + +impl Summary { + 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 == DC_CONTACT_ID_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::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 | Chattype::Undefined => None, + } + }; + + let mut text = get_summarytext_by_raw( + msg.viewtype, + msg.text.as_ref(), + msg.is_forwarded(), + &msg.param, + SUMMARY_CHARACTERS, + context, + ) + .await; + + if text.is_empty() && msg.quoted_text().is_some() { + text = stock_str::reply_noun(context).await + } + + Self { + prefix, + text, + timestamp: msg.get_timestamp(), + state: msg.state, + } + } +} + +/// Returns a summary text. +pub async fn get_summarytext_by_raw( + viewtype: Viewtype, + text: Option>, + was_forwarded: bool, + param: &Params, + approx_characters: usize, + context: &Context, +) -> String { + let mut append_text = true; + let prefix = match 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 param.get_cmd() == SystemMessage::AutocryptSetupMessage { + append_text = false; + stock_str::ac_setup_msg_subject(context).await + } else { + let file_name: String = param + .get_path(Param::File, context) + .unwrap_or(None) + .and_then(|path| { + path.file_name() + .map(|fname| fname.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| String::from("ErrFileName")); + let label = if 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 + } + _ => { + if 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 let Some(text) = text { + if text.as_ref().is_empty() { + prefix + } else if prefix.is_empty() { + dc_truncate(text.as_ref(), approx_characters).to_string() + } else { + let tmp = format!("{} – {}", prefix, text.as_ref()); + dc_truncate(&tmp, approx_characters).to_string() + } + } else { + prefix + }; + + let summary = if was_forwarded { + let tmp = format!( + "{}: {}", + stock_str::forwarded(context).await, + summary_content + ); + dc_truncate(&tmp, approx_characters).to_string() + } else { + summary_content + }; + + summary.split_whitespace().join(" ") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils as test; + + #[async_std::test] + async fn test_get_summarytext_by_raw() { + let d = test::TestContext::new().await; + let ctx = &d.ctx; + + let some_text = Some(" bla \t\n\tbla\n\t".to_string()); + let empty_text = Some("".to_string()); + let no_text: Option = None; + + let mut some_file = Params::new(); + some_file.set(Param::File, "foo.bar"); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Text, + some_text.as_ref(), + false, + &Params::new(), + 50, + ctx + ) + .await, + "bla bla" // for simple text, the type is not added to the summary + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Image, + no_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Image" // file names are not added for images + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Video, + no_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Video" // file names are not added for videos + ); + + assert_eq!( + get_summarytext_by_raw(Viewtype::Gif, no_text.as_ref(), false, &some_file, 50, ctx,) + .await, + "GIF" // file names are not added for GIFs + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Sticker, + no_text.as_ref(), + false, + &some_file, + 50, + ctx, + ) + .await, + "Sticker" // file names are not added for stickers + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Voice, + empty_text.as_ref(), + false, + &some_file, + 50, + ctx, + ) + .await, + "Voice message" // file names are not added for voice messages, empty text is skipped + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Voice, + no_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Voice message" // file names are not added for voice messages + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Voice, + some_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Voice message \u{2013} bla bla" // `\u{2013}` explicitly checks for "EN DASH" + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Audio, + no_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Audio \u{2013} foo.bar" // file name is added for audio + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Audio, + empty_text.as_ref(), + false, + &some_file, + 50, + ctx, + ) + .await, + "Audio \u{2013} foo.bar" // file name is added for audio, empty text is not added + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::Audio, + some_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "Audio \u{2013} foo.bar \u{2013} bla bla" // file name and text added for audio + ); + + assert_eq!( + get_summarytext_by_raw( + Viewtype::File, + some_text.as_ref(), + false, + &some_file, + 50, + ctx + ) + .await, + "File \u{2013} foo.bar \u{2013} bla bla" // file name is added for files + ); + + // Forwarded + assert_eq!( + get_summarytext_by_raw( + Viewtype::Text, + some_text.as_ref(), + true, + &Params::new(), + 50, + ctx + ) + .await, + "Forwarded: bla bla" // for simple text, the type is not added to the summary + ); + assert_eq!( + get_summarytext_by_raw( + Viewtype::File, + some_text.as_ref(), + true, + &some_file, + 50, + ctx + ) + .await, + "Forwarded: File \u{2013} foo.bar \u{2013} bla bla" + ); + + let mut asm_file = Params::new(); + asm_file.set(Param::File, "foo.bar"); + asm_file.set_cmd(SystemMessage::AutocryptSetupMessage); + assert_eq!( + get_summarytext_by_raw(Viewtype::File, no_text.as_ref(), false, &asm_file, 50, ctx) + .await, + "Autocrypt Setup Message" // file name is not added for autocrypt setup messages + ); + } +}