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