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