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