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