1use std::collections::HashMap;
4use std::sync::Arc;
5
6use anyhow::{bail, Result};
7use humansize::{format_size, BINARY};
8use strum::EnumProperty as EnumPropertyTrait;
9use strum_macros::EnumProperty;
10use tokio::sync::RwLock;
11
12use crate::accounts::Accounts;
13use crate::blob::BlobObject;
14use crate::chat::{self, Chat, ChatId, ProtectionStatus};
15use crate::config::Config;
16use crate::contact::{Contact, ContactId, Origin};
17use crate::context::Context;
18use crate::message::{Message, Viewtype};
19use crate::param::Param;
20use crate::tools::timestamp_to_str;
21
22#[derive(Debug, Clone)]
24pub struct StockStrings {
25 translated_stockstrings: Arc<RwLock<HashMap<usize, String>>>,
27}
28
29#[allow(missing_docs)]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, EnumProperty)]
39#[repr(u32)]
40pub enum StockMessage {
41 #[strum(props(fallback = "No messages."))]
42 NoMessages = 1,
43
44 #[strum(props(fallback = "Me"))]
45 SelfMsg = 2,
46
47 #[strum(props(fallback = "Draft"))]
48 Draft = 3,
49
50 #[strum(props(fallback = "Voice message"))]
51 VoiceMessage = 7,
52
53 #[strum(props(fallback = "Image"))]
54 Image = 9,
55
56 #[strum(props(fallback = "Video"))]
57 Video = 10,
58
59 #[strum(props(fallback = "Audio"))]
60 Audio = 11,
61
62 #[strum(props(fallback = "File"))]
63 File = 12,
64
65 #[strum(props(fallback = "GIF"))]
66 Gif = 23,
67
68 #[strum(props(fallback = "Encrypted message"))]
69 EncryptedMsg = 24,
70
71 #[strum(props(fallback = "End-to-end encryption available"))]
72 E2eAvailable = 25,
73
74 #[strum(props(fallback = "No encryption"))]
75 EncrNone = 28,
76
77 #[strum(props(fallback = "This message was encrypted for another setup."))]
78 CantDecryptMsgBody = 29,
79
80 #[strum(props(fallback = "Fingerprints"))]
81 FingerPrints = 30,
82
83 #[strum(props(fallback = "End-to-end encryption preferred"))]
84 E2ePreferred = 34,
85
86 #[strum(props(fallback = "%1$s verified."))]
87 ContactVerified = 35,
88
89 #[strum(props(fallback = "Cannot establish guaranteed end-to-end encryption with %1$s"))]
90 ContactNotVerified = 36,
91
92 #[strum(props(fallback = "Changed setup for %1$s"))]
93 ContactSetupChanged = 37,
94
95 #[strum(props(fallback = "Archived chats"))]
96 ArchivedChats = 40,
97
98 #[strum(props(
99 fallback = "Cannot login as \"%1$s\". Please check if the email address and the password are correct."
100 ))]
101 CannotLogin = 60,
102
103 #[strum(props(fallback = "Location streaming enabled."))]
104 MsgLocationEnabled = 64,
105
106 #[strum(props(fallback = "Location streaming disabled."))]
107 MsgLocationDisabled = 65,
108
109 #[strum(props(fallback = "Location"))]
110 Location = 66,
111
112 #[strum(props(fallback = "Sticker"))]
113 Sticker = 67,
114
115 #[strum(props(fallback = "Device messages"))]
116 DeviceMessages = 68,
117
118 #[strum(props(fallback = "Saved messages"))]
119 SavedMessages = 69,
120
121 #[strum(props(
122 fallback = "Messages in this chat are generated locally by your Delta Chat app. \
123 Its makers use it to inform about app updates and problems during usage."
124 ))]
125 DeviceMessagesHint = 70,
126
127 #[strum(props(fallback = "Welcome to Delta Chat! – \
128 Delta Chat looks and feels like other popular messenger apps, \
129 but does not involve centralized control, \
130 tracking or selling you, friends, colleagues or family out to large organizations.\n\n\
131 Technically, Delta Chat is an email application with a modern chat interface. \
132 Email in a new dress if you will 👻\n\n\
133 Use Delta Chat with anyone out of billions of people: just use their e-mail address. \
134 Recipients don't need to install Delta Chat, visit websites or sign up anywhere - \
135 however, of course, if they like, you may point them to 👉 https://get.delta.chat"))]
136 WelcomeMessage = 71,
137
138 #[strum(props(fallback = "Unknown sender for this chat."))]
139 UnknownSenderForChat = 72,
140
141 #[strum(props(fallback = "Message from %1$s"))]
142 SubjectForNewContact = 73,
143
144 #[strum(props(fallback = "Failed to send message to %1$s."))]
146 FailedSendingTo = 74,
147
148 #[strum(props(fallback = "Video chat invitation"))]
149 VideochatInvitation = 82,
150
151 #[strum(props(fallback = "You are invited to a video chat, click %1$s to join."))]
152 VideochatInviteMsgBody = 83,
153
154 #[strum(props(fallback = "Error:\n\n“%1$s”"))]
155 ConfigurationFailed = 84,
156
157 #[strum(props(
158 fallback = "⚠️ Date or time of your device seem to be inaccurate (%1$s).\n\n\
159 Adjust your clock ⏰🔧 to ensure your messages are received correctly."
160 ))]
161 BadTimeMsgBody = 85,
162
163 #[strum(props(fallback = "⚠️ Your Delta Chat version might be outdated.\n\n\
164 This may cause problems because your chat partners use newer versions - \
165 and you are missing the latest features 😳\n\
166 Please check https://get.delta.chat or your app store for updates."))]
167 UpdateReminderMsgBody = 86,
168
169 #[strum(props(
170 fallback = "Could not find your mail server.\n\nPlease check your internet connection."
171 ))]
172 ErrorNoNetwork = 87,
173
174 #[strum(props(fallback = "Reply"))]
176 ReplyNoun = 90,
177
178 #[strum(props(fallback = "You deleted the \"Saved messages\" chat.\n\n\
179 To use the \"Saved messages\" feature again, create a new chat with yourself."))]
180 SelfDeletedMsgBody = 91,
181
182 #[strum(props(
183 fallback = "⚠️ The \"Delete messages from server\" feature now also deletes messages in folders other than Inbox, DeltaChat and Sent.\n\n\
184 ℹ️ To avoid accidentally deleting messages, we turned it off for you. Please turn it on again at \
185 Settings → \"Chats and Media\" → \"Delete messages from server\" to continue using it."
186 ))]
187 DeleteServerTurnedOff = 92,
188
189 #[strum(props(fallback = "Forwarded"))]
190 Forwarded = 97,
191
192 #[strum(props(
193 fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
194 You may not be able to receive message when the storage is 100%% used.\n\n\
195 👉 Please check if you can delete old data in the provider's webinterface \
196 and consider to enable \"Settings / Delete Old Messages\". \
197 You can check your current storage usage anytime at \"Settings / Connectivity\"."
198 ))]
199 QuotaExceedingMsgBody = 98,
200
201 #[strum(props(fallback = "%1$s message"))]
202 PartialDownloadMsgBody = 99,
203
204 #[strum(props(fallback = "Download maximum available until %1$s"))]
205 DownloadAvailability = 100,
206
207 #[strum(props(fallback = "Multi Device Synchronization"))]
208 SyncMsgSubject = 101,
209
210 #[strum(props(
211 fallback = "This message is used to synchronize data between your devices.\n\n\
212 👉 If you see this message in Delta Chat, please update your Delta Chat apps on all devices."
213 ))]
214 SyncMsgBody = 102,
215
216 #[strum(props(fallback = "Incoming Messages"))]
217 IncomingMessages = 103,
218
219 #[strum(props(fallback = "Outgoing Messages"))]
220 OutgoingMessages = 104,
221
222 #[strum(props(fallback = "Storage on %1$s"))]
223 StorageOnDomain = 105,
224
225 #[strum(props(fallback = "Connected"))]
226 Connected = 107,
227
228 #[strum(props(fallback = "Connecting…"))]
229 Connecting = 108,
230
231 #[strum(props(fallback = "Updating…"))]
232 Updating = 109,
233
234 #[strum(props(fallback = "Sending…"))]
235 Sending = 110,
236
237 #[strum(props(fallback = "Your last message was sent successfully."))]
238 LastMsgSentSuccessfully = 111,
239
240 #[strum(props(fallback = "Error: %1$s"))]
241 Error = 112,
242
243 #[strum(props(fallback = "Not supported by your provider."))]
244 NotSupportedByProvider = 113,
245
246 #[strum(props(fallback = "Messages"))]
247 Messages = 114,
248
249 #[strum(props(fallback = "Broadcast List"))]
250 BroadcastList = 115,
251
252 #[strum(props(fallback = "%1$s of %2$s used"))]
253 PartOfTotallUsed = 116,
254
255 #[strum(props(fallback = "%1$s invited you to join this group.\n\n\
256 Waiting for the device of %2$s to reply…"))]
257 SecureJoinStarted = 117,
258
259 #[strum(props(fallback = "%1$s replied, waiting for being added to the group…"))]
260 SecureJoinReplies = 118,
261
262 #[strum(props(fallback = "Scan to chat with %1$s"))]
263 SetupContactQRDescription = 119,
264
265 #[strum(props(fallback = "Scan to join group %1$s"))]
266 SecureJoinGroupQRDescription = 120,
267
268 #[strum(props(fallback = "Not connected"))]
269 NotConnected = 121,
270
271 #[strum(props(fallback = "%1$s changed their address from %2$s to %3$s"))]
272 AeapAddrChanged = 122,
273
274 #[strum(props(
275 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."
276 ))]
277 AeapExplanationAndLink = 123,
278
279 #[strum(props(fallback = "You changed group name from \"%1$s\" to \"%2$s\"."))]
280 MsgYouChangedGrpName = 124,
281
282 #[strum(props(fallback = "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."))]
283 MsgGrpNameChangedBy = 125,
284
285 #[strum(props(fallback = "You changed the group image."))]
286 MsgYouChangedGrpImg = 126,
287
288 #[strum(props(fallback = "Group image changed by %1$s."))]
289 MsgGrpImgChangedBy = 127,
290
291 #[strum(props(fallback = "You added member %1$s."))]
292 MsgYouAddMember = 128,
293
294 #[strum(props(fallback = "Member %1$s added by %2$s."))]
295 MsgAddMemberBy = 129,
296
297 #[strum(props(fallback = "You removed member %1$s."))]
298 MsgYouDelMember = 130,
299
300 #[strum(props(fallback = "Member %1$s removed by %2$s."))]
301 MsgDelMemberBy = 131,
302
303 #[strum(props(fallback = "You left the group."))]
304 MsgYouLeftGroup = 132,
305
306 #[strum(props(fallback = "Group left by %1$s."))]
307 MsgGroupLeftBy = 133,
308
309 #[strum(props(fallback = "You deleted the group image."))]
310 MsgYouDeletedGrpImg = 134,
311
312 #[strum(props(fallback = "Group image deleted by %1$s."))]
313 MsgGrpImgDeletedBy = 135,
314
315 #[strum(props(fallback = "You enabled location streaming."))]
316 MsgYouEnabledLocation = 136,
317
318 #[strum(props(fallback = "Location streaming enabled by %1$s."))]
319 MsgLocationEnabledBy = 137,
320
321 #[strum(props(fallback = "You disabled message deletion timer."))]
322 MsgYouDisabledEphemeralTimer = 138,
323
324 #[strum(props(fallback = "Message deletion timer is disabled by %1$s."))]
325 MsgEphemeralTimerDisabledBy = 139,
326
327 #[strum(props(fallback = "You set message deletion timer to %1$s s."))]
330 MsgYouEnabledEphemeralTimer = 140,
331
332 #[strum(props(fallback = "Message deletion timer is set to %1$s s by %2$s."))]
333 MsgEphemeralTimerEnabledBy = 141,
334
335 #[strum(props(fallback = "You set message deletion timer to 1 minute."))]
336 MsgYouEphemeralTimerMinute = 142,
337
338 #[strum(props(fallback = "Message deletion timer is set to 1 minute by %1$s."))]
339 MsgEphemeralTimerMinuteBy = 143,
340
341 #[strum(props(fallback = "You set message deletion timer to 1 hour."))]
342 MsgYouEphemeralTimerHour = 144,
343
344 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
345 MsgEphemeralTimerHourBy = 145,
346
347 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
348 MsgYouEphemeralTimerDay = 146,
349
350 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
351 MsgEphemeralTimerDayBy = 147,
352
353 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
354 MsgYouEphemeralTimerWeek = 148,
355
356 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
357 MsgEphemeralTimerWeekBy = 149,
358
359 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
360 MsgYouEphemeralTimerMinutes = 150,
361
362 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
363 MsgEphemeralTimerMinutesBy = 151,
364
365 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
366 MsgYouEphemeralTimerHours = 152,
367
368 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
369 MsgEphemeralTimerHoursBy = 153,
370
371 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
372 MsgYouEphemeralTimerDays = 154,
373
374 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
375 MsgEphemeralTimerDaysBy = 155,
376
377 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
378 MsgYouEphemeralTimerWeeks = 156,
379
380 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
381 MsgEphemeralTimerWeeksBy = 157,
382
383 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
384 BackupTransferQr = 162,
385
386 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
387 BackupTransferMsgBody = 163,
388
389 #[strum(props(fallback = "I added member %1$s."))]
390 MsgIAddMember = 164,
391
392 #[strum(props(fallback = "I removed member %1$s."))]
393 MsgIDelMember = 165,
394
395 #[strum(props(fallback = "I left the group."))]
396 MsgILeftGroup = 166,
397
398 #[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))]
399 ChatProtectionEnabled = 170,
400
401 #[strum(props(fallback = "%1$s sent a message from another device."))]
402 ChatProtectionDisabled = 171,
403
404 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
405 NewGroupSendFirstMessage = 172,
406
407 #[strum(props(fallback = "Member %1$s added."))]
408 MsgAddMember = 173,
409
410 #[strum(props(
411 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
412 ))]
413 InvalidUnencryptedMail = 174,
414
415 #[strum(props(
416 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."
417 ))]
418 CantDecryptOutgoingMsgs = 175,
419
420 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
421 MsgYouReacted = 176,
422
423 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
424 MsgReactedBy = 177,
425
426 #[strum(props(fallback = "Member %1$s removed."))]
427 MsgDelMember = 178,
428
429 #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
430 SecurejoinWait = 190,
431
432 #[strum(props(
433 fallback = "The contact must be online to proceed.\n\nThis process will continue automatically in background."
434 ))]
435 SecurejoinTakesLonger = 192,
436}
437
438impl StockMessage {
439 fn fallback(self) -> &'static str {
443 self.get_str("fallback").unwrap_or_default()
444 }
445}
446
447impl Default for StockStrings {
448 fn default() -> Self {
449 StockStrings::new()
450 }
451}
452
453impl StockStrings {
454 pub fn new() -> Self {
456 Self {
457 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
458 }
459 }
460
461 async fn translated(&self, id: StockMessage) -> String {
462 self.translated_stockstrings
463 .read()
464 .await
465 .get(&(id as usize))
466 .map(AsRef::as_ref)
467 .unwrap_or_else(|| id.fallback())
468 .to_string()
469 }
470
471 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
472 if stockstring.contains("%1") && !id.fallback().contains("%1") {
473 bail!(
474 "translation {} contains invalid %1 placeholder, default is {}",
475 stockstring,
476 id.fallback()
477 );
478 }
479 if stockstring.contains("%2") && !id.fallback().contains("%2") {
480 bail!(
481 "translation {} contains invalid %2 placeholder, default is {}",
482 stockstring,
483 id.fallback()
484 );
485 }
486 self.translated_stockstrings
487 .write()
488 .await
489 .insert(id as usize, stockstring);
490 Ok(())
491 }
492}
493
494async fn translated(context: &Context, id: StockMessage) -> String {
495 context.translated_stockstrings.translated(id).await
496}
497
498trait StockStringMods: AsRef<str> + Sized {
500 fn replace1(&self, replacement: &str) -> String {
502 self.as_ref()
503 .replacen("%1$s", replacement, 1)
504 .replacen("%1$d", replacement, 1)
505 .replacen("%1$@", replacement, 1)
506 }
507
508 fn replace2(&self, replacement: &str) -> String {
513 self.as_ref()
514 .replacen("%2$s", replacement, 1)
515 .replacen("%2$d", replacement, 1)
516 .replacen("%2$@", replacement, 1)
517 }
518
519 fn replace3(&self, replacement: &str) -> String {
524 self.as_ref()
525 .replacen("%3$s", replacement, 1)
526 .replacen("%3$d", replacement, 1)
527 .replacen("%3$@", replacement, 1)
528 }
529}
530
531impl ContactId {
532 async fn get_stock_name(self, context: &Context) -> String {
534 Contact::get_by_id(context, self)
535 .await
536 .map(|contact| contact.get_display_name().to_string())
537 .unwrap_or_else(|_| self.to_string())
538 }
539}
540
541impl StockStringMods for String {}
542
543pub(crate) async fn no_messages(context: &Context) -> String {
545 translated(context, StockMessage::NoMessages).await
546}
547
548pub(crate) async fn self_msg(context: &Context) -> String {
550 translated(context, StockMessage::SelfMsg).await
551}
552
553pub(crate) async fn draft(context: &Context) -> String {
555 translated(context, StockMessage::Draft).await
556}
557
558pub(crate) async fn voice_message(context: &Context) -> String {
560 translated(context, StockMessage::VoiceMessage).await
561}
562
563pub(crate) async fn image(context: &Context) -> String {
565 translated(context, StockMessage::Image).await
566}
567
568pub(crate) async fn video(context: &Context) -> String {
570 translated(context, StockMessage::Video).await
571}
572
573pub(crate) async fn audio(context: &Context) -> String {
575 translated(context, StockMessage::Audio).await
576}
577
578pub(crate) async fn file(context: &Context) -> String {
580 translated(context, StockMessage::File).await
581}
582
583pub(crate) async fn msg_grp_name(
585 context: &Context,
586 from_group: &str,
587 to_group: &str,
588 by_contact: ContactId,
589) -> String {
590 if by_contact == ContactId::SELF {
591 translated(context, StockMessage::MsgYouChangedGrpName)
592 .await
593 .replace1(from_group)
594 .replace2(to_group)
595 } else {
596 translated(context, StockMessage::MsgGrpNameChangedBy)
597 .await
598 .replace1(from_group)
599 .replace2(to_group)
600 .replace3(&by_contact.get_stock_name(context).await)
601 }
602}
603
604pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
605 if by_contact == ContactId::SELF {
606 translated(context, StockMessage::MsgYouChangedGrpImg).await
607 } else {
608 translated(context, StockMessage::MsgGrpImgChangedBy)
609 .await
610 .replace1(&by_contact.get_stock_name(context).await)
611 }
612}
613
614pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
620 let addr = added_member_addr;
621 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
622 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
623 .await
624 .map(|contact| contact.get_authname_or_addr())
625 .unwrap_or_else(|_| addr.to_string()),
626 _ => addr.to_string(),
627 };
628 translated(context, StockMessage::MsgIAddMember)
629 .await
630 .replace1(whom)
631}
632
633pub(crate) async fn msg_add_member_local(
638 context: &Context,
639 added_member_addr: &str,
640 by_contact: ContactId,
641) -> String {
642 let addr = added_member_addr;
643 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
644 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
645 .await
646 .map(|contact| contact.get_display_name().to_string())
647 .unwrap_or_else(|_| addr.to_string()),
648 _ => addr.to_string(),
649 };
650 if by_contact == ContactId::UNDEFINED {
651 translated(context, StockMessage::MsgAddMember)
652 .await
653 .replace1(whom)
654 } else if by_contact == ContactId::SELF {
655 translated(context, StockMessage::MsgYouAddMember)
656 .await
657 .replace1(whom)
658 } else {
659 translated(context, StockMessage::MsgAddMemberBy)
660 .await
661 .replace1(whom)
662 .replace2(&by_contact.get_stock_name(context).await)
663 }
664}
665
666pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
671 let addr = removed_member_addr;
672 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
673 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
674 .await
675 .map(|contact| contact.get_authname_or_addr())
676 .unwrap_or_else(|_| addr.to_string()),
677 _ => addr.to_string(),
678 };
679 translated(context, StockMessage::MsgIDelMember)
680 .await
681 .replace1(whom)
682}
683
684pub(crate) async fn msg_del_member_local(
689 context: &Context,
690 removed_member_addr: &str,
691 by_contact: ContactId,
692) -> String {
693 let addr = removed_member_addr;
694 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
695 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
696 .await
697 .map(|contact| contact.get_display_name().to_string())
698 .unwrap_or_else(|_| addr.to_string()),
699 _ => addr.to_string(),
700 };
701 if by_contact == ContactId::UNDEFINED {
702 translated(context, StockMessage::MsgDelMember)
703 .await
704 .replace1(whom)
705 } else if by_contact == ContactId::SELF {
706 translated(context, StockMessage::MsgYouDelMember)
707 .await
708 .replace1(whom)
709 } else {
710 translated(context, StockMessage::MsgDelMemberBy)
711 .await
712 .replace1(whom)
713 .replace2(&by_contact.get_stock_name(context).await)
714 }
715}
716
717pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
719 translated(context, StockMessage::MsgILeftGroup).await
720}
721
722pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
724 if by_contact == ContactId::SELF {
725 translated(context, StockMessage::MsgYouLeftGroup).await
726 } else {
727 translated(context, StockMessage::MsgGroupLeftBy)
728 .await
729 .replace1(&by_contact.get_stock_name(context).await)
730 }
731}
732
733pub(crate) async fn msg_reacted(
735 context: &Context,
736 by_contact: ContactId,
737 reaction: &str,
738 summary: &str,
739) -> String {
740 if by_contact == ContactId::SELF {
741 translated(context, StockMessage::MsgYouReacted)
742 .await
743 .replace1(reaction)
744 .replace2(summary)
745 } else {
746 translated(context, StockMessage::MsgReactedBy)
747 .await
748 .replace1(&by_contact.get_stock_name(context).await)
749 .replace2(reaction)
750 .replace3(summary)
751 }
752}
753
754pub(crate) async fn gif(context: &Context) -> String {
756 translated(context, StockMessage::Gif).await
757}
758
759pub(crate) async fn e2e_available(context: &Context) -> String {
761 translated(context, StockMessage::E2eAvailable).await
762}
763
764pub(crate) async fn encr_none(context: &Context) -> String {
766 translated(context, StockMessage::EncrNone).await
767}
768
769pub(crate) async fn cant_decrypt_msg_body(context: &Context) -> String {
771 translated(context, StockMessage::CantDecryptMsgBody).await
772}
773
774pub(crate) async fn cant_decrypt_outgoing_msgs(context: &Context) -> String {
776 translated(context, StockMessage::CantDecryptOutgoingMsgs).await
777}
778
779pub(crate) async fn finger_prints(context: &Context) -> String {
781 translated(context, StockMessage::FingerPrints).await
782}
783
784pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
786 if by_contact == ContactId::SELF {
787 translated(context, StockMessage::MsgYouDeletedGrpImg).await
788 } else {
789 translated(context, StockMessage::MsgGrpImgDeletedBy)
790 .await
791 .replace1(&by_contact.get_stock_name(context).await)
792 }
793}
794
795pub(crate) async fn e2e_preferred(context: &Context) -> String {
797 translated(context, StockMessage::E2ePreferred).await
798}
799
800pub(crate) async fn secure_join_started(
802 context: &Context,
803 inviter_contact_id: ContactId,
804) -> String {
805 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
806 translated(context, StockMessage::SecureJoinStarted)
807 .await
808 .replace1(contact.get_display_name())
809 .replace2(contact.get_display_name())
810 } else {
811 format!("secure_join_started: unknown contact {inviter_contact_id}")
812 }
813}
814
815pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
817 translated(context, StockMessage::SecureJoinReplies)
818 .await
819 .replace1(&contact_id.get_stock_name(context).await)
820}
821
822pub(crate) async fn securejoin_wait(context: &Context) -> String {
824 translated(context, StockMessage::SecurejoinWait).await
825}
826
827pub(crate) async fn securejoin_takes_longer(context: &Context) -> String {
829 translated(context, StockMessage::SecurejoinTakesLonger).await
830}
831
832pub(crate) async fn setup_contact_qr_description(
834 context: &Context,
835 display_name: &str,
836 addr: &str,
837) -> String {
838 let name = if display_name.is_empty() {
839 addr.to_owned()
840 } else {
841 display_name.to_owned()
842 };
843 translated(context, StockMessage::SetupContactQRDescription)
844 .await
845 .replace1(&name)
846}
847
848pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
850 translated(context, StockMessage::SecureJoinGroupQRDescription)
851 .await
852 .replace1(chat.get_name())
853}
854
855#[allow(dead_code)]
857pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
858 let addr = contact.get_display_name();
859 translated(context, StockMessage::ContactVerified)
860 .await
861 .replace1(addr)
862}
863
864pub(crate) async fn contact_not_verified(context: &Context, contact: &Contact) -> String {
866 let addr = contact.get_display_name();
867 translated(context, StockMessage::ContactNotVerified)
868 .await
869 .replace1(addr)
870}
871
872pub(crate) async fn contact_setup_changed(context: &Context, contact_addr: &str) -> String {
874 translated(context, StockMessage::ContactSetupChanged)
875 .await
876 .replace1(contact_addr)
877}
878
879pub(crate) async fn archived_chats(context: &Context) -> String {
881 translated(context, StockMessage::ArchivedChats).await
882}
883
884pub(crate) async fn sync_msg_subject(context: &Context) -> String {
886 translated(context, StockMessage::SyncMsgSubject).await
887}
888
889pub(crate) async fn sync_msg_body(context: &Context) -> String {
891 translated(context, StockMessage::SyncMsgBody).await
892}
893
894pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
896 translated(context, StockMessage::CannotLogin)
897 .await
898 .replace1(user)
899}
900
901pub(crate) async fn msg_location_enabled(context: &Context) -> String {
903 translated(context, StockMessage::MsgLocationEnabled).await
904}
905
906pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
908 if contact == ContactId::SELF {
909 translated(context, StockMessage::MsgYouEnabledLocation).await
910 } else {
911 translated(context, StockMessage::MsgLocationEnabledBy)
912 .await
913 .replace1(&contact.get_stock_name(context).await)
914 }
915}
916
917pub(crate) async fn msg_location_disabled(context: &Context) -> String {
919 translated(context, StockMessage::MsgLocationDisabled).await
920}
921
922pub(crate) async fn location(context: &Context) -> String {
924 translated(context, StockMessage::Location).await
925}
926
927pub(crate) async fn sticker(context: &Context) -> String {
929 translated(context, StockMessage::Sticker).await
930}
931
932pub(crate) async fn device_messages(context: &Context) -> String {
934 translated(context, StockMessage::DeviceMessages).await
935}
936
937pub(crate) async fn saved_messages(context: &Context) -> String {
939 translated(context, StockMessage::SavedMessages).await
940}
941
942pub(crate) async fn device_messages_hint(context: &Context) -> String {
944 translated(context, StockMessage::DeviceMessagesHint).await
945}
946
947pub(crate) async fn welcome_message(context: &Context) -> String {
949 translated(context, StockMessage::WelcomeMessage).await
950}
951
952pub(crate) async fn unknown_sender_for_chat(context: &Context) -> String {
954 translated(context, StockMessage::UnknownSenderForChat).await
955}
956
957pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
960 translated(context, StockMessage::SubjectForNewContact)
961 .await
962 .replace1(self_name)
963}
964
965pub(crate) async fn msg_ephemeral_timer_disabled(
967 context: &Context,
968 by_contact: ContactId,
969) -> String {
970 if by_contact == ContactId::SELF {
971 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
972 } else {
973 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
974 .await
975 .replace1(&by_contact.get_stock_name(context).await)
976 }
977}
978
979pub(crate) async fn msg_ephemeral_timer_enabled(
981 context: &Context,
982 timer: &str,
983 by_contact: ContactId,
984) -> String {
985 if by_contact == ContactId::SELF {
986 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
987 .await
988 .replace1(timer)
989 } else {
990 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
991 .await
992 .replace1(timer)
993 .replace2(&by_contact.get_stock_name(context).await)
994 }
995}
996
997pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: ContactId) -> String {
999 if by_contact == ContactId::SELF {
1000 translated(context, StockMessage::MsgYouEphemeralTimerMinute).await
1001 } else {
1002 translated(context, StockMessage::MsgEphemeralTimerMinuteBy)
1003 .await
1004 .replace1(&by_contact.get_stock_name(context).await)
1005 }
1006}
1007
1008pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1010 if by_contact == ContactId::SELF {
1011 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1012 } else {
1013 translated(context, StockMessage::MsgEphemeralTimerHourBy)
1014 .await
1015 .replace1(&by_contact.get_stock_name(context).await)
1016 }
1017}
1018
1019pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1021 if by_contact == ContactId::SELF {
1022 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1023 } else {
1024 translated(context, StockMessage::MsgEphemeralTimerDayBy)
1025 .await
1026 .replace1(&by_contact.get_stock_name(context).await)
1027 }
1028}
1029
1030pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1032 if by_contact == ContactId::SELF {
1033 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1034 } else {
1035 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1036 .await
1037 .replace1(&by_contact.get_stock_name(context).await)
1038 }
1039}
1040
1041pub(crate) async fn videochat_invitation(context: &Context) -> String {
1043 translated(context, StockMessage::VideochatInvitation).await
1044}
1045
1046pub(crate) async fn videochat_invite_msg_body(context: &Context, url: &str) -> String {
1048 translated(context, StockMessage::VideochatInviteMsgBody)
1049 .await
1050 .replace1(url)
1051}
1052
1053pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1055 translated(context, StockMessage::ConfigurationFailed)
1056 .await
1057 .replace1(details)
1058}
1059
1060pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1063 translated(context, StockMessage::BadTimeMsgBody)
1064 .await
1065 .replace1(now)
1066}
1067
1068pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1070 translated(context, StockMessage::UpdateReminderMsgBody).await
1071}
1072
1073pub(crate) async fn error_no_network(context: &Context) -> String {
1075 translated(context, StockMessage::ErrorNoNetwork).await
1076}
1077
1078pub(crate) async fn chat_protection_enabled(context: &Context) -> String {
1080 translated(context, StockMessage::ChatProtectionEnabled).await
1081}
1082
1083pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String {
1085 translated(context, StockMessage::ChatProtectionDisabled)
1086 .await
1087 .replace1(&contact_id.get_stock_name(context).await)
1088}
1089
1090pub(crate) async fn reply_noun(context: &Context) -> String {
1092 translated(context, StockMessage::ReplyNoun).await
1093}
1094
1095pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1097 translated(context, StockMessage::SelfDeletedMsgBody).await
1098}
1099
1100pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1102 translated(context, StockMessage::DeleteServerTurnedOff).await
1103}
1104
1105pub(crate) async fn msg_ephemeral_timer_minutes(
1107 context: &Context,
1108 minutes: &str,
1109 by_contact: ContactId,
1110) -> String {
1111 if by_contact == ContactId::SELF {
1112 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1113 .await
1114 .replace1(minutes)
1115 } else {
1116 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1117 .await
1118 .replace1(minutes)
1119 .replace2(&by_contact.get_stock_name(context).await)
1120 }
1121}
1122
1123pub(crate) async fn msg_ephemeral_timer_hours(
1125 context: &Context,
1126 hours: &str,
1127 by_contact: ContactId,
1128) -> String {
1129 if by_contact == ContactId::SELF {
1130 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1131 .await
1132 .replace1(hours)
1133 } else {
1134 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1135 .await
1136 .replace1(hours)
1137 .replace2(&by_contact.get_stock_name(context).await)
1138 }
1139}
1140
1141pub(crate) async fn msg_ephemeral_timer_days(
1143 context: &Context,
1144 days: &str,
1145 by_contact: ContactId,
1146) -> String {
1147 if by_contact == ContactId::SELF {
1148 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1149 .await
1150 .replace1(days)
1151 } else {
1152 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1153 .await
1154 .replace1(days)
1155 .replace2(&by_contact.get_stock_name(context).await)
1156 }
1157}
1158
1159pub(crate) async fn msg_ephemeral_timer_weeks(
1161 context: &Context,
1162 weeks: &str,
1163 by_contact: ContactId,
1164) -> String {
1165 if by_contact == ContactId::SELF {
1166 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1167 .await
1168 .replace1(weeks)
1169 } else {
1170 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1171 .await
1172 .replace1(weeks)
1173 .replace2(&by_contact.get_stock_name(context).await)
1174 }
1175}
1176
1177pub(crate) async fn forwarded(context: &Context) -> String {
1179 translated(context, StockMessage::Forwarded).await
1180}
1181
1182pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1184 translated(context, StockMessage::QuotaExceedingMsgBody)
1185 .await
1186 .replace1(&format!("{highest_usage}"))
1187 .replace("%%", "%")
1188}
1189
1190pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1192 let size = &format_size(org_bytes, BINARY);
1193 translated(context, StockMessage::PartialDownloadMsgBody)
1194 .await
1195 .replace1(size)
1196}
1197
1198pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1200 translated(context, StockMessage::DownloadAvailability)
1201 .await
1202 .replace1(×tamp_to_str(timestamp))
1203}
1204
1205pub(crate) async fn incoming_messages(context: &Context) -> String {
1207 translated(context, StockMessage::IncomingMessages).await
1208}
1209
1210pub(crate) async fn outgoing_messages(context: &Context) -> String {
1212 translated(context, StockMessage::OutgoingMessages).await
1213}
1214
1215pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1218 translated(context, StockMessage::StorageOnDomain)
1219 .await
1220 .replace1(domain)
1221}
1222
1223pub(crate) async fn not_connected(context: &Context) -> String {
1225 translated(context, StockMessage::NotConnected).await
1226}
1227
1228pub(crate) async fn connected(context: &Context) -> String {
1230 translated(context, StockMessage::Connected).await
1231}
1232
1233pub(crate) async fn connecting(context: &Context) -> String {
1235 translated(context, StockMessage::Connecting).await
1236}
1237
1238pub(crate) async fn updating(context: &Context) -> String {
1240 translated(context, StockMessage::Updating).await
1241}
1242
1243pub(crate) async fn sending(context: &Context) -> String {
1245 translated(context, StockMessage::Sending).await
1246}
1247
1248pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1250 translated(context, StockMessage::LastMsgSentSuccessfully).await
1251}
1252
1253pub(crate) async fn error(context: &Context, error: &str) -> String {
1256 translated(context, StockMessage::Error)
1257 .await
1258 .replace1(error)
1259}
1260
1261pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1263 translated(context, StockMessage::NotSupportedByProvider).await
1264}
1265
1266pub(crate) async fn messages(context: &Context) -> String {
1269 translated(context, StockMessage::Messages).await
1270}
1271
1272pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1274 translated(context, StockMessage::PartOfTotallUsed)
1275 .await
1276 .replace1(part)
1277 .replace2(total)
1278}
1279
1280pub(crate) async fn broadcast_list(context: &Context) -> String {
1283 translated(context, StockMessage::BroadcastList).await
1284}
1285
1286pub(crate) async fn aeap_addr_changed(
1288 context: &Context,
1289 contact_name: &str,
1290 old_addr: &str,
1291 new_addr: &str,
1292) -> String {
1293 translated(context, StockMessage::AeapAddrChanged)
1294 .await
1295 .replace1(contact_name)
1296 .replace2(old_addr)
1297 .replace3(new_addr)
1298}
1299
1300pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1302 translated(context, StockMessage::InvalidUnencryptedMail)
1303 .await
1304 .replace1(provider)
1305}
1306
1307pub(crate) async fn aeap_explanation_and_link(
1308 context: &Context,
1309 old_addr: &str,
1310 new_addr: &str,
1311) -> String {
1312 translated(context, StockMessage::AeapExplanationAndLink)
1313 .await
1314 .replace1(old_addr)
1315 .replace2(new_addr)
1316}
1317
1318pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1320 translated(context, StockMessage::NewGroupSendFirstMessage).await
1321}
1322
1323pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1330 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1331 name
1332 } else {
1333 context.get_primary_self_addr().await?
1334 };
1335 Ok(translated(context, StockMessage::BackupTransferQr)
1336 .await
1337 .replace1(&name))
1338}
1339
1340pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1341 translated(context, StockMessage::BackupTransferMsgBody).await
1342}
1343
1344impl Context {
1345 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1348 self.translated_stockstrings
1349 .set_stock_translation(id, stockstring)
1350 .await?;
1351 Ok(())
1352 }
1353
1354 pub(crate) async fn stock_protection_msg(
1356 &self,
1357 protect: ProtectionStatus,
1358 contact_id: Option<ContactId>,
1359 ) -> String {
1360 match protect {
1361 ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {
1362 if let Some(contact_id) = contact_id {
1363 chat_protection_disabled(self, contact_id).await
1364 } else {
1365 "[Error] No contact_id given".to_string()
1368 }
1369 }
1370 ProtectionStatus::Protected => chat_protection_enabled(self).await,
1371 }
1372 }
1373
1374 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1375 if self.get_config_bool(Config::Bot).await? {
1376 return Ok(());
1377 }
1378
1379 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1382 self.sql
1383 .set_raw_config_bool("self-chat-added", true)
1384 .await?;
1385 ChatId::create_for_contact(self, ContactId::SELF).await?;
1386 }
1387
1388 let image = include_bytes!("../assets/welcome-image.jpg");
1391 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1392 let mut msg = Message::new(Viewtype::Image);
1393 msg.param.set(Param::File, blob.as_name());
1394 msg.param.set(Param::Filename, "welcome-image.jpg");
1395 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1396
1397 let mut msg = Message::new_text(welcome_message(self).await);
1398 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1399 Ok(())
1400 }
1401}
1402
1403impl Accounts {
1404 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1407 self.stockstrings
1408 .set_stock_translation(id, stockstring)
1409 .await?;
1410 Ok(())
1411 }
1412}
1413
1414#[cfg(test)]
1415mod stock_str_tests;