C12.8: sync now, timed sync, pause, offline folders

This commit is contained in:
雷电芽衣 committed 2026-10-19 22:29:27 -04:00
1 parent 4d0d64ff54
commit 22d913d45e
23 files changed
+763 -66

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
//! Sync now, pause, and folders kept whole.
use ely_gpui_component::{buttons::Button, feedback::Banner, forms::Switch, primitives::Severity};
use ely_mail_engine::{AccountId, Protocol};
use gpui::{
AnyElement, Context, InteractiveElement as _, IntoElement, ParentElement as _, Styled as _, div,
};
use crate::mail::{Mail, text};
impl Mail {
pub fn sync_buttons(&self, id: AccountId, cx: &mut Context<Self>) -> AnyElement {
let running = self.syncs.get(&id).is_some_and(|each| each.task.is_some());
let this = cx.weak_entity();
let now = Button::new("mail-sync-now", text(cx, "mail.sync.now", &[]))
.disabled(running || self.paused)
.on_click({
let this = this.clone();
move |_, _, cx| {
this.update(cx, |mail, cx| mail.sync(id, cx)).ok();
}
});
let key = if self.paused {
"mail.sync.resume"
} else {
"mail.sync.pause"
};
let pause = Button::new("mail-sync-pause", text(cx, key, &[])).on_click(move |_, _, cx| {
this.update(cx, |mail, cx| mail.set_paused(!mail.paused, cx))
.ok();
});
div()
.flex()
.gap_2()
.child(now)
.child(pause)
.into_any_element()
}
pub fn paused_banner(&self, cx: &Context<Self>) -> Option<Banner> {
self.paused.then(|| {
Banner::new(
"mail-sync-paused",
Severity::Info,
text(cx, "mail.sync.paused", &[]),
)
})
}
/// One switch per IMAP folder.
pub fn offline_switches(
&self,
id: AccountId,
protocol: Protocol,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
if protocol != Protocol::Imap {
return None;
}
let folders = &self.syncs.get(&id)?.folders;
if folders.is_empty() {
return None;
}
let this = cx.weak_entity();
let switches = folders.iter().map(|folder| {
let mailbox = folder.id;
let this = this.clone();
let label = text(cx, "mail.sync.offline", &[("name", &folder.name)]);
let switch = Switch::new(("mail-offline", mailbox.0 as u64), folder.offline)
.label(label)
.on_change(move |on, _, cx| {
this.update(cx, |mail, cx| mail.set_offline(id, mailbox, on, cx))
.ok();
});
div()
.debug_selector(move || format!("mail-offline-{}", mailbox.0))
.child(switch)
});
Some(
div()
.flex()
.flex_col()
.gap_2()
.children(switches)
.into_any_element(),
)
}
}
+8 -4
View File
@@ -44,6 +44,10 @@ pub struct Mail {
pub adding: Option<Adding>,
pub removing: Option<Removing>,
pub syncs: Syncs,
/// Numbers every sync, even across forgotten accounts.
pub runs: u64,
/// Until resumed, no account syncs.
pub paused: bool,
/// Numbers every request across dialogs.
pub asked: u64,
pub focus: FocusHandle,
@@ -102,12 +106,15 @@ impl Mail {
adding: None,
removing: None,
syncs: Syncs::new(),
runs: 0,
paused: false,
asked: 0,
focus: cx.focus_handle(),
root: WeakEntity::new_invalid(),
side: cx.new(|_| Side::new(this)),
};
mail.read_accounts(cx);
Self::sync_on_a_timer(cx);
mail
}
@@ -126,10 +133,7 @@ impl Mail {
}
mail.accounts = Some(read);
if first {
let ids: Vec<_> = mail.accounts().iter().map(|(id, _)| *id).collect();
for id in ids {
mail.sync(id, cx);
}
mail.sync_all(cx);
}
},
);
+1
View File
@@ -3,6 +3,7 @@
mod add_view;
mod adding;
mod controls;
mod flow;
mod kinds;
mod mail;
+10 -6
View File
@@ -1,16 +1,20 @@
//! The Account menu.
//! The Account and Sync menus.
use gpui::{App, Menu, MenuItem, actions};
use crate::mail::text;
actions!(mail, [AddAccount, RemoveAccount]);
actions!(mail, [AddAccount, RemoveAccount, SyncAll]);
pub const CONTEXT: &str = "mail";
pub fn menus(cx: &App) -> Vec<Menu> {
vec![Menu::new(text(cx, "mail.menu.account", &[])).items([
MenuItem::action(text(cx, "mail.add.open", &[]), AddAccount),
MenuItem::action(text(cx, "mail.remove.open", &[]), RemoveAccount),
])]
vec![
Menu::new(text(cx, "mail.menu.account", &[])).items([
MenuItem::action(text(cx, "mail.add.open", &[]), AddAccount),
MenuItem::action(text(cx, "mail.remove.open", &[]), RemoveAccount),
]),
Menu::new(text(cx, "mail.menu.sync", &[]))
.items([MenuItem::action(text(cx, "mail.sync.now", &[]), SyncAll)]),
]
}
+98 -5
View File
@@ -1,28 +1,39 @@
//! Accounts sync on open and once added.
//! Accounts sync on open, on demand and on a timer.
use std::{
collections::HashMap,
time::{SystemTime, UNIX_EPOCH},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use ely_mail_engine::{AccountId, Protocol, Synced, sync};
use ely_mail_engine::{AccountId, LocalFolder, MailboxId, Protocol, Synced, sync};
use gpui::Context;
use tokio::task::AbortHandle;
use crate::mail::Mail;
/// How often every account syncs while the window is open.
pub const EVERY: Duration = Duration::from_secs(10 * 60);
/// One account's sync as the window shows it.
#[derive(Default)]
pub struct Syncing {
pub task: Option<AbortHandle>,
/// Which sync `task` is; older completions are stale.
pub run: u64,
/// A folder changed mid-sync: sync once more.
pub again: bool,
pub synced: Option<Synced>,
pub failed: Option<String>,
pub folders: Vec<LocalFolder>,
}
pub type Syncs = HashMap<AccountId, Syncing>;
impl Mail {
pub fn sync(&mut self, id: AccountId, cx: &mut Context<Self>) {
if self.paused {
return log::info!("mail: account {} not synced: paused", id.0);
}
let Some((_, account)) = self.accounts().iter().find(|(each, _)| *each == id) else {
return log::info!("mail: sync asked of a gone account");
};
@@ -41,6 +52,9 @@ impl Mail {
}
};
let (account, vault) = (account.clone(), self.vault.clone());
self.runs += 1;
let run = self.runs;
self.syncs.entry(id).or_default().run = run;
let work = async move {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -64,14 +78,90 @@ impl Mail {
let Some(syncing) = mail.syncs.get_mut(&id) else {
return log::info!("mail: a sync ended for a removed account");
};
if syncing.run != run {
return log::info!("mail: account {} sync {run} ended after a newer one", id.0);
}
syncing.task = None;
syncing.failed = failed;
let again = std::mem::take(&mut syncing.again);
mail.read_synced(id, cx);
if again {
log::info!("mail: account {} syncs again for a changed folder", id.0);
mail.sync(id, cx);
}
});
self.syncs.entry(id).or_default().task = Some(task);
self.read_synced(id, cx);
}
pub fn sync_all(&mut self, cx: &mut Context<Self>) {
if self.paused {
return log::info!("mail: sync is paused");
}
let ids: Vec<_> = self.accounts().iter().map(|(id, _)| *id).collect();
for id in ids {
self.sync(id, cx);
}
}
/// Pausing stops running syncs; resuming syncs every account.
pub fn set_paused(&mut self, paused: bool, cx: &mut Context<Self>) {
log::info!("mail: sync paused: {paused}");
self.paused = paused;
if paused {
for syncing in self.syncs.values_mut() {
syncing.again = false;
if let Some(task) = syncing.task.take() {
task.abort();
}
}
} else {
self.sync_all(cx);
}
self.notify(cx);
}
/// Syncs every account each `EVERY` until the window goes.
pub fn sync_on_a_timer(cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
loop {
cx.background_executor().timer(EVERY).await;
if this.update(cx, |mail, cx| mail.sync_all(cx)).is_err() {
break;
}
}
})
.detach();
}
/// Takes the folder's mail whole, then syncs to fetch it.
pub fn set_offline(
&mut self,
id: AccountId,
mailbox: MailboxId,
offline: bool,
cx: &mut Context<Self>,
) {
self.with_store(
cx,
move |store| store.set_offline(mailbox, offline),
move |mail, done, cx| match done {
Ok(()) => {
mail.read_synced(id, cx);
match mail.syncs.get_mut(&id) {
Some(syncing) if offline && syncing.task.is_some() => {
log::info!("mail: account {} syncs again after this one", id.0);
syncing.again = true;
}
_ if offline => mail.sync(id, cx),
_ => {}
}
}
Err(error) => mail.failed("mail.sync.offline_failed", &error, cx),
},
);
}
/// Stops the account's sync and forgets it.
pub fn stop_sync(&mut self, id: AccountId) {
if let Some(task) = self.syncs.remove(&id).and_then(|each| each.task) {
@@ -82,9 +172,12 @@ impl Mail {
fn read_synced(&mut self, id: AccountId, cx: &mut Context<Self>) {
self.with_store(
cx,
move |store| store.synced(id),
move |store| Ok((store.synced(id)?, store.folders(id)?)),
move |mail, read, cx| match (read, mail.syncs.get_mut(&id)) {
(Ok(synced), Some(syncing)) => syncing.synced = Some(synced),
(Ok((synced, folders)), Some(syncing)) => {
syncing.synced = Some(synced);
syncing.folders = folders;
}
(Err(error), Some(_)) => mail.failed("mail.sync.unread", &error, cx),
(_, None) => {}
},
+1
View File
@@ -21,6 +21,7 @@ use crate::{
mail::{Mail, Parts},
};
mod controls;
mod flows;
mod syncs;
+317
View File
@@ -0,0 +1,317 @@
use std::{
net::TcpListener,
sync::mpsc,
time::{Duration, Instant},
};
use ely_mail_engine::{AccountId, LocalFolder, MailboxId, Role, Store};
use gpui::{
Entity, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualTestContext, point, px, size,
};
use super::{Keys, account, settle, window};
use crate::{mail::Mail, syncing::EVERY, view::imap_range};
/// An account on a server that never answers.
fn silent(dir: &std::path::Path, port: u16) -> AccountId {
let mut ann = account("ann@example.com");
ann.incoming.port = port;
let mut store = Store::open(dir).unwrap();
let id = store.add_account(&ann).unwrap();
store
.set_folders(id, &[("INBOX", Some(Role::Inbox))])
.unwrap();
id
}
fn with_secret() -> Keys {
let keys = Keys::default();
let secret = ("account:ann@example.com".to_owned(), "pw".to_owned());
keys.kept.lock().unwrap().extend([secret]);
keys
}
/// Waits on the network task alone, not the window.
fn finished(mail: &Entity<Mail>, id: AccountId, cx: &mut VisualTestContext) {
let deadline = Instant::now() + Duration::from_secs(20);
let done = |cx: &mut VisualTestContext| {
cx.update(|_, app| {
let task = mail.read(app).syncs[&id].task.as_ref();
task.unwrap().is_finished()
})
};
while !done(cx) {
assert!(Instant::now() < deadline, "the sync never ended");
std::thread::sleep(Duration::from_millis(10));
}
}
/// An account whose sync fails at once: no secret saved.
fn failing(dir: &std::path::Path) -> AccountId {
let mut store = Store::open(dir).unwrap();
let ann = store.add_account(&account("ann@example.com")).unwrap();
store
.set_folders(ann, &[("INBOX", Some(Role::Inbox))])
.unwrap();
ann
}
fn failed(mail: &Entity<Mail>, id: AccountId, cx: &mut VisualTestContext) -> bool {
cx.update(|_, app| mail.read(app).syncs[&id].failed.is_some())
}
fn forget(mail: &Entity<Mail>, id: AccountId, cx: &mut VisualTestContext) {
cx.update(|_, app| {
mail.update(app, |mail, _| {
mail.syncs.get_mut(&id).unwrap().failed = None
})
});
}
fn settled_failure(mail: &Entity<Mail>, id: AccountId, cx: &mut VisualTestContext) {
settle(cx, |app| {
mail.read(app)
.syncs
.get(&id)
.is_some_and(|each| each.failed.is_some() && each.task.is_none())
});
}
#[test]
fn every_account_syncs_again_on_the_timer() {
let dir = tempfile::tempdir().unwrap();
let ann = failing(dir.path());
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), Keys::default());
settled_failure(&mail, ann, cx);
forget(&mail, ann, cx);
cx.executor().advance_clock(EVERY);
settled_failure(&mail, ann, cx);
}
#[test]
fn a_paused_sync_waits_until_resumed() {
let dir = tempfile::tempdir().unwrap();
let ann = failing(dir.path());
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), Keys::default());
settled_failure(&mail, ann, cx);
forget(&mail, ann, cx);
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_paused(true, cx)));
cx.update(|_, app| mail.update(app, |mail, cx| mail.sync(ann, cx)));
cx.executor().advance_clock(EVERY);
cx.run_until_parked();
assert!(!failed(&mail, ann, cx), "a paused account synced");
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_paused(false, cx)));
settled_failure(&mail, ann, cx);
}
#[test]
fn a_folder_kept_offline_is_saved_and_synced() {
let dir = tempfile::tempdir().unwrap();
let ann = failing(dir.path());
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), Keys::default());
settle(cx, |app| {
mail.read(app)
.syncs
.get(&ann)
.is_some_and(|each| each.failed.is_some() && !each.folders.is_empty())
});
let inbox = cx.update(|_, app| mail.read(app).syncs[&ann].folders[0].clone());
assert!(!inbox.offline);
forget(&mail, ann, cx);
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_offline(ann, inbox.id, true, cx)));
settle(cx, |app| {
let syncing = &mail.read(app).syncs[&ann];
syncing.folders[0].offline && syncing.failed.is_some() && syncing.task.is_none()
});
assert!(Store::open(dir.path()).unwrap().offline(inbox.id).unwrap());
}
#[test]
fn an_old_end_queued_across_a_resume_keeps_the_new_sync() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let dir = tempfile::tempdir().unwrap();
let ann = silent(dir.path(), listener.local_addr().unwrap().port());
let keys = Keys::default();
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), keys.clone());
settled_failure(&mail, ann, cx);
cx.update(|_, app| mail.update(app, |mail, cx| mail.sync(ann, cx)));
finished(&mail, ann, cx);
keys.kept
.lock()
.unwrap()
.insert("account:ann@example.com".into(), "pw".into());
let resumed = cx.update(|_, app| {
mail.update(app, |mail, cx| {
mail.set_paused(true, cx);
mail.set_paused(false, cx);
mail.syncs[&ann].task.clone().unwrap()
})
});
cx.run_until_parked();
assert!(
cx.update(|_, app| mail.read(app).syncs[&ann].task.is_some()),
"an old end cleared the new sync"
);
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_paused(true, cx)));
let deadline = Instant::now() + Duration::from_secs(5);
while !resumed.is_finished() {
assert!(Instant::now() < deadline, "pausing left the sync running");
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn an_old_end_after_a_failed_removal_keeps_the_new_sync() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let dir = tempfile::tempdir().unwrap();
let ann = silent(dir.path(), listener.local_addr().unwrap().port());
let keys = Keys::default();
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), keys.clone());
settled_failure(&mail, ann, cx);
cx.update(|_, app| {
mail.update(app, |mail, cx| {
mail.stop_sync(ann);
mail.sync(ann, cx);
})
});
finished(&mail, ann, cx);
keys.kept
.lock()
.unwrap()
.insert("account:ann@example.com".into(), "pw".into());
let restarted = cx.update(|_, app| {
mail.update(app, |mail, cx| {
mail.stop_sync(ann);
mail.sync(ann, cx);
mail.syncs[&ann].task.clone().unwrap()
})
});
cx.run_until_parked();
assert!(
cx.update(|_, app| mail.read(app).syncs[&ann].task.is_some()),
"an old end cleared the restarted sync"
);
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_paused(true, cx)));
let deadline = Instant::now() + Duration::from_secs(5);
while !restarted.is_finished() {
assert!(Instant::now() < deadline, "pausing left the sync running");
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn a_folder_kept_offline_mid_sync_syncs_once_more() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let (hang_up, hung) = mpsc::channel::<()>();
let server = std::thread::spawn(move || {
let held = listener.accept().unwrap();
hung.recv().unwrap();
drop((held, listener));
});
let dir = tempfile::tempdir().unwrap();
let ann = silent(dir.path(), port);
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), with_secret());
settle(cx, |app| {
mail.read(app)
.syncs
.get(&ann)
.is_some_and(|each| each.task.is_some() && !each.folders.is_empty())
});
let (inbox, run) = cx.update(|_, app| {
let syncing = &mail.read(app).syncs[&ann];
(syncing.folders[0].id, syncing.run)
});
cx.update(|_, app| mail.update(app, |mail, cx| mail.set_offline(ann, inbox, true, cx)));
settle(cx, |app| mail.read(app).syncs[&ann].again);
hang_up.send(()).unwrap();
server.join().unwrap();
settle(cx, |app| {
let syncing = &mail.read(app).syncs[&ann];
syncing.run == run + 1 && syncing.task.is_none() && syncing.failed.is_some()
});
}
#[test]
fn the_account_page_scrolls_to_its_last_folder() {
let dir = tempfile::tempdir().unwrap();
let ann = failing(dir.path());
let names: Vec<String> = (0..40).map(|n| format!("Folder {n:02}")).collect();
let folders: Vec<_> = names.iter().map(|name| (name.as_str(), None)).collect();
let last = *Store::open(dir.path())
.unwrap()
.set_folders(ann, &folders)
.unwrap()
.last()
.unwrap();
let mut cx = TestAppContext::single();
let (mail, cx) = window(&mut cx, dir.path(), Keys::default());
cx.simulate_resize(size(px(1100.), px(800.)));
settle(cx, |app| {
mail.read(app)
.syncs
.get(&ann)
.is_some_and(|each| each.folders.len() == 40)
});
let selector: &'static str = format!("mail-offline-{}", last.0).leak();
let height = cx.update(|window, _| window.viewport_size().height);
let before = cx
.debug_bounds(selector)
.expect("the last switch is laid out");
assert!(before.bottom() > height, "the folders fit the window");
let at = point(px(400.), height / 2.0);
for _ in 0..20 {
cx.simulate_event(ScrollWheelEvent {
position: at,
delta: ScrollDelta::Pixels(point(px(0.), px(-1000.))),
..Default::default()
});
cx.run_until_parked();
}
let after = cx
.debug_bounds(selector)
.expect("the last switch is laid out");
assert!(
after.bottom() <= height,
"the last switch is out of reach: {after:?}"
);
}
#[test]
fn the_range_names_the_folders_kept_whole() {
let folder = |id, name: &str, offline| LocalFolder {
id: MailboxId(id),
name: name.into(),
role: None,
offline,
};
let cx = TestAppContext::single();
cx.update(|app| {
ely_ui::i18n::install_as("en-US", app);
let rows = |folders: &[LocalFolder]| -> Vec<String> {
imap_range(folders, app)
.into_iter()
.map(|(key, value)| format!("{key}: {value}"))
.collect()
};
let both = [folder(1, "INBOX", true), folder(2, "Sent", false), folder(3, "Work", true)];
assert_eq!(
rows(&both),
[
"Downloaded: Other folders: headers of all mail; text from the last 90 days; attachments when asked",
"Kept whole: Every message in INBOX, Work, with attachments",
]
);
assert_eq!(rows(&[folder(1, "INBOX", true)]), ["Kept whole: Every message in INBOX, with attachments"]);
assert_eq!(
rows(&[folder(2, "Sent", false)]),
["Downloaded: Headers of all mail; text from the last 90 days; attachments when asked"]
);
});
}
+20 -12
View File
@@ -357,23 +357,31 @@ fn two_saves_in_flight_add_an_address_once() {
});
passed(&mail, account("ANN@example.com"), "pw2", cx);
cx.update(|_, app| mail.update(app, |mail, cx| mail.save(cx)));
// Either save may reach the store first.
settle(cx, |app| {
matches!(
mail.read(app).adding.as_ref().map(|adding| &adding.step),
Some(Step::Failed(_))
)
let mail = mail.read(app);
mail.accounts().len() == 1
&& !matches!(
mail.adding.as_ref().map(|adding| &adding.step),
Some(Step::Saving)
)
});
assert_eq!(step_words(&mail, cx), "ANN@example.com is already added.");
cx.update(|_, app| assert_eq!(mail.read(app).accounts().len(), 1));
assert_eq!(
Store::open(dir.path()).unwrap().accounts().unwrap()[0].1,
account("ann@example.com")
);
let won = Store::open(dir.path()).unwrap().accounts().unwrap();
assert_eq!(won.len(), 1);
let (address, secret) = if won[0].1 == account("ann@example.com") {
assert_eq!(step_words(&mail, cx), "ANN@example.com is already added.");
("ann", "pw")
} else {
assert_eq!(won[0].1, account("ANN@example.com"));
cx.update(|_, app| assert!(mail.read(app).adding.is_none()));
("ANN", "pw2")
};
let kept = keys.kept.lock().unwrap();
assert_eq!(kept.len(), 1);
assert_eq!(
kept.get("account:ann@example.com").map(String::as_str),
Some("pw")
kept.get(&format!("account:{address}@example.com"))
.map(String::as_str),
Some(secret)
);
}
+39 -9
View File
@@ -4,11 +4,12 @@ use ely_gpui_component::{
buttons::{Button, ButtonVariant},
data_display::DescriptionList,
feedback::{Banner, EmptyState, ErrorView},
layout::ScrollArea,
overlays::ConfirmDialog,
primitives::{IconName, Severity},
typography::format,
};
use ely_mail_engine::{Account, AccountId, Auth, Protocol, Security, Server, sync};
use ely_mail_engine::{Account, AccountId, Auth, LocalFolder, Protocol, Security, Server, sync};
use ely_platform::AppId;
use ely_ui::Shell;
use gpui::{
@@ -97,18 +98,26 @@ impl Mail {
server(&account.outgoing, cx),
)
.text(label("mail.account.auth"), auth(account.auth, cx));
let (details, failed) = self.sync_rows(*id, account.protocol, details, cx);
let (id, protocol) = (*id, account.protocol);
let (details, failed) = self.sync_rows(id, protocol, details, cx);
let remove = Button::new("mail-remove", label("mail.remove.open"))
.on_click(|_, window, cx| window.dispatch_action(Box::new(menu::RemoveAccount), cx));
div()
let page = div()
.p_6()
.flex()
.flex_col()
.gap_4()
.max_w_128()
.children(self.paused_banner(cx))
.children(failed)
.child(details)
.child(div().child(remove))
.child(self.sync_buttons(id, cx))
.children(self.offline_switches(id, protocol, cx))
.child(div().child(remove));
ScrollArea::new("mail-account-scroll")
.vertical()
.size_full()
.child(page)
.into_any_element()
}
@@ -140,11 +149,9 @@ impl Mail {
.text(label("mail.sync.range"), label("mail.sync.range_pop"))
.text(label("mail.sync.server"), server);
} else {
let days = sync::BODY_DAYS.to_string();
details = details.text(
label("mail.sync.range"),
text(cx, "mail.sync.range_value", &[("days", &days)]),
);
for (key, value) in imap_range(&syncing.folders, cx) {
details = details.text(key, value);
}
}
if let (Some(synced), Protocol::Imap) = (syncing.synced, protocol) {
for (key, n) in [
@@ -206,6 +213,28 @@ impl Mail {
}
}
/// What the rest download, then folders kept whole.
pub fn imap_range(folders: &[LocalFolder], cx: &App) -> Vec<(SharedString, SharedString)> {
let (whole, rest): (Vec<_>, Vec<_>) = folders.iter().partition(|folder| folder.offline);
let label = |key: &str| text(cx, key, &[]);
let days = sync::BODY_DAYS.to_string();
let mut rows = Vec::new();
if whole.is_empty() || !rest.is_empty() {
let key = match whole.is_empty() {
true => "mail.sync.range_value",
false => "mail.sync.range_rest",
};
rows.push((label("mail.sync.range"), text(cx, key, &[("days", &days)])));
}
if !whole.is_empty() {
let sep = label("mail.sync.whole_sep");
let names: Vec<&str> = whole.iter().map(|folder| folder.name.as_str()).collect();
let value = text(cx, "mail.sync.whole_value", &[("names", &names.join(&sep))]);
rows.push((label("mail.sync.whole"), value));
}
rows
}
pub fn protocol_name(protocol: Protocol) -> &'static str {
match protocol {
Protocol::Imap => "IMAP",
@@ -235,6 +264,7 @@ impl Render for Mail {
cx.listener(|mail, _: &menu::AddAccount, window, cx| mail.start_adding(window, cx)),
)
.on_action(cx.listener(|mail, _: &menu::RemoveAccount, _, cx| mail.ask_remove(cx)))
.on_action(cx.listener(|mail, _: &menu::SyncAll, _, cx| mail.sync_all(cx)))
.child(shell)
.children(self.add_dialog(cx))
.children(self.remove_dialog(cx))