deltachat/
stock_str.rs

1//! Module to work with translatable stock strings.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::{Result, bail};
7use humansize::{BINARY, format_size};
8use strum::EnumProperty as EnumPropertyTrait;
9use strum_macros::EnumProperty;
10use tokio::sync::RwLock;
11
12use crate::accounts::Accounts;
13use crate::blob::BlobObject;
14use crate::chat::{self, Chat, ChatId, ProtectionStatus};
15use crate::config::Config;
16use crate::contact::{Contact, ContactId, Origin};
17use crate::context::Context;
18use crate::message::{Message, Viewtype};
19use crate::param::Param;
20use crate::tools::timestamp_to_str;
21
22/// Storage for string translations.
23#[derive(Debug, Clone)]
24pub struct StockStrings {
25    /// Map from stock string ID to the translation.
26    translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
27}
28
29/// Stock strings
30///
31/// These identify the string to return in [Context.stock_str].  The
32/// numbers must stay in sync with `deltachat.h` `DC_STR_*` constants.
33///
34/// See the `stock_*` methods on [Context] to use these.
35///
36/// [Context]: crate::context::Context
37#[allow(missing_docs)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
39#[repr(u32)]
40pub enum StockMessage {
41    #[strum(props(fallback = "No messages."))]
42    NoMessages = 1,
43
44    #[strum(props(fallback = "Me"))]
45    SelfMsg = 2,
46
47    #[strum(props(fallback = "Draft"))]
48    Draft = 3,
49
50    #[strum(props(fallback = "Voice message"))]
51    VoiceMessage = 7,
52
53    #[strum(props(fallback = "Image"))]
54    Image = 9,
55
56    #[strum(props(fallback = "Video"))]
57    Video = 10,
58
59    #[strum(props(fallback = "Audio"))]
60    Audio = 11,
61
62    #[strum(props(fallback = "File"))]
63    File = 12,
64
65    #[strum(props(fallback = "GIF"))]
66    Gif = 23,
67
68    #[strum(props(fallback = "End-to-end encryption available"))]
69    E2eAvailable = 25,
70
71    #[strum(props(fallback = "No encryption"))]
72    EncrNone = 28,
73
74    #[strum(props(fallback = "This message was encrypted for another setup."))]
75    CantDecryptMsgBody = 29,
76
77    #[strum(props(fallback = "Fingerprints"))]
78    FingerPrints = 30,
79
80    #[strum(props(fallback = "%1$s verified."))]
81    ContactVerified = 35,
82
83    #[strum(props(fallback = "Archived chats"))]
84    ArchivedChats = 40,
85
86    #[strum(props(
87        fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
88    ))]
89    CannotLogin = 60,
90
91    #[strum(props(fallback = "Location streaming enabled."))]
92    MsgLocationEnabled = 64,
93
94    #[strum(props(fallback = "Location streaming disabled."))]
95    MsgLocationDisabled = 65,
96
97    #[strum(props(fallback = "Location"))]
98    Location = 66,
99
100    #[strum(props(fallback = "Sticker"))]
101    Sticker = 67,
102
103    #[strum(props(fallback = "Device messages"))]
104    DeviceMessages = 68,
105
106    #[strum(props(fallback = "Saved messages"))]
107    SavedMessages = 69,
108
109    #[strum(props(
110        fallback = "Messages in this chat are generated locally by your Delta Chat app. \
111                    Its makers use it to inform about app updates and problems during usage."
112    ))]
113    DeviceMessagesHint = 70,
114
115    #[strum(props(fallback = "Welcome to Delta Chat! – \
116                    Delta Chat looks and feels like other popular messenger apps, \
117                    but does not involve centralized control, \
118                    tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
119                    Technically, Delta Chat is an email application with a modern chat interface. \
120                    Email in a new dress if you will 👻\n\n\
121                    Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
122                    Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
123                    however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
124    WelcomeMessage = 71,
125
126    #[strum(props(fallback = "Message from %1$s"))]
127    SubjectForNewContact = 73,
128
129    /// Unused. Was used in group chat status messages.
130    #[strum(props(fallback = "Failed to send message to %1$s."))]
131    FailedSendingTo = 74,
132
133    #[strum(props(fallback = "Error:\n\n“%1$s”"))]
134    ConfigurationFailed = 84,
135
136    #[strum(props(
137        fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
138                    Adjust your clock ⏰🔧 to ensure your messages are received correctly."
139    ))]
140    BadTimeMsgBody = 85,
141
142    #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
143                    This may cause problems because your chat partners use newer versions - \
144                    and you are missing the latest features 😳\n\
145                    Please check https://get.delta.chat or your app store for updates."))]
146    UpdateReminderMsgBody = 86,
147
148    #[strum(props(
149        fallback = "Could not find your mail server.\n\nPlease check your internet connection."
150    ))]
151    ErrorNoNetwork = 87,
152
153    // used in summaries, a noun, not a verb (not: "to reply")
154    #[strum(props(fallback = "Reply"))]
155    ReplyNoun = 90,
156
157    #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
158                    To use the \"Saved messages\" feature again, create a new chat with yourself."))]
159    SelfDeletedMsgBody = 91,
160
161    #[strum(props(
162        fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
163                    ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
164                    Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
165    ))]
166    DeleteServerTurnedOff = 92,
167
168    #[strum(props(fallback = "Forwarded"))]
169    Forwarded = 97,
170
171    #[strum(props(
172        fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
173                    You may not be able to receive message when the storage is 100%% used.\n\n\
174                    👉 Please check if you can delete old data in the provider's webinterface \
175                    and consider to enable \"Settings / Delete Old Messages\". \
176                    You can check your current storage usage anytime at \"Settings / Connectivity\"."
177    ))]
178    QuotaExceedingMsgBody = 98,
179
180    #[strum(props(fallback = "%1$s message"))]
181    PartialDownloadMsgBody = 99,
182
183    #[strum(props(fallback = "Download maximum available until %1$s"))]
184    DownloadAvailability = 100,
185
186    #[strum(props(fallback = "Multi Device Synchronization"))]
187    SyncMsgSubject = 101,
188
189    #[strum(props(
190        fallback = "This message is used to synchronize data between your devices.\n\n\
191                    👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
192    ))]
193    SyncMsgBody = 102,
194
195    #[strum(props(fallback = "Incoming Messages"))]
196    IncomingMessages = 103,
197
198    #[strum(props(fallback = "Outgoing Messages"))]
199    OutgoingMessages = 104,
200
201    #[strum(props(fallback = "Storage on %1$s"))]
202    StorageOnDomain = 105,
203
204    #[strum(props(fallback = "Connected"))]
205    Connected = 107,
206
207    #[strum(props(fallback = "Connecting…"))]
208    Connecting = 108,
209
210    #[strum(props(fallback = "Updating…"))]
211    Updating = 109,
212
213    #[strum(props(fallback = "Sending…"))]
214    Sending = 110,
215
216    #[strum(props(fallback = "Your last message was sent successfully."))]
217    LastMsgSentSuccessfully = 111,
218
219    #[strum(props(fallback = "Error: %1$s"))]
220    Error = 112,
221
222    #[strum(props(fallback = "Not supported by your provider."))]
223    NotSupportedByProvider = 113,
224
225    #[strum(props(fallback = "Messages"))]
226    Messages = 114,
227
228    #[strum(props(fallback = "%1$s of %2$s used"))]
229    PartOfTotallUsed = 116,
230
231    #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
232                             Waiting for the device of %2$s to reply…"))]
233    SecureJoinStarted = 117,
234
235    #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
236    SecureJoinReplies = 118,
237
238    #[strum(props(fallback = "Scan to chat with %1$s"))]
239    SetupContactQRDescription = 119,
240
241    #[strum(props(fallback = "Scan to join group %1$s"))]
242    SecureJoinGroupQRDescription = 120,
243
244    #[strum(props(fallback = "Not connected"))]
245    NotConnected = 121,
246
247    #[strum(props(
248        fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
249    ))]
250    AeapExplanationAndLink = 123,
251
252    #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
253    MsgYouChangedGrpName = 124,
254
255    #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
256    MsgGrpNameChangedBy = 125,
257
258    #[strum(props(fallback = "You changed the group image."))]
259    MsgYouChangedGrpImg = 126,
260
261    #[strum(props(fallback = "Group image changed by %1$s."))]
262    MsgGrpImgChangedBy = 127,
263
264    #[strum(props(fallback = "You added member %1$s."))]
265    MsgYouAddMember = 128,
266
267    #[strum(props(fallback = "Member %1$s added by %2$s."))]
268    MsgAddMemberBy = 129,
269
270    #[strum(props(fallback = "You removed member %1$s."))]
271    MsgYouDelMember = 130,
272
273    #[strum(props(fallback = "Member %1$s removed by %2$s."))]
274    MsgDelMemberBy = 131,
275
276    #[strum(props(fallback = "You left the group."))]
277    MsgYouLeftGroup = 132,
278
279    #[strum(props(fallback = "Group left by %1$s."))]
280    MsgGroupLeftBy = 133,
281
282    #[strum(props(fallback = "You deleted the group image."))]
283    MsgYouDeletedGrpImg = 134,
284
285    #[strum(props(fallback = "Group image deleted by %1$s."))]
286    MsgGrpImgDeletedBy = 135,
287
288    #[strum(props(fallback = "You enabled location streaming."))]
289    MsgYouEnabledLocation = 136,
290
291    #[strum(props(fallback = "Location streaming enabled by %1$s."))]
292    MsgLocationEnabledBy = 137,
293
294    #[strum(props(fallback = "You disabled message deletion timer."))]
295    MsgYouDisabledEphemeralTimer = 138,
296
297    #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
298    MsgEphemeralTimerDisabledBy = 139,
299
300    // A fallback message for unknown timer values.
301    // "s" stands for "second" SI unit here.
302    #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
303    MsgYouEnabledEphemeralTimer = 140,
304
305    #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
306    MsgEphemeralTimerEnabledBy = 141,
307
308    #[strum(props(fallback = "You set message deletion timer to 1 minute."))]
309    MsgYouEphemeralTimerMinute = 142,
310
311    #[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
312    MsgEphemeralTimerMinuteBy = 143,
313
314    #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
315    MsgYouEphemeralTimerHour = 144,
316
317    #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
318    MsgEphemeralTimerHourBy = 145,
319
320    #[strum(props(fallback = "You set message deletion timer to 1 day."))]
321    MsgYouEphemeralTimerDay = 146,
322
323    #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
324    MsgEphemeralTimerDayBy = 147,
325
326    #[strum(props(fallback = "You set message deletion timer to 1 week."))]
327    MsgYouEphemeralTimerWeek = 148,
328
329    #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
330    MsgEphemeralTimerWeekBy = 149,
331
332    #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
333    MsgYouEphemeralTimerMinutes = 150,
334
335    #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
336    MsgEphemeralTimerMinutesBy = 151,
337
338    #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
339    MsgYouEphemeralTimerHours = 152,
340
341    #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
342    MsgEphemeralTimerHoursBy = 153,
343
344    #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
345    MsgYouEphemeralTimerDays = 154,
346
347    #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
348    MsgEphemeralTimerDaysBy = 155,
349
350    #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
351    MsgYouEphemeralTimerWeeks = 156,
352
353    #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
354    MsgEphemeralTimerWeeksBy = 157,
355
356    #[strum(props(fallback = "You set message deletion timer to 1 year."))]
357    MsgYouEphemeralTimerYear = 158,
358
359    #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
360    MsgEphemeralTimerYearBy = 159,
361
362    #[strum(props(fallback = "Scan to set up second device for %1$s"))]
363    BackupTransferQr = 162,
364
365    #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
366    BackupTransferMsgBody = 163,
367
368    #[strum(props(fallback = "I added member %1$s."))]
369    MsgIAddMember = 164,
370
371    #[strum(props(fallback = "I removed member %1$s."))]
372    MsgIDelMember = 165,
373
374    #[strum(props(fallback = "I left the group."))]
375    MsgILeftGroup = 166,
376
377    #[strum(props(fallback = "Messages are end-to-end encrypted."))]
378    ChatProtectionEnabled = 170,
379
380    // deprecated 2025-07
381    #[strum(props(fallback = "%1$s sent a message from another device."))]
382    ChatProtectionDisabled = 171,
383
384    #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
385    NewGroupSendFirstMessage = 172,
386
387    #[strum(props(fallback = "Member %1$s added."))]
388    MsgAddMember = 173,
389
390    #[strum(props(
391        fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
392    ))]
393    InvalidUnencryptedMail = 174,
394
395    #[strum(props(
396        fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
397    ))]
398    CantDecryptOutgoingMsgs = 175,
399
400    #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
401    MsgYouReacted = 176,
402
403    #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
404    MsgReactedBy = 177,
405
406    #[strum(props(fallback = "Member %1$s removed."))]
407    MsgDelMember = 178,
408
409    #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
410    SecurejoinWait = 190,
411
412    #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
413
414Please consider donating to help that Delta Chat stays free for everyone.
415
416While Delta Chat is free to use and open source, development costs money.
417Help keeping us to keep Delta Chat independent and make it more awesome in the future.
418
419https://delta.chat/donate"))]
420    DonationRequest = 193,
421
422    #[strum(props(fallback = "Outgoing call"))]
423    OutgoingCall = 194,
424
425    #[strum(props(fallback = "Incoming call"))]
426    IncomingCall = 195,
427
428    #[strum(props(fallback = "Declined call"))]
429    DeclinedCall = 196,
430
431    #[strum(props(fallback = "Canceled call"))]
432    CanceledCall = 197,
433
434    #[strum(props(fallback = "Missed call"))]
435    MissedCall = 198,
436
437    #[strum(props(fallback = "You left the channel."))]
438    MsgYouLeftBroadcast = 200,
439
440    #[strum(props(fallback = "Scan to join channel %1$s"))]
441    SecureJoinBrodcastQRDescription = 201,
442}
443
444impl StockMessage {
445    /// Default untranslated strings for stock messages.
446    ///
447    /// These could be used in logging calls, so no logging here.
448    fn fallback(self) -> &'static str {
449        self.get_str("fallback").unwrap_or_default()
450    }
451}
452
453impl Default for StockStrings {
454    fn default() -> Self {
455        StockStrings::new()
456    }
457}
458
459impl StockStrings {
460    /// Creates a new translated string storage.
461    pub fn new() -> Self {
462        Self {
463            translated_stockstrings: Arc::new(RwLock::new(Default::default())),
464        }
465    }
466
467    async fn translated(&self, id: StockMessage) -> String {
468        self.translated_stockstrings
469            .read()
470            .await
471            .get(&(id as usize))
472            .map(AsRef::as_ref)
473            .unwrap_or_else(|| id.fallback())
474            .to_string()
475    }
476
477    async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
478        if stockstring.contains("%1") && !id.fallback().contains("%1") {
479            bail!(
480                "translation {} contains invalid %1 placeholder, default is {}",
481                stockstring,
482                id.fallback()
483            );
484        }
485        if stockstring.contains("%2") && !id.fallback().contains("%2") {
486            bail!(
487                "translation {} contains invalid %2 placeholder, default is {}",
488                stockstring,
489                id.fallback()
490            );
491        }
492        self.translated_stockstrings
493            .write()
494            .await
495            .insert(id as usize, stockstring);
496        Ok(())
497    }
498}
499
500async fn translated(context: &Context, id: StockMessage) -> String {
501    context.translated_stockstrings.translated(id).await
502}
503
504/// Helper trait only meant to be implemented for [`String`].
505trait StockStringMods: AsRef<str> + Sized {
506    /// Substitutes the first replacement value if one is present.
507    fn replace1(&self, replacement: &str) -> String {
508        self.as_ref()
509            .replacen("%1$s", replacement, 1)
510            .replacen("%1$d", replacement, 1)
511            .replacen("%1$@", replacement, 1)
512    }
513
514    /// Substitutes the second replacement value if one is present.
515    ///
516    /// Be aware you probably should have also called [`StockStringMods::replace1`] if
517    /// you are calling this.
518    fn replace2(&self, replacement: &str) -> String {
519        self.as_ref()
520            .replacen("%2$s", replacement, 1)
521            .replacen("%2$d", replacement, 1)
522            .replacen("%2$@", replacement, 1)
523    }
524
525    /// Substitutes the third replacement value if one is present.
526    ///
527    /// Be aware you probably should have also called [`StockStringMods::replace1`] and
528    /// [`StockStringMods::replace2`] if you are calling this.
529    fn replace3(&self, replacement: &str) -> String {
530        self.as_ref()
531            .replacen("%3$s", replacement, 1)
532            .replacen("%3$d", replacement, 1)
533            .replacen("%3$@", replacement, 1)
534    }
535}
536
537impl ContactId {
538    /// Get contact name, e.g. `Bob`, or `bob@example.net` if no name is set.
539    async fn get_stock_name(self, context: &Context) -> String {
540        Contact::get_by_id(context, self)
541            .await
542            .map(|contact| contact.get_display_name().to_string())
543            .unwrap_or_else(|_| self.to_string())
544    }
545}
546
547impl StockStringMods for String {}
548
549/// Stock string: `No messages.`.
550pub(crate) async fn no_messages(context: &Context) -> String {
551    translated(context, StockMessage::NoMessages).await
552}
553
554/// Stock string: `Me`.
555pub(crate) async fn self_msg(context: &Context) -> String {
556    translated(context, StockMessage::SelfMsg).await
557}
558
559/// Stock string: `Draft`.
560pub(crate) async fn draft(context: &Context) -> String {
561    translated(context, StockMessage::Draft).await
562}
563
564/// Stock string: `Voice message`.
565pub(crate) async fn voice_message(context: &Context) -> String {
566    translated(context, StockMessage::VoiceMessage).await
567}
568
569/// Stock string: `Image`.
570pub(crate) async fn image(context: &Context) -> String {
571    translated(context, StockMessage::Image).await
572}
573
574/// Stock string: `Video`.
575pub(crate) async fn video(context: &Context) -> String {
576    translated(context, StockMessage::Video).await
577}
578
579/// Stock string: `Audio`.
580pub(crate) async fn audio(context: &Context) -> String {
581    translated(context, StockMessage::Audio).await
582}
583
584/// Stock string: `File`.
585pub(crate) async fn file(context: &Context) -> String {
586    translated(context, StockMessage::File).await
587}
588
589/// Stock string: `Group name changed from "%1$s" to "%2$s".`.
590pub(crate) async fn msg_grp_name(
591    context: &Context,
592    from_group: &str,
593    to_group: &str,
594    by_contact: ContactId,
595) -> String {
596    if by_contact == ContactId::SELF {
597        translated(context, StockMessage::MsgYouChangedGrpName)
598            .await
599            .replace1(from_group)
600            .replace2(to_group)
601    } else {
602        translated(context, StockMessage::MsgGrpNameChangedBy)
603            .await
604            .replace1(from_group)
605            .replace2(to_group)
606            .replace3(&by_contact.get_stock_name(context).await)
607    }
608}
609
610pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
611    if by_contact == ContactId::SELF {
612        translated(context, StockMessage::MsgYouChangedGrpImg).await
613    } else {
614        translated(context, StockMessage::MsgGrpImgChangedBy)
615            .await
616            .replace1(&by_contact.get_stock_name(context).await)
617    }
618}
619
620/// Stock string: `I added member %1$s.`.
621/// This one is for sending in group chats.
622///
623/// The `added_member_addr` parameter should be an email address and is looked up in the
624/// contacts to combine with the authorized display name.
625pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
626    let addr = added_member_addr;
627    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
628        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
629            .await
630            .map(|contact| contact.get_authname_or_addr())
631            .unwrap_or_else(|_| addr.to_string()),
632        _ => addr.to_string(),
633    };
634    translated(context, StockMessage::MsgIAddMember)
635        .await
636        .replace1(whom)
637}
638
639/// Stock string: `You added member %1$s.` or `Member %1$s added by %2$s.`.
640///
641/// The `added_member_addr` parameter should be an email address and is looked up in the
642/// contacts to combine with the display name.
643pub(crate) async fn msg_add_member_local(
644    context: &Context,
645    added_member: ContactId,
646    by_contact: ContactId,
647) -> String {
648    let whom = added_member.get_stock_name(context).await;
649    if by_contact == ContactId::UNDEFINED {
650        translated(context, StockMessage::MsgAddMember)
651            .await
652            .replace1(&whom)
653    } else if by_contact == ContactId::SELF {
654        translated(context, StockMessage::MsgYouAddMember)
655            .await
656            .replace1(&whom)
657    } else {
658        translated(context, StockMessage::MsgAddMemberBy)
659            .await
660            .replace1(&whom)
661            .replace2(&by_contact.get_stock_name(context).await)
662    }
663}
664
665/// Stock string: `I removed member %1$s.`.
666///
667/// The `removed_member_addr` parameter should be an email address and is looked up in
668/// the contacts to combine with the display name.
669pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
670    let addr = removed_member_addr;
671    let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
672        Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
673            .await
674            .map(|contact| contact.get_authname_or_addr())
675            .unwrap_or_else(|_| addr.to_string()),
676        _ => addr.to_string(),
677    };
678    translated(context, StockMessage::MsgIDelMember)
679        .await
680        .replace1(whom)
681}
682
683/// Stock string: `I added member %1$s.` or `Member %1$s removed by %2$s.`.
684///
685/// The `removed_member_addr` parameter should be an email address and is looked up in
686/// the contacts to combine with the display name.
687pub(crate) async fn msg_del_member_local(
688    context: &Context,
689    removed_member: ContactId,
690    by_contact: ContactId,
691) -> String {
692    let whom = removed_member.get_stock_name(context).await;
693    if by_contact == ContactId::UNDEFINED {
694        translated(context, StockMessage::MsgDelMember)
695            .await
696            .replace1(&whom)
697    } else if by_contact == ContactId::SELF {
698        translated(context, StockMessage::MsgYouDelMember)
699            .await
700            .replace1(&whom)
701    } else {
702        translated(context, StockMessage::MsgDelMemberBy)
703            .await
704            .replace1(&whom)
705            .replace2(&by_contact.get_stock_name(context).await)
706    }
707}
708
709/// Stock string: `I left the group.`.
710pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
711    translated(context, StockMessage::MsgILeftGroup).await
712}
713
714/// Stock string: `You left the group.` or `Group left by %1$s.`.
715pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
716    if by_contact == ContactId::SELF {
717        translated(context, StockMessage::MsgYouLeftGroup).await
718    } else {
719        translated(context, StockMessage::MsgGroupLeftBy)
720            .await
721            .replace1(&by_contact.get_stock_name(context).await)
722    }
723}
724
725/// Stock string: `You left the channel.`
726pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
727    translated(context, StockMessage::MsgYouLeftBroadcast).await
728}
729
730/// Stock string: `You reacted %1$s to "%2$s"` or `%1$s reacted %2$s to "%3$s"`.
731pub(crate) async fn msg_reacted(
732    context: &Context,
733    by_contact: ContactId,
734    reaction: &str,
735    summary: &str,
736) -> String {
737    if by_contact == ContactId::SELF {
738        translated(context, StockMessage::MsgYouReacted)
739            .await
740            .replace1(reaction)
741            .replace2(summary)
742    } else {
743        translated(context, StockMessage::MsgReactedBy)
744            .await
745            .replace1(&by_contact.get_stock_name(context).await)
746            .replace2(reaction)
747            .replace3(summary)
748    }
749}
750
751/// Stock string: `GIF`.
752pub(crate) async fn gif(context: &Context) -> String {
753    translated(context, StockMessage::Gif).await
754}
755
756/// Stock string: `End-to-end encryption available.`.
757pub(crate) async fn e2e_available(context: &Context) -> String {
758    translated(context, StockMessage::E2eAvailable).await
759}
760
761/// Stock string: `No encryption.`.
762pub(crate) async fn encr_none(context: &Context) -> String {
763    translated(context, StockMessage::EncrNone).await
764}
765
766/// Stock string: `This message was encrypted for another setup.`.
767pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
768    translated(context, StockMessage::CantDecryptMsgBody).await
769}
770
771/// Stock string:`Got outgoing message(s) encrypted for another setup...`.
772pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
773    translated(context, StockMessage::CantDecryptOutgoingMsgs).await
774}
775
776/// Stock string: `Fingerprints`.
777pub(crate) async fn finger_prints(context: &Context) -> String {
778    translated(context, StockMessage::FingerPrints).await
779}
780
781/// Stock string: `Group image deleted.`.
782pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
783    if by_contact == ContactId::SELF {
784        translated(context, StockMessage::MsgYouDeletedGrpImg).await
785    } else {
786        translated(context, StockMessage::MsgGrpImgDeletedBy)
787            .await
788            .replace1(&by_contact.get_stock_name(context).await)
789    }
790}
791
792/// Stock string: `%1$s invited you to join this group. Waiting for the device of %2$s to reply…`.
793pub(crate) async fn secure_join_started(
794    context: &Context,
795    inviter_contact_id: ContactId,
796) -> String {
797    if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
798        translated(context, StockMessage::SecureJoinStarted)
799            .await
800            .replace1(contact.get_display_name())
801            .replace2(contact.get_display_name())
802    } else {
803        format!("secure_join_started: unknown contact {inviter_contact_id}")
804    }
805}
806
807/// Stock string: `%1$s replied, waiting for being added to the group…`.
808pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
809    translated(context, StockMessage::SecureJoinReplies)
810        .await
811        .replace1(&contact_id.get_stock_name(context).await)
812}
813
814/// Stock string: `Establishing guaranteed end-to-end encryption, please wait…`.
815pub(crate) async fn securejoin_wait(context: &Context) -> String {
816    translated(context, StockMessage::SecurejoinWait).await
817}
818
819/// Stock string: `❤️ Seems you're enjoying Delta Chat!`…
820pub(crate) async fn donation_request(context: &Context) -> String {
821    translated(context, StockMessage::DonationRequest).await
822}
823
824/// Stock string: `Outgoing call`.
825pub(crate) async fn outgoing_call(context: &Context) -> String {
826    translated(context, StockMessage::OutgoingCall).await
827}
828
829/// Stock string: `Incoming call`.
830pub(crate) async fn incoming_call(context: &Context) -> String {
831    translated(context, StockMessage::IncomingCall).await
832}
833
834/// Stock string: `Declined call`.
835pub(crate) async fn declined_call(context: &Context) -> String {
836    translated(context, StockMessage::DeclinedCall).await
837}
838
839/// Stock string: `Canceled call`.
840pub(crate) async fn canceled_call(context: &Context) -> String {
841    translated(context, StockMessage::CanceledCall).await
842}
843
844/// Stock string: `Missed call`.
845pub(crate) async fn missed_call(context: &Context) -> String {
846    translated(context, StockMessage::MissedCall).await
847}
848
849/// Stock string: `Scan to chat with %1$s`.
850pub(crate) async fn setup_contact_qr_description(
851    context: &Context,
852    display_name: &str,
853    addr: &str,
854) -> String {
855    let name = if display_name.is_empty() {
856        addr.to_owned()
857    } else {
858        display_name.to_owned()
859    };
860    translated(context, StockMessage::SetupContactQRDescription)
861        .await
862        .replace1(&name)
863}
864
865/// Stock string: `Scan to join group %1$s`.
866pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
867    translated(context, StockMessage::SecureJoinGroupQRDescription)
868        .await
869        .replace1(chat.get_name())
870}
871
872/// Stock string: `Scan to join channel %1$s`.
873pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
874    translated(context, StockMessage::SecureJoinBrodcastQRDescription)
875        .await
876        .replace1(chat.get_name())
877}
878
879/// Stock string: `%1$s verified.`.
880#[allow(dead_code)]
881pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
882    let addr = contact.get_display_name();
883    translated(context, StockMessage::ContactVerified)
884        .await
885        .replace1(addr)
886}
887
888/// Stock string: `Archived chats`.
889pub(crate) async fn archived_chats(context: &Context) -> String {
890    translated(context, StockMessage::ArchivedChats).await
891}
892
893/// Stock string: `Multi Device Synchronization`.
894pub(crate) async fn sync_msg_subject(context: &Context) -> String {
895    translated(context, StockMessage::SyncMsgSubject).await
896}
897
898/// Stock string: `This message is used to synchronize data between your devices.`.
899pub(crate) async fn sync_msg_body(context: &Context) -> String {
900    translated(context, StockMessage::SyncMsgBody).await
901}
902
903/// Stock string: `Cannot login as \"%1$s\". Please check...`.
904pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
905    translated(context, StockMessage::CannotLogin)
906        .await
907        .replace1(user)
908}
909
910/// Stock string: `Location streaming enabled.`.
911pub(crate) async fn msg_location_enabled(context: &Context) -> String {
912    translated(context, StockMessage::MsgLocationEnabled).await
913}
914
915/// Stock string: `Location streaming enabled by ...`.
916pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
917    if contact == ContactId::SELF {
918        translated(context, StockMessage::MsgYouEnabledLocation).await
919    } else {
920        translated(context, StockMessage::MsgLocationEnabledBy)
921            .await
922            .replace1(&contact.get_stock_name(context).await)
923    }
924}
925
926/// Stock string: `Location streaming disabled.`.
927pub(crate) async fn msg_location_disabled(context: &Context) -> String {
928    translated(context, StockMessage::MsgLocationDisabled).await
929}
930
931/// Stock string: `Location`.
932pub(crate) async fn location(context: &Context) -> String {
933    translated(context, StockMessage::Location).await
934}
935
936/// Stock string: `Sticker`.
937pub(crate) async fn sticker(context: &Context) -> String {
938    translated(context, StockMessage::Sticker).await
939}
940
941/// Stock string: `Device messages`.
942pub(crate) async fn device_messages(context: &Context) -> String {
943    translated(context, StockMessage::DeviceMessages).await
944}
945
946/// Stock string: `Saved messages`.
947pub(crate) async fn saved_messages(context: &Context) -> String {
948    translated(context, StockMessage::SavedMessages).await
949}
950
951/// Stock string: `Messages in this chat are generated locally by...`.
952pub(crate) async fn device_messages_hint(context: &Context) -> String {
953    translated(context, StockMessage::DeviceMessagesHint).await
954}
955
956/// Stock string: `Welcome to Delta Chat! – ...`.
957pub(crate) async fn welcome_message(context: &Context) -> String {
958    translated(context, StockMessage::WelcomeMessage).await
959}
960
961/// Stock string: `Message from %1$s`.
962// TODO: This can compute `self_name` itself instead of asking the caller to do this.
963pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
964    translated(context, StockMessage::SubjectForNewContact)
965        .await
966        .replace1(self_name)
967}
968
969/// Stock string: `Message deletion timer is disabled.`.
970pub(crate) async fn msg_ephemeral_timer_disabled(
971    context: &Context,
972    by_contact: ContactId,
973) -> String {
974    if by_contact == ContactId::SELF {
975        translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
976    } else {
977        translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
978            .await
979            .replace1(&by_contact.get_stock_name(context).await)
980    }
981}
982
983/// Stock string: `Message deletion timer is set to %1$s s.`.
984pub(crate) async fn msg_ephemeral_timer_enabled(
985    context: &Context,
986    timer: &str,
987    by_contact: ContactId,
988) -> String {
989    if by_contact == ContactId::SELF {
990        translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
991            .await
992            .replace1(timer)
993    } else {
994        translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
995            .await
996            .replace1(timer)
997            .replace2(&by_contact.get_stock_name(context).await)
998    }
999}
1000
1001/// Stock string: `Message deletion timer is set to 1 minute.`.
1002pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
1003    if by_contact == ContactId::SELF {
1004        translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
1005    } else {
1006        translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
1007            .await
1008            .replace1(&by_contact.get_stock_name(context).await)
1009    }
1010}
1011
1012/// Stock string: `Message deletion timer is set to 1 hour.`.
1013pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1014    if by_contact == ContactId::SELF {
1015        translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1016    } else {
1017        translated(context, StockMessage::MsgEphemeralTimerHourBy)
1018            .await
1019            .replace1(&by_contact.get_stock_name(context).await)
1020    }
1021}
1022
1023/// Stock string: `Message deletion timer is set to 1 day.`.
1024pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1025    if by_contact == ContactId::SELF {
1026        translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1027    } else {
1028        translated(context, StockMessage::MsgEphemeralTimerDayBy)
1029            .await
1030            .replace1(&by_contact.get_stock_name(context).await)
1031    }
1032}
1033
1034/// Stock string: `Message deletion timer is set to 1 week.`.
1035pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1036    if by_contact == ContactId::SELF {
1037        translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1038    } else {
1039        translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1040            .await
1041            .replace1(&by_contact.get_stock_name(context).await)
1042    }
1043}
1044
1045/// Stock string: `Message deletion timer is set to 1 year.`.
1046pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1047    if by_contact == ContactId::SELF {
1048        translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1049    } else {
1050        translated(context, StockMessage::MsgEphemeralTimerYearBy)
1051            .await
1052            .replace1(&by_contact.get_stock_name(context).await)
1053    }
1054}
1055
1056/// Stock string: `Error:\n\n“%1$s”`.
1057pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1058    translated(context, StockMessage::ConfigurationFailed)
1059        .await
1060        .replace1(details)
1061}
1062
1063/// Stock string: `⚠️ Date or time of your device seem to be inaccurate (%1$s)...`.
1064// TODO: This could compute now itself.
1065pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1066    translated(context, StockMessage::BadTimeMsgBody)
1067        .await
1068        .replace1(now)
1069}
1070
1071/// Stock string: `⚠️ Your Delta Chat version might be outdated...`.
1072pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1073    translated(context, StockMessage::UpdateReminderMsgBody).await
1074}
1075
1076/// Stock string: `Could not find your mail server...`.
1077pub(crate) async fn error_no_network(context: &Context) -> String {
1078    translated(context, StockMessage::ErrorNoNetwork).await
1079}
1080
1081/// Stock string: `Messages are end-to-end encrypted.`
1082pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1083    translated(context, StockMessage::ChatProtectionEnabled).await
1084}
1085
1086/// Stock string: `%1$s sent a message from another device.`
1087pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
1088    translated(context, StockMessage::ChatProtectionDisabled)
1089        .await
1090        .replace1(&contact_id.get_stock_name(context).await)
1091}
1092
1093/// Stock string: `Reply`.
1094pub(crate) async fn reply_noun(context: &Context) -> String {
1095    translated(context, StockMessage::ReplyNoun).await
1096}
1097
1098/// Stock string: `You deleted the \"Saved messages\" chat...`.
1099pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1100    translated(context, StockMessage::SelfDeletedMsgBody).await
1101}
1102
1103/// Stock string: `⚠️ The "Delete messages from server" feature now also...`.
1104pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1105    translated(context, StockMessage::DeleteServerTurnedOff).await
1106}
1107
1108/// Stock string: `Message deletion timer is set to %1$s minutes.`.
1109pub(crate) async fn msg_ephemeral_timer_minutes(
1110    context: &Context,
1111    minutes: &str,
1112    by_contact: ContactId,
1113) -> String {
1114    if by_contact == ContactId::SELF {
1115        translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1116            .await
1117            .replace1(minutes)
1118    } else {
1119        translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1120            .await
1121            .replace1(minutes)
1122            .replace2(&by_contact.get_stock_name(context).await)
1123    }
1124}
1125
1126/// Stock string: `Message deletion timer is set to %1$s hours.`.
1127pub(crate) async fn msg_ephemeral_timer_hours(
1128    context: &Context,
1129    hours: &str,
1130    by_contact: ContactId,
1131) -> String {
1132    if by_contact == ContactId::SELF {
1133        translated(context, StockMessage::MsgYouEphemeralTimerHours)
1134            .await
1135            .replace1(hours)
1136    } else {
1137        translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1138            .await
1139            .replace1(hours)
1140            .replace2(&by_contact.get_stock_name(context).await)
1141    }
1142}
1143
1144/// Stock string: `Message deletion timer is set to %1$s days.`.
1145pub(crate) async fn msg_ephemeral_timer_days(
1146    context: &Context,
1147    days: &str,
1148    by_contact: ContactId,
1149) -> String {
1150    if by_contact == ContactId::SELF {
1151        translated(context, StockMessage::MsgYouEphemeralTimerDays)
1152            .await
1153            .replace1(days)
1154    } else {
1155        translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1156            .await
1157            .replace1(days)
1158            .replace2(&by_contact.get_stock_name(context).await)
1159    }
1160}
1161
1162/// Stock string: `Message deletion timer is set to %1$s weeks.`.
1163pub(crate) async fn msg_ephemeral_timer_weeks(
1164    context: &Context,
1165    weeks: &str,
1166    by_contact: ContactId,
1167) -> String {
1168    if by_contact == ContactId::SELF {
1169        translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1170            .await
1171            .replace1(weeks)
1172    } else {
1173        translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1174            .await
1175            .replace1(weeks)
1176            .replace2(&by_contact.get_stock_name(context).await)
1177    }
1178}
1179
1180/// Stock string: `Forwarded`.
1181pub(crate) async fn forwarded(context: &Context) -> String {
1182    translated(context, StockMessage::Forwarded).await
1183}
1184
1185/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
1186pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1187    translated(context, StockMessage::QuotaExceedingMsgBody)
1188        .await
1189        .replace1(&format!("{highest_usage}"))
1190        .replace("%%", "%")
1191}
1192
1193/// Stock string: `%1$s message` with placeholder replaced by human-readable size.
1194pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1195    let size = &format_size(org_bytes, BINARY);
1196    translated(context, StockMessage::PartialDownloadMsgBody)
1197        .await
1198        .replace1(size)
1199}
1200
1201/// Stock string: `Download maximum available until %1$s`.
1202pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1203    translated(context, StockMessage::DownloadAvailability)
1204        .await
1205        .replace1(&timestamp_to_str(timestamp))
1206}
1207
1208/// Stock string: `Incoming Messages`.
1209pub(crate) async fn incoming_messages(context: &Context) -> String {
1210    translated(context, StockMessage::IncomingMessages).await
1211}
1212
1213/// Stock string: `Outgoing Messages`.
1214pub(crate) async fn outgoing_messages(context: &Context) -> String {
1215    translated(context, StockMessage::OutgoingMessages).await
1216}
1217
1218/// Stock string: `Storage on %1$s`.
1219/// `%1$s` will be replaced by the domain of the configured email-address.
1220pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1221    translated(context, StockMessage::StorageOnDomain)
1222        .await
1223        .replace1(domain)
1224}
1225
1226/// Stock string: `Not connected`.
1227pub(crate) async fn not_connected(context: &Context) -> String {
1228    translated(context, StockMessage::NotConnected).await
1229}
1230
1231/// Stock string: `Connected`.
1232pub(crate) async fn connected(context: &Context) -> String {
1233    translated(context, StockMessage::Connected).await
1234}
1235
1236/// Stock string: `Connecting…`.
1237pub(crate) async fn connecting(context: &Context) -> String {
1238    translated(context, StockMessage::Connecting).await
1239}
1240
1241/// Stock string: `Updating…`.
1242pub(crate) async fn updating(context: &Context) -> String {
1243    translated(context, StockMessage::Updating).await
1244}
1245
1246/// Stock string: `Sending…`.
1247pub(crate) async fn sending(context: &Context) -> String {
1248    translated(context, StockMessage::Sending).await
1249}
1250
1251/// Stock string: `Your last message was sent successfully.`.
1252pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1253    translated(context, StockMessage::LastMsgSentSuccessfully).await
1254}
1255
1256/// Stock string: `Error: %1$s…`.
1257/// `%1$s` will be replaced by a possibly more detailed, typically english, error description.
1258pub(crate) async fn error(context: &Context, error: &str) -> String {
1259    translated(context, StockMessage::Error)
1260        .await
1261        .replace1(error)
1262}
1263
1264/// Stock string: `Not supported by your provider.`.
1265pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1266    translated(context, StockMessage::NotSupportedByProvider).await
1267}
1268
1269/// Stock string: `Messages`.
1270/// Used as a subtitle in quota context; can be plural always.
1271pub(crate) async fn messages(context: &Context) -> String {
1272    translated(context, StockMessage::Messages).await
1273}
1274
1275/// Stock string: `%1$s of %2$s used`.
1276pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1277    translated(context, StockMessage::PartOfTotallUsed)
1278        .await
1279        .replace1(part)
1280        .replace2(total)
1281}
1282
1283/// Stock string: `⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet. Tap to learn more.`.
1284pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1285    translated(context, StockMessage::InvalidUnencryptedMail)
1286        .await
1287        .replace1(provider)
1288}
1289
1290pub(crate) async fn aeap_explanation_and_link(
1291    context: &Context,
1292    old_addr: &str,
1293    new_addr: &str,
1294) -> String {
1295    translated(context, StockMessage::AeapExplanationAndLink)
1296        .await
1297        .replace1(old_addr)
1298        .replace2(new_addr)
1299}
1300
1301/// Stock string: `Others will only see this group after you sent a first message.`.
1302pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1303    translated(context, StockMessage::NewGroupSendFirstMessage).await
1304}
1305
1306/// Text to put in the [`Qr::Backup2`] rendered SVG image.
1307///
1308/// The default is "Scan to set up second device for NAME".
1309/// The account name (or address as fallback) are looked up from the context.
1310///
1311/// [`Qr::Backup2`]: crate::qr::Qr::Backup2
1312pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1313    let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1314        name
1315    } else {
1316        context.get_primary_self_addr().await?
1317    };
1318    Ok(translated(context, StockMessage::BackupTransferQr)
1319        .await
1320        .replace1(&name))
1321}
1322
1323pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1324    translated(context, StockMessage::BackupTransferMsgBody).await
1325}
1326
1327impl Context {
1328    /// Set the stock string for the [StockMessage].
1329    ///
1330    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1331        self.translated_stockstrings
1332            .set_stock_translation(id, stockstring)
1333            .await?;
1334        Ok(())
1335    }
1336
1337    /// Returns a stock message saying that protection status has changed.
1338    pub(crate) async fn stock_protection_msg(
1339        &self,
1340        protect: ProtectionStatus,
1341        contact_id: Option<ContactId>,
1342    ) -> String {
1343        match protect {
1344            ProtectionStatus::Unprotected => {
1345                if let Some(contact_id) = contact_id {
1346                    chat_protection_disabled(self, contact_id).await
1347                } else {
1348                    // In a group chat, it's not possible to downgrade verification.
1349                    // In a 1:1 chat, the `contact_id` always has to be provided.
1350                    "[Error] No contact_id given".to_string()
1351                }
1352            }
1353            ProtectionStatus::Protected => messages_e2e_encrypted(self).await,
1354        }
1355    }
1356
1357    pub(crate) async fn update_device_chats(&self) -> Result<()> {
1358        if self.get_config_bool(Config::Bot).await? {
1359            return Ok(());
1360        }
1361
1362        // create saved-messages chat; we do this only once, if the user has deleted the chat,
1363        // he can recreate it manually (make sure we do not re-add it when configure() was called a second time)
1364        if !self.sql.get_raw_config_bool("self-chat-added").await? {
1365            self.sql
1366                .set_raw_config_bool("self-chat-added", true)
1367                .await?;
1368            ChatId::create_for_contact(self, ContactId::SELF).await?;
1369        }
1370
1371        // add welcome-messages. by the label, this is done only once,
1372        // if the user has deleted the message or the chat, it is not added again.
1373        let image = include_bytes!("../assets/welcome-image.jpg");
1374        let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1375        let mut msg = Message::new(Viewtype::Image);
1376        msg.param.set(Param::File, blob.as_name());
1377        msg.param.set(Param::Filename, "welcome-image.jpg");
1378        chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1379
1380        let mut msg = Message::new_text(welcome_message(self).await);
1381        chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1382        Ok(())
1383    }
1384}
1385
1386impl Accounts {
1387    /// Set the stock string for the [StockMessage].
1388    ///
1389    pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1390        self.stockstrings
1391            .set_stock_translation(id, stockstring)
1392            .await?;
1393        Ok(())
1394    }
1395}
1396
1397#[cfg(test)]
1398mod stock_str_tests;