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