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