mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
this PR adds the address to be used by the UI for `window.webxdc.selfAddr` to webxdc-info. UIs need to be changed accordingly and must not use configured_addr any longer. the address is created by sha256(private-key + rfc724_mid) , which results in different addresses for each webxdc, without the option to find out the real address of the user. this also returns the same address for a multi-device-setup - sending totally random self address around might be an alternative, however would require connectivity (both devices may be offline on first start). for existing app, after the change, there will be a new user, resulting eg. in a new highscore, otherwise, things should be mostly fine. this assumption is also important as we might change the thing another time when it comes to multi-transport. ftr, addresses look like `0f187e3f420748b03e3da76543e9a84ecff822687ce7e94f250c04c7c50398bc` now when this is merged, we need to adapt #6230 and file issues for all UI to use `info.selfAddr` closes #6216
442 lines
16 KiB
Rust
442 lines
16 KiB
Rust
//! # Message summary for chatlist.
|
||
|
||
use std::borrow::Cow;
|
||
use std::fmt;
|
||
use std::str;
|
||
|
||
use crate::chat::Chat;
|
||
use crate::constants::Chattype;
|
||
use crate::contact::{Contact, ContactId};
|
||
use crate::context::Context;
|
||
use crate::message::{Message, MessageState, Viewtype};
|
||
use crate::mimeparser::SystemMessage;
|
||
use crate::param::Param;
|
||
use crate::stock_str;
|
||
use crate::stock_str::msg_reacted;
|
||
use crate::tools::truncate;
|
||
use anyhow::Result;
|
||
|
||
/// 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<SummaryPrefix>,
|
||
|
||
/// Summary text, always present.
|
||
pub text: String,
|
||
|
||
/// Message timestamp.
|
||
pub timestamp: i64,
|
||
|
||
/// Message state.
|
||
pub state: MessageState,
|
||
|
||
/// Message preview image path
|
||
pub thumbnail_path: Option<String>,
|
||
}
|
||
|
||
impl Summary {
|
||
/// Constructs chatlist summary
|
||
/// from the provided message, chat and message author contact snapshots.
|
||
pub async fn new_with_reaction_details(
|
||
context: &Context,
|
||
msg: &Message,
|
||
chat: &Chat,
|
||
contact: Option<&Contact>,
|
||
) -> Result<Summary> {
|
||
if let Some((reaction_msg, reaction_contact_id, reaction)) = chat
|
||
.get_last_reaction_if_newer_than(context, msg.timestamp_sort)
|
||
.await?
|
||
{
|
||
// there is a reaction newer than the latest message, show that.
|
||
// sorting and therefore date is still the one of the last message,
|
||
// the reaction is is more sth. that overlays temporarily.
|
||
let summary = reaction_msg.get_summary_text_without_prefix(context).await;
|
||
return Ok(Summary {
|
||
prefix: None,
|
||
text: msg_reacted(context, reaction_contact_id, &reaction, &summary).await,
|
||
timestamp: msg.get_timestamp(), // message timestamp (not reaction) to make timestamps more consistent with chats ordering
|
||
state: msg.state, // message state (not reaction) - indicating if it was me sending the last message
|
||
thumbnail_path: None,
|
||
});
|
||
}
|
||
Self::new(context, msg, chat, contact).await
|
||
}
|
||
|
||
/// Constructs search result summary
|
||
/// from the provided message, chat and message author contact snapshots.
|
||
pub async fn new(
|
||
context: &Context,
|
||
msg: &Message,
|
||
chat: &Chat,
|
||
contact: Option<&Contact>,
|
||
) -> Result<Summary> {
|
||
let prefix = if msg.state == MessageState::OutDraft {
|
||
Some(SummaryPrefix::Draft(stock_str::draft(context).await))
|
||
} else if msg.from_id == ContactId::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::Broadcast | 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 => None,
|
||
}
|
||
};
|
||
|
||
let mut text = msg.get_summary_text(context).await;
|
||
|
||
if text.is_empty() && msg.quoted_text().is_some() {
|
||
text = stock_str::reply_noun(context).await
|
||
}
|
||
|
||
let thumbnail_path = if msg.viewtype == Viewtype::Image
|
||
|| msg.viewtype == Viewtype::Gif
|
||
|| msg.viewtype == Viewtype::Sticker
|
||
{
|
||
msg.get_file(context)
|
||
.and_then(|path| path.to_str().map(|p| p.to_owned()))
|
||
} else if msg.viewtype == Viewtype::Webxdc {
|
||
Some("webxdc-icon://last-msg-id".to_string())
|
||
} else {
|
||
None
|
||
};
|
||
|
||
Ok(Summary {
|
||
prefix,
|
||
text,
|
||
timestamp: msg.get_timestamp(),
|
||
state: msg.state,
|
||
thumbnail_path,
|
||
})
|
||
}
|
||
|
||
/// Returns the [`Summary::text`] attribute truncated to an approximate length.
|
||
pub fn truncated_text(&self, approx_chars: usize) -> Cow<str> {
|
||
truncate(&self.text, approx_chars)
|
||
}
|
||
}
|
||
|
||
impl Message {
|
||
/// Returns a summary text.
|
||
pub(crate) async fn get_summary_text(&self, context: &Context) -> String {
|
||
let summary = self.get_summary_text_without_prefix(context).await;
|
||
|
||
if self.is_forwarded() {
|
||
format!("{}: {}", stock_str::forwarded(context).await, summary)
|
||
} else {
|
||
summary
|
||
}
|
||
}
|
||
|
||
/// Returns a summary text without "Forwarded:" prefix.
|
||
async fn get_summary_text_without_prefix(&self, context: &Context) -> String {
|
||
let (emoji, type_name, type_file, append_text);
|
||
match self.viewtype {
|
||
Viewtype::Image => {
|
||
emoji = Some("📷");
|
||
type_name = Some(stock_str::image(context).await);
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
Viewtype::Gif => {
|
||
emoji = None;
|
||
type_name = Some(stock_str::gif(context).await);
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
Viewtype::Sticker => {
|
||
emoji = None;
|
||
type_name = Some(stock_str::sticker(context).await);
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
Viewtype::Video => {
|
||
emoji = Some("🎥");
|
||
type_name = Some(stock_str::video(context).await);
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
Viewtype::Voice => {
|
||
emoji = Some("🎤");
|
||
type_name = Some(stock_str::voice_message(context).await);
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
Viewtype::Audio => {
|
||
emoji = Some("🎵");
|
||
type_name = Some(stock_str::audio(context).await);
|
||
type_file = self.get_filename();
|
||
append_text = true
|
||
}
|
||
Viewtype::File => {
|
||
if self.param.get_cmd() == SystemMessage::AutocryptSetupMessage {
|
||
emoji = None;
|
||
type_name = Some(stock_str::ac_setup_msg_subject(context).await);
|
||
type_file = None;
|
||
append_text = false;
|
||
} else {
|
||
emoji = Some("📎");
|
||
type_name = Some(stock_str::file(context).await);
|
||
type_file = self.get_filename();
|
||
append_text = true
|
||
}
|
||
}
|
||
Viewtype::VideochatInvitation => {
|
||
emoji = None;
|
||
type_name = Some(stock_str::videochat_invitation(context).await);
|
||
type_file = None;
|
||
append_text = false;
|
||
}
|
||
Viewtype::Webxdc => {
|
||
emoji = None;
|
||
type_name = None;
|
||
type_file = Some(
|
||
self.get_webxdc_info(context)
|
||
.await
|
||
.map(|info| info.name)
|
||
.unwrap_or_else(|_| "ErrWebxdcName".to_string()),
|
||
);
|
||
append_text = true;
|
||
}
|
||
Viewtype::Vcard => {
|
||
emoji = Some("👤");
|
||
type_name = None;
|
||
type_file = self.param.get(Param::Summary1).map(|s| s.to_string());
|
||
append_text = true;
|
||
}
|
||
Viewtype::Text | Viewtype::Unknown => {
|
||
emoji = None;
|
||
if self.param.get_cmd() == SystemMessage::LocationOnly {
|
||
type_name = Some(stock_str::location(context).await);
|
||
type_file = None;
|
||
append_text = false;
|
||
} else {
|
||
type_name = None;
|
||
type_file = None;
|
||
append_text = true;
|
||
}
|
||
}
|
||
};
|
||
|
||
let text = self.text.clone();
|
||
|
||
let summary = if let Some(type_file) = type_file {
|
||
if append_text && !text.is_empty() {
|
||
format!("{type_file} – {text}")
|
||
} else {
|
||
type_file
|
||
}
|
||
} else if append_text && !text.is_empty() {
|
||
if emoji.is_some() {
|
||
text
|
||
} else if let Some(type_name) = type_name {
|
||
format!("{type_name} – {text}")
|
||
} else {
|
||
text
|
||
}
|
||
} else if let Some(type_name) = type_name {
|
||
type_name
|
||
} else {
|
||
"".to_string()
|
||
};
|
||
|
||
let summary = if let Some(emoji) = emoji {
|
||
format!("{emoji} {summary}")
|
||
} else {
|
||
summary
|
||
};
|
||
|
||
summary.split_whitespace().collect::<Vec<&str>>().join(" ")
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::chat::ChatId;
|
||
use crate::param::Param;
|
||
use crate::test_utils::TestContext;
|
||
|
||
async fn assert_summary_texts(msg: &Message, ctx: &Context, expected: &str) {
|
||
assert_eq!(msg.get_summary_text(ctx).await, expected);
|
||
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, expected);
|
||
}
|
||
|
||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||
async fn test_get_summary_text() {
|
||
let d = TestContext::new_alice().await;
|
||
let ctx = &d.ctx;
|
||
let chat_id = ChatId::create_for_contact(ctx, ContactId::SELF)
|
||
.await
|
||
.unwrap();
|
||
let some_text = " bla \t\n\tbla\n\t".to_string();
|
||
|
||
let msg = Message::new_text(some_text.to_string());
|
||
assert_summary_texts(&msg, ctx, "bla bla").await; // for simple text, the type is not added to the summary
|
||
|
||
let mut msg = Message::new(Viewtype::Image);
|
||
msg.set_file("foo.jpg", None);
|
||
assert_summary_texts(&msg, ctx, "📷 Image").await; // file names are not added for images
|
||
|
||
let mut msg = Message::new(Viewtype::Image);
|
||
msg.set_text(some_text.to_string());
|
||
msg.set_file("foo.jpg", None);
|
||
assert_summary_texts(&msg, ctx, "📷 bla bla").await; // type is visible by emoji if text is set
|
||
|
||
let mut msg = Message::new(Viewtype::Video);
|
||
msg.set_file("foo.mp4", None);
|
||
assert_summary_texts(&msg, ctx, "🎥 Video").await; // file names are not added for videos
|
||
|
||
let mut msg = Message::new(Viewtype::Video);
|
||
msg.set_text(some_text.to_string());
|
||
msg.set_file("foo.mp4", None);
|
||
assert_summary_texts(&msg, ctx, "🎥 bla bla").await; // type is visible by emoji if text is set
|
||
|
||
let mut msg = Message::new(Viewtype::Gif);
|
||
msg.set_file("foo.gif", None);
|
||
assert_summary_texts(&msg, ctx, "GIF").await; // file names are not added for GIFs
|
||
|
||
let mut msg = Message::new(Viewtype::Gif);
|
||
msg.set_text(some_text.to_string());
|
||
msg.set_file("foo.gif", None);
|
||
assert_summary_texts(&msg, ctx, "GIF \u{2013} bla bla").await; // file names are not added for GIFs
|
||
|
||
let mut msg = Message::new(Viewtype::Sticker);
|
||
msg.set_file("foo.png", None);
|
||
assert_summary_texts(&msg, ctx, "Sticker").await; // file names are not added for stickers
|
||
|
||
let mut msg = Message::new(Viewtype::Voice);
|
||
msg.set_file("foo.mp3", None);
|
||
assert_summary_texts(&msg, ctx, "🎤 Voice message").await; // file names are not added for voice messages
|
||
|
||
let mut msg = Message::new(Viewtype::Voice);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.mp3", None);
|
||
assert_summary_texts(&msg, ctx, "🎤 bla bla").await;
|
||
|
||
let mut msg = Message::new(Viewtype::Audio);
|
||
msg.set_file("foo.mp3", None);
|
||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3").await; // file name is added for audio
|
||
|
||
let mut msg = Message::new(Viewtype::Audio);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.mp3", None);
|
||
assert_summary_texts(&msg, ctx, "🎵 foo.mp3 \u{2013} bla bla").await; // file name and text added for audio
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
let bytes = include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc");
|
||
msg.set_file_from_bytes(ctx, "foo.xdc", bytes, None)
|
||
.await
|
||
.unwrap();
|
||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||
assert_eq!(msg.viewtype, Viewtype::Webxdc);
|
||
assert_summary_texts(&msg, ctx, "nice app!").await;
|
||
msg.set_text(some_text.clone());
|
||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||
assert_summary_texts(&msg, ctx, "nice app! \u{2013} bla bla").await;
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_file("foo.bar", None);
|
||
assert_summary_texts(&msg, ctx, "📎 foo.bar").await; // file name is added for files
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
assert_summary_texts(&msg, ctx, "📎 foo.bar \u{2013} bla bla").await; // file name is added for files
|
||
|
||
let mut msg = Message::new(Viewtype::VideochatInvitation);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
assert_summary_texts(&msg, ctx, "Video chat invitation").await; // text is not added for videochat invitations
|
||
|
||
let mut msg = Message::new(Viewtype::Vcard);
|
||
msg.set_file_from_bytes(ctx, "foo.vcf", b"", None)
|
||
.await
|
||
.unwrap();
|
||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||
// If a vCard can't be parsed, the message becomes `Viewtype::File`.
|
||
assert_eq!(msg.viewtype, Viewtype::File);
|
||
assert_summary_texts(&msg, ctx, "📎 foo.vcf").await;
|
||
msg.set_text(some_text.clone());
|
||
assert_summary_texts(&msg, ctx, "📎 foo.vcf \u{2013} bla bla").await;
|
||
|
||
for vt in [Viewtype::Vcard, Viewtype::File] {
|
||
let mut msg = Message::new(vt);
|
||
msg.set_file_from_bytes(
|
||
ctx,
|
||
"alice.vcf",
|
||
b"BEGIN:VCARD\n\
|
||
VERSION:4.0\n\
|
||
FN:Alice Wonderland\n\
|
||
EMAIL;TYPE=work:alice@example.org\n\
|
||
END:VCARD",
|
||
None,
|
||
)
|
||
.await
|
||
.unwrap();
|
||
chat_id.set_draft(ctx, Some(&mut msg)).await.unwrap();
|
||
assert_eq!(msg.viewtype, Viewtype::Vcard);
|
||
assert_summary_texts(&msg, ctx, "👤 Alice Wonderland").await;
|
||
}
|
||
|
||
// Forwarded
|
||
let mut msg = Message::new_text(some_text.clone());
|
||
msg.param.set_int(Param::Forwarded, 1);
|
||
assert_eq!(msg.get_summary_text(ctx).await, "Forwarded: bla bla"); // for simple text, the type is not added to the summary
|
||
assert_eq!(msg.get_summary_text_without_prefix(ctx).await, "bla bla"); // skipping prefix used for reactions summaries
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_text(some_text.clone());
|
||
msg.set_file("foo.bar", None);
|
||
msg.param.set_int(Param::Forwarded, 1);
|
||
assert_eq!(
|
||
msg.get_summary_text(ctx).await,
|
||
"Forwarded: 📎 foo.bar \u{2013} bla bla"
|
||
);
|
||
assert_eq!(
|
||
msg.get_summary_text_without_prefix(ctx).await,
|
||
"📎 foo.bar \u{2013} bla bla"
|
||
); // skipping prefix used for reactions summaries
|
||
|
||
let mut msg = Message::new(Viewtype::File);
|
||
msg.set_text(some_text.clone());
|
||
msg.param.set(Param::File, "foo.bar");
|
||
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||
assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages
|
||
}
|
||
}
|