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