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