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