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};
15use crate::config::Config;
16use crate::contact::{Contact, ContactId};
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 = "Fingerprints"))]
75 FingerPrints = 30,
76
77 #[strum(props(fallback = "%1$s verified."))]
78 ContactVerified = 35,
79
80 #[strum(props(fallback = "Archived chats"))]
81 ArchivedChats = 40,
82
83 #[strum(props(
84 fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
85 ))]
86 CannotLogin = 60,
87
88 #[strum(props(fallback = "Location streaming enabled."))]
89 MsgLocationEnabled = 64,
90
91 #[strum(props(fallback = "Location streaming disabled."))]
92 MsgLocationDisabled = 65,
93
94 #[strum(props(fallback = "Location"))]
95 Location = 66,
96
97 #[strum(props(fallback = "Sticker"))]
98 Sticker = 67,
99
100 #[strum(props(fallback = "Device messages"))]
101 DeviceMessages = 68,
102
103 #[strum(props(fallback = "Saved messages"))]
104 SavedMessages = 69,
105
106 #[strum(props(
107 fallback = "Messages in this chat are generated locally by your Delta Chat app. \
108 Its makers use it to inform about app updates and problems during usage."
109 ))]
110 DeviceMessagesHint = 70,
111
112 #[strum(props(fallback = "Welcome to Delta Chat! – \
113 Delta Chat looks and feels like other popular messenger apps, \
114 but does not involve centralized control, \
115 tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
116 Technically, Delta Chat is an email application with a modern chat interface. \
117 Email in a new dress if you will 👻\n\n\
118 Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
119 Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
120 however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
121 WelcomeMessage = 71,
122
123 #[strum(props(fallback = "Message from %1$s"))]
124 SubjectForNewContact = 73,
125
126 #[strum(props(fallback = "Failed to send message to %1$s."))]
128 FailedSendingTo = 74,
129
130 #[strum(props(fallback = "Error:\n\n“%1$s”"))]
131 ConfigurationFailed = 84,
132
133 #[strum(props(
134 fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
135 Adjust your clock ⏰🔧 to ensure your messages are received correctly."
136 ))]
137 BadTimeMsgBody = 85,
138
139 #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
140 This may cause problems because your chat partners use newer versions - \
141 and you are missing the latest features 😳\n\
142 Please check https://get.delta.chat or your app store for updates."))]
143 UpdateReminderMsgBody = 86,
144
145 #[strum(props(
146 fallback = "Could not find your mail server.\n\nPlease check your internet connection."
147 ))]
148 ErrorNoNetwork = 87,
149
150 #[strum(props(fallback = "Reply"))]
152 ReplyNoun = 90,
153
154 #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
155 To use the \"Saved messages\" feature again, create a new chat with yourself."))]
156 SelfDeletedMsgBody = 91,
157
158 #[strum(props(fallback = "Forwarded"))]
159 Forwarded = 97,
160
161 #[strum(props(
162 fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
163 You may not be able to receive message when the storage is 100%% used.\n\n\
164 👉 Please check if you can delete old data in the provider's webinterface \
165 and consider to enable \"Settings / Delete Old Messages\". \
166 You can check your current storage usage anytime at \"Settings / Connectivity\"."
167 ))]
168 QuotaExceedingMsgBody = 98,
169
170 #[strum(props(fallback = "%1$s message"))]
171 PartialDownloadMsgBody = 99,
172
173 #[strum(props(fallback = "Download maximum available until %1$s"))]
174 DownloadAvailability = 100,
175
176 #[strum(props(fallback = "Multi Device Synchronization"))]
177 SyncMsgSubject = 101,
178
179 #[strum(props(
180 fallback = "This message is used to synchronize data between your devices.\n\n\
181 👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
182 ))]
183 SyncMsgBody = 102,
184
185 #[strum(props(fallback = "Incoming Messages"))]
186 IncomingMessages = 103,
187
188 #[strum(props(fallback = "Outgoing Messages"))]
189 OutgoingMessages = 104,
190
191 #[strum(props(fallback = "Storage on %1$s"))]
192 StorageOnDomain = 105,
193
194 #[strum(props(fallback = "Connected"))]
195 Connected = 107,
196
197 #[strum(props(fallback = "Connecting…"))]
198 Connecting = 108,
199
200 #[strum(props(fallback = "Updating…"))]
201 Updating = 109,
202
203 #[strum(props(fallback = "Sending…"))]
204 Sending = 110,
205
206 #[strum(props(fallback = "Your last message was sent successfully."))]
207 LastMsgSentSuccessfully = 111,
208
209 #[strum(props(fallback = "Error: %1$s"))]
210 Error = 112,
211
212 #[strum(props(fallback = "Not supported by your provider."))]
213 NotSupportedByProvider = 113,
214
215 #[strum(props(fallback = "Messages"))]
216 Messages = 114,
217
218 #[strum(props(fallback = "%1$s of %2$s used"))]
219 PartOfTotallUsed = 116,
220
221 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
222 Waiting for the device of %2$s to reply…"))]
223 SecureJoinStarted = 117,
224
225 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
226 SecureJoinReplies = 118,
227
228 #[strum(props(fallback = "Scan to chat with %1$s"))]
229 SetupContactQRDescription = 119,
230
231 #[strum(props(fallback = "Scan to join group %1$s"))]
232 SecureJoinGroupQRDescription = 120,
233
234 #[strum(props(fallback = "Not connected"))]
235 NotConnected = 121,
236
237 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
238 MsgYouChangedGrpName = 124,
239
240 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
241 MsgGrpNameChangedBy = 125,
242
243 #[strum(props(fallback = "You changed the group image."))]
244 MsgYouChangedGrpImg = 126,
245
246 #[strum(props(fallback = "Group image changed by %1$s."))]
247 MsgGrpImgChangedBy = 127,
248
249 #[strum(props(fallback = "You added member %1$s."))]
250 MsgYouAddMember = 128,
251
252 #[strum(props(fallback = "Member %1$s added by %2$s."))]
253 MsgAddMemberBy = 129,
254
255 #[strum(props(fallback = "You removed member %1$s."))]
256 MsgYouDelMember = 130,
257
258 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
259 MsgDelMemberBy = 131,
260
261 #[strum(props(fallback = "You left the group."))]
262 MsgYouLeftGroup = 132,
263
264 #[strum(props(fallback = "Group left by %1$s."))]
265 MsgGroupLeftBy = 133,
266
267 #[strum(props(fallback = "You deleted the group image."))]
268 MsgYouDeletedGrpImg = 134,
269
270 #[strum(props(fallback = "Group image deleted by %1$s."))]
271 MsgGrpImgDeletedBy = 135,
272
273 #[strum(props(fallback = "You enabled location streaming."))]
274 MsgYouEnabledLocation = 136,
275
276 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
277 MsgLocationEnabledBy = 137,
278
279 #[strum(props(fallback = "You disabled message deletion timer."))]
280 MsgYouDisabledEphemeralTimer = 138,
281
282 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
283 MsgEphemeralTimerDisabledBy = 139,
284
285 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
288 MsgYouEnabledEphemeralTimer = 140,
289
290 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
291 MsgEphemeralTimerEnabledBy = 141,
292
293 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
294 MsgYouEphemeralTimerHour = 144,
295
296 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
297 MsgEphemeralTimerHourBy = 145,
298
299 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
300 MsgYouEphemeralTimerDay = 146,
301
302 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
303 MsgEphemeralTimerDayBy = 147,
304
305 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
306 MsgYouEphemeralTimerWeek = 148,
307
308 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
309 MsgEphemeralTimerWeekBy = 149,
310
311 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
312 MsgYouEphemeralTimerMinutes = 150,
313
314 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
315 MsgEphemeralTimerMinutesBy = 151,
316
317 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
318 MsgYouEphemeralTimerHours = 152,
319
320 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
321 MsgEphemeralTimerHoursBy = 153,
322
323 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
324 MsgYouEphemeralTimerDays = 154,
325
326 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
327 MsgEphemeralTimerDaysBy = 155,
328
329 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
330 MsgYouEphemeralTimerWeeks = 156,
331
332 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
333 MsgEphemeralTimerWeeksBy = 157,
334
335 #[strum(props(fallback = "You set message deletion timer to 1 year."))]
336 MsgYouEphemeralTimerYear = 158,
337
338 #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
339 MsgEphemeralTimerYearBy = 159,
340
341 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
342 BackupTransferQr = 162,
343
344 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
345 BackupTransferMsgBody = 163,
346
347 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
348 ChatProtectionEnabled = 170,
349
350 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
351 NewGroupSendFirstMessage = 172,
352
353 #[strum(props(fallback = "Member %1$s added."))]
354 MsgAddMember = 173,
355
356 #[strum(props(
357 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
358 ))]
359 InvalidUnencryptedMail = 174,
360
361 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
362 MsgYouReacted = 176,
363
364 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
365 MsgReactedBy = 177,
366
367 #[strum(props(fallback = "Member %1$s removed."))]
368 MsgDelMember = 178,
369
370 #[strum(props(fallback = "Establishing connection, please wait…"))]
371 SecurejoinWait = 190,
372
373 #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
374
375Please consider donating to help that Delta Chat stays free for everyone.
376
377While Delta Chat is free to use and open source, development costs money.
378Help keeping us to keep Delta Chat independent and make it more awesome in the future.
379
380https://delta.chat/donate"))]
381 DonationRequest = 193,
382
383 #[strum(props(fallback = "Outgoing call"))]
384 OutgoingCall = 194,
385
386 #[strum(props(fallback = "Incoming call"))]
387 IncomingCall = 195,
388
389 #[strum(props(fallback = "Declined call"))]
390 DeclinedCall = 196,
391
392 #[strum(props(fallback = "Canceled call"))]
393 CanceledCall = 197,
394
395 #[strum(props(fallback = "Missed call"))]
396 MissedCall = 198,
397
398 #[strum(props(fallback = "You left the channel."))]
399 MsgYouLeftBroadcast = 200,
400
401 #[strum(props(fallback = "Scan to join channel %1$s"))]
402 SecureJoinBrodcastQRDescription = 201,
403
404 #[strum(props(fallback = "You joined the channel."))]
405 MsgYouJoinedBroadcast = 202,
406
407 #[strum(props(fallback = "%1$s invited you to join this channel.\n\n\
408 Waiting for the device of %2$s to reply…"))]
409 SecureJoinBroadcastStarted = 203,
410
411 #[strum(props(
412 fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
413 ))]
414 StatsMsgBody = 210,
415
416 #[strum(props(fallback = "Proxy Enabled"))]
417 ProxyEnabled = 220,
418
419 #[strum(props(
420 fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
421 ))]
422 ProxyEnabledDescription = 221,
423
424 #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
425 ChatUnencryptedExplanation = 230,
426}
427
428impl StockMessage {
429 fn fallback(self) -> &'static str {
433 self.get_str("fallback").unwrap_or_default()
434 }
435}
436
437impl Default for StockStrings {
438 fn default() -> Self {
439 StockStrings::new()
440 }
441}
442
443impl StockStrings {
444 pub fn new() -> Self {
446 Self {
447 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
448 }
449 }
450
451 async fn translated(&self, id: StockMessage) -> String {
452 self.translated_stockstrings
453 .read()
454 .await
455 .get(&(id as usize))
456 .map(AsRef::as_ref)
457 .unwrap_or_else(|| id.fallback())
458 .to_string()
459 }
460
461 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
462 if stockstring.contains("%1") && !id.fallback().contains("%1") {
463 bail!(
464 "translation {} contains invalid %1 placeholder, default is {}",
465 stockstring,
466 id.fallback()
467 );
468 }
469 if stockstring.contains("%2") && !id.fallback().contains("%2") {
470 bail!(
471 "translation {} contains invalid %2 placeholder, default is {}",
472 stockstring,
473 id.fallback()
474 );
475 }
476 self.translated_stockstrings
477 .write()
478 .await
479 .insert(id as usize, stockstring);
480 Ok(())
481 }
482}
483
484async fn translated(context: &Context, id: StockMessage) -> String {
485 context.translated_stockstrings.translated(id).await
486}
487
488trait StockStringMods: AsRef<str> + Sized {
490 fn replace1(&self, replacement: &str) -> String {
492 self.as_ref()
493 .replacen("%1$s", replacement, 1)
494 .replacen("%1$d", replacement, 1)
495 .replacen("%1$@", replacement, 1)
496 }
497
498 fn replace2(&self, replacement: &str) -> String {
503 self.as_ref()
504 .replacen("%2$s", replacement, 1)
505 .replacen("%2$d", replacement, 1)
506 .replacen("%2$@", replacement, 1)
507 }
508
509 fn replace3(&self, replacement: &str) -> String {
514 self.as_ref()
515 .replacen("%3$s", replacement, 1)
516 .replacen("%3$d", replacement, 1)
517 .replacen("%3$@", replacement, 1)
518 }
519}
520
521impl ContactId {
522 async fn get_stock_name(self, context: &Context) -> String {
524 Contact::get_by_id(context, self)
525 .await
526 .map(|contact| contact.get_display_name().to_string())
527 .unwrap_or_else(|_| self.to_string())
528 }
529}
530
531impl StockStringMods for String {}
532
533pub(crate) async fn no_messages(context: &Context) -> String {
535 translated(context, StockMessage::NoMessages).await
536}
537
538pub(crate) async fn self_msg(context: &Context) -> String {
540 translated(context, StockMessage::SelfMsg).await
541}
542
543pub(crate) async fn draft(context: &Context) -> String {
545 translated(context, StockMessage::Draft).await
546}
547
548pub(crate) async fn voice_message(context: &Context) -> String {
550 translated(context, StockMessage::VoiceMessage).await
551}
552
553pub(crate) async fn image(context: &Context) -> String {
555 translated(context, StockMessage::Image).await
556}
557
558pub(crate) async fn video(context: &Context) -> String {
560 translated(context, StockMessage::Video).await
561}
562
563pub(crate) async fn audio(context: &Context) -> String {
565 translated(context, StockMessage::Audio).await
566}
567
568pub(crate) async fn file(context: &Context) -> String {
570 translated(context, StockMessage::File).await
571}
572
573pub(crate) async fn msg_grp_name(
575 context: &Context,
576 from_group: &str,
577 to_group: &str,
578 by_contact: ContactId,
579) -> String {
580 if by_contact == ContactId::SELF {
581 translated(context, StockMessage::MsgYouChangedGrpName)
582 .await
583 .replace1(from_group)
584 .replace2(to_group)
585 } else {
586 translated(context, StockMessage::MsgGrpNameChangedBy)
587 .await
588 .replace1(from_group)
589 .replace2(to_group)
590 .replace3(&by_contact.get_stock_name(context).await)
591 }
592}
593
594pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
595 if by_contact == ContactId::SELF {
596 translated(context, StockMessage::MsgYouChangedGrpImg).await
597 } else {
598 translated(context, StockMessage::MsgGrpImgChangedBy)
599 .await
600 .replace1(&by_contact.get_stock_name(context).await)
601 }
602}
603
604pub(crate) async fn msg_add_member_local(
609 context: &Context,
610 added_member: ContactId,
611 by_contact: ContactId,
612) -> String {
613 let whom = added_member.get_stock_name(context).await;
614 if by_contact == ContactId::UNDEFINED {
615 translated(context, StockMessage::MsgAddMember)
616 .await
617 .replace1(&whom)
618 } else if by_contact == ContactId::SELF {
619 translated(context, StockMessage::MsgYouAddMember)
620 .await
621 .replace1(&whom)
622 } else {
623 translated(context, StockMessage::MsgAddMemberBy)
624 .await
625 .replace1(&whom)
626 .replace2(&by_contact.get_stock_name(context).await)
627 }
628}
629
630pub(crate) async fn msg_del_member_local(
635 context: &Context,
636 removed_member: ContactId,
637 by_contact: ContactId,
638) -> String {
639 let whom = removed_member.get_stock_name(context).await;
640 if by_contact == ContactId::UNDEFINED {
641 translated(context, StockMessage::MsgDelMember)
642 .await
643 .replace1(&whom)
644 } else if by_contact == ContactId::SELF {
645 translated(context, StockMessage::MsgYouDelMember)
646 .await
647 .replace1(&whom)
648 } else {
649 translated(context, StockMessage::MsgDelMemberBy)
650 .await
651 .replace1(&whom)
652 .replace2(&by_contact.get_stock_name(context).await)
653 }
654}
655
656pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
658 if by_contact == ContactId::SELF {
659 translated(context, StockMessage::MsgYouLeftGroup).await
660 } else {
661 translated(context, StockMessage::MsgGroupLeftBy)
662 .await
663 .replace1(&by_contact.get_stock_name(context).await)
664 }
665}
666
667pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
669 translated(context, StockMessage::MsgYouLeftBroadcast).await
670}
671
672pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
674 translated(context, StockMessage::MsgYouJoinedBroadcast).await
675}
676
677pub(crate) async fn secure_join_broadcast_started(
679 context: &Context,
680 inviter_contact_id: ContactId,
681) -> String {
682 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
683 translated(context, StockMessage::SecureJoinBroadcastStarted)
684 .await
685 .replace1(contact.get_display_name())
686 .replace2(contact.get_display_name())
687 } else {
688 format!("secure_join_started: unknown contact {inviter_contact_id}")
689 }
690}
691
692pub(crate) async fn msg_reacted(
694 context: &Context,
695 by_contact: ContactId,
696 reaction: &str,
697 summary: &str,
698) -> String {
699 if by_contact == ContactId::SELF {
700 translated(context, StockMessage::MsgYouReacted)
701 .await
702 .replace1(reaction)
703 .replace2(summary)
704 } else {
705 translated(context, StockMessage::MsgReactedBy)
706 .await
707 .replace1(&by_contact.get_stock_name(context).await)
708 .replace2(reaction)
709 .replace3(summary)
710 }
711}
712
713pub(crate) async fn gif(context: &Context) -> String {
715 translated(context, StockMessage::Gif).await
716}
717
718pub(crate) async fn e2e_available(context: &Context) -> String {
720 translated(context, StockMessage::E2eAvailable).await
721}
722
723pub(crate) async fn encr_none(context: &Context) -> String {
725 translated(context, StockMessage::EncrNone).await
726}
727
728pub(crate) async fn finger_prints(context: &Context) -> String {
730 translated(context, StockMessage::FingerPrints).await
731}
732
733pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
735 if by_contact == ContactId::SELF {
736 translated(context, StockMessage::MsgYouDeletedGrpImg).await
737 } else {
738 translated(context, StockMessage::MsgGrpImgDeletedBy)
739 .await
740 .replace1(&by_contact.get_stock_name(context).await)
741 }
742}
743
744pub(crate) async fn secure_join_started(
746 context: &Context,
747 inviter_contact_id: ContactId,
748) -> String {
749 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
750 translated(context, StockMessage::SecureJoinStarted)
751 .await
752 .replace1(contact.get_display_name())
753 .replace2(contact.get_display_name())
754 } else {
755 format!("secure_join_started: unknown contact {inviter_contact_id}")
756 }
757}
758
759pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
761 translated(context, StockMessage::SecureJoinReplies)
762 .await
763 .replace1(&contact_id.get_stock_name(context).await)
764}
765
766pub(crate) async fn securejoin_wait(context: &Context) -> String {
768 translated(context, StockMessage::SecurejoinWait).await
769}
770
771pub(crate) async fn donation_request(context: &Context) -> String {
773 translated(context, StockMessage::DonationRequest).await
774}
775
776pub(crate) async fn outgoing_call(context: &Context) -> String {
778 translated(context, StockMessage::OutgoingCall).await
779}
780
781pub(crate) async fn incoming_call(context: &Context) -> String {
783 translated(context, StockMessage::IncomingCall).await
784}
785
786pub(crate) async fn declined_call(context: &Context) -> String {
788 translated(context, StockMessage::DeclinedCall).await
789}
790
791pub(crate) async fn canceled_call(context: &Context) -> String {
793 translated(context, StockMessage::CanceledCall).await
794}
795
796pub(crate) async fn missed_call(context: &Context) -> String {
798 translated(context, StockMessage::MissedCall).await
799}
800
801pub(crate) async fn setup_contact_qr_description(
803 context: &Context,
804 display_name: &str,
805 addr: &str,
806) -> String {
807 let name = if display_name.is_empty() {
808 addr.to_owned()
809 } else {
810 display_name.to_owned()
811 };
812 translated(context, StockMessage::SetupContactQRDescription)
813 .await
814 .replace1(&name)
815}
816
817pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
819 translated(context, StockMessage::SecureJoinGroupQRDescription)
820 .await
821 .replace1(chat.get_name())
822}
823
824pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
826 translated(context, StockMessage::SecureJoinBrodcastQRDescription)
827 .await
828 .replace1(chat.get_name())
829}
830
831#[allow(dead_code)]
833pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
834 let addr = contact.get_display_name();
835 translated(context, StockMessage::ContactVerified)
836 .await
837 .replace1(addr)
838}
839
840pub(crate) async fn archived_chats(context: &Context) -> String {
842 translated(context, StockMessage::ArchivedChats).await
843}
844
845pub(crate) async fn sync_msg_subject(context: &Context) -> String {
847 translated(context, StockMessage::SyncMsgSubject).await
848}
849
850pub(crate) async fn sync_msg_body(context: &Context) -> String {
852 translated(context, StockMessage::SyncMsgBody).await
853}
854
855pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
857 translated(context, StockMessage::CannotLogin)
858 .await
859 .replace1(user)
860}
861
862pub(crate) async fn msg_location_enabled(context: &Context) -> String {
864 translated(context, StockMessage::MsgLocationEnabled).await
865}
866
867pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
869 if contact == ContactId::SELF {
870 translated(context, StockMessage::MsgYouEnabledLocation).await
871 } else {
872 translated(context, StockMessage::MsgLocationEnabledBy)
873 .await
874 .replace1(&contact.get_stock_name(context).await)
875 }
876}
877
878pub(crate) async fn msg_location_disabled(context: &Context) -> String {
880 translated(context, StockMessage::MsgLocationDisabled).await
881}
882
883pub(crate) async fn location(context: &Context) -> String {
885 translated(context, StockMessage::Location).await
886}
887
888pub(crate) async fn sticker(context: &Context) -> String {
890 translated(context, StockMessage::Sticker).await
891}
892
893pub(crate) async fn device_messages(context: &Context) -> String {
895 translated(context, StockMessage::DeviceMessages).await
896}
897
898pub(crate) async fn saved_messages(context: &Context) -> String {
900 translated(context, StockMessage::SavedMessages).await
901}
902
903pub(crate) async fn device_messages_hint(context: &Context) -> String {
905 translated(context, StockMessage::DeviceMessagesHint).await
906}
907
908pub(crate) async fn welcome_message(context: &Context) -> String {
910 translated(context, StockMessage::WelcomeMessage).await
911}
912
913pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
916 translated(context, StockMessage::SubjectForNewContact)
917 .await
918 .replace1(self_name)
919}
920
921pub(crate) async fn msg_ephemeral_timer_disabled(
923 context: &Context,
924 by_contact: ContactId,
925) -> String {
926 if by_contact == ContactId::SELF {
927 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
928 } else {
929 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
930 .await
931 .replace1(&by_contact.get_stock_name(context).await)
932 }
933}
934
935pub(crate) async fn msg_ephemeral_timer_enabled(
937 context: &Context,
938 timer: &str,
939 by_contact: ContactId,
940) -> String {
941 if by_contact == ContactId::SELF {
942 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
943 .await
944 .replace1(timer)
945 } else {
946 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
947 .await
948 .replace1(timer)
949 .replace2(&by_contact.get_stock_name(context).await)
950 }
951}
952
953pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
955 if by_contact == ContactId::SELF {
956 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
957 } else {
958 translated(context, StockMessage::MsgEphemeralTimerHourBy)
959 .await
960 .replace1(&by_contact.get_stock_name(context).await)
961 }
962}
963
964pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
966 if by_contact == ContactId::SELF {
967 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
968 } else {
969 translated(context, StockMessage::MsgEphemeralTimerDayBy)
970 .await
971 .replace1(&by_contact.get_stock_name(context).await)
972 }
973}
974
975pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
977 if by_contact == ContactId::SELF {
978 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
979 } else {
980 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
981 .await
982 .replace1(&by_contact.get_stock_name(context).await)
983 }
984}
985
986pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
988 if by_contact == ContactId::SELF {
989 translated(context, StockMessage::MsgYouEphemeralTimerYear).await
990 } else {
991 translated(context, StockMessage::MsgEphemeralTimerYearBy)
992 .await
993 .replace1(&by_contact.get_stock_name(context).await)
994 }
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 messages_e2e_encrypted(context: &Context) -> String {
1024 translated(context, StockMessage::ChatProtectionEnabled).await
1025}
1026
1027pub(crate) async fn reply_noun(context: &Context) -> String {
1029 translated(context, StockMessage::ReplyNoun).await
1030}
1031
1032pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1034 translated(context, StockMessage::SelfDeletedMsgBody).await
1035}
1036
1037pub(crate) async fn msg_ephemeral_timer_minutes(
1039 context: &Context,
1040 minutes: &str,
1041 by_contact: ContactId,
1042) -> String {
1043 if by_contact == ContactId::SELF {
1044 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1045 .await
1046 .replace1(minutes)
1047 } else {
1048 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1049 .await
1050 .replace1(minutes)
1051 .replace2(&by_contact.get_stock_name(context).await)
1052 }
1053}
1054
1055pub(crate) async fn msg_ephemeral_timer_hours(
1057 context: &Context,
1058 hours: &str,
1059 by_contact: ContactId,
1060) -> String {
1061 if by_contact == ContactId::SELF {
1062 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1063 .await
1064 .replace1(hours)
1065 } else {
1066 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1067 .await
1068 .replace1(hours)
1069 .replace2(&by_contact.get_stock_name(context).await)
1070 }
1071}
1072
1073pub(crate) async fn msg_ephemeral_timer_days(
1075 context: &Context,
1076 days: &str,
1077 by_contact: ContactId,
1078) -> String {
1079 if by_contact == ContactId::SELF {
1080 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1081 .await
1082 .replace1(days)
1083 } else {
1084 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1085 .await
1086 .replace1(days)
1087 .replace2(&by_contact.get_stock_name(context).await)
1088 }
1089}
1090
1091pub(crate) async fn msg_ephemeral_timer_weeks(
1093 context: &Context,
1094 weeks: &str,
1095 by_contact: ContactId,
1096) -> String {
1097 if by_contact == ContactId::SELF {
1098 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1099 .await
1100 .replace1(weeks)
1101 } else {
1102 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1103 .await
1104 .replace1(weeks)
1105 .replace2(&by_contact.get_stock_name(context).await)
1106 }
1107}
1108
1109pub(crate) async fn forwarded(context: &Context) -> String {
1111 translated(context, StockMessage::Forwarded).await
1112}
1113
1114pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1116 translated(context, StockMessage::QuotaExceedingMsgBody)
1117 .await
1118 .replace1(&format!("{highest_usage}"))
1119 .replace("%%", "%")
1120}
1121
1122pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1124 let size = &format_size(org_bytes, BINARY);
1125 translated(context, StockMessage::PartialDownloadMsgBody)
1126 .await
1127 .replace1(size)
1128}
1129
1130pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1132 translated(context, StockMessage::DownloadAvailability)
1133 .await
1134 .replace1(×tamp_to_str(timestamp))
1135}
1136
1137pub(crate) async fn incoming_messages(context: &Context) -> String {
1139 translated(context, StockMessage::IncomingMessages).await
1140}
1141
1142pub(crate) async fn outgoing_messages(context: &Context) -> String {
1144 translated(context, StockMessage::OutgoingMessages).await
1145}
1146
1147pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1150 translated(context, StockMessage::StorageOnDomain)
1151 .await
1152 .replace1(domain)
1153}
1154
1155pub(crate) async fn not_connected(context: &Context) -> String {
1157 translated(context, StockMessage::NotConnected).await
1158}
1159
1160pub(crate) async fn connected(context: &Context) -> String {
1162 translated(context, StockMessage::Connected).await
1163}
1164
1165pub(crate) async fn connecting(context: &Context) -> String {
1167 translated(context, StockMessage::Connecting).await
1168}
1169
1170pub(crate) async fn updating(context: &Context) -> String {
1172 translated(context, StockMessage::Updating).await
1173}
1174
1175pub(crate) async fn sending(context: &Context) -> String {
1177 translated(context, StockMessage::Sending).await
1178}
1179
1180pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1182 translated(context, StockMessage::LastMsgSentSuccessfully).await
1183}
1184
1185pub(crate) async fn error(context: &Context, error: &str) -> String {
1188 translated(context, StockMessage::Error)
1189 .await
1190 .replace1(error)
1191}
1192
1193pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1195 translated(context, StockMessage::NotSupportedByProvider).await
1196}
1197
1198pub(crate) async fn messages(context: &Context) -> String {
1201 translated(context, StockMessage::Messages).await
1202}
1203
1204pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1206 translated(context, StockMessage::PartOfTotallUsed)
1207 .await
1208 .replace1(part)
1209 .replace2(total)
1210}
1211
1212pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1214 translated(context, StockMessage::InvalidUnencryptedMail)
1215 .await
1216 .replace1(provider)
1217}
1218
1219pub(crate) async fn stats_msg_body(context: &Context) -> String {
1221 translated(context, StockMessage::StatsMsgBody).await
1222}
1223
1224pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1226 translated(context, StockMessage::NewGroupSendFirstMessage).await
1227}
1228
1229pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1236 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1237 name
1238 } else {
1239 context.get_primary_self_addr().await?
1240 };
1241 Ok(translated(context, StockMessage::BackupTransferQr)
1242 .await
1243 .replace1(&name))
1244}
1245
1246pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1247 translated(context, StockMessage::BackupTransferMsgBody).await
1248}
1249
1250pub(crate) async fn proxy_enabled(context: &Context) -> String {
1252 translated(context, StockMessage::ProxyEnabled).await
1253}
1254
1255pub(crate) async fn proxy_description(context: &Context) -> String {
1257 translated(context, StockMessage::ProxyEnabledDescription).await
1258}
1259
1260pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1262 translated(context, StockMessage::ChatUnencryptedExplanation).await
1263}
1264
1265impl Context {
1266 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1269 self.translated_stockstrings
1270 .set_stock_translation(id, stockstring)
1271 .await?;
1272 Ok(())
1273 }
1274
1275 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1276 if self.get_config_bool(Config::Bot).await? {
1277 return Ok(());
1278 }
1279
1280 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1283 self.sql
1284 .set_raw_config_bool("self-chat-added", true)
1285 .await?;
1286 ChatId::create_for_contact(self, ContactId::SELF).await?;
1287 }
1288
1289 let image = include_bytes!("../assets/welcome-image.jpg");
1292 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1293 let mut msg = Message::new(Viewtype::Image);
1294 msg.param.set(Param::File, blob.as_name());
1295 msg.param.set(Param::Filename, "welcome-image.jpg");
1296 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1297
1298 let mut msg = Message::new_text(welcome_message(self).await);
1299 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1300 Ok(())
1301 }
1302}
1303
1304impl Accounts {
1305 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1308 self.stockstrings
1309 .set_stock_translation(id, stockstring)
1310 .await?;
1311 Ok(())
1312 }
1313}
1314
1315#[cfg(test)]
1316mod stock_str_tests;