deltachat/
stock_str.rs

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