1use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::{bail, Result};
7use humansize::{format_size, BINARY};
8use strum::EnumProperty as EnumPropertyTrait;
9use strum_macros::EnumProperty;
10use tokio::sync::RwLock;
11
12use crate::accounts::Accounts;
13use crate::blob::BlobObject;
14use crate::chat::{self, Chat, ChatId, ProtectionStatus};
15use crate::config::Config;
16use crate::contact::{Contact, ContactId, Origin};
17use crate::context::Context;
18use crate::message::{Message, Viewtype};
19use crate::param::Param;
20use crate::tools::timestamp_to_str;
21
22#[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 = "Broadcast List"))]
241 BroadcastList = 115,
242
243 #[strum(props(fallback = "%1$s of %2$s used"))]
244 PartOfTotallUsed = 116,
245
246 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
247 Waiting for the device of %2$s to reply…"))]
248 SecureJoinStarted = 117,
249
250 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
251 SecureJoinReplies = 118,
252
253 #[strum(props(fallback = "Scan to chat with %1$s"))]
254 SetupContactQRDescription = 119,
255
256 #[strum(props(fallback = "Scan to join group %1$s"))]
257 SecureJoinGroupQRDescription = 120,
258
259 #[strum(props(fallback = "Not connected"))]
260 NotConnected = 121,
261
262 #[strum(props(
263 fallback = "You changed your email address from %1$s to %2$s.\n\nIf you now send a message to a verified group, contacts there will automatically replace the old with your new address.\n\nIt's highly advised to set up your old email provider to forward all emails to your new email address. Otherwise you might miss messages of contacts who did not get your new address yet."
264 ))]
265 AeapExplanationAndLink = 123,
266
267 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
268 MsgYouChangedGrpName = 124,
269
270 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
271 MsgGrpNameChangedBy = 125,
272
273 #[strum(props(fallback = "You changed the group image."))]
274 MsgYouChangedGrpImg = 126,
275
276 #[strum(props(fallback = "Group image changed by %1$s."))]
277 MsgGrpImgChangedBy = 127,
278
279 #[strum(props(fallback = "You added member %1$s."))]
280 MsgYouAddMember = 128,
281
282 #[strum(props(fallback = "Member %1$s added by %2$s."))]
283 MsgAddMemberBy = 129,
284
285 #[strum(props(fallback = "You removed member %1$s."))]
286 MsgYouDelMember = 130,
287
288 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
289 MsgDelMemberBy = 131,
290
291 #[strum(props(fallback = "You left the group."))]
292 MsgYouLeftGroup = 132,
293
294 #[strum(props(fallback = "Group left by %1$s."))]
295 MsgGroupLeftBy = 133,
296
297 #[strum(props(fallback = "You deleted the group image."))]
298 MsgYouDeletedGrpImg = 134,
299
300 #[strum(props(fallback = "Group image deleted by %1$s."))]
301 MsgGrpImgDeletedBy = 135,
302
303 #[strum(props(fallback = "You enabled location streaming."))]
304 MsgYouEnabledLocation = 136,
305
306 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
307 MsgLocationEnabledBy = 137,
308
309 #[strum(props(fallback = "You disabled message deletion timer."))]
310 MsgYouDisabledEphemeralTimer = 138,
311
312 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
313 MsgEphemeralTimerDisabledBy = 139,
314
315 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
318 MsgYouEnabledEphemeralTimer = 140,
319
320 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
321 MsgEphemeralTimerEnabledBy = 141,
322
323 #[strum(props(fallback = "You set message deletion timer to 1 minute."))]
324 MsgYouEphemeralTimerMinute = 142,
325
326 #[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
327 MsgEphemeralTimerMinuteBy = 143,
328
329 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
330 MsgYouEphemeralTimerHour = 144,
331
332 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
333 MsgEphemeralTimerHourBy = 145,
334
335 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
336 MsgYouEphemeralTimerDay = 146,
337
338 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
339 MsgEphemeralTimerDayBy = 147,
340
341 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
342 MsgYouEphemeralTimerWeek = 148,
343
344 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
345 MsgEphemeralTimerWeekBy = 149,
346
347 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
348 MsgYouEphemeralTimerMinutes = 150,
349
350 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
351 MsgEphemeralTimerMinutesBy = 151,
352
353 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
354 MsgYouEphemeralTimerHours = 152,
355
356 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
357 MsgEphemeralTimerHoursBy = 153,
358
359 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
360 MsgYouEphemeralTimerDays = 154,
361
362 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
363 MsgEphemeralTimerDaysBy = 155,
364
365 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
366 MsgYouEphemeralTimerWeeks = 156,
367
368 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
369 MsgEphemeralTimerWeeksBy = 157,
370
371 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
372 BackupTransferQr = 162,
373
374 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
375 BackupTransferMsgBody = 163,
376
377 #[strum(props(fallback = "I added member %1$s."))]
378 MsgIAddMember = 164,
379
380 #[strum(props(fallback = "I removed member %1$s."))]
381 MsgIDelMember = 165,
382
383 #[strum(props(fallback = "I left the group."))]
384 MsgILeftGroup = 166,
385
386 #[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
387 ChatProtectionEnabled = 170,
388
389 #[strum(props(fallback = "%1$s sent a message from another device."))]
390 ChatProtectionDisabled = 171,
391
392 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
393 NewGroupSendFirstMessage = 172,
394
395 #[strum(props(fallback = "Member %1$s added."))]
396 MsgAddMember = 173,
397
398 #[strum(props(
399 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
400 ))]
401 InvalidUnencryptedMail = 174,
402
403 #[strum(props(
404 fallback = "⚠️ It seems you are using Delta Chat on multiple devices that cannot decrypt each other's outgoing messages. To fix this, on the older device use \"Settings / Add Second Device\" and follow the instructions."
405 ))]
406 CantDecryptOutgoingMsgs = 175,
407
408 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
409 MsgYouReacted = 176,
410
411 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
412 MsgReactedBy = 177,
413
414 #[strum(props(fallback = "Member %1$s removed."))]
415 MsgDelMember = 178,
416
417 #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
418 SecurejoinWait = 190,
419}
420
421impl StockMessage {
422 fn fallback(self) -> &'static str {
426 self.get_str("fallback").unwrap_or_default()
427 }
428}
429
430impl Default for StockStrings {
431 fn default() -> Self {
432 StockStrings::new()
433 }
434}
435
436impl StockStrings {
437 pub fn new() -> Self {
439 Self {
440 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
441 }
442 }
443
444 async fn translated(&self, id: StockMessage) -> String {
445 self.translated_stockstrings
446 .read()
447 .await
448 .get(&(id as usize))
449 .map(AsRef::as_ref)
450 .unwrap_or_else(|| id.fallback())
451 .to_string()
452 }
453
454 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
455 if stockstring.contains("%1") && !id.fallback().contains("%1") {
456 bail!(
457 "translation {} contains invalid %1 placeholder, default is {}",
458 stockstring,
459 id.fallback()
460 );
461 }
462 if stockstring.contains("%2") && !id.fallback().contains("%2") {
463 bail!(
464 "translation {} contains invalid %2 placeholder, default is {}",
465 stockstring,
466 id.fallback()
467 );
468 }
469 self.translated_stockstrings
470 .write()
471 .await
472 .insert(id as usize, stockstring);
473 Ok(())
474 }
475}
476
477async fn translated(context: &Context, id: StockMessage) -> String {
478 context.translated_stockstrings.translated(id).await
479}
480
481trait StockStringMods: AsRef<str> + Sized {
483 fn replace1(&self, replacement: &str) -> String {
485 self.as_ref()
486 .replacen("%1$s", replacement, 1)
487 .replacen("%1$d", replacement, 1)
488 .replacen("%1$@", replacement, 1)
489 }
490
491 fn replace2(&self, replacement: &str) -> String {
496 self.as_ref()
497 .replacen("%2$s", replacement, 1)
498 .replacen("%2$d", replacement, 1)
499 .replacen("%2$@", replacement, 1)
500 }
501
502 fn replace3(&self, replacement: &str) -> String {
507 self.as_ref()
508 .replacen("%3$s", replacement, 1)
509 .replacen("%3$d", replacement, 1)
510 .replacen("%3$@", replacement, 1)
511 }
512}
513
514impl ContactId {
515 async fn get_stock_name(self, context: &Context) -> String {
517 Contact::get_by_id(context, self)
518 .await
519 .map(|contact| contact.get_display_name().to_string())
520 .unwrap_or_else(|_| self.to_string())
521 }
522}
523
524impl StockStringMods for String {}
525
526pub(crate) async fn no_messages(context: &Context) -> String {
528 translated(context, StockMessage::NoMessages).await
529}
530
531pub(crate) async fn self_msg(context: &Context) -> String {
533 translated(context, StockMessage::SelfMsg).await
534}
535
536pub(crate) async fn draft(context: &Context) -> String {
538 translated(context, StockMessage::Draft).await
539}
540
541pub(crate) async fn voice_message(context: &Context) -> String {
543 translated(context, StockMessage::VoiceMessage).await
544}
545
546pub(crate) async fn image(context: &Context) -> String {
548 translated(context, StockMessage::Image).await
549}
550
551pub(crate) async fn video(context: &Context) -> String {
553 translated(context, StockMessage::Video).await
554}
555
556pub(crate) async fn audio(context: &Context) -> String {
558 translated(context, StockMessage::Audio).await
559}
560
561pub(crate) async fn file(context: &Context) -> String {
563 translated(context, StockMessage::File).await
564}
565
566pub(crate) async fn msg_grp_name(
568 context: &Context,
569 from_group: &str,
570 to_group: &str,
571 by_contact: ContactId,
572) -> String {
573 if by_contact == ContactId::SELF {
574 translated(context, StockMessage::MsgYouChangedGrpName)
575 .await
576 .replace1(from_group)
577 .replace2(to_group)
578 } else {
579 translated(context, StockMessage::MsgGrpNameChangedBy)
580 .await
581 .replace1(from_group)
582 .replace2(to_group)
583 .replace3(&by_contact.get_stock_name(context).await)
584 }
585}
586
587pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
588 if by_contact == ContactId::SELF {
589 translated(context, StockMessage::MsgYouChangedGrpImg).await
590 } else {
591 translated(context, StockMessage::MsgGrpImgChangedBy)
592 .await
593 .replace1(&by_contact.get_stock_name(context).await)
594 }
595}
596
597pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
603 let addr = added_member_addr;
604 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
605 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
606 .await
607 .map(|contact| contact.get_authname_or_addr())
608 .unwrap_or_else(|_| addr.to_string()),
609 _ => addr.to_string(),
610 };
611 translated(context, StockMessage::MsgIAddMember)
612 .await
613 .replace1(whom)
614}
615
616pub(crate) async fn msg_add_member_local(
621 context: &Context,
622 added_member: ContactId,
623 by_contact: ContactId,
624) -> String {
625 let whom = added_member.get_stock_name(context).await;
626 if by_contact == ContactId::UNDEFINED {
627 translated(context, StockMessage::MsgAddMember)
628 .await
629 .replace1(&whom)
630 } else if by_contact == ContactId::SELF {
631 translated(context, StockMessage::MsgYouAddMember)
632 .await
633 .replace1(&whom)
634 } else {
635 translated(context, StockMessage::MsgAddMemberBy)
636 .await
637 .replace1(&whom)
638 .replace2(&by_contact.get_stock_name(context).await)
639 }
640}
641
642pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
647 let addr = removed_member_addr;
648 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
649 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
650 .await
651 .map(|contact| contact.get_authname_or_addr())
652 .unwrap_or_else(|_| addr.to_string()),
653 _ => addr.to_string(),
654 };
655 translated(context, StockMessage::MsgIDelMember)
656 .await
657 .replace1(whom)
658}
659
660pub(crate) async fn msg_del_member_local(
665 context: &Context,
666 removed_member: ContactId,
667 by_contact: ContactId,
668) -> String {
669 let whom = removed_member.get_stock_name(context).await;
670 if by_contact == ContactId::UNDEFINED {
671 translated(context, StockMessage::MsgDelMember)
672 .await
673 .replace1(&whom)
674 } else if by_contact == ContactId::SELF {
675 translated(context, StockMessage::MsgYouDelMember)
676 .await
677 .replace1(&whom)
678 } else {
679 translated(context, StockMessage::MsgDelMemberBy)
680 .await
681 .replace1(&whom)
682 .replace2(&by_contact.get_stock_name(context).await)
683 }
684}
685
686pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
688 translated(context, StockMessage::MsgILeftGroup).await
689}
690
691pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
693 if by_contact == ContactId::SELF {
694 translated(context, StockMessage::MsgYouLeftGroup).await
695 } else {
696 translated(context, StockMessage::MsgGroupLeftBy)
697 .await
698 .replace1(&by_contact.get_stock_name(context).await)
699 }
700}
701
702pub(crate) async fn msg_reacted(
704 context: &Context,
705 by_contact: ContactId,
706 reaction: &str,
707 summary: &str,
708) -> String {
709 if by_contact == ContactId::SELF {
710 translated(context, StockMessage::MsgYouReacted)
711 .await
712 .replace1(reaction)
713 .replace2(summary)
714 } else {
715 translated(context, StockMessage::MsgReactedBy)
716 .await
717 .replace1(&by_contact.get_stock_name(context).await)
718 .replace2(reaction)
719 .replace3(summary)
720 }
721}
722
723pub(crate) async fn gif(context: &Context) -> String {
725 translated(context, StockMessage::Gif).await
726}
727
728pub(crate) async fn e2e_available(context: &Context) -> String {
730 translated(context, StockMessage::E2eAvailable).await
731}
732
733pub(crate) async fn encr_none(context: &Context) -> String {
735 translated(context, StockMessage::EncrNone).await
736}
737
738pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
740 translated(context, StockMessage::CantDecryptMsgBody).await
741}
742
743pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
745 translated(context, StockMessage::CantDecryptOutgoingMsgs).await
746}
747
748pub(crate) async fn finger_prints(context: &Context) -> String {
750 translated(context, StockMessage::FingerPrints).await
751}
752
753pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
755 if by_contact == ContactId::SELF {
756 translated(context, StockMessage::MsgYouDeletedGrpImg).await
757 } else {
758 translated(context, StockMessage::MsgGrpImgDeletedBy)
759 .await
760 .replace1(&by_contact.get_stock_name(context).await)
761 }
762}
763
764pub(crate) async fn secure_join_started(
766 context: &Context,
767 inviter_contact_id: ContactId,
768) -> String {
769 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
770 translated(context, StockMessage::SecureJoinStarted)
771 .await
772 .replace1(contact.get_display_name())
773 .replace2(contact.get_display_name())
774 } else {
775 format!("secure_join_started: unknown contact {inviter_contact_id}")
776 }
777}
778
779pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
781 translated(context, StockMessage::SecureJoinReplies)
782 .await
783 .replace1(&contact_id.get_stock_name(context).await)
784}
785
786pub(crate) async fn securejoin_wait(context: &Context) -> String {
788 translated(context, StockMessage::SecurejoinWait).await
789}
790
791pub(crate) async fn setup_contact_qr_description(
793 context: &Context,
794 display_name: &str,
795 addr: &str,
796) -> String {
797 let name = if display_name.is_empty() {
798 addr.to_owned()
799 } else {
800 display_name.to_owned()
801 };
802 translated(context, StockMessage::SetupContactQRDescription)
803 .await
804 .replace1(&name)
805}
806
807pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
809 translated(context, StockMessage::SecureJoinGroupQRDescription)
810 .await
811 .replace1(chat.get_name())
812}
813
814#[allow(dead_code)]
816pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
817 let addr = contact.get_display_name();
818 translated(context, StockMessage::ContactVerified)
819 .await
820 .replace1(addr)
821}
822
823pub(crate) async fn archived_chats(context: &Context) -> String {
825 translated(context, StockMessage::ArchivedChats).await
826}
827
828pub(crate) async fn sync_msg_subject(context: &Context) -> String {
830 translated(context, StockMessage::SyncMsgSubject).await
831}
832
833pub(crate) async fn sync_msg_body(context: &Context) -> String {
835 translated(context, StockMessage::SyncMsgBody).await
836}
837
838pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
840 translated(context, StockMessage::CannotLogin)
841 .await
842 .replace1(user)
843}
844
845pub(crate) async fn msg_location_enabled(context: &Context) -> String {
847 translated(context, StockMessage::MsgLocationEnabled).await
848}
849
850pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
852 if contact == ContactId::SELF {
853 translated(context, StockMessage::MsgYouEnabledLocation).await
854 } else {
855 translated(context, StockMessage::MsgLocationEnabledBy)
856 .await
857 .replace1(&contact.get_stock_name(context).await)
858 }
859}
860
861pub(crate) async fn msg_location_disabled(context: &Context) -> String {
863 translated(context, StockMessage::MsgLocationDisabled).await
864}
865
866pub(crate) async fn location(context: &Context) -> String {
868 translated(context, StockMessage::Location).await
869}
870
871pub(crate) async fn sticker(context: &Context) -> String {
873 translated(context, StockMessage::Sticker).await
874}
875
876pub(crate) async fn device_messages(context: &Context) -> String {
878 translated(context, StockMessage::DeviceMessages).await
879}
880
881pub(crate) async fn saved_messages(context: &Context) -> String {
883 translated(context, StockMessage::SavedMessages).await
884}
885
886pub(crate) async fn device_messages_hint(context: &Context) -> String {
888 translated(context, StockMessage::DeviceMessagesHint).await
889}
890
891pub(crate) async fn welcome_message(context: &Context) -> String {
893 translated(context, StockMessage::WelcomeMessage).await
894}
895
896pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
898 translated(context, StockMessage::UnknownSenderForChat).await
899}
900
901pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
904 translated(context, StockMessage::SubjectForNewContact)
905 .await
906 .replace1(self_name)
907}
908
909pub(crate) async fn msg_ephemeral_timer_disabled(
911 context: &Context,
912 by_contact: ContactId,
913) -> String {
914 if by_contact == ContactId::SELF {
915 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
916 } else {
917 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
918 .await
919 .replace1(&by_contact.get_stock_name(context).await)
920 }
921}
922
923pub(crate) async fn msg_ephemeral_timer_enabled(
925 context: &Context,
926 timer: &str,
927 by_contact: ContactId,
928) -> String {
929 if by_contact == ContactId::SELF {
930 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
931 .await
932 .replace1(timer)
933 } else {
934 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
935 .await
936 .replace1(timer)
937 .replace2(&by_contact.get_stock_name(context).await)
938 }
939}
940
941pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
943 if by_contact == ContactId::SELF {
944 translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
945 } else {
946 translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
947 .await
948 .replace1(&by_contact.get_stock_name(context).await)
949 }
950}
951
952pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
954 if by_contact == ContactId::SELF {
955 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
956 } else {
957 translated(context, StockMessage::MsgEphemeralTimerHourBy)
958 .await
959 .replace1(&by_contact.get_stock_name(context).await)
960 }
961}
962
963pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
965 if by_contact == ContactId::SELF {
966 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
967 } else {
968 translated(context, StockMessage::MsgEphemeralTimerDayBy)
969 .await
970 .replace1(&by_contact.get_stock_name(context).await)
971 }
972}
973
974pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
976 if by_contact == ContactId::SELF {
977 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
978 } else {
979 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
980 .await
981 .replace1(&by_contact.get_stock_name(context).await)
982 }
983}
984
985pub(crate) async fn videochat_invitation(context: &Context) -> String {
987 translated(context, StockMessage::VideochatInvitation).await
988}
989
990pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
992 translated(context, StockMessage::VideochatInviteMsgBody)
993 .await
994 .replace1(url)
995}
996
997pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
999 translated(context, StockMessage::ConfigurationFailed)
1000 .await
1001 .replace1(details)
1002}
1003
1004pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1007 translated(context, StockMessage::BadTimeMsgBody)
1008 .await
1009 .replace1(now)
1010}
1011
1012pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1014 translated(context, StockMessage::UpdateReminderMsgBody).await
1015}
1016
1017pub(crate) async fn error_no_network(context: &Context) -> String {
1019 translated(context, StockMessage::ErrorNoNetwork).await
1020}
1021
1022pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
1024 translated(context, StockMessage::ChatProtectionEnabled).await
1025}
1026
1027pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
1029 translated(context, StockMessage::ChatProtectionDisabled)
1030 .await
1031 .replace1(&contact_id.get_stock_name(context).await)
1032}
1033
1034pub(crate) async fn reply_noun(context: &Context) -> String {
1036 translated(context, StockMessage::ReplyNoun).await
1037}
1038
1039pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1041 translated(context, StockMessage::SelfDeletedMsgBody).await
1042}
1043
1044pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1046 translated(context, StockMessage::DeleteServerTurnedOff).await
1047}
1048
1049pub(crate) async fn msg_ephemeral_timer_minutes(
1051 context: &Context,
1052 minutes: &str,
1053 by_contact: ContactId,
1054) -> String {
1055 if by_contact == ContactId::SELF {
1056 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1057 .await
1058 .replace1(minutes)
1059 } else {
1060 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1061 .await
1062 .replace1(minutes)
1063 .replace2(&by_contact.get_stock_name(context).await)
1064 }
1065}
1066
1067pub(crate) async fn msg_ephemeral_timer_hours(
1069 context: &Context,
1070 hours: &str,
1071 by_contact: ContactId,
1072) -> String {
1073 if by_contact == ContactId::SELF {
1074 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1075 .await
1076 .replace1(hours)
1077 } else {
1078 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1079 .await
1080 .replace1(hours)
1081 .replace2(&by_contact.get_stock_name(context).await)
1082 }
1083}
1084
1085pub(crate) async fn msg_ephemeral_timer_days(
1087 context: &Context,
1088 days: &str,
1089 by_contact: ContactId,
1090) -> String {
1091 if by_contact == ContactId::SELF {
1092 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1093 .await
1094 .replace1(days)
1095 } else {
1096 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1097 .await
1098 .replace1(days)
1099 .replace2(&by_contact.get_stock_name(context).await)
1100 }
1101}
1102
1103pub(crate) async fn msg_ephemeral_timer_weeks(
1105 context: &Context,
1106 weeks: &str,
1107 by_contact: ContactId,
1108) -> String {
1109 if by_contact == ContactId::SELF {
1110 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1111 .await
1112 .replace1(weeks)
1113 } else {
1114 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1115 .await
1116 .replace1(weeks)
1117 .replace2(&by_contact.get_stock_name(context).await)
1118 }
1119}
1120
1121pub(crate) async fn forwarded(context: &Context) -> String {
1123 translated(context, StockMessage::Forwarded).await
1124}
1125
1126pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1128 translated(context, StockMessage::QuotaExceedingMsgBody)
1129 .await
1130 .replace1(&format!("{highest_usage}"))
1131 .replace("%%", "%")
1132}
1133
1134pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1136 let size = &format_size(org_bytes, BINARY);
1137 translated(context, StockMessage::PartialDownloadMsgBody)
1138 .await
1139 .replace1(size)
1140}
1141
1142pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1144 translated(context, StockMessage::DownloadAvailability)
1145 .await
1146 .replace1(×tamp_to_str(timestamp))
1147}
1148
1149pub(crate) async fn incoming_messages(context: &Context) -> String {
1151 translated(context, StockMessage::IncomingMessages).await
1152}
1153
1154pub(crate) async fn outgoing_messages(context: &Context) -> String {
1156 translated(context, StockMessage::OutgoingMessages).await
1157}
1158
1159pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1162 translated(context, StockMessage::StorageOnDomain)
1163 .await
1164 .replace1(domain)
1165}
1166
1167pub(crate) async fn not_connected(context: &Context) -> String {
1169 translated(context, StockMessage::NotConnected).await
1170}
1171
1172pub(crate) async fn connected(context: &Context) -> String {
1174 translated(context, StockMessage::Connected).await
1175}
1176
1177pub(crate) async fn connecting(context: &Context) -> String {
1179 translated(context, StockMessage::Connecting).await
1180}
1181
1182pub(crate) async fn updating(context: &Context) -> String {
1184 translated(context, StockMessage::Updating).await
1185}
1186
1187pub(crate) async fn sending(context: &Context) -> String {
1189 translated(context, StockMessage::Sending).await
1190}
1191
1192pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1194 translated(context, StockMessage::LastMsgSentSuccessfully).await
1195}
1196
1197pub(crate) async fn error(context: &Context, error: &str) -> String {
1200 translated(context, StockMessage::Error)
1201 .await
1202 .replace1(error)
1203}
1204
1205pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1207 translated(context, StockMessage::NotSupportedByProvider).await
1208}
1209
1210pub(crate) async fn messages(context: &Context) -> String {
1213 translated(context, StockMessage::Messages).await
1214}
1215
1216pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1218 translated(context, StockMessage::PartOfTotallUsed)
1219 .await
1220 .replace1(part)
1221 .replace2(total)
1222}
1223
1224pub(crate) async fn broadcast_list(context: &Context) -> String {
1227 translated(context, StockMessage::BroadcastList).await
1228}
1229
1230pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1232 translated(context, StockMessage::InvalidUnencryptedMail)
1233 .await
1234 .replace1(provider)
1235}
1236
1237pub(crate) async fn aeap_explanation_and_link(
1238 context: &Context,
1239 old_addr: &str,
1240 new_addr: &str,
1241) -> String {
1242 translated(context, StockMessage::AeapExplanationAndLink)
1243 .await
1244 .replace1(old_addr)
1245 .replace2(new_addr)
1246}
1247
1248pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1250 translated(context, StockMessage::NewGroupSendFirstMessage).await
1251}
1252
1253pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1260 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1261 name
1262 } else {
1263 context.get_primary_self_addr().await?
1264 };
1265 Ok(translated(context, StockMessage::BackupTransferQr)
1266 .await
1267 .replace1(&name))
1268}
1269
1270pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1271 translated(context, StockMessage::BackupTransferMsgBody).await
1272}
1273
1274impl Context {
1275 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1278 self.translated_stockstrings
1279 .set_stock_translation(id, stockstring)
1280 .await?;
1281 Ok(())
1282 }
1283
1284 pub(crate) async fn stock_protection_msg(
1286 &self,
1287 protect: ProtectionStatus,
1288 contact_id: Option<ContactId>,
1289 ) -> String {
1290 match protect {
1291 ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
1292 if let Some(contact_id) = contact_id {
1293 chat_protection_disabled(self, contact_id).await
1294 } else {
1295 "[Error] No contact_id given".to_string()
1298 }
1299 }
1300 ProtectionStatus::Protected => chat_protection_enabled(self).await,
1301 }
1302 }
1303
1304 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1305 if self.get_config_bool(Config::Bot).await? {
1306 return Ok(());
1307 }
1308
1309 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1312 self.sql
1313 .set_raw_config_bool("self-chat-added", true)
1314 .await?;
1315 ChatId::create_for_contact(self, ContactId::SELF).await?;
1316 }
1317
1318 let image = include_bytes!("../assets/welcome-image.jpg");
1321 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1322 let mut msg = Message::new(Viewtype::Image);
1323 msg.param.set(Param::File, blob.as_name());
1324 msg.param.set(Param::Filename, "welcome-image.jpg");
1325 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1326
1327 let mut msg = Message::new_text(welcome_message(self).await);
1328 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1329 Ok(())
1330 }
1331}
1332
1333impl Accounts {
1334 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1337 self.stockstrings
1338 .set_stock_translation(id, stockstring)
1339 .await?;
1340 Ok(())
1341 }
1342}
1343
1344#[cfg(test)]
1345mod stock_str_tests;