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