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 hour."))]
306 MsgYouEphemeralTimerHour = 144,
307
308 #[strum(props(fallback = "Message deletion timer is set to 1 hour by %1$s."))]
309 MsgEphemeralTimerHourBy = 145,
310
311 #[strum(props(fallback = "You set message deletion timer to 1 day."))]
312 MsgYouEphemeralTimerDay = 146,
313
314 #[strum(props(fallback = "Message deletion timer is set to 1 day by %1$s."))]
315 MsgEphemeralTimerDayBy = 147,
316
317 #[strum(props(fallback = "You set message deletion timer to 1 week."))]
318 MsgYouEphemeralTimerWeek = 148,
319
320 #[strum(props(fallback = "Message deletion timer is set to 1 week by %1$s."))]
321 MsgEphemeralTimerWeekBy = 149,
322
323 #[strum(props(fallback = "You set message deletion timer to %1$s minutes."))]
324 MsgYouEphemeralTimerMinutes = 150,
325
326 #[strum(props(fallback = "Message deletion timer is set to %1$s minutes by %2$s."))]
327 MsgEphemeralTimerMinutesBy = 151,
328
329 #[strum(props(fallback = "You set message deletion timer to %1$s hours."))]
330 MsgYouEphemeralTimerHours = 152,
331
332 #[strum(props(fallback = "Message deletion timer is set to %1$s hours by %2$s."))]
333 MsgEphemeralTimerHoursBy = 153,
334
335 #[strum(props(fallback = "You set message deletion timer to %1$s days."))]
336 MsgYouEphemeralTimerDays = 154,
337
338 #[strum(props(fallback = "Message deletion timer is set to %1$s days by %2$s."))]
339 MsgEphemeralTimerDaysBy = 155,
340
341 #[strum(props(fallback = "You set message deletion timer to %1$s weeks."))]
342 MsgYouEphemeralTimerWeeks = 156,
343
344 #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))]
345 MsgEphemeralTimerWeeksBy = 157,
346
347 #[strum(props(fallback = "You set message deletion timer to 1 year."))]
348 MsgYouEphemeralTimerYear = 158,
349
350 #[strum(props(fallback = "Message deletion timer is set to 1 year by %1$s."))]
351 MsgEphemeralTimerYearBy = 159,
352
353 #[strum(props(fallback = "Scan to set up second device for %1$s"))]
354 BackupTransferQr = 162,
355
356 #[strum(props(fallback = "ℹ️ Account transferred to your second device."))]
357 BackupTransferMsgBody = 163,
358
359 #[strum(props(fallback = "I added member %1$s."))]
360 MsgIAddMember = 164,
361
362 #[strum(props(fallback = "I removed member %1$s."))]
363 MsgIDelMember = 165,
364
365 #[strum(props(fallback = "I left the group."))]
366 MsgILeftGroup = 166,
367
368 #[strum(props(fallback = "Messages are end-to-end encrypted."))]
369 ChatProtectionEnabled = 170,
370
371 #[strum(props(fallback = "%1$s sent a message from another device."))]
373 ChatProtectionDisabled = 171,
374
375 #[strum(props(fallback = "Others will only see this group after you sent a first message."))]
376 NewGroupSendFirstMessage = 172,
377
378 #[strum(props(fallback = "Member %1$s added."))]
379 MsgAddMember = 173,
380
381 #[strum(props(
382 fallback = "⚠️ Your email provider %1$s requires end-to-end encryption which is not setup yet."
383 ))]
384 InvalidUnencryptedMail = 174,
385
386 #[strum(props(fallback = "You reacted %1$s to \"%2$s\""))]
387 MsgYouReacted = 176,
388
389 #[strum(props(fallback = "%1$s reacted %2$s to \"%3$s\""))]
390 MsgReactedBy = 177,
391
392 #[strum(props(fallback = "Member %1$s removed."))]
393 MsgDelMember = 178,
394
395 #[strum(props(fallback = "Establishing guaranteed end-to-end encryption, please wait…"))]
396 SecurejoinWait = 190,
397
398 #[strum(props(fallback = "❤️ Seems you're enjoying Delta Chat!
399
400Please consider donating to help that Delta Chat stays free for everyone.
401
402While Delta Chat is free to use and open source, development costs money.
403Help keeping us to keep Delta Chat independent and make it more awesome in the future.
404
405https://delta.chat/donate"))]
406 DonationRequest = 193,
407
408 #[strum(props(fallback = "Outgoing call"))]
409 OutgoingCall = 194,
410
411 #[strum(props(fallback = "Incoming call"))]
412 IncomingCall = 195,
413
414 #[strum(props(fallback = "Declined call"))]
415 DeclinedCall = 196,
416
417 #[strum(props(fallback = "Canceled call"))]
418 CanceledCall = 197,
419
420 #[strum(props(fallback = "Missed call"))]
421 MissedCall = 198,
422
423 #[strum(props(fallback = "You left the channel."))]
424 MsgYouLeftBroadcast = 200,
425
426 #[strum(props(fallback = "Scan to join channel %1$s"))]
427 SecureJoinBrodcastQRDescription = 201,
428
429 #[strum(props(fallback = "You joined the channel."))]
430 MsgYouJoinedBroadcast = 202,
431
432 #[strum(props(
433 fallback = "The attachment contains anonymous usage statistics, which helps us improve Delta Chat. Thank you!"
434 ))]
435 StatsMsgBody = 210,
436
437 #[strum(props(fallback = "Proxy Enabled"))]
438 ProxyEnabled = 220,
439
440 #[strum(props(
441 fallback = "You are using a proxy. If you're having trouble connecting, try a different proxy."
442 ))]
443 ProxyEnabledDescription = 221,
444
445 #[strum(props(fallback = "Messages in this chat use classic email and are not encrypted."))]
446 ChatUnencryptedExplanation = 230,
447}
448
449impl StockMessage {
450 fn fallback(self) -> &'static str {
454 self.get_str("fallback").unwrap_or_default()
455 }
456}
457
458impl Default for StockStrings {
459 fn default() -> Self {
460 StockStrings::new()
461 }
462}
463
464impl StockStrings {
465 pub fn new() -> Self {
467 Self {
468 translated_stockstrings: Arc::new(RwLock::new(Default::default())),
469 }
470 }
471
472 async fn translated(&self, id: StockMessage) -> String {
473 self.translated_stockstrings
474 .read()
475 .await
476 .get(&(id as usize))
477 .map(AsRef::as_ref)
478 .unwrap_or_else(|| id.fallback())
479 .to_string()
480 }
481
482 async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
483 if stockstring.contains("%1") && !id.fallback().contains("%1") {
484 bail!(
485 "translation {} contains invalid %1 placeholder, default is {}",
486 stockstring,
487 id.fallback()
488 );
489 }
490 if stockstring.contains("%2") && !id.fallback().contains("%2") {
491 bail!(
492 "translation {} contains invalid %2 placeholder, default is {}",
493 stockstring,
494 id.fallback()
495 );
496 }
497 self.translated_stockstrings
498 .write()
499 .await
500 .insert(id as usize, stockstring);
501 Ok(())
502 }
503}
504
505async fn translated(context: &Context, id: StockMessage) -> String {
506 context.translated_stockstrings.translated(id).await
507}
508
509trait StockStringMods: AsRef<str> + Sized {
511 fn replace1(&self, replacement: &str) -> String {
513 self.as_ref()
514 .replacen("%1$s", replacement, 1)
515 .replacen("%1$d", replacement, 1)
516 .replacen("%1$@", replacement, 1)
517 }
518
519 fn replace2(&self, replacement: &str) -> String {
524 self.as_ref()
525 .replacen("%2$s", replacement, 1)
526 .replacen("%2$d", replacement, 1)
527 .replacen("%2$@", replacement, 1)
528 }
529
530 fn replace3(&self, replacement: &str) -> String {
535 self.as_ref()
536 .replacen("%3$s", replacement, 1)
537 .replacen("%3$d", replacement, 1)
538 .replacen("%3$@", replacement, 1)
539 }
540}
541
542impl ContactId {
543 async fn get_stock_name(self, context: &Context) -> String {
545 Contact::get_by_id(context, self)
546 .await
547 .map(|contact| contact.get_display_name().to_string())
548 .unwrap_or_else(|_| self.to_string())
549 }
550}
551
552impl StockStringMods for String {}
553
554pub(crate) async fn no_messages(context: &Context) -> String {
556 translated(context, StockMessage::NoMessages).await
557}
558
559pub(crate) async fn self_msg(context: &Context) -> String {
561 translated(context, StockMessage::SelfMsg).await
562}
563
564pub(crate) async fn draft(context: &Context) -> String {
566 translated(context, StockMessage::Draft).await
567}
568
569pub(crate) async fn voice_message(context: &Context) -> String {
571 translated(context, StockMessage::VoiceMessage).await
572}
573
574pub(crate) async fn image(context: &Context) -> String {
576 translated(context, StockMessage::Image).await
577}
578
579pub(crate) async fn video(context: &Context) -> String {
581 translated(context, StockMessage::Video).await
582}
583
584pub(crate) async fn audio(context: &Context) -> String {
586 translated(context, StockMessage::Audio).await
587}
588
589pub(crate) async fn file(context: &Context) -> String {
591 translated(context, StockMessage::File).await
592}
593
594pub(crate) async fn msg_grp_name(
596 context: &Context,
597 from_group: &str,
598 to_group: &str,
599 by_contact: ContactId,
600) -> String {
601 if by_contact == ContactId::SELF {
602 translated(context, StockMessage::MsgYouChangedGrpName)
603 .await
604 .replace1(from_group)
605 .replace2(to_group)
606 } else {
607 translated(context, StockMessage::MsgGrpNameChangedBy)
608 .await
609 .replace1(from_group)
610 .replace2(to_group)
611 .replace3(&by_contact.get_stock_name(context).await)
612 }
613}
614
615pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId) -> String {
616 if by_contact == ContactId::SELF {
617 translated(context, StockMessage::MsgYouChangedGrpImg).await
618 } else {
619 translated(context, StockMessage::MsgGrpImgChangedBy)
620 .await
621 .replace1(&by_contact.get_stock_name(context).await)
622 }
623}
624
625pub(crate) async fn msg_add_member_remote(context: &Context, added_member_addr: &str) -> String {
631 let addr = added_member_addr;
632 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
633 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
634 .await
635 .map(|contact| contact.get_authname_or_addr())
636 .unwrap_or_else(|_| addr.to_string()),
637 _ => addr.to_string(),
638 };
639 translated(context, StockMessage::MsgIAddMember)
640 .await
641 .replace1(whom)
642}
643
644pub(crate) async fn msg_add_member_local(
649 context: &Context,
650 added_member: ContactId,
651 by_contact: ContactId,
652) -> String {
653 let whom = added_member.get_stock_name(context).await;
654 if by_contact == ContactId::UNDEFINED {
655 translated(context, StockMessage::MsgAddMember)
656 .await
657 .replace1(&whom)
658 } else if by_contact == ContactId::SELF {
659 translated(context, StockMessage::MsgYouAddMember)
660 .await
661 .replace1(&whom)
662 } else {
663 translated(context, StockMessage::MsgAddMemberBy)
664 .await
665 .replace1(&whom)
666 .replace2(&by_contact.get_stock_name(context).await)
667 }
668}
669
670pub(crate) async fn msg_del_member_remote(context: &Context, removed_member_addr: &str) -> String {
675 let addr = removed_member_addr;
676 let whom = &match Contact::lookup_id_by_addr(context, addr, Origin::Unknown).await {
677 Ok(Some(contact_id)) => Contact::get_by_id(context, contact_id)
678 .await
679 .map(|contact| contact.get_authname_or_addr())
680 .unwrap_or_else(|_| addr.to_string()),
681 _ => addr.to_string(),
682 };
683 translated(context, StockMessage::MsgIDelMember)
684 .await
685 .replace1(whom)
686}
687
688pub(crate) async fn msg_del_member_local(
693 context: &Context,
694 removed_member: ContactId,
695 by_contact: ContactId,
696) -> String {
697 let whom = removed_member.get_stock_name(context).await;
698 if by_contact == ContactId::UNDEFINED {
699 translated(context, StockMessage::MsgDelMember)
700 .await
701 .replace1(&whom)
702 } else if by_contact == ContactId::SELF {
703 translated(context, StockMessage::MsgYouDelMember)
704 .await
705 .replace1(&whom)
706 } else {
707 translated(context, StockMessage::MsgDelMemberBy)
708 .await
709 .replace1(&whom)
710 .replace2(&by_contact.get_stock_name(context).await)
711 }
712}
713
714pub(crate) async fn msg_group_left_remote(context: &Context) -> String {
716 translated(context, StockMessage::MsgILeftGroup).await
717}
718
719pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactId) -> String {
721 if by_contact == ContactId::SELF {
722 translated(context, StockMessage::MsgYouLeftGroup).await
723 } else {
724 translated(context, StockMessage::MsgGroupLeftBy)
725 .await
726 .replace1(&by_contact.get_stock_name(context).await)
727 }
728}
729
730pub(crate) async fn msg_you_left_broadcast(context: &Context) -> String {
732 translated(context, StockMessage::MsgYouLeftBroadcast).await
733}
734
735pub(crate) async fn msg_you_joined_broadcast(context: &Context) -> String {
737 translated(context, StockMessage::MsgYouJoinedBroadcast).await
738}
739
740pub(crate) async fn msg_reacted(
742 context: &Context,
743 by_contact: ContactId,
744 reaction: &str,
745 summary: &str,
746) -> String {
747 if by_contact == ContactId::SELF {
748 translated(context, StockMessage::MsgYouReacted)
749 .await
750 .replace1(reaction)
751 .replace2(summary)
752 } else {
753 translated(context, StockMessage::MsgReactedBy)
754 .await
755 .replace1(&by_contact.get_stock_name(context).await)
756 .replace2(reaction)
757 .replace3(summary)
758 }
759}
760
761pub(crate) async fn gif(context: &Context) -> String {
763 translated(context, StockMessage::Gif).await
764}
765
766pub(crate) async fn e2e_available(context: &Context) -> String {
768 translated(context, StockMessage::E2eAvailable).await
769}
770
771pub(crate) async fn encr_none(context: &Context) -> String {
773 translated(context, StockMessage::EncrNone).await
774}
775
776pub(crate) async fn finger_prints(context: &Context) -> String {
778 translated(context, StockMessage::FingerPrints).await
779}
780
781pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId) -> String {
783 if by_contact == ContactId::SELF {
784 translated(context, StockMessage::MsgYouDeletedGrpImg).await
785 } else {
786 translated(context, StockMessage::MsgGrpImgDeletedBy)
787 .await
788 .replace1(&by_contact.get_stock_name(context).await)
789 }
790}
791
792pub(crate) async fn secure_join_started(
794 context: &Context,
795 inviter_contact_id: ContactId,
796) -> String {
797 if let Ok(contact) = Contact::get_by_id(context, inviter_contact_id).await {
798 translated(context, StockMessage::SecureJoinStarted)
799 .await
800 .replace1(contact.get_display_name())
801 .replace2(contact.get_display_name())
802 } else {
803 format!("secure_join_started: unknown contact {inviter_contact_id}")
804 }
805}
806
807pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String {
809 translated(context, StockMessage::SecureJoinReplies)
810 .await
811 .replace1(&contact_id.get_stock_name(context).await)
812}
813
814pub(crate) async fn securejoin_wait(context: &Context) -> String {
816 translated(context, StockMessage::SecurejoinWait).await
817}
818
819pub(crate) async fn donation_request(context: &Context) -> String {
821 translated(context, StockMessage::DonationRequest).await
822}
823
824pub(crate) async fn outgoing_call(context: &Context) -> String {
826 translated(context, StockMessage::OutgoingCall).await
827}
828
829pub(crate) async fn incoming_call(context: &Context) -> String {
831 translated(context, StockMessage::IncomingCall).await
832}
833
834pub(crate) async fn declined_call(context: &Context) -> String {
836 translated(context, StockMessage::DeclinedCall).await
837}
838
839pub(crate) async fn canceled_call(context: &Context) -> String {
841 translated(context, StockMessage::CanceledCall).await
842}
843
844pub(crate) async fn missed_call(context: &Context) -> String {
846 translated(context, StockMessage::MissedCall).await
847}
848
849pub(crate) async fn setup_contact_qr_description(
851 context: &Context,
852 display_name: &str,
853 addr: &str,
854) -> String {
855 let name = if display_name.is_empty() {
856 addr.to_owned()
857 } else {
858 display_name.to_owned()
859 };
860 translated(context, StockMessage::SetupContactQRDescription)
861 .await
862 .replace1(&name)
863}
864
865pub(crate) async fn secure_join_group_qr_description(context: &Context, chat: &Chat) -> String {
867 translated(context, StockMessage::SecureJoinGroupQRDescription)
868 .await
869 .replace1(chat.get_name())
870}
871
872pub(crate) async fn secure_join_broadcast_qr_description(context: &Context, chat: &Chat) -> String {
874 translated(context, StockMessage::SecureJoinBrodcastQRDescription)
875 .await
876 .replace1(chat.get_name())
877}
878
879#[allow(dead_code)]
881pub(crate) async fn contact_verified(context: &Context, contact: &Contact) -> String {
882 let addr = contact.get_display_name();
883 translated(context, StockMessage::ContactVerified)
884 .await
885 .replace1(addr)
886}
887
888pub(crate) async fn archived_chats(context: &Context) -> String {
890 translated(context, StockMessage::ArchivedChats).await
891}
892
893pub(crate) async fn sync_msg_subject(context: &Context) -> String {
895 translated(context, StockMessage::SyncMsgSubject).await
896}
897
898pub(crate) async fn sync_msg_body(context: &Context) -> String {
900 translated(context, StockMessage::SyncMsgBody).await
901}
902
903pub(crate) async fn cannot_login(context: &Context, user: &str) -> String {
905 translated(context, StockMessage::CannotLogin)
906 .await
907 .replace1(user)
908}
909
910pub(crate) async fn msg_location_enabled(context: &Context) -> String {
912 translated(context, StockMessage::MsgLocationEnabled).await
913}
914
915pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactId) -> String {
917 if contact == ContactId::SELF {
918 translated(context, StockMessage::MsgYouEnabledLocation).await
919 } else {
920 translated(context, StockMessage::MsgLocationEnabledBy)
921 .await
922 .replace1(&contact.get_stock_name(context).await)
923 }
924}
925
926pub(crate) async fn msg_location_disabled(context: &Context) -> String {
928 translated(context, StockMessage::MsgLocationDisabled).await
929}
930
931pub(crate) async fn location(context: &Context) -> String {
933 translated(context, StockMessage::Location).await
934}
935
936pub(crate) async fn sticker(context: &Context) -> String {
938 translated(context, StockMessage::Sticker).await
939}
940
941pub(crate) async fn device_messages(context: &Context) -> String {
943 translated(context, StockMessage::DeviceMessages).await
944}
945
946pub(crate) async fn saved_messages(context: &Context) -> String {
948 translated(context, StockMessage::SavedMessages).await
949}
950
951pub(crate) async fn device_messages_hint(context: &Context) -> String {
953 translated(context, StockMessage::DeviceMessagesHint).await
954}
955
956pub(crate) async fn welcome_message(context: &Context) -> String {
958 translated(context, StockMessage::WelcomeMessage).await
959}
960
961pub(crate) async fn subject_for_new_contact(context: &Context, self_name: &str) -> String {
964 translated(context, StockMessage::SubjectForNewContact)
965 .await
966 .replace1(self_name)
967}
968
969pub(crate) async fn msg_ephemeral_timer_disabled(
971 context: &Context,
972 by_contact: ContactId,
973) -> String {
974 if by_contact == ContactId::SELF {
975 translated(context, StockMessage::MsgYouDisabledEphemeralTimer).await
976 } else {
977 translated(context, StockMessage::MsgEphemeralTimerDisabledBy)
978 .await
979 .replace1(&by_contact.get_stock_name(context).await)
980 }
981}
982
983pub(crate) async fn msg_ephemeral_timer_enabled(
985 context: &Context,
986 timer: &str,
987 by_contact: ContactId,
988) -> String {
989 if by_contact == ContactId::SELF {
990 translated(context, StockMessage::MsgYouEnabledEphemeralTimer)
991 .await
992 .replace1(timer)
993 } else {
994 translated(context, StockMessage::MsgEphemeralTimerEnabledBy)
995 .await
996 .replace1(timer)
997 .replace2(&by_contact.get_stock_name(context).await)
998 }
999}
1000
1001pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: ContactId) -> String {
1003 if by_contact == ContactId::SELF {
1004 translated(context, StockMessage::MsgYouEphemeralTimerHour).await
1005 } else {
1006 translated(context, StockMessage::MsgEphemeralTimerHourBy)
1007 .await
1008 .replace1(&by_contact.get_stock_name(context).await)
1009 }
1010}
1011
1012pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: ContactId) -> String {
1014 if by_contact == ContactId::SELF {
1015 translated(context, StockMessage::MsgYouEphemeralTimerDay).await
1016 } else {
1017 translated(context, StockMessage::MsgEphemeralTimerDayBy)
1018 .await
1019 .replace1(&by_contact.get_stock_name(context).await)
1020 }
1021}
1022
1023pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: ContactId) -> String {
1025 if by_contact == ContactId::SELF {
1026 translated(context, StockMessage::MsgYouEphemeralTimerWeek).await
1027 } else {
1028 translated(context, StockMessage::MsgEphemeralTimerWeekBy)
1029 .await
1030 .replace1(&by_contact.get_stock_name(context).await)
1031 }
1032}
1033
1034pub(crate) async fn msg_ephemeral_timer_year(context: &Context, by_contact: ContactId) -> String {
1036 if by_contact == ContactId::SELF {
1037 translated(context, StockMessage::MsgYouEphemeralTimerYear).await
1038 } else {
1039 translated(context, StockMessage::MsgEphemeralTimerYearBy)
1040 .await
1041 .replace1(&by_contact.get_stock_name(context).await)
1042 }
1043}
1044
1045pub(crate) async fn configuration_failed(context: &Context, details: &str) -> String {
1047 translated(context, StockMessage::ConfigurationFailed)
1048 .await
1049 .replace1(details)
1050}
1051
1052pub(crate) async fn bad_time_msg_body(context: &Context, now: &str) -> String {
1055 translated(context, StockMessage::BadTimeMsgBody)
1056 .await
1057 .replace1(now)
1058}
1059
1060pub(crate) async fn update_reminder_msg_body(context: &Context) -> String {
1062 translated(context, StockMessage::UpdateReminderMsgBody).await
1063}
1064
1065pub(crate) async fn error_no_network(context: &Context) -> String {
1067 translated(context, StockMessage::ErrorNoNetwork).await
1068}
1069
1070pub(crate) async fn messages_e2e_encrypted(context: &Context) -> String {
1072 translated(context, StockMessage::ChatProtectionEnabled).await
1073}
1074
1075pub(crate) async fn reply_noun(context: &Context) -> String {
1077 translated(context, StockMessage::ReplyNoun).await
1078}
1079
1080pub(crate) async fn self_deleted_msg_body(context: &Context) -> String {
1082 translated(context, StockMessage::SelfDeletedMsgBody).await
1083}
1084
1085pub(crate) async fn delete_server_turned_off(context: &Context) -> String {
1087 translated(context, StockMessage::DeleteServerTurnedOff).await
1088}
1089
1090pub(crate) async fn msg_ephemeral_timer_minutes(
1092 context: &Context,
1093 minutes: &str,
1094 by_contact: ContactId,
1095) -> String {
1096 if by_contact == ContactId::SELF {
1097 translated(context, StockMessage::MsgYouEphemeralTimerMinutes)
1098 .await
1099 .replace1(minutes)
1100 } else {
1101 translated(context, StockMessage::MsgEphemeralTimerMinutesBy)
1102 .await
1103 .replace1(minutes)
1104 .replace2(&by_contact.get_stock_name(context).await)
1105 }
1106}
1107
1108pub(crate) async fn msg_ephemeral_timer_hours(
1110 context: &Context,
1111 hours: &str,
1112 by_contact: ContactId,
1113) -> String {
1114 if by_contact == ContactId::SELF {
1115 translated(context, StockMessage::MsgYouEphemeralTimerHours)
1116 .await
1117 .replace1(hours)
1118 } else {
1119 translated(context, StockMessage::MsgEphemeralTimerHoursBy)
1120 .await
1121 .replace1(hours)
1122 .replace2(&by_contact.get_stock_name(context).await)
1123 }
1124}
1125
1126pub(crate) async fn msg_ephemeral_timer_days(
1128 context: &Context,
1129 days: &str,
1130 by_contact: ContactId,
1131) -> String {
1132 if by_contact == ContactId::SELF {
1133 translated(context, StockMessage::MsgYouEphemeralTimerDays)
1134 .await
1135 .replace1(days)
1136 } else {
1137 translated(context, StockMessage::MsgEphemeralTimerDaysBy)
1138 .await
1139 .replace1(days)
1140 .replace2(&by_contact.get_stock_name(context).await)
1141 }
1142}
1143
1144pub(crate) async fn msg_ephemeral_timer_weeks(
1146 context: &Context,
1147 weeks: &str,
1148 by_contact: ContactId,
1149) -> String {
1150 if by_contact == ContactId::SELF {
1151 translated(context, StockMessage::MsgYouEphemeralTimerWeeks)
1152 .await
1153 .replace1(weeks)
1154 } else {
1155 translated(context, StockMessage::MsgEphemeralTimerWeeksBy)
1156 .await
1157 .replace1(weeks)
1158 .replace2(&by_contact.get_stock_name(context).await)
1159 }
1160}
1161
1162pub(crate) async fn forwarded(context: &Context) -> String {
1164 translated(context, StockMessage::Forwarded).await
1165}
1166
1167pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
1169 translated(context, StockMessage::QuotaExceedingMsgBody)
1170 .await
1171 .replace1(&format!("{highest_usage}"))
1172 .replace("%%", "%")
1173}
1174
1175pub(crate) async fn partial_download_msg_body(context: &Context, org_bytes: u32) -> String {
1177 let size = &format_size(org_bytes, BINARY);
1178 translated(context, StockMessage::PartialDownloadMsgBody)
1179 .await
1180 .replace1(size)
1181}
1182
1183pub(crate) async fn download_availability(context: &Context, timestamp: i64) -> String {
1185 translated(context, StockMessage::DownloadAvailability)
1186 .await
1187 .replace1(×tamp_to_str(timestamp))
1188}
1189
1190pub(crate) async fn incoming_messages(context: &Context) -> String {
1192 translated(context, StockMessage::IncomingMessages).await
1193}
1194
1195pub(crate) async fn outgoing_messages(context: &Context) -> String {
1197 translated(context, StockMessage::OutgoingMessages).await
1198}
1199
1200pub(crate) async fn storage_on_domain(context: &Context, domain: &str) -> String {
1203 translated(context, StockMessage::StorageOnDomain)
1204 .await
1205 .replace1(domain)
1206}
1207
1208pub(crate) async fn not_connected(context: &Context) -> String {
1210 translated(context, StockMessage::NotConnected).await
1211}
1212
1213pub(crate) async fn connected(context: &Context) -> String {
1215 translated(context, StockMessage::Connected).await
1216}
1217
1218pub(crate) async fn connecting(context: &Context) -> String {
1220 translated(context, StockMessage::Connecting).await
1221}
1222
1223pub(crate) async fn updating(context: &Context) -> String {
1225 translated(context, StockMessage::Updating).await
1226}
1227
1228pub(crate) async fn sending(context: &Context) -> String {
1230 translated(context, StockMessage::Sending).await
1231}
1232
1233pub(crate) async fn last_msg_sent_successfully(context: &Context) -> String {
1235 translated(context, StockMessage::LastMsgSentSuccessfully).await
1236}
1237
1238pub(crate) async fn error(context: &Context, error: &str) -> String {
1241 translated(context, StockMessage::Error)
1242 .await
1243 .replace1(error)
1244}
1245
1246pub(crate) async fn not_supported_by_provider(context: &Context) -> String {
1248 translated(context, StockMessage::NotSupportedByProvider).await
1249}
1250
1251pub(crate) async fn messages(context: &Context) -> String {
1254 translated(context, StockMessage::Messages).await
1255}
1256
1257pub(crate) async fn part_of_total_used(context: &Context, part: &str, total: &str) -> String {
1259 translated(context, StockMessage::PartOfTotallUsed)
1260 .await
1261 .replace1(part)
1262 .replace2(total)
1263}
1264
1265pub(crate) async fn unencrypted_email(context: &Context, provider: &str) -> String {
1267 translated(context, StockMessage::InvalidUnencryptedMail)
1268 .await
1269 .replace1(provider)
1270}
1271
1272pub(crate) async fn stats_msg_body(context: &Context) -> String {
1274 translated(context, StockMessage::StatsMsgBody).await
1275}
1276
1277pub(crate) async fn aeap_explanation_and_link(
1278 context: &Context,
1279 old_addr: &str,
1280 new_addr: &str,
1281) -> String {
1282 translated(context, StockMessage::AeapExplanationAndLink)
1283 .await
1284 .replace1(old_addr)
1285 .replace2(new_addr)
1286}
1287
1288pub(crate) async fn new_group_send_first_message(context: &Context) -> String {
1290 translated(context, StockMessage::NewGroupSendFirstMessage).await
1291}
1292
1293pub(crate) async fn backup_transfer_qr(context: &Context) -> Result<String> {
1300 let name = if let Some(name) = context.get_config(Config::Displayname).await? {
1301 name
1302 } else {
1303 context.get_primary_self_addr().await?
1304 };
1305 Ok(translated(context, StockMessage::BackupTransferQr)
1306 .await
1307 .replace1(&name))
1308}
1309
1310pub(crate) async fn backup_transfer_msg_body(context: &Context) -> String {
1311 translated(context, StockMessage::BackupTransferMsgBody).await
1312}
1313
1314pub(crate) async fn proxy_enabled(context: &Context) -> String {
1316 translated(context, StockMessage::ProxyEnabled).await
1317}
1318
1319pub(crate) async fn proxy_description(context: &Context) -> String {
1321 translated(context, StockMessage::ProxyEnabledDescription).await
1322}
1323
1324pub(crate) async fn chat_unencrypted_explanation(context: &Context) -> String {
1326 translated(context, StockMessage::ChatUnencryptedExplanation).await
1327}
1328
1329impl Context {
1330 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1333 self.translated_stockstrings
1334 .set_stock_translation(id, stockstring)
1335 .await?;
1336 Ok(())
1337 }
1338
1339 pub(crate) async fn update_device_chats(&self) -> Result<()> {
1340 if self.get_config_bool(Config::Bot).await? {
1341 return Ok(());
1342 }
1343
1344 if !self.sql.get_raw_config_bool("self-chat-added").await? {
1347 self.sql
1348 .set_raw_config_bool("self-chat-added", true)
1349 .await?;
1350 ChatId::create_for_contact(self, ContactId::SELF).await?;
1351 }
1352
1353 let image = include_bytes!("../assets/welcome-image.jpg");
1356 let blob = BlobObject::create_and_deduplicate_from_bytes(self, image, "welcome.jpg")?;
1357 let mut msg = Message::new(Viewtype::Image);
1358 msg.param.set(Param::File, blob.as_name());
1359 msg.param.set(Param::Filename, "welcome-image.jpg");
1360 chat::add_device_msg(self, Some("core-welcome-image"), Some(&mut msg)).await?;
1361
1362 let mut msg = Message::new_text(welcome_message(self).await);
1363 chat::add_device_msg(self, Some("core-welcome"), Some(&mut msg)).await?;
1364 Ok(())
1365 }
1366}
1367
1368impl Accounts {
1369 pub async fn set_stock_translation(&self, id: StockMessage, stockstring: String) -> Result<()> {
1372 self.stockstrings
1373 .set_stock_translation(id, stockstring)
1374 .await?;
1375 Ok(())
1376 }
1377}
1378
1379#[cfg(test)]
1380mod stock_str_tests;