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() {
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::Broadcast
107 || chat.typ == Chattype::Mailinglist
108 || chat.is_self_talk()
109 {
110 if msg.is_info() || contact.is_none() {
111 None
112 } else {
113 msg.get_override_sender_name()
114 .or_else(|| contact.map(|contact| msg.get_sender_name(contact)))
115 .map(SummaryPrefix::Username)
116 }
117 } else {
118 None
119 };
120
121 let mut text = msg.get_summary_text(context).await;
122
123 if text.is_empty() && msg.quoted_text().is_some() {
124 text = stock_str::reply_noun(context).await
125 }
126
127 let thumbnail_path = if msg.viewtype == Viewtype::Image
128 || msg.viewtype == Viewtype::Gif
129 || msg.viewtype == Viewtype::Sticker
130 {
131 msg.get_file(context)
132 .and_then(|path| path.to_str().map(|p| p.to_owned()))
133 } else if msg.viewtype == Viewtype::Webxdc {
134 Some("webxdc-icon://last-msg-id".to_string())
135 } else {
136 None
137 };
138
139 Ok(Summary {
140 prefix,
141 text,
142 timestamp: msg.get_timestamp(),
143 state: msg.state,
144 thumbnail_path,
145 })
146 }
147
148 pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
150 truncate(&self.text, approx_chars)
151 }
152}
153
154impl Message {
155 pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
157 let summary = self.get_summary_text_without_prefix(context).await;
158
159 if self.is_forwarded() {
160 format!("{}: {}", stock_str::forwarded(context).await, summary)
161 } else {
162 summary
163 }
164 }
165
166 async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
168 let (emoji, type_name, type_file, append_text);
169 match self.viewtype {
170 Viewtype::Image => {
171 emoji = Some("📷");
172 type_name = Some(stock_str::image(context).await);
173 type_file = None;
174 append_text = true;
175 }
176 Viewtype::Gif => {
177 emoji = None;
178 type_name = Some(stock_str::gif(context).await);
179 type_file = None;
180 append_text = true;
181 }
182 Viewtype::Sticker => {
183 emoji = None;
184 type_name = Some(stock_str::sticker(context).await);
185 type_file = None;
186 append_text = true;
187 }
188 Viewtype::Video => {
189 emoji = Some("🎥");
190 type_name = Some(stock_str::video(context).await);
191 type_file = None;
192 append_text = true;
193 }
194 Viewtype::Voice => {
195 emoji = Some("🎤");
196 type_name = Some(stock_str::voice_message(context).await);
197 type_file = None;
198 append_text = true;
199 }
200 Viewtype::Audio => {
201 emoji = Some("🎵");
202 type_name = Some(stock_str::audio(context).await);
203 type_file = self.get_filename();
204 append_text = true
205 }
206 Viewtype::File => {
207 emoji = Some("📎");
208 type_name = Some(stock_str::file(context).await);
209 type_file = self.get_filename();
210 append_text = true
211 }
212 Viewtype::VideochatInvitation => {
213 emoji = None;
214 type_name = Some(stock_str::videochat_invitation(context).await);
215 type_file = None;
216 append_text = false;
217 }
218 Viewtype::Webxdc => {
219 emoji = None;
220 type_name = None;
221 type_file = Some(
222 self.get_webxdc_info(context)
223 .await
224 .map(|info| info.name)
225 .unwrap_or_else(|_| "ErrWebxdcName".to_string()),
226 );
227 append_text = true;
228 }
229 Viewtype::Vcard => {
230 emoji = Some("👤");
231 type_name = None;
232 type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
233 append_text = true;
234 }
235 Viewtype::Text | Viewtype::Unknown => {
236 emoji = None;
237 if self.param.get_cmd() == SystemMessage::LocationOnly {
238 type_name = Some(stock_str::location(context).await);
239 type_file = None;
240 append_text = false;
241 } else {
242 type_name = None;
243 type_file = None;
244 append_text = true;
245 }
246 }
247 };
248
249 let text = self.text.clone();
250
251 let summary = if let Some(type_file) = type_file {
252 if append_text && !text.is_empty() {
253 format!("{type_file} – {text}")
254 } else {
255 type_file
256 }
257 } else if append_text && !text.is_empty() {
258 if emoji.is_some() {
259 text
260 } else if let Some(type_name) = type_name {
261 format!("{type_name} – {text}")
262 } else {
263 text
264 }
265 } else if let Some(type_name) = type_name {
266 type_name
267 } else {
268 "".to_string()
269 };
270
271 let summary = if let Some(emoji) = emoji {
272 format!("{emoji} {summary}")
273 } else {
274 summary
275 };
276
277 summary.split_whitespace().collect::<Vec<&str>>().join(" ")
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use std::path::PathBuf;
284
285 use super::*;
286 use crate::chat::ChatId;
287 use crate::param::Param;
288 use crate::test_utils::TestContext;
289
290 async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
291 assert_eq!(msg.get_summary_text(ctx).await, expected);
292 assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
293 }
294
295 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
296 async fn test_get_summary_text() {
297 let d = TestContext::new_alice().await;
298 let ctx = &d.ctx;
299 let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
300 .await
301 .unwrap();
302 let some_text = " bla \t\n\tbla\n\t".to_string();
303
304 async fn write_file_to_blobdir(d: &TestContext) -> PathBuf {
305 let bytes = &[38, 209, 39, 29]; let file = d.get_blobdir().join("random_filename_392438");
307 tokio::fs::write(&file, bytes).await.unwrap();
308 file
309 }
310
311 let msg = Message::new_text(some_text.to_string());
312 assert_summary_texts(&msg, ctx, "bla bla").await; let file = write_file_to_blobdir(&d).await;
315 let mut msg = Message::new(Viewtype::Image);
316 msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
317 .unwrap();
318 assert_summary_texts(&msg, ctx, "📷 Image").await; let file = write_file_to_blobdir(&d).await;
321 let mut msg = Message::new(Viewtype::Image);
322 msg.set_text(some_text.to_string());
323 msg.set_file_and_deduplicate(&d, &file, Some("foo.jpg"), None)
324 .unwrap();
325 assert_summary_texts(&msg, ctx, "📷 bla bla").await; let file = write_file_to_blobdir(&d).await;
328 let mut msg = Message::new(Viewtype::Video);
329 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
330 .unwrap();
331 assert_summary_texts(&msg, ctx, "🎥 Video").await; let file = write_file_to_blobdir(&d).await;
334 let mut msg = Message::new(Viewtype::Video);
335 msg.set_text(some_text.to_string());
336 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp4"), None)
337 .unwrap();
338 assert_summary_texts(&msg, ctx, "🎥 bla bla").await; let file = write_file_to_blobdir(&d).await;
341 let mut msg = Message::new(Viewtype::Gif);
342 msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
343 .unwrap();
344 assert_summary_texts(&msg, ctx, "GIF").await; let file = write_file_to_blobdir(&d).await;
347 let mut msg = Message::new(Viewtype::Gif);
348 msg.set_text(some_text.to_string());
349 msg.set_file_and_deduplicate(&d, &file, Some("foo.gif"), None)
350 .unwrap();
351 assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; let file = write_file_to_blobdir(&d).await;
354 let mut msg = Message::new(Viewtype::Sticker);
355 msg.set_file_and_deduplicate(&d, &file, Some("foo.png"), None)
356 .unwrap();
357 assert_summary_texts(&msg, ctx, "Sticker").await; let file = write_file_to_blobdir(&d).await;
360 let mut msg = Message::new(Viewtype::Voice);
361 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
362 .unwrap();
363 assert_summary_texts(&msg, ctx, "🎤 Voice message").await; let file = write_file_to_blobdir(&d).await;
366 let mut msg = Message::new(Viewtype::Voice);
367 msg.set_text(some_text.clone());
368 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
369 .unwrap();
370 assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
371
372 let file = write_file_to_blobdir(&d).await;
373 let mut msg = Message::new(Viewtype::Audio);
374 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
375 .unwrap();
376 assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; let file = write_file_to_blobdir(&d).await;
379 let mut msg = Message::new(Viewtype::Audio);
380 msg.set_text(some_text.clone());
381 msg.set_file_and_deduplicate(&d, &file, Some("foo.mp3"), None)
382 .unwrap();
383 assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; let mut msg = Message::new(Viewtype::File);
386 let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
387 msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
388 .unwrap();
389 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
390 assert_eq!(msg.viewtype, Viewtype::Webxdc);
391 assert_summary_texts(&msg, ctx, "nice app!").await;
392 msg.set_text(some_text.clone());
393 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
394 assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
395
396 let file = write_file_to_blobdir(&d).await;
397 let mut msg = Message::new(Viewtype::File);
398 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
399 .unwrap();
400 assert_summary_texts(&msg, ctx, "📎 foo.bar").await; let file = write_file_to_blobdir(&d).await;
403 let mut msg = Message::new(Viewtype::File);
404 msg.set_text(some_text.clone());
405 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
406 .unwrap();
407 assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; let file = write_file_to_blobdir(&d).await;
410 let mut msg = Message::new(Viewtype::VideochatInvitation);
411 msg.set_text(some_text.clone());
412 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
413 .unwrap();
414 assert_summary_texts(&msg, ctx, "Video chat invitation").await; let mut msg = Message::new(Viewtype::Vcard);
417 msg.set_file_from_bytes(ctx, "foo.vcf", b"", None).unwrap();
418 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
419 assert_eq!(msg.viewtype, Viewtype::File);
421 assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
422 msg.set_text(some_text.clone());
423 assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
424
425 for vt in [Viewtype::Vcard, Viewtype::File] {
426 let mut msg = Message::new(vt);
427 msg.set_file_from_bytes(
428 ctx,
429 "alice.vcf",
430 b"BEGIN:VCARD\n\
431 VERSION:4.0\n\
432 FN:Alice Wonderland\n\
433 EMAIL;TYPE=work:alice@example.org\n\
434 END:VCARD",
435 None,
436 )
437 .unwrap();
438 chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
439 assert_eq!(msg.viewtype, Viewtype::Vcard);
440 assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
441 }
442
443 let mut msg = Message::new_text(some_text.clone());
445 msg.param.set_int(Param::Forwarded, 1);
446 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;
450 let mut msg = Message::new(Viewtype::File);
451 msg.set_text(some_text.clone());
452 msg.set_file_and_deduplicate(&d, &file, Some("foo.bar"), None)
453 .unwrap();
454 msg.param.set_int(Param::Forwarded, 1);
455 assert_eq!(
456 msg.get_summary_text(ctx).await,
457 "Forwarded: 📎 foo.bar \u{2013} bla bla"
458 );
459 assert_eq!(
460 msg.get_summary_text_without_prefix(ctx).await,
461 "📎 foo.bar \u{2013} bla bla"
462 ); let mut msg = Message::new(Viewtype::File);
465 msg.set_file_from_bytes(ctx, "autocrypt-setup-message.html", b"data", None)
466 .unwrap();
467 msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
468 assert_summary_texts(&msg, ctx, "📎 autocrypt-setup-message.html").await;
469 }
471}