Compare commits

..

4 Commits

Author SHA1 Message Date
B. Petersen
fd57253cf3 use DC_DOWNLOAD_NO_URL 2021-08-25 13:45:06 +02:00
B. Petersen
25ee17cc6a draft, 2nd round 2021-08-25 13:45:04 +02:00
B. Petersen
1df131c6ae wording 2021-08-25 13:43:46 +02:00
B. Petersen
aa942b0c2a draft a possible download-api 2021-08-25 13:43:05 +02:00
17 changed files with 161 additions and 240 deletions

View File

@@ -1,18 +1,5 @@
# Changelog
## 1.60.0
### Added
- add device message to warn about QUOTA #2621
- add SOCKS5 support #2474 #2620
### Changes
- don't emit multiple events with the same import/export progress number #2639
- reduce message length limit to 5000 chars #2615
### Fixes
- keep event emitter from closing when there are no accounts #2636
## 1.59.0
### Added

4
Cargo.lock generated
View File

@@ -1121,7 +1121,7 @@ dependencies = [
[[package]]
name = "deltachat"
version = "1.60.0"
version = "1.59.0"
dependencies = [
"ansi_term",
"anyhow",
@@ -1201,7 +1201,7 @@ dependencies = [
[[package]]
name = "deltachat_ffi"
version = "1.60.0"
version = "1.59.0"
dependencies = [
"anyhow",
"async-std",

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.60.0"
version = "1.59.0"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"
license = "MPL-2.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.60.0"
version = "1.59.0"
description = "Deltachat FFI"
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
edition = "2018"

View File

@@ -377,9 +377,6 @@ int dc_set_config (dc_context_t* context, const char*
* an error (no warning as it should be shown to the user) is logged but the attachment is sent anyway.
* - `sys.config_keys` = get a space-separated list of all config-keys available.
* The config-keys are the keys that can be passed to the parameter `key` of this function.
* - `quota_exceeding` = 0: quota is unknown or in normal range;
* >=80: quota is about to exceed, the value is the concrete percentage,
* a device message is added when that happens, however, that value may still be interesting for bots.
*
* @memberof dc_context_t
* @param context The context object. For querying system values, this can be NULL.
@@ -3906,6 +3903,54 @@ int dc_msg_get_videochat_type (const dc_msg_t* msg);
int dc_msg_has_html (dc_msg_t* msg);
/**
* Check if the message is completely downloaded
* or if some further action is needed.
*
* The function returns one of:
* - @ref DC_DOWNLOAD_NO_URL - The message does not need any further download action
* and should be rendered as usual.
* - @ref DC_DOWNLOAD_AVAILABLE - There is additional content to download.
* Tn addition to the usual message rendering,
* the UI shall show a download button that starts dc_schedule_download()
* - @ref DC_DOWNLOAD_IN_PROGRESS - Download was started with dc_schedule_download() and is still in progress.
* On progress changes and if the download fails or succeeds,
* the event @ref DC_EVENT_DOWNLOAD_PROGRESS will be emitted.
* - @ref DC_DOWNLOAD_DONE - Download finished successfully
* - @ref DC_DOWNLOAD_FAILURE - Download error, the user may start over calling dc_schedule_download() again.
*
* @memberof dc_msg_t
* @param msg The message object.
* @return One of the @ref DC_DOWNLOAD values
*/
int dc_msg_download_status(const dc_msg_t* msg);
/**
* Advices the core to start downloading a message.
* This function is typically called when the user hits the "Download" button
* that is shown by the UI in case dc_msg_download_status()
* returns @ref DC_DOWNLOAD_AVAILABLE or @ref DC_DOWNLOAD_FAILURE.
*
* The UI may want to show a file selector and let the user chose a download location.
* The file name in the file selector may be prefilled using dc_msg_get_filename().
*
* During the download, the progress, errors and success
* are reported using @ref DC_EVENT_DOWNLOAD_PROGRESS.
*
* Once the @ref DC_EVENT_DOWNLOAD_PROGRESS reports success,
* The file can be accessed as usual using dc_msg_get_file().
*
* @memberof dc_context_t
* @param context The context object.
* @param path Path to the destination file.
* You can specify NULL here to download
* to a reasonable file name in the internal blob-directory.
* @param msg_id Message-ID to download the content for.
*/
void dc_schedule_download(dc_context_t* context, int msg_id, const char* path);
/**
* Set the text of a message object.
* This does not alter any information in the database; this may be done by dc_send_msg() later.
@@ -5156,6 +5201,16 @@ void dc_event_unref(dc_event_t* event);
*/
#define DC_EVENT_CONNECTIVITY_CHANGED 2100
/**
* Inform about the progress of a download started by dc_schedule_download().
*
* @param data1 (int) Message-ID the progress is reported for.
* @param data2 (int) 0=error, 1-999=progress in permille, 1000=success and done
*/
#define DC_EVENT_DOWNLOAD_PROGRESS 2120
/**
* @}
*/
@@ -5286,6 +5341,31 @@ void dc_event_unref(dc_event_t* event);
/**
* @defgroup DC_DOWNLOAD DC_DOWNLOAD
*
* These constants describe the download state of a message.
* The download state can be retrieved using dc_msg_download_status()
* and usually changes after calling dc_schedule_download().
*
* @addtogroup DC_DOWNLOAD
* @{
*/
#define DC_DOWNLOAD_NO_URL 10 ///< Download not needed, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_AVAILABLE 20 ///< Download available, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_IN_PROGRESS 30 ///< Download in progress, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_DONE 40 ///< Download done, see dc_msg_download_status() for details.
#define DC_DOWNLOAD_FAILURE 50 ///< Download failed, see dc_msg_download_status() for details.
/**
* @}
*/
/*
* TODO: Strings need some doumentation about used placeholders.
*
* @defgroup DC_STR DC_STR
*
* These constants are used to define strings using dc_set_stock_translation().
@@ -5678,13 +5758,6 @@ void dc_event_unref(dc_event_t* event);
/// Used in message summary text for notifications and chatlist.
#define DC_STR_FORWARDED 97
/// "Quota exceeding, already %1$s%% used."
///
/// Used as device message text.
///
/// `%1$s` will be replaced by the percentage used
#define DC_STR_QUOTA_EXCEEDING_MSG_BODY 98
/**
* @}
*/

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
if ! command -v grcov >/dev/null; then
if ! which grcov 2>/dev/null 1>&2; then
echo >&2 '`grcov` not found. Check README at https://github.com/mozilla/grcov for setup instructions.'
echo >&2 'Run `cargo install grcov` to build `grcov` from source.'
exit 1

View File

@@ -2,7 +2,7 @@
use std::collections::BTreeMap;
use async_std::channel::{self, Receiver, Sender};
use async_std::channel::{Receiver, Sender};
use async_std::fs;
use async_std::path::PathBuf;
use async_std::prelude::*;
@@ -22,13 +22,6 @@ pub struct Accounts {
config: Config,
accounts: Arc<RwLock<BTreeMap<u32, Context>>>,
emitter: EventEmitter,
/// Sender side of the fake event channel.
///
/// We never send any events over this channel, but hold it during the account manager lifetime
/// to prevent `EventEmitter` from returning `None` as long as account manager is alive, even if
/// it holds no accounts which could emit events.
fake_sender: Sender<crate::events::Event>,
}
impl Accounts {
@@ -64,11 +57,6 @@ impl Accounts {
let accounts = config.load_accounts().await?;
let emitter = EventEmitter::new();
// Fake event stream to prevent event emitter from closing.
let (fake_sender, fake_receiver) = channel::bounded(1);
emitter.sender.send(fake_receiver).await?;
for account in accounts.values() {
emitter.add_account(account).await?;
}
@@ -78,7 +66,6 @@ impl Accounts {
config,
accounts: Arc::new(RwLock::new(accounts)),
emitter,
fake_sender,
})
}
@@ -276,18 +263,18 @@ impl Accounts {
#[derive(Debug, Clone)]
pub struct EventEmitter {
/// Aggregate stream of events from all accounts.
stream: Arc<RwLock<futures::stream::SelectAll<Receiver<crate::events::Event>>>>,
stream: Arc<RwLock<futures::stream::SelectAll<crate::events::EventEmitter>>>,
/// Sender for the channel where new account emitters will be pushed.
sender: Sender<Receiver<crate::events::Event>>,
sender: Sender<crate::events::EventEmitter>,
/// Receiver for the channel where new account emitters will be pushed.
receiver: Receiver<Receiver<crate::events::Event>>,
receiver: Receiver<crate::events::EventEmitter>,
}
impl EventEmitter {
pub fn new() -> Self {
let (sender, receiver) = channel::unbounded();
let (sender, receiver) = async_std::channel::unbounded();
Self {
stream: Arc::new(RwLock::new(futures::stream::SelectAll::new())),
sender,
@@ -315,9 +302,7 @@ impl EventEmitter {
/// Add event emitter of a new account to the aggregate event emitter.
pub async fn add_account(&self, context: &Context) -> Result<()> {
self.sender
.send(context.get_event_emitter().into_inner())
.await?;
self.sender.send(context.get_event_emitter()).await?;
Ok(())
}
}
@@ -717,30 +702,4 @@ mod tests {
Ok(())
}
#[async_std::test]
async fn test_no_accounts_event_emitter() -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts").into();
let accounts = Accounts::new("my_os".into(), p.clone()).await?;
// Make sure there are no accounts.
assert_eq!(accounts.accounts.read().await.len(), 0);
// Create event emitter.
let mut event_emitter = accounts.get_event_emitter().await;
// Test that event emitter does not return `None` immediately.
let duration = std::time::Duration::from_millis(1);
assert!(async_std::future::timeout(duration, event_emitter.recv())
.await
.is_err());
// When account manager is dropped, event emitter is exhausted.
drop(accounts);
assert_eq!(event_emitter.recv().await?, None);
Ok(())
}
}

View File

@@ -156,11 +156,6 @@ pub enum Config {
#[strum(props(default = "0"))]
NotifyAboutWrongPw,
/// If a warning about exceeding quota was shown recently,
/// this is the percentage of quota at the time the warning was given.
/// Unset, when quota falls below minimal warning threshold again.
QuotaExceeding,
/// address to webrtc instance to use for videochats
WebrtcInstance,

View File

@@ -423,12 +423,6 @@ impl Context {
.await?
.to_string(),
);
res.insert(
"quota_exceeding",
self.get_config_int(Config::QuotaExceeding)
.await?
.to_string(),
);
let elapsed = self.creation_time.elapsed();
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));

View File

@@ -62,10 +62,6 @@ impl Events {
pub struct EventEmitter(Receiver<Event>);
impl EventEmitter {
pub(crate) fn into_inner(self) -> Receiver<Event> {
self.0
}
/// Blocking recv of an event. Return `None` if the `Sender` has been droped.
pub fn recv_sync(&self) -> Option<Event> {
async_std::task::block_on(self.recv())

View File

@@ -6,8 +6,9 @@
use std::{cmp, cmp::max, collections::BTreeMap};
use anyhow::{anyhow, bail, format_err, Context as _, Result};
use async_imap::types::{
Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse,
use async_imap::{
error::Result as ImapResult,
types::{Fetch, Flag, Mailbox, Name, NameAttribute, Quota, QuotaRoot, UnsolicitedResponse},
};
use async_std::channel::Receiver;
use async_std::prelude::*;
@@ -258,7 +259,7 @@ impl Imap {
let oauth2 = self.config.oauth2;
let connection_res: Result<Client> = if self.config.lp.security == Socket::Starttls
let connection_res: ImapResult<Client> = if self.config.lp.security == Socket::Starttls
|| self.config.lp.security == Socket::Plain
{
let config = &mut self.config;
@@ -353,7 +354,7 @@ impl Imap {
Ok(())
}
Err(err) => {
Err((err, _)) => {
let imap_user = self.config.lp.user.to_owned();
let message = stock_str::cannot_login(context, &imap_user).await;

View File

@@ -3,9 +3,10 @@ use std::{
time::Duration,
};
use anyhow::{Context as _, Result};
use async_imap::Client as ImapClient;
use async_imap::{
error::{Error as ImapError, Result as ImapResult},
Client as ImapClient,
};
use async_smtp::ServerAddress;
use async_std::net::{self, TcpStream};
@@ -39,12 +40,24 @@ impl DerefMut for Client {
}
impl Client {
pub async fn login(self, username: &str, password: &str) -> Result<Session> {
let Client { inner, .. } = self;
pub async fn login(
self,
username: &str,
password: &str,
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session = inner
.login(username, password)
.await
.map_err(|(err, _client)| err)?;
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
@@ -52,12 +65,21 @@ impl Client {
self,
auth_type: &str,
authenticator: impl async_imap::Authenticator,
) -> Result<Session> {
let Client { inner, .. } = self;
let session = inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, _client)| err)?;
) -> std::result::Result<Session, (ImapError, Self)> {
let Client { inner, is_secure } = self;
let session =
inner
.authenticate(auth_type, authenticator)
.await
.map_err(|(err, client)| {
(
err,
Client {
is_secure,
inner: client,
},
)
})?;
Ok(Session { inner: session })
}
@@ -65,7 +87,7 @@ impl Client {
addr: impl net::ToSocketAddrs,
domain: &str,
strict_tls: bool,
) -> Result<Self> {
) -> ImapResult<Self> {
let stream = TcpStream::connect(addr).await?;
let tls = dc_build_tls(strict_tls);
let tls_stream: Box<dyn SessionStream> = Box::new(tls.connect(domain, stream).await?);
@@ -74,7 +96,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.context("failed to read greeting")?;
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: true,
@@ -82,14 +104,14 @@ impl Client {
})
}
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> Result<Self> {
pub async fn connect_insecure(addr: impl net::ToSocketAddrs) -> ImapResult<Self> {
let stream: Box<dyn SessionStream> = Box::new(TcpStream::connect(addr).await?);
let mut client = ImapClient::new(stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")?;
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: false,
@@ -101,11 +123,15 @@ impl Client {
target_addr: &ServerAddress,
strict_tls: bool,
socks5_config: Socks5Config,
) -> Result<Self> {
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let tls = dc_build_tls(strict_tls);
@@ -116,7 +142,7 @@ impl Client {
let _greeting = client
.read_response()
.await
.context("failed to read greeting")?;
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: true,
@@ -127,18 +153,22 @@ impl Client {
pub async fn connect_insecure_socks5(
target_addr: &ServerAddress,
socks5_config: Socks5Config,
) -> Result<Self> {
) -> ImapResult<Self> {
let socks5_stream: Box<dyn SessionStream> = Box::new(
socks5_config
match socks5_config
.connect(target_addr, Some(Duration::from_secs(IMAP_TIMEOUT)))
.await?,
.await
{
Ok(s) => s,
Err(e) => return ImapResult::Err(async_imap::error::Error::Bad(e.to_string())),
},
);
let mut client = ImapClient::new(socks5_stream);
let _greeting = client
.read_response()
.await
.context("failed to read greeting")?;
.ok_or_else(|| ImapError::Bad("failed to read greeting".to_string()))?;
Ok(Client {
is_secure: false,
@@ -146,7 +176,7 @@ impl Client {
})
}
pub async fn secure(self, domain: &str, strict_tls: bool) -> Result<Client> {
pub async fn secure(self, domain: &str, strict_tls: bool) -> ImapResult<Client> {
if self.is_secure {
Ok(self)
} else {

View File

@@ -507,16 +507,14 @@ async fn import_backup(context: &Context, backup_to_import: &Path) -> Result<()>
let archive = Archive::new(backup_file);
let mut entries = archive.entries()?;
let mut last_progress = 0;
while let Some(file) = entries.next().await {
let f = &mut file?;
let current_pos = f.raw_file_position();
let progress = 1000 * current_pos / file_size;
if progress != last_progress && progress > 10 && progress < 1000 {
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
context.emit_event(EventType::ImexProgress(progress as usize));
last_progress = progress;
}
if f.path()?.file_name() == Some(OsStr::new(DBFILE_BACKUP_NAME)) {
@@ -739,7 +737,6 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
let count = read_dir.len();
let mut written_files = 0;
let mut last_progress = 0;
for entry in read_dir.into_iter() {
let entry = entry?;
let name = entry.file_name();
@@ -757,10 +754,9 @@ async fn export_backup_inner(context: &Context, temp_path: &PathBuf) -> Result<(
written_files += 1;
let progress = 1000 * written_files / count;
if progress != last_progress && progress > 10 && progress < 1000 {
if progress > 10 && progress < 1000 {
// We already emitted ImexProgress(10) above
emit_event!(context, EventType::ImexProgress(progress));
last_progress = progress;
}
}

View File

@@ -1152,10 +1152,7 @@ async fn perform_job_action(
sql::housekeeping(context).await.ok_or_log(context);
Status::Finished(Ok(()))
}
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
Ok(status) => status,
Err(err) => Status::Finished(Err(err)),
},
Action::UpdateRecentQuota => context.update_recent_quota(connection.inbox()).await,
};
info!(context, "Finished immediate try {} of job {}", tries, job);

View File

@@ -4,17 +4,13 @@ use anyhow::{anyhow, Result};
use async_imap::types::{Quota, QuotaResource};
use indexmap::IndexMap;
use crate::chat::add_device_msg_with_importance;
use crate::config::Config;
use crate::constants::Viewtype;
use crate::context::Context;
use crate::dc_tools::time;
use crate::imap::scan_folders::get_watched_folders;
use crate::imap::Imap;
use crate::job::{Action, Status};
use crate::message::Message;
use crate::param::Params;
use crate::{job, stock_str, EventType};
use crate::{job, EventType};
/// warn about a nearly full mailbox after this usage percentage is reached.
/// quota icon is "yellow".
@@ -24,12 +20,6 @@ pub const QUOTA_WARN_THRESHOLD_PERCENTAGE: u64 = 80;
// this threshold only makes the quota icon "red".
pub const QUOTA_ERROR_THRESHOLD_PERCENTAGE: u64 = 99;
/// if quota is below this value (again),
/// QuotaExceeding is cleared.
/// This value should be a bit below QUOTA_WARN_THRESHOLD_PERCENTAGE to
/// avoid jittering and lots of warnings when quota is exactly at the warning threshold.
pub const QUOTA_ALLCLEAR_PERCENTAGE: u64 = 75;
// if recent quota is older,
// it is re-fetched on dc_get_connectivity_html()
pub const QUOTA_MAX_AGE_SECONDS: i64 = 60;
@@ -73,29 +63,6 @@ async fn get_unique_quota_roots_and_usage(
Ok(unique_quota_roots)
}
fn get_highest_usage<'t>(
unique_quota_roots: &'t IndexMap<String, Vec<QuotaResource>>,
) -> Result<(u64, &'t String, &QuotaResource)> {
let mut highest: Option<(u64, &'t String, &QuotaResource)> = None;
for (name, resources) in unique_quota_roots {
for r in resources {
let usage_percent = r.get_usage_percentage();
match highest {
None => {
highest = Some((usage_percent, name, r));
}
Some((up, ..)) => {
if up <= usage_percent {
highest = Some((usage_percent, name, r));
}
}
};
}
}
highest.ok_or_else(|| anyhow!("no quota_resource found, this is unexpected"))
}
impl Context {
// Adds a job to update `quota.recent`
pub(crate) async fn schedule_quota_update(&self) {
@@ -110,17 +77,11 @@ impl Context {
/// Updates `quota.recent`, sets `quota.modified` to the current time
/// and emits an event to let the UIs update connectivity view.
///
/// Moreover, once each time quota gets larger than `QUOTA_WARN_THRESHOLD_PERCENTAGE`,
/// a device message is added.
/// As the message is added only once, the user is not spammed
/// in case for some providers the quota is always at ~100%
/// and new space is allocated as needed.
///
/// Called in response to `Action::UpdateRecentQuota`.
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Result<Status> {
pub(crate) async fn update_recent_quota(&self, imap: &mut Imap) -> Status {
if let Err(err) = imap.prepare(self).await {
warn!(self, "could not connect: {:?}", err);
return Ok(Status::RetryNow);
return Status::RetryNow;
}
let quota = if imap.can_check_quota() {
@@ -130,51 +91,12 @@ impl Context {
Err(anyhow!("Quota not supported by your provider."))
};
if let Ok(quota) = &quota {
match get_highest_usage(quota) {
Ok((highest, _, _)) => {
if highest >= QUOTA_WARN_THRESHOLD_PERCENTAGE {
if self.get_config_int(Config::QuotaExceeding).await? == 0 {
self.set_config(Config::QuotaExceeding, Some(&highest.to_string()))
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.text = Some(stock_str::quota_exceeding(self, highest).await);
add_device_msg_with_importance(self, None, Some(&mut msg), true)
.await?;
}
} else if highest <= QUOTA_ALLCLEAR_PERCENTAGE {
self.set_config(Config::QuotaExceeding, None).await?;
}
}
Err(err) => warn!(self, "cannot get highest quota usage: {:?}", err),
}
}
*self.quota.write().await = Some(QuotaInfo {
recent: quota,
modified: time(),
});
self.emit_event(EventType::ConnectivityChanged);
Ok(Status::Finished(Ok(())))
}
}
#[cfg(test)]
mod tests {
use crate::quota::{
QUOTA_ALLCLEAR_PERCENTAGE, QUOTA_ERROR_THRESHOLD_PERCENTAGE,
QUOTA_WARN_THRESHOLD_PERCENTAGE,
};
#[allow(clippy::assertions_on_constants)]
#[async_std::test]
async fn test_quota_thresholds() -> anyhow::Result<()> {
assert!(QUOTA_ALLCLEAR_PERCENTAGE > 50);
assert!(QUOTA_ALLCLEAR_PERCENTAGE < QUOTA_WARN_THRESHOLD_PERCENTAGE);
assert!(QUOTA_WARN_THRESHOLD_PERCENTAGE < QUOTA_ERROR_THRESHOLD_PERCENTAGE);
assert!(QUOTA_ERROR_THRESHOLD_PERCENTAGE < 100);
Ok(())
Status::Finished(Ok(()))
}
}

View File

@@ -598,8 +598,6 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
);
}
context.schedule_quota_update().await;
if let Err(e) = context
.set_config(Config::LastHousekeeping, Some(&time().to_string()))
.await

View File

@@ -258,15 +258,6 @@ pub enum StockMessage {
#[strum(props(fallback = "Forwarded"))]
Forwarded = 97,
#[strum(props(
fallback = "⚠️ Your provider's storage is about to exceed, already %1$s%% are used.\n\n\
You may not be able to receive message when the storage is 100%% used.\n\n\
👉 Please check if you can delete old data in the provider's webinterface \
and consider to enable \"Settings / Delete Old Messages\". \
You can check your current storage usage anytime at \"Settings / Connectivity\"."
))]
QuotaExceedingMsgBody = 98,
}
impl StockMessage {
@@ -849,14 +840,6 @@ pub(crate) async fn forwarded(context: &Context) -> String {
translated(context, StockMessage::Forwarded).await
}
/// Stock string: `⚠️ Your provider's storage is about to exceed...`.
pub(crate) async fn quota_exceeding(context: &Context, highest_usage: u64) -> String {
translated(context, StockMessage::QuotaExceedingMsgBody)
.await
.replace1(format!("{}", highest_usage))
.replace("%%", "%")
}
impl Context {
/// Set the stock string for the [StockMessage].
///
@@ -1040,16 +1023,6 @@ mod tests {
);
}
#[async_std::test]
async fn test_quota_exceeding_stock_str() -> anyhow::Result<()> {
let t = TestContext::new().await;
let str = quota_exceeding(&t, 81).await;
assert!(str.contains("81% "));
assert!(str.contains("100% "));
assert!(!str.contains("%%"));
Ok(())
}
#[async_std::test]
async fn test_update_device_chats() {
let t = TestContext::new().await;