diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 0a8c53ea0..bfd1cc7e7 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -32,7 +32,7 @@ use deltachat::context::Context; use deltachat::ephemeral::Timer as EphemeralTimer; use deltachat::key::DcKey; use deltachat::message::MsgId; -use deltachat::stock::StockMessage; +use deltachat::stock_str::StockMessage; use deltachat::*; mod dc_array; diff --git a/src/chat.rs b/src/chat.rs index ccffe2fb0..637a466c5 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -1,6 +1,5 @@ //! # Chat module -use std::borrow::Cow; use std::convert::TryFrom; use std::str::FromStr; use std::time::{Duration, SystemTime}; @@ -39,11 +38,7 @@ use crate::mimeparser::SystemMessage; use crate::param::{Param, Params}; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::sql; -use crate::stock::{ - ArchivedChats, DeadDrop, DeviceMessages, E2eAvailable, E2ePreferred, EncrNone, MsgAddMember, - MsgDelMember, MsgGroupLeft, MsgGrpImgChanged, MsgGrpImgDeleted, MsgGrpName, NewGroupDraft, - SavedMessages, SelfDeletedMsgBody, VideochatInviteMsgBody, -}; +use crate::stock_str; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone)] @@ -401,7 +396,7 @@ impl ChatId { if chat.is_self_talk() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(SelfDeletedMsgBody::stock_str(context).await.into()); + msg.text = Some(stock_str::self_deleted_msg_body(context).await); add_device_msg(&context, None, Some(&mut msg)).await?; } @@ -667,10 +662,10 @@ impl ChatId { }) .map(|peerstate| peerstate.prefer_encrypt) { - Some(EncryptPreference::Mutual) => E2ePreferred::stock_str(context).await, - Some(EncryptPreference::NoPreference) => E2eAvailable::stock_str(context).await, - Some(EncryptPreference::Reset) => EncrNone::stock_str(context).await, - None => EncrNone::stock_str(context).await, + Some(EncryptPreference::Mutual) => stock_str::e2e_preferred(context).await, + Some(EncryptPreference::NoPreference) => stock_str::e2e_available(context).await, + Some(EncryptPreference::Reset) => stock_str::encr_none(context).await, + None => stock_str::encr_none(context).await, }; if !ret.is_empty() { ret.push('\n') @@ -793,9 +788,9 @@ impl Chat { } Ok(mut chat) => { if chat.id.is_deaddrop() { - chat.name = DeadDrop::stock_str(context).await.into(); + chat.name = stock_str::dead_drop(context).await; } else if chat.id.is_archived_link() { - let tempname = ArchivedChats::stock_str(context).await; + let tempname = stock_str::archived_chats(context).await; let cnt = dc_get_archived_cnt(context).await; chat.name = format!("{} ({})", tempname, cnt); } else { @@ -810,9 +805,9 @@ impl Chat { chat.name = chat_name; } if chat.param.exists(Param::Selftalk) { - chat.name = SavedMessages::stock_str(context).await.into(); + chat.name = stock_str::saved_messages(context).await; } else if chat.param.exists(Param::Devicetalk) { - chat.name = DeviceMessages::stock_str(context).await.into(); + chat.name = stock_str::device_messages(context).await; } } Ok(chat) @@ -1411,10 +1406,9 @@ pub(crate) async fn update_device_icon(context: &Context) -> Result<(), Error> { async fn update_special_chat_name( context: &Context, contact_id: u32, - name: Cow<'static, str>, + name: String, ) -> Result<(), Error> { if let Ok((chat_id, _)) = lookup_by_contact_id(context, contact_id).await { - let name: String = name.into(); // the `!= name` condition avoids unneeded writes context .sql @@ -1431,13 +1425,13 @@ pub(crate) async fn update_special_chat_names(context: &Context) -> Result<(), E update_special_chat_name( context, DC_CONTACT_ID_DEVICE, - DeviceMessages::stock_str(context).await, + stock_str::device_messages(context).await, ) .await?; update_special_chat_name( context, DC_CONTACT_ID_SELF, - SavedMessages::stock_str(context).await, + stock_str::saved_messages(context).await, ) .await?; Ok(()) @@ -1822,9 +1816,8 @@ pub async fn send_videochat_invitation(context: &Context, chat_id: ChatId) -> Re let mut msg = Message::new(Viewtype::VideochatInvitation); msg.param.set(Param::WebrtcRoom, &instance); msg.text = Some( - VideochatInviteMsgBody::stock_str(context, Message::parse_webrtc_instance(&instance).1) - .await - .to_string(), + stock_str::videochat_invite_msg_body(context, Message::parse_webrtc_instance(&instance).1) + .await, ); send_msg(context, chat_id, &mut msg).await } @@ -2161,9 +2154,7 @@ pub async fn create_group_chat( let chat_name = improve_single_line_input(chat_name); ensure!(!chat_name.is_empty(), "Invalid chat name"); - let draft_txt = NewGroupDraft::stock_str(context, &chat_name) - .await - .to_string(); + let draft_txt = stock_str::new_group_draft(context, &chat_name).await; let grpid = dc_create_id(); context.sql.execute( @@ -2340,11 +2331,8 @@ pub(crate) async fn add_contact_to_chat_ex( } if chat.param.get_int(Param::Unpromoted).unwrap_or_default() == 0 { msg.viewtype = Viewtype::Text; - msg.text = Some( - MsgAddMember::stock_str(context, contact.get_addr(), DC_CONTACT_ID_SELF) - .await - .to_string(), - ); + msg.text = + Some(stock_str::msg_add_member(context, contact.get_addr(), DC_CONTACT_ID_SELF).await); msg.param.set_cmd(SystemMessage::MemberAddedToGroup); msg.param.set(Param::Arg, contact.get_addr()); msg.param.set_int(Param::Arg2, from_handshake.into()); @@ -2538,20 +2526,16 @@ pub async fn remove_contact_from_chat( msg.viewtype = Viewtype::Text; if contact.id == DC_CONTACT_ID_SELF { set_group_explicitly_left(context, chat.grpid).await?; - msg.text = Some( - MsgGroupLeft::stock_str(context, DC_CONTACT_ID_SELF) - .await - .to_string(), - ); + msg.text = + Some(stock_str::msg_group_left(context, DC_CONTACT_ID_SELF).await); } else { msg.text = Some( - MsgDelMember::stock_str( + stock_str::msg_del_member( context, contact.get_addr(), DC_CONTACT_ID_SELF, ) - .await - .to_string(), + .await, ); } msg.param.set_cmd(SystemMessage::MemberRemovedFromGroup); @@ -2647,9 +2631,8 @@ pub async fn set_chat_name( if chat.is_promoted() && !chat.is_mailing_list() { msg.viewtype = Viewtype::Text; msg.text = Some( - MsgGrpName::stock_str(context, &chat.name, &new_name, DC_CONTACT_ID_SELF) - .await - .to_string(), + stock_str::msg_grp_name(context, &chat.name, &new_name, DC_CONTACT_ID_SELF) + .await, ); msg.param.set_cmd(SystemMessage::GroupNameChanged); if !chat.name.is_empty() { @@ -2706,11 +2689,7 @@ pub async fn set_chat_profile_image( if new_image.as_ref().is_empty() { chat.param.remove(Param::ProfileImage); msg.param.remove(Param::Arg); - msg.text = Some( - MsgGrpImgDeleted::stock_str(context, DC_CONTACT_ID_SELF) - .await - .to_string(), - ); + msg.text = Some(stock_str::msg_grp_img_deleted(context, DC_CONTACT_ID_SELF).await); } else { let image_blob = match BlobObject::from_path(context, Path::new(new_image.as_ref())) { Ok(blob) => Ok(blob), @@ -2724,11 +2703,7 @@ pub async fn set_chat_profile_image( image_blob.recode_to_avatar_size(context).await?; chat.param.set(Param::ProfileImage, image_blob.as_name()); msg.param.set(Param::Arg, image_blob.as_name()); - msg.text = Some( - MsgGrpImgChanged::stock_str(context, DC_CONTACT_ID_SELF) - .await - .to_string(), - ); + msg.text = Some(stock_str::msg_grp_img_changed(context, DC_CONTACT_ID_SELF).await); } chat.update_param(context).await?; if chat.is_promoted() && !chat.is_mailing_list() { @@ -3206,7 +3181,7 @@ mod tests { assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(chat.can_send()); - assert_eq!(chat.name, SavedMessages::stock_str(&t).await); + assert_eq!(chat.name, stock_str::saved_messages(&t).await); assert!(chat.get_profile_image(&t).await.is_some()); } @@ -3222,7 +3197,7 @@ mod tests { assert!(chat.visibility == ChatVisibility::Normal); assert!(!chat.is_device_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, DeadDrop::stock_str(&t).await); + assert_eq!(chat.name, stock_str::dead_drop(&t).await); } #[async_std::test] @@ -3299,7 +3274,7 @@ mod tests { assert!(chat.is_device_talk()); assert!(!chat.is_self_talk()); assert!(!chat.can_send()); - assert_eq!(chat.name, DeviceMessages::stock_str(&t).await); + assert_eq!(chat.name, stock_str::device_messages(&t).await); assert!(chat.get_profile_image(&t).await.is_some()); // delete device message, make sure it is not added again diff --git a/src/chatlist.rs b/src/chatlist.rs index 6d8f610f1..138203328 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -14,7 +14,7 @@ use crate::context::Context; use crate::ephemeral::delete_expired_messages; use crate::lot::Lot; use crate::message::{Message, MessageState, MsgId}; -use crate::stock::NoMessages; +use crate::stock_str; /// An object representing a single chatlist in memory. /// @@ -385,7 +385,7 @@ impl Chatlist { ret.text2 = None; } else if lastmsg.is_none() || lastmsg.as_ref().unwrap().from_id == DC_CONTACT_ID_UNDEFINED { - ret.text2 = Some(NoMessages::stock_str(context).await.to_string()); + ret.text2 = Some(stock_str::no_messages(context).await.to_string()); } else { ret.fill(&mut lastmsg.unwrap(), chat, lastcontact.as_ref(), context) .await; @@ -440,7 +440,7 @@ mod tests { use crate::chat::{create_group_chat, ProtectionStatus}; use crate::constants::Viewtype; - use crate::stock::StockMessage; + use crate::stock_str::StockMessage; use crate::test_utils::TestContext; #[async_std::test] diff --git a/src/config.rs b/src/config.rs index c4aba0357..b1c773009 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,7 +13,7 @@ use crate::job; use crate::message::MsgId; use crate::mimefactory::RECOMMENDED_FILE_SIZE; use crate::provider::{get_provider_by_id, Provider}; -use crate::stock::StatusLine; +use crate::stock_str; /// The available configuration keys. #[derive( @@ -173,7 +173,7 @@ impl Context { // Default values match key { - Config::Selfstatus => Some(StatusLine::stock_str(self).await.into_owned()), + Config::Selfstatus => Some(stock_str::status_line(self).await), Config::ConfiguredInboxFolder => Some("INBOX".to_owned()), _ => key.get_str("default").map(|s| s.to_string()), } @@ -258,7 +258,7 @@ impl Context { } } Config::Selfstatus => { - let def = StatusLine::stock_str(self).await; + let def = stock_str::status_line(self).await; let val = if value.is_none() || value.unwrap() == def { None } else { diff --git a/src/configure/mod.rs b/src/configure/mod.rs index e23764d18..f048c0120 100644 --- a/src/configure/mod.rs +++ b/src/configure/mod.rs @@ -19,7 +19,7 @@ use crate::message::Message; use crate::oauth2::dc_get_oauth2_addr; use crate::provider::{Protocol, Socket, UsernamePattern}; use crate::smtp::Smtp; -use crate::stock::{ConfigurationFailed, ErrorNoNetwork}; +use crate::stock_str; use crate::{chat, e2ee, provider}; use crate::{config::Config, dc_tools::time}; use crate::{ @@ -127,14 +127,13 @@ impl Context { self, 0, Some( - ConfigurationFailed::stock_str( + stock_str::configuration_failed( self, // We are using Anyhow's .context() and to show the // inner error, too, we need the {:#}: format!("{:#}", err), ) .await - .to_string() ) ); Err(err) @@ -591,7 +590,7 @@ async fn nicer_configuration_error(context: &Context, errors: Vec E2ePreferred::stock_str(context).await, - EncryptPreference::NoPreference => E2eAvailable::stock_str(context).await, - EncryptPreference::Reset => EncrNone::stock_str(context).await, + EncryptPreference::Mutual => stock_str::e2e_preferred(context).await, + EncryptPreference::NoPreference => stock_str::e2e_available(context).await, + EncryptPreference::Reset => stock_str::encr_none(context).await, }; ret += &format!( "{}\n{}:", stock_message, - FingerPrints::stock_str(context).await + stock_str::finger_prints(context).await ); let fingerprint_self = SignedPublicKey::load_self(context) @@ -767,7 +767,7 @@ impl Contact { cat_fingerprint(&mut ret, &loginparam.addr, &fingerprint_self, ""); } } else { - ret += &EncrNone::stock_str(context).await; + ret += &stock_str::encr_none(context).await; } } @@ -1507,7 +1507,7 @@ mod tests { // check SELF let contact = Contact::load_from_db(&t, DC_CONTACT_ID_SELF).await.unwrap(); assert_eq!(DC_CONTACT_ID_SELF, 1); - assert_eq!(contact.get_name(), SelfMsg::stock_str(&t).await); + assert_eq!(contact.get_name(), stock_str::self_msg(&t).await); assert_eq!(contact.get_addr(), ""); // we're not configured assert!(!contact.is_blocked()); } diff --git a/src/dc_receive_imf.rs b/src/dc_receive_imf.rs index c1b15c912..cb597b6a7 100644 --- a/src/dc_receive_imf.rs +++ b/src/dc_receive_imf.rs @@ -26,10 +26,7 @@ use crate::mimeparser::{parse_message_ids, AvatarAction, MimeMessage, SystemMess use crate::param::{Param, Params}; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus}; use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device}; -use crate::stock::{ - MsgAddMember, MsgDelMember, MsgGroupLeft, MsgGrpImgChanged, MsgGrpImgDeleted, MsgGrpName, - MsgLocationEnabled, UnknownSenderForChat, -}; +use crate::stock_str; use crate::{contact, location}; // IndexSet is like HashSet but maintains order of insertion @@ -1168,9 +1165,7 @@ async fn create_or_lookup_group( let mut better_msg: String = From::from(""); if mime_parser.is_system_message == SystemMessage::LocationStreamingEnabled { - better_msg = MsgLocationEnabled::stock_str_by(context, from_id) - .await - .to_string(); + better_msg = stock_str::msg_location_enabled_by(context, from_id).await; set_better_msg(mime_parser, &better_msg); } @@ -1235,11 +1230,9 @@ async fn create_or_lookup_group( Some(contact_id) => { mime_parser.is_system_message = SystemMessage::MemberRemovedFromGroup; better_msg = if contact_id == from_id { - MsgGroupLeft::stock_str(context, from_id).await.to_string() + stock_str::msg_group_left(context, from_id).await } else { - MsgDelMember::stock_str(context, &removed_addr, from_id) - .await - .to_string() + stock_str::msg_del_member(context, &removed_addr, from_id).await }; } None => warn!(context, "removed {:?} has no contact_id", removed_addr), @@ -1248,13 +1241,11 @@ async fn create_or_lookup_group( let field = mime_parser.get(HeaderDef::ChatGroupMemberAdded).cloned(); if let Some(added_member) = field { mime_parser.is_system_message = SystemMessage::MemberAddedToGroup; - better_msg = MsgAddMember::stock_str(context, &added_member, from_id) - .await - .to_string(); + better_msg = stock_str::msg_add_member(context, &added_member, from_id).await; X_MrAddToGrp = Some(added_member); } else if let Some(old_name) = mime_parser.get(HeaderDef::ChatGroupNameChanged) { X_MrGrpNameChanged = true; - better_msg = MsgGrpName::stock_str( + better_msg = stock_str::msg_grp_name( context, old_name, if let Some(ref name) = grpname { @@ -1264,8 +1255,7 @@ async fn create_or_lookup_group( }, from_id as u32, ) - .await - .to_string(); + .await; mime_parser.is_system_message = SystemMessage::GroupNameChanged; } else if let Some(value) = mime_parser.get(HeaderDef::ChatContent) { if value == "group-avatar-changed" { @@ -1274,12 +1264,12 @@ async fn create_or_lookup_group( // apart from that, the group-avatar is send along with various other messages mime_parser.is_system_message = SystemMessage::GroupImageChanged; better_msg = match avatar_action { - AvatarAction::Delete => MsgGrpImgDeleted::stock_str(context, from_id) - .await - .to_string(), - AvatarAction::Change(_) => MsgGrpImgChanged::stock_str(context, from_id) - .await - .to_string(), + AvatarAction::Delete => { + stock_str::msg_grp_img_deleted(context, from_id).await + } + AvatarAction::Change(_) => { + stock_str::msg_grp_img_changed(context, from_id).await + } }; } } @@ -1298,7 +1288,7 @@ async fn create_or_lookup_group( // but still show the message as part of the chat. // After all, the sender has a reference/in-reply-to that // points to this chat. - let s = UnknownSenderForChat::stock_str(context).await; + let s = stock_str::unknown_sender_for_chat(context).await; mime_parser.repl_msg_by_error(s.to_string()); } @@ -1976,16 +1966,13 @@ fn dc_create_incoming_rfc724_mid( #[cfg(test)] mod tests { use super::*; - use crate::constants::{DC_CONTACT_ID_INFO, DC_GCL_NO_SPECIALS}; + + use crate::chat::{ChatItem, ChatVisibility}; + use crate::chatlist::Chatlist; + use crate::constants::{DC_CHAT_ID_DEADDROP, DC_CONTACT_ID_INFO, DC_GCL_NO_SPECIALS}; use crate::message::ContactRequestDecision::*; use crate::message::Message; - use crate::stock::FailedSendingTo; - use crate::test_utils::TestContext; - use crate::{ - chat::{ChatItem, ChatVisibility}, - constants::DC_CHAT_ID_DEADDROP, - }; - use crate::{chatlist::Chatlist, test_utils::get_chat_msg}; + use crate::test_utils::{get_chat_msg, TestContext}; #[test] fn test_hex_hash() { @@ -2647,7 +2634,7 @@ mod tests { assert_eq!( last_msg.text, Some( - FailedSendingTo::stock_str(&t, "assidhfaaspocwaeofi@gmail.com") + stock_str::failed_sending_to(&t, "assidhfaaspocwaeofi@gmail.com") .await .to_string(), ) diff --git a/src/dc_tools.rs b/src/dc_tools.rs index 1cd1d5fd6..ce182935a 100644 --- a/src/dc_tools.rs +++ b/src/dc_tools.rs @@ -22,7 +22,7 @@ use crate::context::Context; use crate::events::EventType; use crate::message::Message; use crate::provider::get_provider_update_timestamp; -use crate::stock::{BadTimeMsgBody, UpdateReminderMsgBody}; +use crate::stock_str; /// Shortens a string to a specified length and adds "[...]" to the /// end of the shortened string. @@ -169,15 +169,14 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam if now < known_past_timestamp { let mut msg = Message::new(Viewtype::Text); msg.text = Some( - BadTimeMsgBody::stock_str( + stock_str::bad_time_msg_body( context, Local .timestamp(now, 0) .format("%Y-%m-%d %H:%M:%S") .to_string(), ) - .await - .to_string(), + .await, ); add_device_msg_with_importance( context, @@ -201,7 +200,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) { if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(UpdateReminderMsgBody::stock_str(context).await.into()); + msg.text = Some(stock_str::update_reminder_msg_body(context).await); add_device_msg( context, Some( diff --git a/src/ephemeral.rs b/src/ephemeral.rs index ab2e82996..585fcdfd2 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -56,7 +56,6 @@ //! the database entries which are expired either according to their //! ephemeral message timers or global `delete_server_after` setting. -use std::borrow::Cow; use std::convert::{TryFrom, TryInto}; use std::num::ParseIntError; use std::str::FromStr; @@ -76,12 +75,7 @@ use crate::events::EventType; use crate::message::{Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::sql; -use crate::stock::{ - MsgEphemeralTimerDay, MsgEphemeralTimerDays, MsgEphemeralTimerDisabled, - MsgEphemeralTimerEnabled, MsgEphemeralTimerHour, MsgEphemeralTimerHours, - MsgEphemeralTimerMinute, MsgEphemeralTimerMinutes, MsgEphemeralTimerWeek, - MsgEphemeralTimerWeeks, -}; +use crate::stock_str; #[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] pub enum Timer { @@ -222,43 +216,43 @@ pub(crate) async fn stock_ephemeral_timer_changed( context: &Context, timer: Timer, from_id: u32, -) -> Cow<'static, str> { +) -> String { match timer { - Timer::Disabled => MsgEphemeralTimerDisabled::stock_str(context, from_id).await, + Timer::Disabled => stock_str::msg_ephemeral_timer_disabled(context, from_id).await, Timer::Enabled { duration } => match duration { 0..=59 => { - MsgEphemeralTimerEnabled::stock_str(context, timer.to_string(), from_id).await + stock_str::msg_ephemeral_timer_enabled(context, timer.to_string(), from_id).await } - 60 => MsgEphemeralTimerMinute::stock_str(context, from_id).await, + 60 => stock_str::msg_ephemeral_timer_minute(context, from_id).await, 61..=3599 => { - MsgEphemeralTimerMinutes::stock_str( + stock_str::msg_ephemeral_timer_minutes( context, format!("{}", (f64::from(duration) / 6.0).round() / 10.0), from_id, ) .await } - 3600 => MsgEphemeralTimerHour::stock_str(context, from_id).await, + 3600 => stock_str::msg_ephemeral_timer_hour(context, from_id).await, 3601..=86399 => { - MsgEphemeralTimerHours::stock_str( + stock_str::msg_ephemeral_timer_hours( context, format!("{}", (f64::from(duration) / 360.0).round() / 10.0), from_id, ) .await } - 86400 => MsgEphemeralTimerDay::stock_str(context, from_id).await, + 86400 => stock_str::msg_ephemeral_timer_day(context, from_id).await, 86401..=604_799 => { - MsgEphemeralTimerDays::stock_str( + stock_str::msg_ephemeral_timer_days( context, format!("{}", (f64::from(duration) / 8640.0).round() / 10.0), from_id, ) .await } - 604_800 => MsgEphemeralTimerWeek::stock_str(context, from_id).await, + 604_800 => stock_str::msg_ephemeral_timer_week(context, from_id).await, _ => { - MsgEphemeralTimerWeeks::stock_str( + stock_str::msg_ephemeral_timer_weeks( context, format!("{}", (f64::from(duration) / 60480.0).round() / 10.0), from_id, diff --git a/src/imap/mod.rs b/src/imap/mod.rs index f0eab58a1..4279a620e 100644 --- a/src/imap/mod.rs +++ b/src/imap/mod.rs @@ -35,7 +35,7 @@ use crate::oauth2::dc_get_oauth2_access_token; use crate::param::Params; use crate::provider::Socket; use crate::scheduler::InterruptInfo; -use crate::stock::CannotLogin; +use crate::stock_str; mod client; mod idle; @@ -255,9 +255,7 @@ impl Imap { Err((err, _)) => { let imap_user = self.config.lp.user.to_owned(); - let message = CannotLogin::stock_str(context, &imap_user) - .await - .to_string(); + let message = stock_str::cannot_login(context, &imap_user).await; warn!(context, "{} ({})", message, err); diff --git a/src/imex.rs b/src/imex.rs index 1b36528c2..89aa8d843 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -29,7 +29,7 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::pgp; use crate::sql::{self, Sql}; -use crate::stock::{AcSetupMsgBody, AcSetupMsgSubject}; +use crate::stock_str; use ::pgp::types::KeyTrait; use async_tar::Archive; @@ -275,8 +275,8 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result"); Ok(format!( concat!( @@ -904,7 +904,7 @@ mod tests { use super::*; use crate::pgp::{split_armored_data, HEADER_AUTOCRYPT, HEADER_SETUPCODE}; - use crate::stock::StockMessage; + use crate::stock_str::StockMessage; use crate::test_utils::{alice_keypair, TestContext}; use ::pgp::armor::BlockType; diff --git a/src/lib.rs b/src/lib.rs index fd9849ec0..132873637 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,7 @@ pub mod qr; pub mod securejoin; mod simplify; mod smtp; -pub mod stock; +pub mod stock_str; mod token; #[macro_use] mod dehtml; diff --git a/src/location.rs b/src/location.rs index 67afda02f..a751fbb6b 100644 --- a/src/location.rs +++ b/src/location.rs @@ -14,7 +14,7 @@ use crate::job::{self, Job}; use crate::message::{Message, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Params; -use crate::stock::{MsgLocationDisabled, MsgLocationEnabled}; +use crate::stock_str; /// Location record #[derive(Debug, Clone, Default)] @@ -212,13 +212,13 @@ pub async fn send_locations_to_chat(context: &Context, chat_id: ChatId, seconds: { if 0 != seconds && !is_sending_locations_before { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(MsgLocationEnabled::stock_str(context).await.to_string()); + msg.text = Some(stock_str::msg_location_enabled(context).await); msg.param.set_cmd(SystemMessage::LocationStreamingEnabled); chat::send_msg(context, chat_id, &mut msg) .await .unwrap_or_default(); } else if 0 == seconds && is_sending_locations_before { - let stock_str = MsgLocationDisabled::stock_str(context).await; + let stock_str = stock_str::msg_location_disabled(context).await; chat::add_info_msg(context, chat_id, stock_str).await; } context.emit_event(EventType::ChatModified(chat_id)); @@ -710,7 +710,7 @@ pub(crate) async fn job_maybe_send_locations_ended( paramsv![chat_id], ).await); - let stock_str = MsgLocationDisabled::stock_str(context).await; + let stock_str = stock_str::msg_location_disabled(context).await; chat::add_info_msg(context, chat_id, stock_str).await; context.emit_event(EventType::ChatModified(chat_id)); } diff --git a/src/message.rs b/src/message.rs index 887b6f41c..8a48077d9 100644 --- a/src/message.rs +++ b/src/message.rs @@ -26,10 +26,7 @@ use crate::lot::{Lot, LotState, Meaning}; use crate::mimeparser::{FailureReport, SystemMessage}; use crate::param::{Param, Params}; use crate::pgp::split_armored_data; -use crate::stock::{ - AcSetupMsgSubject, Audio, Draft, FailedSendingTo, File, Gif, Image, Location, ReplyNoun, - SelfMsg, Sticker, Video, VideochatInvitation, VoiceMessage, -}; +use crate::stock_str; use std::collections::BTreeMap; // In practice, the user additionally cuts the string themselves @@ -1058,14 +1055,14 @@ impl Lot { context: &Context, ) { if msg.state == MessageState::OutDraft { - self.text1 = Some(Draft::stock_str(context).await.to_owned().into()); + self.text1 = Some(stock_str::draft(context).await); self.text1_meaning = Meaning::Text1Draft; } else if msg.from_id == DC_CONTACT_ID_SELF { if msg.is_info() || chat.is_self_talk() { self.text1 = None; self.text1_meaning = Meaning::None; } else { - self.text1 = Some(SelfMsg::stock_str(context).await.to_owned().into()); + self.text1 = Some(stock_str::self_msg(context).await); self.text1_meaning = Meaning::Text1Self; } } else { @@ -1098,7 +1095,7 @@ impl Lot { .await; if text2.is_empty() && msg.quoted_text().is_some() { - text2 = ReplyNoun::stock_str(context).await.into_owned() + text2 = stock_str::reply_noun(context).await } self.text2 = Some(text2); @@ -1550,15 +1547,15 @@ pub async fn get_summarytext_by_raw( ) -> String { let mut append_text = true; let prefix = match viewtype { - Viewtype::Image => Image::stock_str(context).await.into_owned(), - Viewtype::Gif => Gif::stock_str(context).await.into_owned(), - Viewtype::Sticker => Sticker::stock_str(context).await.into_owned(), - Viewtype::Video => Video::stock_str(context).await.into_owned(), - Viewtype::Voice => VoiceMessage::stock_str(context).await.into_owned(), + Viewtype::Image => stock_str::image(context).await, + Viewtype::Gif => stock_str::gif(context).await, + Viewtype::Sticker => stock_str::sticker(context).await, + Viewtype::Video => stock_str::video(context).await, + Viewtype::Voice => stock_str::voice_message(context).await, Viewtype::Audio | Viewtype::File => { if param.get_cmd() == SystemMessage::AutocryptSetupMessage { append_text = false; - AcSetupMsgSubject::stock_str(context).await.to_string() + stock_str::ac_setup_msg_subject(context).await } else { let file_name: String = param .get_path(Param::File, context) @@ -1569,23 +1566,23 @@ pub async fn get_summarytext_by_raw( }) .unwrap_or_else(|| String::from("ErrFileName")); let label = if viewtype == Viewtype::Audio { - Audio::stock_str(context).await + stock_str::audio(context).await } else { - File::stock_str(context).await + stock_str::file(context).await }; format!("{} – {}", label, file_name) } } Viewtype::VideochatInvitation => { append_text = false; - VideochatInvitation::stock_str(context).await.into_owned() + stock_str::videochat_invitation(context).await } _ => { if param.get_cmd() != SystemMessage::LocationOnly { "".to_string() } else { append_text = false; - Location::stock_str(context).await.to_string() + stock_str::location(context).await } } }; @@ -1844,7 +1841,7 @@ async fn ndn_maybe_add_info_msg( let contact = Contact::load_from_db(context, contact_id).await?; // Tell the user which of the recipients failed if we know that (because in // a group, this might otherwise be unclear) - let text = FailedSendingTo::stock_str(context, contact.get_display_name()).await; + let text = stock_str::failed_sending_to(context, contact.get_display_name()).await; chat::add_info_msg(context, chat_id, text).await; context.emit_event(EventType::ChatModified(chat_id)); } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 1bec369f4..2b9a6a5bd 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -21,10 +21,7 @@ use crate::mimeparser::SystemMessage; use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::simplify::escape_message_footer_marks; -use crate::stock::{ - AcSetupMsgBody, AcSetupMsgSubject, EncryptedMsg, ReadRcpt, ReadRcptMailBody, StatusLine, - SubjectForNewContact, -}; +use crate::stock_str; use std::convert::TryInto; // attachments of 25 mb brutto should work on the majority of providers @@ -142,7 +139,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { ) .await?; - let default_str = StatusLine::stock_str(context).await.to_string(); + let default_str = stock_str::status_line(context).await; let factory = MimeFactory { from_addr, from_displayname, @@ -180,7 +177,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .get_config(Config::Displayname) .await .unwrap_or_default(); - let default_str = StatusLine::stock_str(context).await.to_string(); + let default_str = stock_str::status_line(context).await; let selfstatus = context .get_config(Config::Selfstatus) .await @@ -342,9 +339,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { match self.loaded { Loaded::Message { ref chat } => { if self.msg.param.get_cmd() == SystemMessage::AutocryptSetupMessage { - AcSetupMsgSubject::stock_str(self.context) - .await - .into_owned() + stock_str::ac_setup_msg_subject(self.context).await } else if chat.typ == Chattype::Group { let re = if self.in_reply_to.is_empty() { "" @@ -386,14 +381,12 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .unwrap_or_default(), }; - SubjectForNewContact::stock_str(self.context, self_name) - .await - .to_string() + stock_str::subject_for_new_contact(self.context, self_name).await } } } } - Loaded::MDN { .. } => ReadRcpt::stock_str(self.context).await.into_owned(), + Loaded::MDN { .. } => stock_str::read_rcpt(self.context).await, } } @@ -797,7 +790,7 @@ impl<'a, 'b> MimeFactory<'a, 'b> { unprotected_headers .push(Header::new("Autocrypt-Setup-Message".into(), "v1".into())); - placeholdertext = Some(AcSetupMsgBody::stock_str(self.context).await.to_string()); + placeholdertext = Some(stock_str::ac_setup_msg_body(self.context).await); } SystemMessage::SecurejoinMessage => { let msg = &self.msg; @@ -1055,11 +1048,11 @@ impl<'a, 'b> MimeFactory<'a, 'b> { .get_int(Param::GuaranteeE2ee) .unwrap_or_default() { - EncryptedMsg::stock_str(self.context).await.into_owned() + stock_str::encrypted_msg(self.context).await } else { self.msg.get_summarytext(self.context, 32).await }; - let p2 = ReadRcptMailBody::stock_str(self.context, p1).await; + let p2 = stock_str::read_rcpt_mail_body(self.context, p1).await; let message_text = format!("{}\r\n", p2); message = message.child( PartBuilder::new() diff --git a/src/mimeparser.rs b/src/mimeparser.rs index a2436f9e1..a9e374631 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -27,7 +27,7 @@ use crate::message; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::simplify::simplify; -use crate::stock::CantDecryptMsgBody; +use crate::stock_str; /// A parsed MIME message. /// @@ -629,7 +629,7 @@ impl MimeMessage { // we currently do not try to decrypt non-autocrypt messages // at all. If we see an encrypted part, we set // decrypting_failed. - let msg_body = CantDecryptMsgBody::stock_str(context).await; + let msg_body = stock_str::cant_decrypt_msg_body(context).await; let txt = format!("[{}]", msg_body); let part = Part { diff --git a/src/peerstate.rs b/src/peerstate.rs index f2f9e33e2..6a074e1de 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -13,7 +13,7 @@ use crate::context::Context; use crate::events::EventType; use crate::key::{DcKey, Fingerprint, SignedPublicKey}; use crate::sql::Sql; -use crate::stock::ContactSetupChanged; +use crate::stock_str; #[derive(Debug)] pub enum PeerstateKeyType { @@ -281,7 +281,7 @@ impl<'a> Peerstate<'a> { .await .unwrap_or_default(); - let msg = ContactSetupChanged::stock_str(context, self.addr.clone()).await; + let msg = stock_str::contact_setup_changed(context, self.addr.clone()).await; chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, EventType::ChatModified(contact_chat_id)); diff --git a/src/securejoin/mod.rs b/src/securejoin/mod.rs index efc11b812..89c35c481 100644 --- a/src/securejoin/mod.rs +++ b/src/securejoin/mod.rs @@ -23,7 +23,7 @@ use crate::param::Param; use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus, ToSave}; use crate::qr::check_qr; use crate::sql; -use crate::stock::{ContactNotVerified, ContactVerified}; +use crate::stock_str; use crate::token; mod bobstate; @@ -822,7 +822,7 @@ async fn secure_connection_established(context: &Context, contact_chat_id: ChatI } else { "?" }; - let msg = ContactVerified::stock_str(context, addr).await; + let msg = stock_str::contact_verified(context, addr).await; chat::add_info_msg(context, contact_chat_id, msg).await; emit_event!(context, EventType::ChatModified(contact_chat_id)); info!(context, "StockMessage::ContactVerified posted to 1:1 chat"); @@ -835,7 +835,7 @@ async fn could_not_establish_secure_connection( ) { let contact_id = chat_id_2_contact_id(context, contact_chat_id).await; let contact = Contact::get_by_id(context, contact_id).await; - let msg = ContactNotVerified::stock_str( + let msg = stock_str::contact_not_verified( context, if let Ok(ref contact) = contact { contact.get_addr() diff --git a/src/smtp/mod.rs b/src/smtp/mod.rs index 3959d2f03..1fc8e71b0 100644 --- a/src/smtp/mod.rs +++ b/src/smtp/mod.rs @@ -13,7 +13,7 @@ use crate::events::EventType; use crate::login_param::{dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam}; use crate::oauth2::dc_get_oauth2_access_token; use crate::provider::Socket; -use crate::stock::ServerResponse; +use crate::stock_str; /// SMTP write and read timeout in seconds. const SMTP_TIMEOUT: u64 = 30; @@ -111,13 +111,12 @@ impl Smtp { ) .await; if let Err(ref err) = res { - let message = ServerResponse::stock_str( + let message = stock_str::server_response( context, format!("SMTP {}:{}", lp.smtp.server, lp.smtp.port), err.to_string(), ) - .await - .to_string(); + .await; context.emit_event(EventType::ErrorNetwork(message)); }; diff --git a/src/sql.rs b/src/sql.rs index 3605cdaac..bd5f36703 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -22,7 +22,7 @@ use crate::message::Message; use crate::param::{Param, Params}; use crate::peerstate::Peerstate; use crate::provider::get_provider_by_domain; -use crate::stock::DeleteServerTurnedOff; +use crate::stock_str; #[macro_export] macro_rules! paramsv { @@ -1541,7 +1541,7 @@ CREATE INDEX devmsglabels_index1 ON devmsglabels (label); // So, for people who have delete_server enabled, disable it and add a hint to the devicechat: if context.get_config_delete_server_after().await.is_some() { let mut msg = Message::new(Viewtype::Text); - msg.text = Some(DeleteServerTurnedOff::stock_str(context).await.into()); + msg.text = Some(stock_str::delete_server_turned_off(context).await); add_device_msg(context, None, Some(&mut msg)).await?; context.set_config(DeleteServerAfter, Some("0")).await?; } diff --git a/src/stock.rs b/src/stock.rs deleted file mode 100644 index a70259719..000000000 --- a/src/stock.rs +++ /dev/null @@ -1,1623 +0,0 @@ -//! Module to work with translatable stock strings - -use std::borrow::Cow; - -use anyhow::{bail, Error}; -use strum::EnumProperty; -use strum_macros::EnumProperty; - -use crate::blob::BlobObject; -use crate::chat; -use crate::chat::ProtectionStatus; -use crate::config::Config; -use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; -use crate::contact::{Contact, Origin}; -use crate::context::Context; -use crate::message::Message; -use crate::param::Param; - -/// Stock strings -/// -/// These identify the string to return in [Context.stock_str]. The -/// numbers must stay in sync with `deltachat.h` `DC_STR_*` constants. -/// -/// See the `stock_*` methods on [Context] to use these. -/// -/// [Context]: crate::context::Context -#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)] -#[repr(u32)] -pub enum StockMessage { - #[strum(props(fallback = "No messages."))] - NoMessages = 1, - - #[strum(props(fallback = "Me"))] - SelfMsg = 2, - - #[strum(props(fallback = "Draft"))] - Draft = 3, - - #[strum(props(fallback = "Voice message"))] - VoiceMessage = 7, - - #[strum(props(fallback = "Contact requests"))] - DeadDrop = 8, - - #[strum(props(fallback = "Image"))] - Image = 9, - - #[strum(props(fallback = "Video"))] - Video = 10, - - #[strum(props(fallback = "Audio"))] - Audio = 11, - - #[strum(props(fallback = "File"))] - File = 12, - - #[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))] - StatusLine = 13, - - #[strum(props(fallback = "Hello, I\'ve just created the group \"%1$s\" for us."))] - NewGroupDraft = 14, - - #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))] - MsgGrpName = 15, - - #[strum(props(fallback = "Group image changed."))] - MsgGrpImgChanged = 16, - - #[strum(props(fallback = "Member %1$s added."))] - MsgAddMember = 17, - - #[strum(props(fallback = "Member %1$s removed."))] - MsgDelMember = 18, - - #[strum(props(fallback = "Group left."))] - MsgGroupLeft = 19, - - #[strum(props(fallback = "GIF"))] - Gif = 23, - - #[strum(props(fallback = "Encrypted message"))] - EncryptedMsg = 24, - - #[strum(props(fallback = "End-to-end encryption available."))] - E2eAvailable = 25, - - #[strum(props(fallback = "No encryption."))] - EncrNone = 28, - - #[strum(props(fallback = "This message was encrypted for another setup."))] - CantDecryptMsgBody = 29, - - #[strum(props(fallback = "Fingerprints"))] - FingerPrints = 30, - - #[strum(props(fallback = "Return receipt"))] - ReadRcpt = 31, - - #[strum(props(fallback = "This is a return receipt for the message \"%1$s\"."))] - ReadRcptMailBody = 32, - - #[strum(props(fallback = "Group image deleted."))] - MsgGrpImgDeleted = 33, - - #[strum(props(fallback = "End-to-end encryption preferred."))] - E2ePreferred = 34, - - #[strum(props(fallback = "%1$s verified."))] - ContactVerified = 35, - - #[strum(props(fallback = "Cannot verify %1$s"))] - ContactNotVerified = 36, - - #[strum(props(fallback = "Changed setup for %1$s"))] - ContactSetupChanged = 37, - - #[strum(props(fallback = "Archived chats"))] - ArchivedChats = 40, - - #[strum(props(fallback = "Autocrypt Setup Message"))] - AcSetupMsgSubject = 42, - - #[strum(props( - fallback = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\nTo decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device." - ))] - AcSetupMsgBody = 43, - - #[strum(props( - fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct." - ))] - CannotLogin = 60, - - #[strum(props(fallback = "Could not connect to %1$s: %2$s"))] - ServerResponse = 61, - - #[strum(props(fallback = "%1$s by %2$s."))] - MsgActionByUser = 62, - - #[strum(props(fallback = "%1$s by me."))] - MsgActionByMe = 63, - - #[strum(props(fallback = "Location streaming enabled."))] - MsgLocationEnabled = 64, - - #[strum(props(fallback = "Location streaming disabled."))] - MsgLocationDisabled = 65, - - #[strum(props(fallback = "Location"))] - Location = 66, - - #[strum(props(fallback = "Sticker"))] - Sticker = 67, - - #[strum(props(fallback = "Device messages"))] - DeviceMessages = 68, - - #[strum(props(fallback = "Saved messages"))] - SavedMessages = 69, - - #[strum(props( - fallback = "Messages in this chat are generated locally by your Delta Chat app. \ - Its makers use it to inform about app updates and problems during usage." - ))] - DeviceMessagesHint = 70, - - #[strum(props(fallback = "Welcome to Delta Chat! – \ - Delta Chat looks and feels like other popular messenger apps, \ - but does not involve centralized control, \ - tracking or selling you, friends, colleagues or family out to large organizations.\n\n\ - Technically, Delta Chat is an email application with a modern chat interface. \ - Email in a new dress if you will šŸ‘»\n\n\ - Use Delta Chat with anyone out of billions of people: just use their e-mail address. \ - Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \ - however, of course, if they like, you may point them to šŸ‘‰ https://get.delta.chat"))] - WelcomeMessage = 71, - - #[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))] - UnknownSenderForChat = 72, - - #[strum(props(fallback = "Message from %1$s"))] - SubjectForNewContact = 73, - - #[strum(props(fallback = "Failed to send message to %1$s."))] - FailedSendingTo = 74, - - #[strum(props(fallback = "Message deletion timer is disabled."))] - MsgEphemeralTimerDisabled = 75, - - // A fallback message for unknown timer values. - // "s" stands for "second" SI unit here. - #[strum(props(fallback = "Message deletion timer is set to %1$s s."))] - MsgEphemeralTimerEnabled = 76, - - #[strum(props(fallback = "Message deletion timer is set to 1 minute."))] - MsgEphemeralTimerMinute = 77, - - #[strum(props(fallback = "Message deletion timer is set to 1 hour."))] - MsgEphemeralTimerHour = 78, - - #[strum(props(fallback = "Message deletion timer is set to 1 day."))] - MsgEphemeralTimerDay = 79, - - #[strum(props(fallback = "Message deletion timer is set to 1 week."))] - MsgEphemeralTimerWeek = 80, - - #[strum(props(fallback = "Video chat invitation"))] - VideochatInvitation = 82, - - #[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))] - VideochatInviteMsgBody = 83, - - #[strum(props(fallback = "Error:\n\nā€œ%1$sā€"))] - ConfigurationFailed = 84, - - #[strum(props( - fallback = "āš ļø Date or time of your device seem to be inaccurate (%1$s).\n\n\ - Adjust your clock ā°šŸ”§ to ensure your messages are received correctly." - ))] - BadTimeMsgBody = 85, - - #[strum(props(fallback = "āš ļø Your Delta Chat version might be outdated.\n\n\ - This may cause problems because your chat partners use newer versions - \ - and you are missing the latest features 😳\n\ - Please check https://get.delta.chat or your app store for updates."))] - UpdateReminderMsgBody = 86, - - #[strum(props( - fallback = "Could not find your mail server.\n\nPlease check your internet connection." - ))] - ErrorNoNetwork = 87, - - #[strum(props(fallback = "Chat protection enabled."))] - ProtectionEnabled = 88, - - #[strum(props(fallback = "Chat protection disabled."))] - ProtectionDisabled = 89, - - // used in summaries, a noun, not a verb (not: "to reply") - #[strum(props(fallback = "Reply"))] - ReplyNoun = 90, - - #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\ - To use the \"Saved messages\" feature again, create a new chat with yourself."))] - SelfDeletedMsgBody = 91, - - #[strum(props( - fallback = "āš ļø The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\ - ā„¹ļø To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \ - Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it." - ))] - DeleteServerTurnedOff = 92, - - #[strum(props(fallback = "Message deletion timer is set to %1$s minutes."))] - MsgEphemeralTimerMinutes = 93, - - #[strum(props(fallback = "Message deletion timer is set to %1$s hours."))] - MsgEphemeralTimerHours = 94, - - #[strum(props(fallback = "Message deletion timer is set to %1$s days."))] - MsgEphemeralTimerDays = 95, - - #[strum(props(fallback = "Message deletion timer is set to %1$s weeks."))] - MsgEphemeralTimerWeeks = 96, -} - -impl StockMessage { - /// Default untranslated strings for stock messages. - /// - /// These could be used in logging calls, so no logging here. - fn fallback(self) -> &'static str { - self.get_str("fallback").unwrap_or_default() - } -} - -/// Builder for a stock string. -/// -/// See [`NoMessages`] or any other stock string in this module for an example of how to use -/// this. -struct StockString<'a> { - context: &'a Context, -} - -impl<'a> StockString<'a> { - /// Creates a new [`StockString`] builder. - fn new(context: &'a Context) -> Self { - Self { context } - } - - /// Looks up a translation and returns a further builder. - /// - /// This will look up the translation in the [`Context`] if one is registered. It - /// returns a further builder type which can be used to substitute replacement strings - /// or build the final message. - async fn id(self, id: StockMessage) -> TranslatedStockString<'a> { - TranslatedStockString { - context: self.context, - message: self - .context - .translated_stockstrings - .read() - .await - .get(&(id as usize)) - .map(|s| Cow::Owned(s.to_owned())) - .unwrap_or_else(|| Cow::Borrowed(id.fallback())), - } - } -} - -/// Stock string builder which allows retrieval of the message. -/// -/// This builder allows retrieval of the message using [`TranslatedStockString::msg`], if it -/// needs substitutions first however it provides further builder methods. -struct TranslatedStockString<'a> { - context: &'a Context, - message: Cow<'static, str>, -} - -impl<'a> TranslatedStockString<'a> { - /// Retrieves the built message. - fn msg(self) -> Cow<'static, str> { - self.message - } - - /// Substitutes the first replacement value if one is present. - fn replace1(self, replacement: impl AsRef) -> Self { - let msg = self - .message - .as_ref() - .replacen("%1$s", replacement.as_ref(), 1) - .replacen("%1$d", replacement.as_ref(), 1) - .replacen("%1$@", replacement.as_ref(), 1); - Self { - context: self.context, - message: Cow::Owned(msg), - } - } - - /// Substitutes the second replacement value if one is present. - /// - /// Be aware you probably should have also called [`TranslatedStockString::replace1`] if - /// you are calling this. - fn replace2(self, replacement: impl AsRef) -> Self { - let msg = self - .message - .as_ref() - .replacen("%2$s", replacement.as_ref(), 1) - .replacen("%2$d", replacement.as_ref(), 1) - .replacen("%2$@", replacement.as_ref(), 1); - Self { - context: self.context, - message: Cow::Owned(msg), - } - } - - /// Augments the message by saying it was performed by a user. - /// - /// This looks up the display name of `contact` and uses the [`MsgActionByMe`] and - /// [`MsgActionByUser`] stock strings to turn the stock string in one that says the - /// action was performed by this user. - /// - /// E.g. this turns `Group image changed.` into `Group image changed by me.` or `Group - /// image changed by Alice`. - /// - /// Note that the original message should end in a `.`. - async fn action_by_contact(self, contact: u32) -> TranslatedStockString<'a> { - let message = self.message.as_ref().trim_end_matches('.'); - let message = match contact { - DC_CONTACT_ID_SELF => MsgActionByMe::stock_str(self.context, message).await, - _ => { - let displayname = Contact::get_by_id(self.context, contact) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_else(|_| format!("{}", contact)); - MsgActionByUser::stock_str(self.context, message, displayname).await - } - }; - TranslatedStockString { - context: self.context, - message, - } - } -} - -#[derive(Debug)] -pub(crate) enum NoMessages {} - -impl NoMessages { - /// Stock string: `No messages.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::NoMessages) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum SelfMsg {} - -impl SelfMsg { - /// Stock string: `Me`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::SelfMsg) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Draft {} - -impl Draft { - /// Stock string: `Draft`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Draft) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum VoiceMessage {} - -impl VoiceMessage { - /// Stock string: `Voice message`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::VoiceMessage) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum DeadDrop {} - -impl DeadDrop { - /// Stock string: `Contact requests`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::DeadDrop) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Image {} - -impl Image { - /// Stock string: `Image`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Image) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Video {} - -impl Video { - /// Stock string: `Video`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Video) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Audio {} - -impl Audio { - /// Stock string: `Audio`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Audio) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum File {} - -impl File { - /// Stock string: `File`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context).id(StockMessage::File).await.msg() - } -} - -#[derive(Debug)] -pub(crate) enum StatusLine {} - -impl StatusLine { - /// Stock string: `Sent with my Delta Chat Messenger: https://delta.chat`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::StatusLine) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum NewGroupDraft {} - -impl NewGroupDraft { - /// Stock string: `Hello, I've just created the group "%1$s" for us.`. - pub async fn stock_str(context: &Context, group_name: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::NewGroupDraft) - .await - .replace1(group_name) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgGrpName {} - -impl MsgGrpName { - /// Stock string: `Group name changed from "%1$s" to "%2$s".`. - pub async fn stock_str( - context: &Context, - from_group: impl AsRef, - to_group: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgGrpName) - .await - .replace1(from_group) - .replace2(to_group) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgGrpImgChanged {} - -impl MsgGrpImgChanged { - /// Stock string: `Group image changed.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgGrpImgChanged) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgAddMember {} - -impl MsgAddMember { - /// Stock string: `Member %1$s added.`. - /// - /// The `added_member_addr` parameter should be an email address and is looked up in the - /// contacts to combine with the display name. - pub async fn stock_str( - context: &Context, - added_member_addr: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - let addr = added_member_addr.as_ref(); - let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { - Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_else(|_| addr.to_string()), - _ => addr.to_string(), - }; - StockString::new(context) - .id(StockMessage::MsgAddMember) - .await - .replace1(who) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgDelMember {} - -impl MsgDelMember { - /// Stock string: `Member %1$s removed.`. - /// - /// The `removed_member_addr` parameter should be an email address and is looked up in - /// the contacts to combine with the display name. - pub async fn stock_str( - context: &Context, - removed_member_addr: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - let addr = removed_member_addr.as_ref(); - let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { - Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) - .await - .map(|contact| contact.get_name_n_addr()) - .unwrap_or_else(|_| addr.to_string()), - _ => addr.to_string(), - }; - StockString::new(context) - .id(StockMessage::MsgDelMember) - .await - .replace1(who) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgGroupLeft {} - -impl MsgGroupLeft { - /// Stock string: `Group left.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgGroupLeft) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Gif {} - -impl Gif { - /// Stock string: `GIF`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context).id(StockMessage::Gif).await.msg() - } -} - -#[derive(Debug)] -pub(crate) enum EncryptedMsg {} - -impl EncryptedMsg { - /// Stock string: `Encrypted message`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::EncryptedMsg) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum E2eAvailable {} - -impl E2eAvailable { - /// Stock string: `End-to-end encryption available.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::E2eAvailable) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum EncrNone {} - -impl EncrNone { - /// Stock string: `No encryption.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::EncrNone) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum CantDecryptMsgBody {} - -impl CantDecryptMsgBody { - /// Stock string: `This message was encrypted for another setup.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::CantDecryptMsgBody) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum FingerPrints {} - -impl FingerPrints { - /// Stock string: `Fingerprints`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::FingerPrints) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ReadRcpt {} - -impl ReadRcpt { - /// Stock string: `Return receipt`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ReadRcpt) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ReadRcptMailBody {} - -impl ReadRcptMailBody { - /// Stock string: `This is a return receipt for the message "%1$s".`. - pub async fn stock_str(context: &Context, message: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ReadRcptMailBody) - .await - .replace1(message) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgGrpImgDeleted {} - -impl MsgGrpImgDeleted { - /// Stock string: `Group image deleted.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgGrpImgDeleted) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum E2ePreferred {} - -impl E2ePreferred { - /// Stock string: `End-to-end encryption preferred.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::E2ePreferred) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ContactVerified {} - -impl ContactVerified { - /// Stock string: `%1$s verified.`. - pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ContactVerified) - .await - .replace1(contact_addr) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ContactNotVerified {} - -impl ContactNotVerified { - /// Stock string: `Cannot verify %1$s`. - pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ContactNotVerified) - .await - .replace1(contact_addr) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ContactSetupChanged {} - -impl ContactSetupChanged { - /// Stock string: `Changed setup for %1$s`. - pub async fn stock_str(context: &Context, contact_addr: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ContactSetupChanged) - .await - .replace1(contact_addr) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ArchivedChats {} - -impl ArchivedChats { - /// Stock string: `Archived chats`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ArchivedChats) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum AcSetupMsgSubject {} - -impl AcSetupMsgSubject { - /// Stock string: `Autocrypt Setup Message`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::AcSetupMsgSubject) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum AcSetupMsgBody {} - -impl AcSetupMsgBody { - /// Stock string: `This is the Autocrypt Setup Message used to transfer...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::AcSetupMsgBody) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum CannotLogin {} - -impl CannotLogin { - /// Stock string: `Cannot login as \"%1$s\". Please check...`. - pub async fn stock_str(context: &Context, user: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::CannotLogin) - .await - .replace1(user) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ServerResponse {} - -impl ServerResponse { - /// Stock string: `Could not connect to %1$s: %2$s`. - pub async fn stock_str( - context: &Context, - server: impl AsRef, - details: impl AsRef, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ServerResponse) - .await - .replace1(server) - .replace2(details) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgActionByUser {} - -impl MsgActionByUser { - /// Stock string: `%1$s by %2$s.`. - pub async fn stock_str( - context: &Context, - action: impl AsRef, - user: impl AsRef, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgActionByUser) - .await - .replace1(action) - .replace2(user) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgActionByMe {} - -impl MsgActionByMe { - /// Stock string: `%1$s by me.`. - pub async fn stock_str(context: &Context, action: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgActionByMe) - .await - .replace1(action) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgLocationEnabled {} - -impl MsgLocationEnabled { - /// Stock string: `Location streaming enabled.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgLocationEnabled) - .await - .msg() - } - - /// Stock string: `Location streaming enabled.`. - pub async fn stock_str_by(context: &Context, contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgLocationEnabled) - .await - .action_by_contact(contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgLocationDisabled {} - -impl MsgLocationDisabled { - /// Stock string: `Location streaming disabled.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgLocationDisabled) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Location {} - -impl Location { - /// Stock string: `Location`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Location) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum Sticker {} - -impl Sticker { - /// Stock string: `Sticker`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::Sticker) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum DeviceMessages {} - -impl DeviceMessages { - /// Stock string: `Device messages`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::DeviceMessages) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum SavedMessages {} - -impl SavedMessages { - /// Stock string: `Saved messages`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::SavedMessages) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum DeviceMessagesHint {} - -impl DeviceMessagesHint { - /// Stock string: `Messages in this chat are generated locally by...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::DeviceMessagesHint) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum WelcomeMessage {} - -impl WelcomeMessage { - /// Stock string: `Welcome to Delta Chat! – ...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::WelcomeMessage) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum UnknownSenderForChat {} - -impl UnknownSenderForChat { - /// Stock string: `Unknown sender for this chat. See 'info' for more details.`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::UnknownSenderForChat) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum SubjectForNewContact {} - -impl SubjectForNewContact { - /// Stock string: `Message from %1$s`. - // TODO: This can compute `self_name` itself instead of asking the caller to do this. - pub async fn stock_str(context: &Context, self_name: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::SubjectForNewContact) - .await - .replace1(self_name) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum FailedSendingTo {} - -impl FailedSendingTo { - /// Stock string: `Failed to send message to %1$s.`. - pub async fn stock_str(context: &Context, name: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::FailedSendingTo) - .await - .replace1(name) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerDisabled {} - -impl MsgEphemeralTimerDisabled { - /// Stock string: `Message deletion timer is disabled.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerDisabled) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerEnabled {} - -impl MsgEphemeralTimerEnabled { - /// Stock string: `Message deletion timer is set to %1$s s.`. - pub async fn stock_str( - context: &Context, - timer: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerEnabled) - .await - .replace1(timer) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerMinute {} - -impl MsgEphemeralTimerMinute { - /// Stock string: `Message deletion timer is set to 1 minute.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerMinute) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerHour {} - -impl MsgEphemeralTimerHour { - /// Stock string: `Message deletion timer is set to 1 hour.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerHour) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerDay {} - -impl MsgEphemeralTimerDay { - /// Stock string: `Message deletion timer is set to 1 day.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerDay) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerWeek {} - -impl MsgEphemeralTimerWeek { - /// Stock string: `Message deletion timer is set to 1 week.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerWeek) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum VideochatInvitation {} - -impl VideochatInvitation { - /// Stock string: `Video chat invitation`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::VideochatInvitation) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum VideochatInviteMsgBody {} - -impl VideochatInviteMsgBody { - /// Stock string: `You are invited to a video chat, click %1$s to join.`. - pub async fn stock_str(context: &Context, url: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::VideochatInviteMsgBody) - .await - .replace1(url) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ConfigurationFailed {} - -impl ConfigurationFailed { - /// Stock string: `Error:\n\nā€œ%1$sā€`. - pub async fn stock_str(context: &Context, details: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ConfigurationFailed) - .await - .replace1(details) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum BadTimeMsgBody {} - -impl BadTimeMsgBody { - /// Stock string: `āš ļø Date or time of your device seem to be inaccurate (%1$s)...`. - // TODO: This could compute now itself. - pub async fn stock_str(context: &Context, now: impl AsRef) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::BadTimeMsgBody) - .await - .replace1(now) - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum UpdateReminderMsgBody {} - -impl UpdateReminderMsgBody { - /// Stock string: `āš ļø Your Delta Chat version might be outdated...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::UpdateReminderMsgBody) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ErrorNoNetwork {} - -impl ErrorNoNetwork { - /// Stock string: `Could not find your mail server...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ErrorNoNetwork) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ProtectionEnabled {} - -impl ProtectionEnabled { - /// Stock string: `Chat protection enabled.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ProtectionEnabled) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ProtectionDisabled {} - -impl ProtectionDisabled { - /// Stock string: `Chat protection disabled.`. - pub async fn stock_str(context: &Context, by_contact: u32) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ProtectionDisabled) - .await - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum ReplyNoun {} - -impl ReplyNoun { - /// Stock string: `Reply`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::ReplyNoun) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum SelfDeletedMsgBody {} - -impl SelfDeletedMsgBody { - /// Stock string: `You deleted the \"Saved messages\" chat...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::SelfDeletedMsgBody) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum DeleteServerTurnedOff {} - -impl DeleteServerTurnedOff { - /// Stock string: `āš ļø The "Delete messages from server" feature now also...`. - pub async fn stock_str(context: &Context) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::DeleteServerTurnedOff) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerMinutes {} - -impl MsgEphemeralTimerMinutes { - /// Stock string: `Message deletion timer is set to %1$s minutes.`. - pub async fn stock_str( - context: &Context, - minutes: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerMinutes) - .await - .replace1(minutes) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerHours {} - -impl MsgEphemeralTimerHours { - /// Stock string: `Message deletion timer is set to %1$s hours.`. - pub async fn stock_str( - context: &Context, - hours: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerHours) - .await - .replace1(hours) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerDays {} - -impl MsgEphemeralTimerDays { - /// Stock string: `Message deletion timer is set to %1$s days.`. - pub async fn stock_str( - context: &Context, - days: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerDays) - .await - .replace1(days) - .action_by_contact(by_contact) - .await - .msg() - } -} - -#[derive(Debug)] -pub(crate) enum MsgEphemeralTimerWeeks {} - -impl MsgEphemeralTimerWeeks { - /// Stock string: `Message deletion timer is set to %1$s weeks.`. - pub async fn stock_str( - context: &Context, - weeks: impl AsRef, - by_contact: u32, - ) -> Cow<'static, str> { - StockString::new(context) - .id(StockMessage::MsgEphemeralTimerWeeks) - .await - .replace1(weeks) - .action_by_contact(by_contact) - .await - .msg() - } -} - -impl Context { - /// Set the stock string for the [StockMessage]. - /// - pub async fn set_stock_translation( - &self, - id: StockMessage, - stockstring: String, - ) -> Result<(), Error> { - if stockstring.contains("%1") && !id.fallback().contains("%1") { - bail!( - "translation {} contains invalid %1 placeholder, default is {}", - stockstring, - id.fallback() - ); - } - if stockstring.contains("%2") && !id.fallback().contains("%2") { - bail!( - "translation {} contains invalid %2 placeholder, default is {}", - stockstring, - id.fallback() - ); - } - self.translated_stockstrings - .write() - .await - .insert(id as usize, stockstring); - Ok(()) - } - - /// Returns a stock message saying that protection status has changed. - pub(crate) async fn stock_protection_msg( - &self, - protect: ProtectionStatus, - from_id: u32, - ) -> String { - match protect { - ProtectionStatus::Unprotected => ProtectionEnabled::stock_str(self, from_id).await, - ProtectionStatus::Protected => ProtectionDisabled::stock_str(self, from_id).await, - } - .to_string() - } - - pub(crate) async fn update_device_chats(&self) -> Result<(), Error> { - if self.get_config_bool(Config::Bot).await { - return Ok(()); - } - - // create saved-messages chat; we do this only once, if the user has deleted the chat, - // he can recreate it manually (make sure we do not re-add it when configure() was called a second time) - if !self.sql.get_raw_config_bool(&self, "self-chat-added").await { - self.sql - .set_raw_config_bool(&self, "self-chat-added", true) - .await?; - chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF).await?; - } - - // add welcome-messages. by the label, this is done only once, - // if the user has deleted the message or the chat, it is not added again. - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(DeviceMessagesHint::stock_str(self).await.to_string()); - chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg)).await?; - - let image = include_bytes!("../assets/welcome-image.jpg"); - let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image).await?; - let mut msg = Message::new(Viewtype::Image); - msg.param.set(Param::File, blob.as_name()); - chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg)).await?; - - let mut msg = Message::new(Viewtype::Text); - msg.text = Some(WelcomeMessage::stock_str(self).await.to_string()); - chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg)).await?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::TestContext; - - use crate::constants::DC_CONTACT_ID_SELF; - - use crate::chat::Chat; - use crate::chatlist::Chatlist; - use num_traits::ToPrimitive; - - #[test] - fn test_enum_mapping() { - assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1); - assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2); - } - - #[test] - fn test_fallback() { - assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); - } - - #[async_std::test] - async fn test_set_stock_translation() { - let t = TestContext::new().await; - t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) - .await - .unwrap(); - assert_eq!(NoMessages::stock_str(&t).await, "xyz") - } - - #[async_std::test] - async fn test_set_stock_translation_wrong_replacements() { - let t = TestContext::new().await; - assert!(t - .ctx - .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) - .await - .is_err()); - assert!(t - .ctx - .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) - .await - .is_err()); - } - - #[async_std::test] - async fn test_stock_str() { - let t = TestContext::new().await; - assert_eq!(NoMessages::stock_str(&t).await, "No messages."); - } - - #[async_std::test] - async fn test_stock_string_repl_str() { - let t = TestContext::new().await; - // uses %1$s substitution - assert_eq!(ContactVerified::stock_str(&t, "Foo").await, "Foo verified."); - // We have no string using %1$d to test... - } - - #[async_std::test] - async fn test_stock_string_repl_str2() { - let t = TestContext::new().await; - assert_eq!( - ServerResponse::stock_str(&t, "foo", "bar").await, - "Could not connect to foo: bar" - ); - } - - #[async_std::test] - async fn test_stock_system_msg_simple() { - let t = TestContext::new().await; - assert_eq!( - MsgLocationEnabled::stock_str(&t).await, - "Location streaming enabled." - ) - } - - #[async_std::test] - async fn test_stock_system_msg_add_member_by_me() { - let t = TestContext::new().await; - assert_eq!( - MsgAddMember::stock_str(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, - "Member alice@example.com added by me." - ) - } - - #[async_std::test] - async fn test_stock_system_msg_add_member_by_me_with_displayname() { - let t = TestContext::new().await; - Contact::create(&t, "Alice", "alice@example.com") - .await - .expect("failed to create contact"); - assert_eq!( - MsgAddMember::stock_str(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, - "Member Alice (alice@example.com) added by me." - ); - } - - #[async_std::test] - async fn test_stock_system_msg_add_member_by_other_with_displayname() { - let t = TestContext::new().await; - let contact_id = { - Contact::create(&t, "Alice", "alice@example.com") - .await - .expect("Failed to create contact Alice"); - Contact::create(&t, "Bob", "bob@example.com") - .await - .expect("failed to create bob") - }; - assert_eq!( - MsgAddMember::stock_str(&t, "alice@example.com", contact_id,).await, - "Member Alice (alice@example.com) added by Bob (bob@example.com)." - ); - } - - #[async_std::test] - async fn test_update_device_chats() { - let t = TestContext::new().await; - t.update_device_chats().await.ok(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 2); - - let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0)).await.unwrap(); - let (self_talk_id, device_chat_id) = if chat0.is_self_talk() { - (chats.get_chat_id(0), chats.get_chat_id(1)) - } else { - (chats.get_chat_id(1), chats.get_chat_id(0)) - }; - - // delete self-talk first; this adds a message to device-chat about how self-talk can be restored - let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(); - self_talk_id.delete(&t).await.ok(); - assert_eq!( - chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(), - device_chat_msgs_before + 1 - ); - - // delete device chat - device_chat_id.delete(&t).await.ok(); - - // check, that the chatlist is empty - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - - // a subsequent call to update_device_chats() must not re-add manally deleted messages or chats - t.update_device_chats().await.ok(); - let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); - assert_eq!(chats.len(), 0); - } -} diff --git a/src/stock_str.rs b/src/stock_str.rs new file mode 100644 index 000000000..8b529ebdb --- /dev/null +++ b/src/stock_str.rs @@ -0,0 +1,1084 @@ +//! Module to work with translatable stock strings + +use std::future::Future; +use std::pin::Pin; + +use anyhow::{bail, Error}; +use strum::EnumProperty; +use strum_macros::EnumProperty; + +use crate::blob::BlobObject; +use crate::chat; +use crate::chat::ProtectionStatus; +use crate::config::Config; +use crate::constants::{Viewtype, DC_CONTACT_ID_SELF}; +use crate::contact::{Contact, Origin}; +use crate::context::Context; +use crate::message::Message; +use crate::param::Param; + +/// Stock strings +/// +/// These identify the string to return in [Context.stock_str]. The +/// numbers must stay in sync with `deltachat.h` `DC_STR_*` constants. +/// +/// See the `stock_*` methods on [Context] to use these. +/// +/// [Context]: crate::context::Context +#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)] +#[repr(u32)] +pub enum StockMessage { + #[strum(props(fallback = "No messages."))] + NoMessages = 1, + + #[strum(props(fallback = "Me"))] + SelfMsg = 2, + + #[strum(props(fallback = "Draft"))] + Draft = 3, + + #[strum(props(fallback = "Voice message"))] + VoiceMessage = 7, + + #[strum(props(fallback = "Contact requests"))] + DeadDrop = 8, + + #[strum(props(fallback = "Image"))] + Image = 9, + + #[strum(props(fallback = "Video"))] + Video = 10, + + #[strum(props(fallback = "Audio"))] + Audio = 11, + + #[strum(props(fallback = "File"))] + File = 12, + + #[strum(props(fallback = "Sent with my Delta Chat Messenger: https://delta.chat"))] + StatusLine = 13, + + #[strum(props(fallback = "Hello, I\'ve just created the group \"%1$s\" for us."))] + NewGroupDraft = 14, + + #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\"."))] + MsgGrpName = 15, + + #[strum(props(fallback = "Group image changed."))] + MsgGrpImgChanged = 16, + + #[strum(props(fallback = "Member %1$s added."))] + MsgAddMember = 17, + + #[strum(props(fallback = "Member %1$s removed."))] + MsgDelMember = 18, + + #[strum(props(fallback = "Group left."))] + MsgGroupLeft = 19, + + #[strum(props(fallback = "GIF"))] + Gif = 23, + + #[strum(props(fallback = "Encrypted message"))] + EncryptedMsg = 24, + + #[strum(props(fallback = "End-to-end encryption available."))] + E2eAvailable = 25, + + #[strum(props(fallback = "No encryption."))] + EncrNone = 28, + + #[strum(props(fallback = "This message was encrypted for another setup."))] + CantDecryptMsgBody = 29, + + #[strum(props(fallback = "Fingerprints"))] + FingerPrints = 30, + + #[strum(props(fallback = "Return receipt"))] + ReadRcpt = 31, + + #[strum(props(fallback = "This is a return receipt for the message \"%1$s\"."))] + ReadRcptMailBody = 32, + + #[strum(props(fallback = "Group image deleted."))] + MsgGrpImgDeleted = 33, + + #[strum(props(fallback = "End-to-end encryption preferred."))] + E2ePreferred = 34, + + #[strum(props(fallback = "%1$s verified."))] + ContactVerified = 35, + + #[strum(props(fallback = "Cannot verify %1$s"))] + ContactNotVerified = 36, + + #[strum(props(fallback = "Changed setup for %1$s"))] + ContactSetupChanged = 37, + + #[strum(props(fallback = "Archived chats"))] + ArchivedChats = 40, + + #[strum(props(fallback = "Autocrypt Setup Message"))] + AcSetupMsgSubject = 42, + + #[strum(props( + fallback = "This is the Autocrypt Setup Message used to transfer your key between clients.\n\nTo decrypt and use your key, open the message in an Autocrypt-compliant client and enter the setup code presented on the generating device." + ))] + AcSetupMsgBody = 43, + + #[strum(props( + fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct." + ))] + CannotLogin = 60, + + #[strum(props(fallback = "Could not connect to %1$s: %2$s"))] + ServerResponse = 61, + + #[strum(props(fallback = "%1$s by %2$s."))] + MsgActionByUser = 62, + + #[strum(props(fallback = "%1$s by me."))] + MsgActionByMe = 63, + + #[strum(props(fallback = "Location streaming enabled."))] + MsgLocationEnabled = 64, + + #[strum(props(fallback = "Location streaming disabled."))] + MsgLocationDisabled = 65, + + #[strum(props(fallback = "Location"))] + Location = 66, + + #[strum(props(fallback = "Sticker"))] + Sticker = 67, + + #[strum(props(fallback = "Device messages"))] + DeviceMessages = 68, + + #[strum(props(fallback = "Saved messages"))] + SavedMessages = 69, + + #[strum(props( + fallback = "Messages in this chat are generated locally by your Delta Chat app. \ + Its makers use it to inform about app updates and problems during usage." + ))] + DeviceMessagesHint = 70, + + #[strum(props(fallback = "Welcome to Delta Chat! – \ + Delta Chat looks and feels like other popular messenger apps, \ + but does not involve centralized control, \ + tracking or selling you, friends, colleagues or family out to large organizations.\n\n\ + Technically, Delta Chat is an email application with a modern chat interface. \ + Email in a new dress if you will šŸ‘»\n\n\ + Use Delta Chat with anyone out of billions of people: just use their e-mail address. \ + Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \ + however, of course, if they like, you may point them to šŸ‘‰ https://get.delta.chat"))] + WelcomeMessage = 71, + + #[strum(props(fallback = "Unknown sender for this chat. See 'info' for more details."))] + UnknownSenderForChat = 72, + + #[strum(props(fallback = "Message from %1$s"))] + SubjectForNewContact = 73, + + #[strum(props(fallback = "Failed to send message to %1$s."))] + FailedSendingTo = 74, + + #[strum(props(fallback = "Message deletion timer is disabled."))] + MsgEphemeralTimerDisabled = 75, + + // A fallback message for unknown timer values. + // "s" stands for "second" SI unit here. + #[strum(props(fallback = "Message deletion timer is set to %1$s s."))] + MsgEphemeralTimerEnabled = 76, + + #[strum(props(fallback = "Message deletion timer is set to 1 minute."))] + MsgEphemeralTimerMinute = 77, + + #[strum(props(fallback = "Message deletion timer is set to 1 hour."))] + MsgEphemeralTimerHour = 78, + + #[strum(props(fallback = "Message deletion timer is set to 1 day."))] + MsgEphemeralTimerDay = 79, + + #[strum(props(fallback = "Message deletion timer is set to 1 week."))] + MsgEphemeralTimerWeek = 80, + + #[strum(props(fallback = "Video chat invitation"))] + VideochatInvitation = 82, + + #[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))] + VideochatInviteMsgBody = 83, + + #[strum(props(fallback = "Error:\n\nā€œ%1$sā€"))] + ConfigurationFailed = 84, + + #[strum(props( + fallback = "āš ļø Date or time of your device seem to be inaccurate (%1$s).\n\n\ + Adjust your clock ā°šŸ”§ to ensure your messages are received correctly." + ))] + BadTimeMsgBody = 85, + + #[strum(props(fallback = "āš ļø Your Delta Chat version might be outdated.\n\n\ + This may cause problems because your chat partners use newer versions - \ + and you are missing the latest features 😳\n\ + Please check https://get.delta.chat or your app store for updates."))] + UpdateReminderMsgBody = 86, + + #[strum(props( + fallback = "Could not find your mail server.\n\nPlease check your internet connection." + ))] + ErrorNoNetwork = 87, + + #[strum(props(fallback = "Chat protection enabled."))] + ProtectionEnabled = 88, + + #[strum(props(fallback = "Chat protection disabled."))] + ProtectionDisabled = 89, + + // used in summaries, a noun, not a verb (not: "to reply") + #[strum(props(fallback = "Reply"))] + ReplyNoun = 90, + + #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\ + To use the \"Saved messages\" feature again, create a new chat with yourself."))] + SelfDeletedMsgBody = 91, + + #[strum(props( + fallback = "āš ļø The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\ + ā„¹ļø To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \ + Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it." + ))] + DeleteServerTurnedOff = 92, + + #[strum(props(fallback = "Message deletion timer is set to %1$s minutes."))] + MsgEphemeralTimerMinutes = 93, + + #[strum(props(fallback = "Message deletion timer is set to %1$s hours."))] + MsgEphemeralTimerHours = 94, + + #[strum(props(fallback = "Message deletion timer is set to %1$s days."))] + MsgEphemeralTimerDays = 95, + + #[strum(props(fallback = "Message deletion timer is set to %1$s weeks."))] + MsgEphemeralTimerWeeks = 96, +} + +impl StockMessage { + /// Default untranslated strings for stock messages. + /// + /// These could be used in logging calls, so no logging here. + fn fallback(self) -> &'static str { + self.get_str("fallback").unwrap_or_default() + } +} + +async fn translated(context: &Context, id: StockMessage) -> String { + context + .translated_stockstrings + .read() + .await + .get(&(id as usize)) + .map(AsRef::as_ref) + .unwrap_or_else(|| id.fallback()) + .to_string() +} + +/// Helper trait only meant to be implemented for [`String`]. +trait StockStringMods: AsRef + Sized { + /// Substitutes the first replacement value if one is present. + fn replace1(&self, replacement: impl AsRef) -> String { + self.as_ref() + .replacen("%1$s", replacement.as_ref(), 1) + .replacen("%1$d", replacement.as_ref(), 1) + .replacen("%1$@", replacement.as_ref(), 1) + } + + /// Substitutes the second replacement value if one is present. + /// + /// Be aware you probably should have also called [`TranslatedStockString::replace1`] if + /// you are calling this. + fn replace2(&self, replacement: impl AsRef) -> String { + self.as_ref() + .replacen("%2$s", replacement.as_ref(), 1) + .replacen("%2$d", replacement.as_ref(), 1) + .replacen("%2$@", replacement.as_ref(), 1) + } + + /// Augments the message by saying it was performed by a user. + /// + /// This looks up the display name of `contact` and uses the [`MsgActionByMe`] and + /// [`MsgActionByUser`] stock strings to turn the stock string in one that says the + /// action was performed by this user. + /// + /// E.g. this turns `Group image changed.` into `Group image changed by me.` or `Group + /// image changed by Alice.`. + /// + /// Note that the original message should end in a `.`. + fn action_by_contact<'a>( + self, + context: &'a Context, + contact_id: u32, + ) -> Pin + Send + 'a>> + where + Self: Send + 'a, + { + Box::pin(async move { + let message = self.as_ref().trim_end_matches('.'); + match contact_id { + DC_CONTACT_ID_SELF => msg_action_by_me(context, message).await, + _ => { + let displayname = Contact::get_by_id(context, contact_id) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| contact_id.to_string()); + msg_action_by_user(context, message, displayname).await + } + } + }) + } +} + +impl StockStringMods for String {} + +/// Stock string: `No messages.`. +pub(crate) async fn no_messages(context: &Context) -> String { + translated(context, StockMessage::NoMessages).await +} + +/// Stock string: `Me`. +pub(crate) async fn self_msg(context: &Context) -> String { + translated(context, StockMessage::SelfMsg).await +} + +/// Stock string: `Draft`. +pub(crate) async fn draft(context: &Context) -> String { + translated(context, StockMessage::Draft).await +} + +/// Stock string: `Voice message`. +pub(crate) async fn voice_message(context: &Context) -> String { + translated(context, StockMessage::VoiceMessage).await +} + +/// Stock string: `Contact requests`. +pub(crate) async fn dead_drop(context: &Context) -> String { + translated(context, StockMessage::DeadDrop).await +} + +/// Stock string: `Image`. +pub(crate) async fn image(context: &Context) -> String { + translated(context, StockMessage::Image).await +} + +/// Stock string: `Video`. +pub(crate) async fn video(context: &Context) -> String { + translated(context, StockMessage::Video).await +} + +/// Stock string: `Audio`. +pub(crate) async fn audio(context: &Context) -> String { + translated(context, StockMessage::Audio).await +} + +/// Stock string: `File`. +pub(crate) async fn file(context: &Context) -> String { + translated(context, StockMessage::File).await +} + +/// Stock string: `Sent with my Delta Chat Messenger: https://delta.chat`. +pub(crate) async fn status_line(context: &Context) -> String { + translated(context, StockMessage::StatusLine).await +} + +/// Stock string: `Hello, I've just created the group "%1$s" for us.`. +pub(crate) async fn new_group_draft(context: &Context, group_name: impl AsRef) -> String { + translated(context, StockMessage::NewGroupDraft) + .await + .replace1(group_name) +} + +/// Stock string: `Group name changed from "%1$s" to "%2$s".`. +pub(crate) async fn msg_grp_name( + context: &Context, + from_group: impl AsRef, + to_group: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgGrpName) + .await + .replace1(from_group) + .replace2(to_group) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Group image changed.`. +pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgGrpImgChanged) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Member %1$s added.`. +/// +/// The `added_member_addr` parameter should be an email address and is looked up in the +/// contacts to combine with the display name. +pub(crate) async fn msg_add_member( + context: &Context, + added_member_addr: impl AsRef, + by_contact: u32, +) -> String { + let addr = added_member_addr.as_ref(); + let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { + Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| addr.to_string()), + _ => addr.to_string(), + }; + translated(context, StockMessage::MsgAddMember) + .await + .replace1(who) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Member %1$s removed.`. +/// +/// The `removed_member_addr` parameter should be an email address and is looked up in +/// the contacts to combine with the display name. +pub(crate) async fn msg_del_member( + context: &Context, + removed_member_addr: impl AsRef, + by_contact: u32, +) -> String { + let addr = removed_member_addr.as_ref(); + let who = match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await { + Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id) + .await + .map(|contact| contact.get_name_n_addr()) + .unwrap_or_else(|_| addr.to_string()), + _ => addr.to_string(), + }; + translated(context, StockMessage::MsgDelMember) + .await + .replace1(who) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Group left.`. +pub(crate) async fn msg_group_left(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgGroupLeft) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `GIF`. +pub(crate) async fn gif(context: &Context) -> String { + translated(context, StockMessage::Gif).await +} + +/// Stock string: `Encrypted message`. +pub(crate) async fn encrypted_msg(context: &Context) -> String { + translated(context, StockMessage::EncryptedMsg).await +} + +/// Stock string: `End-to-end encryption available.`. +pub(crate) async fn e2e_available(context: &Context) -> String { + translated(context, StockMessage::E2eAvailable).await +} + +/// Stock string: `No encryption.`. +pub(crate) async fn encr_none(context: &Context) -> String { + translated(context, StockMessage::EncrNone).await +} + +/// Stock string: `This message was encrypted for another setup.`. +pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String { + translated(context, StockMessage::CantDecryptMsgBody).await +} + +/// Stock string: `Fingerprints`. +pub(crate) async fn finger_prints(context: &Context) -> String { + translated(context, StockMessage::FingerPrints).await +} + +/// Stock string: `Return receipt`. +pub(crate) async fn read_rcpt(context: &Context) -> String { + translated(context, StockMessage::ReadRcpt).await +} + +/// Stock string: `This is a return receipt for the message "%1$s".`. +pub(crate) async fn read_rcpt_mail_body(context: &Context, message: impl AsRef) -> String { + translated(context, StockMessage::ReadRcptMailBody) + .await + .replace1(message) +} + +/// Stock string: `Group image deleted.`. +pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgGrpImgDeleted) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `End-to-end encryption preferred.`. +pub(crate) async fn e2e_preferred(context: &Context) -> String { + translated(context, StockMessage::E2ePreferred).await +} + +/// Stock string: `%1$s verified.`. +pub(crate) async fn contact_verified(context: &Context, contact_addr: impl AsRef) -> String { + translated(context, StockMessage::ContactVerified) + .await + .replace1(contact_addr) +} + +/// Stock string: `Cannot verify %1$s`. +pub(crate) async fn contact_not_verified( + context: &Context, + contact_addr: impl AsRef, +) -> String { + translated(context, StockMessage::ContactNotVerified) + .await + .replace1(contact_addr) +} + +/// Stock string: `Changed setup for %1$s`. +pub(crate) async fn contact_setup_changed( + context: &Context, + contact_addr: impl AsRef, +) -> String { + translated(context, StockMessage::ContactSetupChanged) + .await + .replace1(contact_addr) +} + +/// Stock string: `Archived chats`. +pub(crate) async fn archived_chats(context: &Context) -> String { + translated(context, StockMessage::ArchivedChats).await +} + +/// Stock string: `Autocrypt Setup Message`. +pub(crate) async fn ac_setup_msg_subject(context: &Context) -> String { + translated(context, StockMessage::AcSetupMsgSubject).await +} + +/// Stock string: `This is the Autocrypt Setup Message used to transfer...`. +pub(crate) async fn ac_setup_msg_body(context: &Context) -> String { + translated(context, StockMessage::AcSetupMsgBody).await +} + +/// Stock string: `Cannot login as \"%1$s\". Please check...`. +pub(crate) async fn cannot_login(context: &Context, user: impl AsRef) -> String { + translated(context, StockMessage::CannotLogin) + .await + .replace1(user) +} + +/// Stock string: `Could not connect to %1$s: %2$s`. +pub(crate) async fn server_response( + context: &Context, + server: impl AsRef, + details: impl AsRef, +) -> String { + translated(context, StockMessage::ServerResponse) + .await + .replace1(server) + .replace2(details) +} + +/// Stock string: `%1$s by %2$s.`. +pub(crate) async fn msg_action_by_user( + context: &Context, + action: impl AsRef, + user: impl AsRef, +) -> String { + translated(context, StockMessage::MsgActionByUser) + .await + .replace1(action) + .replace2(user) +} + +/// Stock string: `%1$s by me.`. +pub(crate) async fn msg_action_by_me(context: &Context, action: impl AsRef) -> String { + translated(context, StockMessage::MsgActionByMe) + .await + .replace1(action) +} + +/// Stock string: `Location streaming enabled.`. +pub(crate) async fn msg_location_enabled(context: &Context) -> String { + translated(context, StockMessage::MsgLocationEnabled).await +} + +/// Stock string: `Location streaming enabled by ...`. +pub(crate) async fn msg_location_enabled_by(context: &Context, contact: u32) -> String { + translated(context, StockMessage::MsgLocationEnabled) + .await + .action_by_contact(context, contact) + .await +} + +/// Stock string: `Location streaming disabled.`. +pub(crate) async fn msg_location_disabled(context: &Context) -> String { + translated(context, StockMessage::MsgLocationDisabled).await +} + +/// Stock string: `Location`. +pub(crate) async fn location(context: &Context) -> String { + translated(context, StockMessage::Location).await +} + +/// Stock string: `Sticker`. +pub(crate) async fn sticker(context: &Context) -> String { + translated(context, StockMessage::Sticker).await +} + +/// Stock string: `Device messages`. +pub(crate) async fn device_messages(context: &Context) -> String { + translated(context, StockMessage::DeviceMessages).await +} + +/// Stock string: `Saved messages`. +pub(crate) async fn saved_messages(context: &Context) -> String { + translated(context, StockMessage::SavedMessages).await +} + +/// Stock string: `Messages in this chat are generated locally by...`. +pub(crate) async fn device_messages_hint(context: &Context) -> String { + translated(context, StockMessage::DeviceMessagesHint).await +} + +/// Stock string: `Welcome to Delta Chat! – ...`. +pub(crate) async fn welcome_message(context: &Context) -> String { + translated(context, StockMessage::WelcomeMessage).await +} + +/// Stock string: `Unknown sender for this chat. See 'info' for more details.`. +pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String { + translated(context, StockMessage::UnknownSenderForChat).await +} + +/// Stock string: `Message from %1$s`. +// TODO: This can compute `self_name` itself instead of asking the caller to do this. +pub(crate) async fn subject_for_new_contact( + context: &Context, + self_name: impl AsRef, +) -> String { + translated(context, StockMessage::SubjectForNewContact) + .await + .replace1(self_name) +} + +/// Stock string: `Failed to send message to %1$s.`. +pub(crate) async fn failed_sending_to(context: &Context, name: impl AsRef) -> String { + translated(context, StockMessage::FailedSendingTo) + .await + .replace1(name) +} + +/// Stock string: `Message deletion timer is disabled.`. +pub(crate) async fn msg_ephemeral_timer_disabled(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgEphemeralTimerDisabled) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to %1$s s.`. +pub(crate) async fn msg_ephemeral_timer_enabled( + context: &Context, + timer: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgEphemeralTimerEnabled) + .await + .replace1(timer) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to 1 minute.`. +pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgEphemeralTimerMinute) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to 1 hour.`. +pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgEphemeralTimerHour) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to 1 day.`. +pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgEphemeralTimerDay) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to 1 week.`. +pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::MsgEphemeralTimerWeek) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Video chat invitation`. +pub(crate) async fn videochat_invitation(context: &Context) -> String { + translated(context, StockMessage::VideochatInvitation).await +} + +/// Stock string: `You are invited to a video chat, click %1$s to join.`. +pub(crate) async fn videochat_invite_msg_body(context: &Context, url: impl AsRef) -> String { + translated(context, StockMessage::VideochatInviteMsgBody) + .await + .replace1(url) +} + +/// Stock string: `Error:\n\nā€œ%1$sā€`. +pub(crate) async fn configuration_failed(context: &Context, details: impl AsRef) -> String { + translated(context, StockMessage::ConfigurationFailed) + .await + .replace1(details) +} + +/// Stock string: `āš ļø Date or time of your device seem to be inaccurate (%1$s)...`. +// TODO: This could compute now itself. +pub(crate) async fn bad_time_msg_body(context: &Context, now: impl AsRef) -> String { + translated(context, StockMessage::BadTimeMsgBody) + .await + .replace1(now) +} + +/// Stock string: `āš ļø Your Delta Chat version might be outdated...`. +pub(crate) async fn update_reminder_msg_body(context: &Context) -> String { + translated(context, StockMessage::UpdateReminderMsgBody).await +} + +/// Stock string: `Could not find your mail server...`. +pub(crate) async fn error_no_network(context: &Context) -> String { + translated(context, StockMessage::ErrorNoNetwork).await +} + +/// Stock string: `Chat protection enabled.`. +pub(crate) async fn protection_enabled(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::ProtectionEnabled) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Chat protection disabled.`. +pub(crate) async fn protection_disabled(context: &Context, by_contact: u32) -> String { + translated(context, StockMessage::ProtectionDisabled) + .await + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Reply`. +pub(crate) async fn reply_noun(context: &Context) -> String { + translated(context, StockMessage::ReplyNoun).await +} + +/// Stock string: `You deleted the \"Saved messages\" chat...`. +pub(crate) async fn self_deleted_msg_body(context: &Context) -> String { + translated(context, StockMessage::SelfDeletedMsgBody).await +} + +/// Stock string: `āš ļø The "Delete messages from server" feature now also...`. +pub(crate) async fn delete_server_turned_off(context: &Context) -> String { + translated(context, StockMessage::DeleteServerTurnedOff).await +} + +/// Stock string: `Message deletion timer is set to %1$s minutes.`. +pub(crate) async fn msg_ephemeral_timer_minutes( + context: &Context, + minutes: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgEphemeralTimerMinutes) + .await + .replace1(minutes) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to %1$s hours.`. +pub(crate) async fn msg_ephemeral_timer_hours( + context: &Context, + hours: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgEphemeralTimerHours) + .await + .replace1(hours) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to %1$s days.`. +pub(crate) async fn msg_ephemeral_timer_days( + context: &Context, + days: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgEphemeralTimerDays) + .await + .replace1(days) + .action_by_contact(context, by_contact) + .await +} + +/// Stock string: `Message deletion timer is set to %1$s weeks.`. +pub(crate) async fn msg_ephemeral_timer_weeks( + context: &Context, + weeks: impl AsRef, + by_contact: u32, +) -> String { + translated(context, StockMessage::MsgEphemeralTimerWeeks) + .await + .replace1(weeks) + .action_by_contact(context, by_contact) + .await +} + +impl Context { + /// Set the stock string for the [StockMessage]. + /// + pub async fn set_stock_translation( + &self, + id: StockMessage, + stockstring: String, + ) -> Result<(), Error> { + if stockstring.contains("%1") && !id.fallback().contains("%1") { + bail!( + "translation {} contains invalid %1 placeholder, default is {}", + stockstring, + id.fallback() + ); + } + if stockstring.contains("%2") && !id.fallback().contains("%2") { + bail!( + "translation {} contains invalid %2 placeholder, default is {}", + stockstring, + id.fallback() + ); + } + self.translated_stockstrings + .write() + .await + .insert(id as usize, stockstring); + Ok(()) + } + + /// Returns a stock message saying that protection status has changed. + pub(crate) async fn stock_protection_msg( + &self, + protect: ProtectionStatus, + from_id: u32, + ) -> String { + match protect { + ProtectionStatus::Unprotected => protection_enabled(self, from_id).await, + ProtectionStatus::Protected => protection_disabled(self, from_id).await, + } + .to_string() + } + + pub(crate) async fn update_device_chats(&self) -> Result<(), Error> { + if self.get_config_bool(Config::Bot).await { + return Ok(()); + } + + // create saved-messages chat; we do this only once, if the user has deleted the chat, + // he can recreate it manually (make sure we do not re-add it when configure() was called a second time) + if !self.sql.get_raw_config_bool(&self, "self-chat-added").await { + self.sql + .set_raw_config_bool(&self, "self-chat-added", true) + .await?; + chat::create_by_contact_id(&self, DC_CONTACT_ID_SELF).await?; + } + + // add welcome-messages. by the label, this is done only once, + // if the user has deleted the message or the chat, it is not added again. + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(device_messages_hint(self).await); + chat::add_device_msg(&self, Some("core-about-device-chat"), Some(&mut msg)).await?; + + let image = include_bytes!("../assets/welcome-image.jpg"); + let blob = BlobObject::create(&self, "welcome-image.jpg".to_string(), image).await?; + let mut msg = Message::new(Viewtype::Image); + msg.param.set(Param::File, blob.as_name()); + chat::add_device_msg(&self, Some("core-welcome-image"), Some(&mut msg)).await?; + + let mut msg = Message::new(Viewtype::Text); + msg.text = Some(welcome_message(self).await); + chat::add_device_msg(&self, Some("core-welcome"), Some(&mut msg)).await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestContext; + + use crate::constants::DC_CONTACT_ID_SELF; + + use crate::chat::Chat; + use crate::chatlist::Chatlist; + use num_traits::ToPrimitive; + + #[test] + fn test_enum_mapping() { + assert_eq!(StockMessage::NoMessages.to_usize().unwrap(), 1); + assert_eq!(StockMessage::SelfMsg.to_usize().unwrap(), 2); + } + + #[test] + fn test_fallback() { + assert_eq!(StockMessage::NoMessages.fallback(), "No messages."); + } + + #[async_std::test] + async fn test_set_stock_translation() { + let t = TestContext::new().await; + t.set_stock_translation(StockMessage::NoMessages, "xyz".to_string()) + .await + .unwrap(); + assert_eq!(no_messages(&t).await, "xyz") + } + + #[async_std::test] + async fn test_set_stock_translation_wrong_replacements() { + let t = TestContext::new().await; + assert!(t + .ctx + .set_stock_translation(StockMessage::NoMessages, "xyz %1$s ".to_string()) + .await + .is_err()); + assert!(t + .ctx + .set_stock_translation(StockMessage::NoMessages, "xyz %2$s ".to_string()) + .await + .is_err()); + } + + #[async_std::test] + async fn test_stock_str() { + let t = TestContext::new().await; + assert_eq!(no_messages(&t).await, "No messages."); + } + + #[async_std::test] + async fn test_stock_string_repl_str() { + let t = TestContext::new().await; + // uses %1$s substitution + assert_eq!(contact_verified(&t, "Foo").await, "Foo verified."); + // We have no string using %1$d to test... + } + + #[async_std::test] + async fn test_stock_string_repl_str2() { + let t = TestContext::new().await; + assert_eq!( + server_response(&t, "foo", "bar").await, + "Could not connect to foo: bar" + ); + } + + #[async_std::test] + async fn test_stock_system_msg_simple() { + let t = TestContext::new().await; + assert_eq!( + msg_location_enabled(&t).await, + "Location streaming enabled." + ) + } + + #[async_std::test] + async fn test_stock_system_msg_add_member_by_me() { + let t = TestContext::new().await; + assert_eq!( + msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, + "Member alice@example.com added by me." + ) + } + + #[async_std::test] + async fn test_stock_system_msg_add_member_by_me_with_displayname() { + let t = TestContext::new().await; + Contact::create(&t, "Alice", "alice@example.com") + .await + .expect("failed to create contact"); + assert_eq!( + msg_add_member(&t, "alice@example.com", DC_CONTACT_ID_SELF).await, + "Member Alice (alice@example.com) added by me." + ); + } + + #[async_std::test] + async fn test_stock_system_msg_add_member_by_other_with_displayname() { + let t = TestContext::new().await; + let contact_id = { + Contact::create(&t, "Alice", "alice@example.com") + .await + .expect("Failed to create contact Alice"); + Contact::create(&t, "Bob", "bob@example.com") + .await + .expect("failed to create bob") + }; + assert_eq!( + msg_add_member(&t, "alice@example.com", contact_id,).await, + "Member Alice (alice@example.com) added by Bob (bob@example.com)." + ); + } + + #[async_std::test] + async fn test_update_device_chats() { + let t = TestContext::new().await; + t.update_device_chats().await.ok(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 2); + + let chat0 = Chat::load_from_db(&t, chats.get_chat_id(0)).await.unwrap(); + let (self_talk_id, device_chat_id) = if chat0.is_self_talk() { + (chats.get_chat_id(0), chats.get_chat_id(1)) + } else { + (chats.get_chat_id(1), chats.get_chat_id(0)) + }; + + // delete self-talk first; this adds a message to device-chat about how self-talk can be restored + let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(); + self_talk_id.delete(&t).await.ok(); + assert_eq!( + chat::get_chat_msgs(&t, device_chat_id, 0, None).await.len(), + device_chat_msgs_before + 1 + ); + + // delete device chat + device_chat_id.delete(&t).await.ok(); + + // check, that the chatlist is empty + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + + // a subsequent call to update_device_chats() must not re-add manally deleted messages or chats + t.update_device_chats().await.ok(); + let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap(); + assert_eq!(chats.len(), 0); + } +}