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 = "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 #[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 #[strum(props(fallback = "Reply"))]
167 ReplyNoun = 90,
168
169 #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
170 To use the \"Saved messages\" feature again, create a new chat with yourself."))]
171 SelfDeletedMsgBody = 91,
172
173 #[strum(props(
174 fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
175 ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
176 Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
177 ))]
178 DeleteServerTurnedOff = 92,
179
180 #[strum(props(fallback = "Forwarded"))]
181 Forwarded = 97,
182
183 #[strum(props(
184 fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
185 You may not be able to receive message when the storage is 100%% used.\n\n\
186 👉 Please check if you can delete old data in the provider's webinterface \
187 and consider to enable \"Settings / Delete Old Messages\". \
188 You can check your current storage usage anytime at \"Settings / Connectivity\"."
189 ))]
190 QuotaExceedingMsgBody = 98,
191
192 #[strum(props(fallback = "%1$s message"))]
193 PartialDownloadMsgBody = 99,
194
195 #[strum(props(fallback = "Download maximum available until %1$s"))]
196 DownloadAvailability = 100,
197
198 #[strum(props(fallback = "Multi Device Synchronization"))]
199 SyncMsgSubject = 101,
200
201 #[strum(props(
202 fallback = "This message is used to synchronize data between your devices.\n\n\
203 👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
204 ))]
205 SyncMsgBody = 102,
206
207 #[strum(props(fallback = "Incoming Messages"))]
208 IncomingMessages = 103,
209
210 #[strum(props(fallback = "Outgoing Messages"))]
211 OutgoingMessages = 104,
212
213 #[strum(props(fallback = "Storage on %1$s"))]
214 StorageOnDomain = 105,
215
216 #[strum(props(fallback = "Connected"))]
217 Connected = 107,
218
219 #[strum(props(fallback = "Connecting…"))]
220 Connecting = 108,
221
222 #[strum(props(fallback = "Updating…"))]
223 Updating = 109,
224
225 #[strum(props(fallback = "Sending…"))]
226 Sending = 110,
227
228 #[strum(props(fallback = "Your last message was sent successfully."))]
229 LastMsgSentSuccessfully = 111,
230
231 #[strum(props(fallback = "Error: %1$s"))]
232 Error = 112,
233
234 #[strum(props(fallback = "Not supported by your provider."))]
235 NotSupportedByProvider = 113,
236
237 #[strum(props(fallback = "Messages"))]
238 Messages = 114,
239
240 #[strum(props(fallback = "%1$s of %2$s used"))]
241 PartOfTotallUsed = 116,
242
243 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
244 Waiting for the device of %2$s to reply…"))]
245 SecureJoinStarted = 117,
246
247 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
248 SecureJoinReplies = 118,
249
250 #[strum(props(fallback = "Scan to chat with %1$s"))]
251 SetupContactQRDescription = 119,
252
253 #[strum(props(fallback = "Scan to join group %1$s"))]
254 SecureJoinGroupQRDescription = 120,
255
256 #[strum(props(fallback = "Not connected"))]
257 NotConnected = 121,
258
259 #[strum(props(
260 fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
261 ))]
262 AeapExplanationAndLink = 123,
263
264 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
265 MsgYouChangedGrpName = 124,
266
267 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
268 MsgGrpNameChangedBy = 125,
269
270 #[strum(props(fallback = "You changed the group image."))]
271 MsgYouChangedGrpImg = 126,
272
273 #[strum(props(fallback = "Group image changed by %1$s."))]
274 MsgGrpImgChangedBy = 127,
275
276 #[strum(props(fallback = "You added member %1$s."))]
277 MsgYouAddMember = 128,
278
279 #[strum(props(fallback = "Member %1$s added by %2$s."))]
280 MsgAddMemberBy = 129,
281
282 #[strum(props(fallback = "You removed member %1$s."))]
283 MsgYouDelMember = 130,
284
285 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
286 MsgDelMemberBy = 131,
287
288 #[strum(props(fallback = "You left the group."))]
289 MsgYouLeftGroup = 132,
290
291 #[strum(props(fallback = "Group left by %1$s."))]
292 MsgGroupLeftBy = 133,
293
294 #[strum(props(fallback = "You deleted the group image."))]
295 MsgYouDeletedGrpImg = 134,
296
297 #[strum(props(fallback = "Group image deleted by %1$s."))]
298 MsgGrpImgDeletedBy = 135,
299
300 #[strum(props(fallback = "You enabled location streaming."))]
301 MsgYouEnabledLocation = 136,
302
303 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
304 MsgLocationEnabledBy = 137,
305
306 #[strum(props(fallback = "You disabled message deletion timer."))]
307 MsgYouDisabledEphemeralTimer = 138,
308
309 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
310 MsgEphemeralTimerDisabledBy = 139,
311
312 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
315 MsgYouEnabledEphemeralTimer = 140,
316
317 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
318 MsgEphemeralTimerEnabledBy = 141,
319
320 #[strum(props(fallback = "You set message deletion timer to 1 minute."))]
321 MsgYouEphemeralTimerMinute = 142,
322
323 #[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
324 MsgEphemeralTimerMinuteBy = 143,
325
326 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
327 MsgYouEphemeralTimerHour = 144,
328
329 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
330 MsgEphemeralTimerHourBy = 145,
331
332 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
333 MsgYouEphemeralTimerDay = 146,
334
335 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
336 MsgEphemeralTimerDayBy = 147,
337
338 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
339 MsgYouEphemeralTimerWeek = 148,
340
341 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
342 MsgEphemeralTimerWeekBy = 149,
343
344 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
345 MsgYouEphemeralTimerMinutes = 150,
346
347 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
348 MsgEphemeralTimerMinutesBy = 151,
349
350 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
351 MsgYouEphemeralTimerHours = 152,
352
353 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
354 MsgEphemeralTimerHoursBy = 153,
355
356 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
357 MsgYouEphemeralTimerDays = 154,
358
359 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
360 MsgEphemeralTimerDaysBy = 155,
361
362 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
363 MsgYouEphemeralTimerWeeks = 156,
364
365 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
366 MsgEphemeralTimerWeeksBy = 157,
367
368 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
369 BackupTransferQr = 162,
370
371 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
372 BackupTransferMsgBody = 163,
373
374 #[strum(props(fallback = "I added member %1$s."))]
375 MsgIAddMember = 164,
376
377 #[strum(props(fallback = "I removed member %1$s."))]
378 MsgIDelMember = 165,
379
380 #[strum(props(fallback = "I left the group."))]
381 MsgILeftGroup = 166,
382
383 #[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
384 ChatProtectionEnabled = 170,
385
386 #[strum(props(fallback = "%1$s sent a message from another device."))]
387 ChatProtectionDisabled = 171,
388
389 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
390 NewGroupSendFirstMessage = 172,
391
392 #[strum(props(fallback = "Member %1$s added."))]
393 MsgAddMember = 173,
394
395 #[strum(props(
396 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
397 ))]
398 InvalidUnencryptedMail = 174,
399
400 #[strum(props(
401 fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
402 ))]
403 CantDecryptOutgoingMsgs = 175,
404
405 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
406 MsgYouReacted = 176,
407
408 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
409 MsgReactedBy = 177,
410
411 #[strum(props(fallback = "Member %1$s removed."))]
412 MsgDelMember = 178,
413
414 #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
415 SecurejoinWait = 190,
416}
417
418impl StockMessage {
419 fn fallback(self) -> &'static str {
423 self.get_str("fallback").unwrap_or_default()
424 }
425}
426
427impl Default for StockStrings {
428 fn default() -> Self {
429 StockStrings::new()
430 }
431}
432
433impl StockStrings {
434 pub fn new() -> Self {
436 Self {
437 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
438 }
439 }
440
441 async fn translated(&self, id: StockMessage) -> String {
442 self.translated_stockstrings
443 .read()
444 .await
445 .get(&(id as usize))
446 .map(AsRef::as_ref)
447 .unwrap_or_else(|| id.fallback())
448 .to_string()
449 }
450
451 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
452 if stockstring.contains("%1") && !id.fallback().contains("%1") {
453 bail!(
454 "translation {} contains invalid %1 placeholder, default is {}",
455 stockstring,
456 id.fallback()
457 );
458 }
459 if stockstring.contains("%2") && !id.fallback().contains("%2") {
460 bail!(
461 "translation {} contains invalid %2 placeholder, default is {}",
462 stockstring,
463 id.fallback()
464 );
465 }
466 self.translated_stockstrings
467 .write()
468 .await
469 .insert(id as usize, stockstring);
470 Ok(())
471 }
472}
473
474async fn translated(context: &Context, id: StockMessage) -> String {
475 context.translated_stockstrings.translated(id).await
476}
477
478trait StockStringMods: AsRef<str> + Sized {
480 fn replace1(&self, replacement: &str) -> String {
482 self.as_ref()
483 .replacen("%1$s", replacement, 1)
484 .replacen("%1$d", replacement, 1)
485 .replacen("%1$@", replacement, 1)
486 }
487
488 fn replace2(&self, replacement: &str) -> String {
493 self.as_ref()
494 .replacen("%2$s", replacement, 1)
495 .replacen("%2$d", replacement, 1)
496 .replacen("%2$@", replacement, 1)
497 }
498
499 fn replace3(&self, replacement: &str) -> String {
504 self.as_ref()
505 .replacen("%3$s", replacement, 1)
506 .replacen("%3$d", replacement, 1)
507 .replacen("%3$@", replacement, 1)
508 }
509}
510
511impl ContactId {
512 async fn get_stock_name(self, context: &Context) -> String {
514 Contact::get_by_id(context, self)
515 .await
516 .map(|contact| contact.get_display_name().to_string())
517 .unwrap_or_else(|_| self.to_string())
518 }
519}
520
521impl StockStringMods for String {}
522
523pub(crate) async fn no_messages(context: &Context) -> String {
525 translated(context, StockMessage::NoMessages).await
526}
527
528pub(crate) async fn self_msg(context: &Context) -> String {
530 translated(context, StockMessage::SelfMsg).await
531}
532
533pub(crate) async fn draft(context: &Context) -> String {
535 translated(context, StockMessage::Draft).await
536}
537
538pub(crate) async fn voice_message(context: &Context) -> String {
540 translated(context, StockMessage::VoiceMessage).await
541}
542
543pub(crate) async fn image(context: &Context) -> String {
545 translated(context, StockMessage::Image).await
546}
547
548pub(crate) async fn video(context: &Context) -> String {
550 translated(context, StockMessage::Video).await
551}
552
553pub(crate) async fn audio(context: &Context) -> String {
555 translated(context, StockMessage::Audio).await
556}
557
558pub(crate) async fn file(context: &Context) -> String {
560 translated(context, StockMessage::File).await
561}
562
563pub(crate) async fn msg_grp_name(
565 context: &Context,
566 from_group: &str,
567 to_group: &str,
568 by_contact: ContactId,
569) -> String {
570 if by_contact == ContactId::SELF {
571 translated(context, StockMessage::MsgYouChangedGrpName)
572 .await
573 .replace1(from_group)
574 .replace2(to_group)
575 } else {
576 translated(context, StockMessage::MsgGrpNameChangedBy)
577 .await
578 .replace1(from_group)
579 .replace2(to_group)
580 .replace3(&by_contact.get_stock_name(context).await)
581 }
582}
583
584pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
585 if by_contact == ContactId::SELF {
586 translated(context, StockMessage::MsgYouChangedGrpImg).await
587 } else {
588 translated(context, StockMessage::MsgGrpImgChangedBy)
589 .await
590 .replace1(&by_contact.get_stock_name(context).await)
591 }
592}
593
594pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
600 let addr = added_member_addr;
601 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
602 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
603 .await
604 .map(|contact| contact.get_authname_or_addr())
605 .unwrap_or_else(|_| addr.to_string()),
606 _ => addr.to_string(),
607 };
608 translated(context, StockMessage::MsgIAddMember)
609 .await
610 .replace1(whom)
611}
612
613pub(crate) async fn msg_add_member_local(
618 context: &Context,
619 added_member: ContactId,
620 by_contact: ContactId,
621) -> String {
622 let whom = added_member.get_stock_name(context).await;
623 if by_contact == ContactId::UNDEFINED {
624 translated(context, StockMessage::MsgAddMember)
625 .await
626 .replace1(&whom)
627 } else if by_contact == ContactId::SELF {
628 translated(context, StockMessage::MsgYouAddMember)
629 .await
630 .replace1(&whom)
631 } else {
632 translated(context, StockMessage::MsgAddMemberBy)
633 .await
634 .replace1(&whom)
635 .replace2(&by_contact.get_stock_name(context).await)
636 }
637}
638
639pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
644 let addr = removed_member_addr;
645 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
646 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
647 .await
648 .map(|contact| contact.get_authname_or_addr())
649 .unwrap_or_else(|_| addr.to_string()),
650 _ => addr.to_string(),
651 };
652 translated(context, StockMessage::MsgIDelMember)
653 .await
654 .replace1(whom)
655}
656
657pub(crate) async fn msg_del_member_local(
662 context: &Context,
663 removed_member: ContactId,
664 by_contact: ContactId,
665) -> String {
666 let whom = removed_member.get_stock_name(context).await;
667 if by_contact == ContactId::UNDEFINED {
668 translated(context, StockMessage::MsgDelMember)
669 .await
670 .replace1(&whom)
671 } else if by_contact == ContactId::SELF {
672 translated(context, StockMessage::MsgYouDelMember)
673 .await
674 .replace1(&whom)
675 } else {
676 translated(context, StockMessage::MsgDelMemberBy)
677 .await
678 .replace1(&whom)
679 .replace2(&by_contact.get_stock_name(context).await)
680 }
681}
682
683pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
685 translated(context, StockMessage::MsgILeftGroup).await
686}
687
688pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
690 if by_contact == ContactId::SELF {
691 translated(context, StockMessage::MsgYouLeftGroup).await
692 } else {
693 translated(context, StockMessage::MsgGroupLeftBy)
694 .await
695 .replace1(&by_contact.get_stock_name(context).await)
696 }
697}
698
699pub(crate) async fn msg_reacted(
701 context: &Context,
702 by_contact: ContactId,
703 reaction: &str,
704 summary: &str,
705) -> String {
706 if by_contact == ContactId::SELF {
707 translated(context, StockMessage::MsgYouReacted)
708 .await
709 .replace1(reaction)
710 .replace2(summary)
711 } else {
712 translated(context, StockMessage::MsgReactedBy)
713 .await
714 .replace1(&by_contact.get_stock_name(context).await)
715 .replace2(reaction)
716 .replace3(summary)
717 }
718}
719
720pub(crate) async fn gif(context: &Context) -> String {
722 translated(context, StockMessage::Gif).await
723}
724
725pub(crate) async fn e2e_available(context: &Context) -> String {
727 translated(context, StockMessage::E2eAvailable).await
728}
729
730pub(crate) async fn encr_none(context: &Context) -> String {
732 translated(context, StockMessage::EncrNone).await
733}
734
735pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
737 translated(context, StockMessage::CantDecryptMsgBody).await
738}
739
740pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
742 translated(context, StockMessage::CantDecryptOutgoingMsgs).await
743}
744
745pub(crate) async fn finger_prints(context: &Context) -> String {
747 translated(context, StockMessage::FingerPrints).await
748}
749
750pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
752 if by_contact == ContactId::SELF {
753 translated(context, StockMessage::MsgYouDeletedGrpImg).await
754 } else {
755 translated(context, StockMessage::MsgGrpImgDeletedBy)
756 .await
757 .replace1(&by_contact.get_stock_name(context).await)
758 }
759}
760
761pub(crate) async fn secure_join_started(
763 context: &Context,
764 inviter_contact_id: ContactId,
765) -> String {
766 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
767 translated(context, StockMessage::SecureJoinStarted)
768 .await
769 .replace1(contact.get_display_name())
770 .replace2(contact.get_display_name())
771 } else {
772 format!("secure_join_started: unknown contact {inviter_contact_id}")
773 }
774}
775
776pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
778 translated(context, StockMessage::SecureJoinReplies)
779 .await
780 .replace1(&contact_id.get_stock_name(context).await)
781}
782
783pub(crate) async fn securejoin_wait(context: &Context) -> String {
785 translated(context, StockMessage::SecurejoinWait).await
786}
787
788pub(crate) async fn setup_contact_qr_description(
790 context: &Context,
791 display_name: &str,
792 addr: &str,
793) -> String {
794 let name = if display_name.is_empty() {
795 addr.to_owned()
796 } else {
797 display_name.to_owned()
798 };
799 translated(context, StockMessage::SetupContactQRDescription)
800 .await
801 .replace1(&name)
802}
803
804pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
806 translated(context, StockMessage::SecureJoinGroupQRDescription)
807 .await
808 .replace1(chat.get_name())
809}
810
811#[allow(dead_code)]
813pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
814 let addr = contact.get_display_name();
815 translated(context, StockMessage::ContactVerified)
816 .await
817 .replace1(addr)
818}
819
820pub(crate) async fn archived_chats(context: &Context) -> String {
822 translated(context, StockMessage::ArchivedChats).await
823}
824
825pub(crate) async fn sync_msg_subject(context: &Context) -> String {
827 translated(context, StockMessage::SyncMsgSubject).await
828}
829
830pub(crate) async fn sync_msg_body(context: &Context) -> String {
832 translated(context, StockMessage::SyncMsgBody).await
833}
834
835pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
837 translated(context, StockMessage::CannotLogin)
838 .await
839 .replace1(user)
840}
841
842pub(crate) async fn msg_location_enabled(context: &Context) -> String {
844 translated(context, StockMessage::MsgLocationEnabled).await
845}
846
847pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
849 if contact == ContactId::SELF {
850 translated(context, StockMessage::MsgYouEnabledLocation).await
851 } else {
852 translated(context, StockMessage::MsgLocationEnabledBy)
853 .await
854 .replace1(&contact.get_stock_name(context).await)
855 }
856}
857
858pub(crate) async fn msg_location_disabled(context: &Context) -> String {
860 translated(context, StockMessage::MsgLocationDisabled).await
861}
862
863pub(crate) async fn location(context: &Context) -> String {
865 translated(context, StockMessage::Location).await
866}
867
868pub(crate) async fn sticker(context: &Context) -> String {
870 translated(context, StockMessage::Sticker).await
871}
872
873pub(crate) async fn device_messages(context: &Context) -> String {
875 translated(context, StockMessage::DeviceMessages).await
876}
877
878pub(crate) async fn saved_messages(context: &Context) -> String {
880 translated(context, StockMessage::SavedMessages).await
881}
882
883pub(crate) async fn device_messages_hint(context: &Context) -> String {
885 translated(context, StockMessage::DeviceMessagesHint).await
886}
887
888pub(crate) async fn welcome_message(context: &Context) -> String {
890 translated(context, StockMessage::WelcomeMessage).await
891}
892
893pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
895 translated(context, StockMessage::UnknownSenderForChat).await
896}
897
898pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
901 translated(context, StockMessage::SubjectForNewContact)
902 .await
903 .replace1(self_name)
904}
905
906pub(crate) async fn msg_ephemeral_timer_disabled(
908 context: &Context,
909 by_contact: ContactId,
910) -> String {
911 if by_contact == ContactId::SELF {
912 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
913 } else {
914 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
915 .await
916 .replace1(&by_contact.get_stock_name(context).await)
917 }
918}
919
920pub(crate) async fn msg_ephemeral_timer_enabled(
922 context: &Context,
923 timer: &str,
924 by_contact: ContactId,
925) -> String {
926 if by_contact == ContactId::SELF {
927 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
928 .await
929 .replace1(timer)
930 } else {
931 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
932 .await
933 .replace1(timer)
934 .replace2(&by_contact.get_stock_name(context).await)
935 }
936}
937
938pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
940 if by_contact == ContactId::SELF {
941 translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
942 } else {
943 translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
944 .await
945 .replace1(&by_contact.get_stock_name(context).await)
946 }
947}
948
949pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
951 if by_contact == ContactId::SELF {
952 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
953 } else {
954 translated(context, StockMessage::MsgEphemeralTimerHourBy)
955 .await
956 .replace1(&by_contact.get_stock_name(context).await)
957 }
958}
959
960pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
962 if by_contact == ContactId::SELF {
963 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
964 } else {
965 translated(context, StockMessage::MsgEphemeralTimerDayBy)
966 .await
967 .replace1(&by_contact.get_stock_name(context).await)
968 }
969}
970
971pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
973 if by_contact == ContactId::SELF {
974 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
975 } else {
976 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
977 .await
978 .replace1(&by_contact.get_stock_name(context).await)
979 }
980}
981
982pub(crate) async fn videochat_invitation(context: &Context) -> String {
984 translated(context, StockMessage::VideochatInvitation).await
985}
986
987pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
989 translated(context, StockMessage::VideochatInviteMsgBody)
990 .await
991 .replace1(url)
992}
993
994pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
996 translated(context, StockMessage::ConfigurationFailed)
997 .await
998 .replace1(details)
999}
1000
1001pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1004 translated(context, StockMessage::BadTimeMsgBody)
1005 .await
1006 .replace1(now)
1007}
1008
1009pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1011 translated(context, StockMessage::UpdateReminderMsgBody).await
1012}
1013
1014pub(crate) async fn error_no_network(context: &Context) -> String {
1016 translated(context, StockMessage::ErrorNoNetwork).await
1017}
1018
1019pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
1021 translated(context, StockMessage::ChatProtectionEnabled).await
1022}
1023
1024pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
1026 translated(context, StockMessage::ChatProtectionDisabled)
1027 .await
1028 .replace1(&contact_id.get_stock_name(context).await)
1029}
1030
1031pub(crate) async fn reply_noun(context: &Context) -> String {
1033 translated(context, StockMessage::ReplyNoun).await
1034}
1035
1036pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1038 translated(context, StockMessage::SelfDeletedMsgBody).await
1039}
1040
1041pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1043 translated(context, StockMessage::DeleteServerTurnedOff).await
1044}
1045
1046pub(crate) async fn msg_ephemeral_timer_minutes(
1048 context: &Context,
1049 minutes: &str,
1050 by_contact: ContactId,
1051) -> String {
1052 if by_contact == ContactId::SELF {
1053 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1054 .await
1055 .replace1(minutes)
1056 } else {
1057 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1058 .await
1059 .replace1(minutes)
1060 .replace2(&by_contact.get_stock_name(context).await)
1061 }
1062}
1063
1064pub(crate) async fn msg_ephemeral_timer_hours(
1066 context: &Context,
1067 hours: &str,
1068 by_contact: ContactId,
1069) -> String {
1070 if by_contact == ContactId::SELF {
1071 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1072 .await
1073 .replace1(hours)
1074 } else {
1075 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1076 .await
1077 .replace1(hours)
1078 .replace2(&by_contact.get_stock_name(context).await)
1079 }
1080}
1081
1082pub(crate) async fn msg_ephemeral_timer_days(
1084 context: &Context,
1085 days: &str,
1086 by_contact: ContactId,
1087) -> String {
1088 if by_contact == ContactId::SELF {
1089 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1090 .await
1091 .replace1(days)
1092 } else {
1093 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1094 .await
1095 .replace1(days)
1096 .replace2(&by_contact.get_stock_name(context).await)
1097 }
1098}
1099
1100pub(crate) async fn msg_ephemeral_timer_weeks(
1102 context: &Context,
1103 weeks: &str,
1104 by_contact: ContactId,
1105) -> String {
1106 if by_contact == ContactId::SELF {
1107 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1108 .await
1109 .replace1(weeks)
1110 } else {
1111 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1112 .await
1113 .replace1(weeks)
1114 .replace2(&by_contact.get_stock_name(context).await)
1115 }
1116}
1117
1118pub(crate) async fn forwarded(context: &Context) -> String {
1120 translated(context, StockMessage::Forwarded).await
1121}
1122
1123pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1125 translated(context, StockMessage::QuotaExceedingMsgBody)
1126 .await
1127 .replace1(&format!("{highest_usage}"))
1128 .replace("%%", "%")
1129}
1130
1131pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1133 let size = &format_size(org_bytes, BINARY);
1134 translated(context, StockMessage::PartialDownloadMsgBody)
1135 .await
1136 .replace1(size)
1137}
1138
1139pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1141 translated(context, StockMessage::DownloadAvailability)
1142 .await
1143 .replace1(×tamp_to_str(timestamp))
1144}
1145
1146pub(crate) async fn incoming_messages(context: &Context) -> String {
1148 translated(context, StockMessage::IncomingMessages).await
1149}
1150
1151pub(crate) async fn outgoing_messages(context: &Context) -> String {
1153 translated(context, StockMessage::OutgoingMessages).await
1154}
1155
1156pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1159 translated(context, StockMessage::StorageOnDomain)
1160 .await
1161 .replace1(domain)
1162}
1163
1164pub(crate) async fn not_connected(context: &Context) -> String {
1166 translated(context, StockMessage::NotConnected).await
1167}
1168
1169pub(crate) async fn connected(context: &Context) -> String {
1171 translated(context, StockMessage::Connected).await
1172}
1173
1174pub(crate) async fn connecting(context: &Context) -> String {
1176 translated(context, StockMessage::Connecting).await
1177}
1178
1179pub(crate) async fn updating(context: &Context) -> String {
1181 translated(context, StockMessage::Updating).await
1182}
1183
1184pub(crate) async fn sending(context: &Context) -> String {
1186 translated(context, StockMessage::Sending).await
1187}
1188
1189pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1191 translated(context, StockMessage::LastMsgSentSuccessfully).await
1192}
1193
1194pub(crate) async fn error(context: &Context, error: &str) -> String {
1197 translated(context, StockMessage::Error)
1198 .await
1199 .replace1(error)
1200}
1201
1202pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1204 translated(context, StockMessage::NotSupportedByProvider).await
1205}
1206
1207pub(crate) async fn messages(context: &Context) -> String {
1210 translated(context, StockMessage::Messages).await
1211}
1212
1213pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1215 translated(context, StockMessage::PartOfTotallUsed)
1216 .await
1217 .replace1(part)
1218 .replace2(total)
1219}
1220
1221pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1223 translated(context, StockMessage::InvalidUnencryptedMail)
1224 .await
1225 .replace1(provider)
1226}
1227
1228pub(crate) async fn aeap_explanation_and_link(
1229 context: &Context,
1230 old_addr: &str,
1231 new_addr: &str,
1232) -> String {
1233 translated(context, StockMessage::AeapExplanationAndLink)
1234 .await
1235 .replace1(old_addr)
1236 .replace2(new_addr)
1237}
1238
1239pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1241 translated(context, StockMessage::NewGroupSendFirstMessage).await
1242}
1243
1244pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1251 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1252 name
1253 } else {
1254 context.get_primary_self_addr().await?
1255 };
1256 Ok(translated(context, StockMessage::BackupTransferQr)
1257 .await
1258 .replace1(&name))
1259}
1260
1261pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1262 translated(context, StockMessage::BackupTransferMsgBody).await
1263}
1264
1265impl Context {
1266 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1269 self.translated_stockstrings
1270 .set_stock_translation(id, stockstring)
1271 .await?;
1272 Ok(())
1273 }
1274
1275 pub(crate) async fn stock_protection_msg(
1277 &self,
1278 protect: ProtectionStatus,
1279 contact_id: Option<ContactId>,
1280 ) -> String {
1281 match protect {
1282 ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
1283 if let Some(contact_id) = contact_id {
1284 chat_protection_disabled(self, contact_id).await
1285 } else {
1286 "[Error] No contact_id given".to_string()
1289 }
1290 }
1291 ProtectionStatus::Protected => chat_protection_enabled(self).await,
1292 }
1293 }
1294
1295 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1296 if self.get_config_bool(Config::Bot).await? {
1297 return Ok(());
1298 }
1299
1300 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1303 self.sql
1304 .set_raw_config_bool("self-chat-added", true)
1305 .await?;
1306 ChatId::create_for_contact(self, ContactId::SELF).await?;
1307 }
1308
1309 let image = include_bytes!("../assets/welcome-image.jpg");
1312 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1313 let mut msg = Message::new(Viewtype::Image);
1314 msg.param.set(Param::File, blob.as_name());
1315 msg.param.set(Param::Filename, "welcome-image.jpg");
1316 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1317
1318 let mut msg = Message::new_text(welcome_message(self).await);
1319 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1320 Ok(())
1321 }
1322}
1323
1324impl Accounts {
1325 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1328 self.stockstrings
1329 .set_stock_translation(id, stockstring)
1330 .await?;
1331 Ok(())
1332 }
1333}
1334
1335#[cfg(test)]
1336mod stock_str_tests;