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