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