diff --git a/src/app.rs b/src/app.rs index 1c22853..feab97a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,9 +10,10 @@ use std::sync::Arc; use fumi_core::address::Address; use fumi_core::crypto::{unb32, KEY_LEN}; use fumi_core::store::Store; +use iced::time::{self, Duration}; use iced::widget::qr_code; use iced::widget::text_editor::{self, Action}; -use iced::{clipboard, Task}; +use iced::{clipboard, Subscription, Task}; use crate::ops::{self, Folder, OpenedMsg, Row, Snapshot}; @@ -136,6 +137,9 @@ pub enum Message { MessageSelected(Folder, String), FetchPressed, FetchCancelled, + /// A live re-list while a fetch runs: the store commits each envelope + /// as it verifies, so the list can show mail arriving mid-fetch. + FetchTick, ComposePressed, ComposeToEdited(String), ComposeSubjectEdited(String), @@ -369,6 +373,16 @@ impl Carrier { self.store.account().ok().flatten() } + /// While a fetch runs, tick a few times a second so the list shows mail + /// as it lands instead of freezing until the whole fetch returns. + pub fn subscription(&self) -> Subscription { + if self.busy.as_deref() == Some("fetching") { + time::every(Duration::from_millis(250)).map(|_| Message::FetchTick) + } else { + Subscription::none() + } + } + pub fn update(&mut self, message: Message) -> Task { match message { // Text editor actions must survive the busy guard: typing in the @@ -389,6 +403,17 @@ impl Carrier { } Task::none() } + Message::FetchTick => { + if self.busy.as_deref() != Some("fetching") { + return Task::none(); + } + let master = self.master(); + let read = self.marks.snapshot(); + if let Ok(lists) = ops::lists(&self.store, &master, &read) { + self.lists = lists; + } + Task::none() + } // Results pass regardless of the busy flag: each one clears the // operation it belongs to, so swallowing it would deadlock. Message::PinOffered(result) => { @@ -974,3 +999,39 @@ mod tests { assert_eq!(app.busy.as_deref(), Some("fetching")); } } + +#[cfg(test)] +mod tick_tests { + use super::*; + use fumi_core::message::{build_frontmatter, message_id, seal}; + + /// The store is committed envelope by envelope, so a tick mid-fetch + /// sees a message that the running fetch has already verified. + #[test] + fn fetch_tick_lists_mail_as_it_lands() { + let dir = tempfile::tempdir().unwrap().keep(); + let (mut app, _task) = Carrier::boot(dir.clone()); + let master = ops::new_master(); + app.master = Some(master); + + let me = ops::account(&app.store, &master).unwrap(); + let addr = fumi_core::address::Address::parse("me@localhost").unwrap(); + app.store.set_account(&addr).unwrap(); + let body = build_frontmatter(&[("Subject", "live")], "tick"); + let envelope = seal(me.me(), &me.me().pk(), body.as_bytes(), 1000, true).unwrap(); + let id = message_id(&envelope); + app.store + .store_inbox(&id, &envelope, 1000, false, false) + .unwrap(); + + app.busy = Some("fetching".into()); + let _ = app.update(Message::FetchTick); + assert_eq!(app.busy.as_deref(), Some("fetching")); + assert_eq!(app.lists[0].len(), 1); + assert_eq!(app.lists[0][0].subject, "live"); + + // A tick outside a fetch is a no-op. + app.busy = None; + let _ = app.update(Message::FetchTick); + } +} diff --git a/src/main.rs b/src/main.rs index 70cb40a..2d090ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ fn main() -> iced::Result { } iced::application("carrier", Carrier::update, view::view) .theme(|_| theme::theme()) + .subscription(Carrier::subscription) .window_size(iced::Size::new(1080.0, 640.0)) .run_with(move || { let (state, task) = Carrier::boot(dir); diff --git a/src/view.rs b/src/view.rs index 31eaa54..a35b1da 100644 --- a/src/view.rs +++ b/src/view.rs @@ -198,9 +198,12 @@ fn status<'a>(app: &'a Carrier) -> Element<'a, Message> { let mut line = row![].spacing(10).align_y(Alignment::Center); match (&app.busy, &app.status) { (Some(busy), _) => { - line = line.push(muted(format!("{busy}…"))); if busy == "fetching" { + let inbox = app.lists.first().map(|rows| rows.len()).unwrap_or(0); + line = line.push(muted(format!("fetching… {inbox} in inbox"))); line = line.push(quiet_action("Cancel", Message::FetchCancelled)); + } else { + line = line.push(muted(format!("{busy}…"))); } } (None, Some(status)) => {