1use std::borrow::Cow;
4use std::fmt;
5use std::str;
6
7use anyhow::Result;
8use num_traits::FromPrimitive;
9
10use crate::calls::{CallState, call_state};
11use crate::chat::Chat;
12use crate::constants::Chattype;
13use crate::contact::{Contact, ContactId};
14use crate::context::Context;
15use crate::message::{Message, MessageState, Viewtype};
16use crate::mimeparser::SystemMessage;
17use crate::param::Param;
18use crate::stock_str;
19use crate::stock_str::msg_reacted;
20use crate::tools::truncate;
21
22#[derive(Debug)]
24pub enum SummaryPrefix {
25 Username(String),
27
28 Draft(String),
30
31 Me(String),
33}
34
35impl fmt::Display for SummaryPrefix {
36 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
37 match self {
38 SummaryPrefix::Username(username) => write!(f, "{username}"),
39 SummaryPrefix::Draft(text) => write!(f, "{text}"),
40 SummaryPrefix::Me(text) => write!(f, "{text}"),
41 }
42 }
43}
44
45#[derive(Debug, Default)]
47pub struct Summary {
48 pub prefix: Option<SummaryPrefix>,
50
51 pub text: String,
53
54 pub timestamp: i64,
56
57 pub state: MessageState,
59
60 pub thumbnail_path: Option<String>,
62}
63
64impl Summary {
65 pub async fn new_with_reaction_details(
68 context: &Context,
69 msg: &Message,
70 chat: &Chat,
71 contact: Option<&Contact>,
72 ) -> Result<Summary> {
73 if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
74 .get_last_reaction_if_newer_than(context, msg.timestamp_sort)
75 .await?
76 {
77 let summary = reaction_msg.get_summary_text_without_prefix(context).await;
81 return Ok(Summary {
82 prefix: None,
83 text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
84 timestamp: msg.get_timestamp(), state: msg.state, thumbnail_path: None,
87 });
88 }
89 Self::new(context, msg, chat, contact).await
90 }
91
92 pub async fn new(
95 context: &Context,
96 msg: &Message,
97 chat: &Chat,
98 contact: Option<&Contact>,
99 ) -> Result<Summary> {
100 let prefix = if msg.state == MessageState::OutDraft {
101 Some(SummaryPrefix::Draft(stock_str::draft(context).await))
102 } else if msg.from_id == ContactId::SELF {
103 if msg.is_info() || msg.viewtype == Viewtype::Call || chat.typ == Chattype::OutBroadcast
104 {
105 None
106 } else {
107 Some(SummaryPrefix::Me(stock_str::self_msg(context).await))
108 }
109 } else if chat.typ == Chattype::Group
110 || chat.typ == Chattype::Mailinglist
111 || chat.is_self_talk()
112 {
113 if msg.is_info() || contact.is_none() {
114 None
115 } else {
116 msg.get_override_sender_name()
117 .or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
118 .map(SummaryPrefix::Username)
119 }
120 } else {
121 None
122 };
123
124 let mut text = msg.get_summary_text(context).await;
125
126 if text.is_empty() && msg.quoted_text().is_some() {
127 text = stock_str::reply_noun(context).await
128 }
129
130 let thumbnail_path = if msg.viewtype == Viewtype::Image
131 || msg.viewtype == Viewtype::Gif
132 || msg.viewtype == Viewtype::Sticker
133 {
134 msg.get_file(context)
135 .and_then(|path| path.to_str().map(|p| p.to_owned()))
136 } else if msg.viewtype == Viewtype::Webxdc {
137 Some("webxdc-icon://last-msg-id".to_string())
138 } else {
139 None
140 };
141
142 Ok(Summary {
143 prefix,
144 text,
145 timestamp: msg.get_timestamp(),
146 state: msg.state,
147 thumbnail_path,
148 })
149 }
150
151 pub fn truncated_text(&self, approx_chars: usize) -> Cow<'_, str> {
153 truncate(&self.text, approx_chars)
154 }
155}
156
157impl Message {
158 pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
160 let summary = self.get_summary_text_without_prefix(context).await;
161
162 if self.is_forwarded() {
163 format!("{}: {}", stock_str::forwarded(context).await, summary)
164 } else {
165 summary
166 }
167 }
168
169 async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
171 let (emoji, type_name, type_file, append_text);
172 let viewtype = match self
173 .param
174 .get_i64(Param::PostMessageViewtype)
175 .and_then(Viewtype::from_i64)
176 {
177 Some(vt) => vt,
178 None => self.viewtype,
179 };
180 match viewtype {
181 Viewtype::Image => {
182 emoji = Some("📷");
183 type_name = Some(stock_str::image(context).await);
184 type_file = None;
185 append_text = true;
186 }
187 Viewtype::Gif => {
188 emoji = None;
189 type_name = Some(stock_str::gif(context).await);
190 type_file = None;
191 append_text = true;
192 }
193 Viewtype::Sticker => {
194 emoji = None;
195 type_name = Some(stock_str::sticker(context).await);
196 type_file = None;
197 append_text = true;
198 }
199 Viewtype::Video => {
200 emoji = Some("🎥");
201 type_name = Some(stock_str::video(context).await);
202 type_file = None;
203 append_text = true;
204 }
205 Viewtype::Voice => {
206 emoji = Some("🎤");
207 type_name = Some(stock_str::voice_message(context).await);
208 type_file = None;
209 append_text = true;
210 }
211 Viewtype::Audio => {
212 emoji = Some("🎵");
213 type_name = Some(stock_str::audio(context).await);
214 type_file = self.get_filename();
215 append_text = true
216 }
217 Viewtype::File => {
218 emoji = Some("📎");
219 type_name = Some(stock_str::file(context).await);
220 type_file = self.get_filename();
221 append_text = true
222 }
223 Viewtype::Webxdc => {
224 type_name = None;
225 if self.viewtype == Viewtype::Webxdc {
226 emoji = None;
227 type_file = Some(
228 self.get_webxdc_info(context)
229 .await
230 .map(|info| info.name)
231 .unwrap_or_else(|_| "ErrWebxdcName".to_string()),
232 );
233 } else {
234 emoji = Some("📱");
235 type_file = Some(viewtype.to_locale_string(context).await);
236 }
237 append_text = true;
238 }
239 Viewtype::Vcard => {
240 emoji = Some("👤");
241 type_name = None;
242 if self.viewtype == Viewtype::Vcard {
243 type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
244 } else {
245 type_file = None;
246 }
247 append_text = true;
248 }
249 Viewtype::Call => {
250 let call_info = context.load_call_by_id(self.id).await.unwrap_or(None);
251 let has_video = call_info.is_some_and(|c| c.has_video_initially());
252 let call_state = call_state(context, self.id)
253 .await
254 .unwrap_or(CallState::Alerting);
255 emoji = Some(if has_video { "🎥" } else { "📞" });
256 type_name = Some(match call_state {
257 CallState::Alerting | CallState::Active | CallState::Completed { .. } => {
258 if self.from_id == ContactId::SELF {
259 stock_str::outgoing_call(context, has_video).await
260 } else {
261 stock_str::incoming_call(context, has_video).await
262 }
263 }
264 CallState::Missed => stock_str::missed_call(context).await,
265 CallState::Declined => stock_str::declined_call(context).await,
266 CallState::Canceled => stock_str::canceled_call(context).await,
267 });
268 type_file = None;
269 append_text = false
270 }
271 Viewtype::Text | Viewtype::Unknown => {
272 emoji = None;
273 if self.param.get_cmd() == SystemMessage::LocationOnly {
274 type_name = Some(stock_str::location(context).await);
275 type_file = None;
276 append_text = false;
277 } else {
278 type_name = None;
279 type_file = None;
280 append_text = true;
281 }
282 }
283 };
284
285 let text = self.text.clone();
286
287 let summary = if let Some(type_file) = type_file {
288 if append_text && !text.is_empty() {
289 format!("{type_file} – {text}")
290 } else {
291 type_file
292 }
293 } else if append_text && !text.is_empty() {
294 if emoji.is_some() {
295 text
296 } else if let Some(type_name) = type_name {
297 format!("{type_name} – {text}")
298 } else {
299 text
300 }
301 } else if let Some(type_name) = type_name {
302 type_name
303 } else {
304 "".to_string()
305 };
306
307 let summary = if let Some(emoji) = emoji {
308 format!("{emoji} {summary}")
309 } else {
310 summary
311 };
312
313 summary.split_whitespace().collect::<Vec<&str>>().join(" ")
314 }
315}
316
317#[cfg(test)]
318pub async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
320 assert_eq!(msg.get_summary_text(ctx).await, expected);
321 assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
322}
323
324#[cfg(test)]
325mod tests {
326 use std::path::PathBuf;
327
328 use super::*;
329 use crate::chat::ChatId;
330 use crate::param::Param;
331 use crate::test_utils::TestContext;
332
333 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
334 async fn test_get_summary_text() {
335 let d = TestContext::new_alice().await;
336 let ctx = &d.ctx;
337 let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
338 .await
339 .unwrap();
340 let some_text = " bla \t\n\tbla\n\t".to_string();
341
342 async fn write_file_to_blobdir(d: &TestContext) -> PathBuf {
343 let bytes = &[38, 209, 39, 29]; let file = d.get_blobdir().join("random_filename_392438");
345 tokio::fs::write(&file, bytes).await.unwrap();
346 file
347 }
348
349 let msg = Message::new_text(some_text.to_string());
350 assert_summary_texts(&msg, ctx, "bla bla").await; let file = write_file_to_blobdir(&d).await;
353 let mut msg = Message::new(Viewtype::Image);
354 msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
355 .unwrap();
356 assert_summary_texts(&msg, ctx, "📷 Image").await; let file = write_file_to_blobdir(&d).await;
359 let mut msg = Message::new(Viewtype::Image);
360 msg.set_text(some_text.to_string());
361 msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
362 .unwrap();
363 assert_summary_texts(&msg, ctx, "📷 bla bla").await; let file = write_file_to_blobdir(&d).await;
366 let mut msg = Message::new(Viewtype::Video);
367 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
368 .unwrap();
369 assert_summary_texts(&msg, ctx, "🎥 Video").await; let file = write_file_to_blobdir(&d).await;
372 let mut msg = Message::new(Viewtype::Video);
373 msg.set_text(some_text.to_string());
374 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
375 .unwrap();
376 assert_summary_texts(&msg, ctx, "🎥 bla bla").await; let file = write_file_to_blobdir(&d).await;
379 let mut msg = Message::new(Viewtype::Gif);
380 msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
381 .unwrap();
382 assert_summary_texts(&msg, ctx, "GIF").await; let file = write_file_to_blobdir(&d).await;
385 let mut msg = Message::new(Viewtype::Gif);
386 msg.set_text(some_text.to_string());
387 msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
388 .unwrap();
389 assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; let file = write_file_to_blobdir(&d).await;
392 let mut msg = Message::new(Viewtype::Sticker);
393 msg.set_file_and_deduplicate(&d, &file, Some("foo.png"), None)
394 .unwrap();
395 assert_summary_texts(&msg, ctx, "Sticker").await; let file = write_file_to_blobdir(&d).await;
398 let mut msg = Message::new(Viewtype::Voice);
399 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
400 .unwrap();
401 assert_summary_texts(&msg, ctx, "🎤 Voice message").await; let file = write_file_to_blobdir(&d).await;
404 let mut msg = Message::new(Viewtype::Voice);
405 msg.set_text(some_text.clone());
406 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
407 .unwrap();
408 assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
409
410 let file = write_file_to_blobdir(&d).await;
411 let mut msg = Message::new(Viewtype::Audio);
412 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
413 .unwrap();
414 assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; let file = write_file_to_blobdir(&d).await;
417 let mut msg = Message::new(Viewtype::Audio);
418 msg.set_text(some_text.clone());
419 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
420 .unwrap();
421 assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::File);
424 let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
425 msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
426 .unwrap();
427 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
428 assert_eq!(msg.viewtype, Viewtype::Webxdc);
429 assert_summary_texts(&msg, ctx, "nice app!").await;
430 msg.set_text(some_text.clone());
431 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
432 assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
433
434 let file = write_file_to_blobdir(&d).await;
435 let mut msg = Message::new(Viewtype::File);
436 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
437 .unwrap();
438 assert_summary_texts(&msg, ctx, "📎 foo.bar").await; let file = write_file_to_blobdir(&d).await;
441 let mut msg = Message::new(Viewtype::File);
442 msg.set_text(some_text.clone());
443 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
444 .unwrap();
445 assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::Vcard);
448 msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
449 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
450 assert_eq!(msg.viewtype, Viewtype::File);
452 assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
453 msg.set_text(some_text.clone());
454 assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
455
456 for vt in [Viewtype::Vcard, Viewtype::File] {
457 let mut msg = Message::new(vt);
458 msg.set_file_from_bytes(
459 ctx,
460 "alice.vcf",
461 b"BEGIN:VCARD\n\
462 VERSION:4.0\n\
463 FN:Alice Wonderland\n\
464 EMAIL;TYPE=work:alice@example.org\n\
465 END:VCARD",
466 None,
467 )
468 .unwrap();
469 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
470 assert_eq!(msg.viewtype, Viewtype::Vcard);
471 assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
472 }
473
474 let mut msg = Message::new_text(some_text.clone());
476 msg.param.set_int(Param::Forwarded, 1);
477 assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); let file = write_file_to_blobdir(&d).await;
481 let mut msg = Message::new(Viewtype::File);
482 msg.set_text(some_text.clone());
483 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
484 .unwrap();
485 msg.param.set_int(Param::Forwarded, 1);
486 assert_eq!(
487 msg.get_summary_text(ctx).await,
488 "Forwarded: 📎 foo.bar \u{2013} bla bla"
489 );
490 assert_eq!(
491 msg.get_summary_text_without_prefix(ctx).await,
492 "📎 foo.bar \u{2013} bla bla"
493 ); let mut msg = Message::new(Viewtype::File);
496 msg.set_file_from_bytes(ctx, "autocrypt-setup-message.html", b"data", None)
497 .unwrap();
498 msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
499 assert_summary_texts(&msg, ctx, "📎 autocrypt-setup-message.html").await;
500 }
502}