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