feat: list the store live while a fetch runs

This commit is contained in:
randogoth 2026-10-02 00:00:41 +03:00
parent 50299547e5
commit 7ed2889966
3 changed files with 67 additions and 2 deletions

View file

@ -10,9 +10,10 @@ use std::sync::Arc;
use fumi_core::address::Address; use fumi_core::address::Address;
use fumi_core::crypto::{unb32, KEY_LEN}; use fumi_core::crypto::{unb32, KEY_LEN};
use fumi_core::store::Store; use fumi_core::store::Store;
use iced::time::{self, Duration};
use iced::widget::qr_code; use iced::widget::qr_code;
use iced::widget::text_editor::{self, Action}; use iced::widget::text_editor::{self, Action};
use iced::{clipboard, Task}; use iced::{clipboard, Subscription, Task};
use crate::ops::{self, Folder, OpenedMsg, Row, Snapshot}; use crate::ops::{self, Folder, OpenedMsg, Row, Snapshot};
@ -136,6 +137,9 @@ pub enum Message {
MessageSelected(Folder, String), MessageSelected(Folder, String),
FetchPressed, FetchPressed,
FetchCancelled, 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, ComposePressed,
ComposeToEdited(String), ComposeToEdited(String),
ComposeSubjectEdited(String), ComposeSubjectEdited(String),
@ -369,6 +373,16 @@ impl Carrier {
self.store.account().ok().flatten() 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<Message> {
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<Message> { pub fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
// Text editor actions must survive the busy guard: typing in the // Text editor actions must survive the busy guard: typing in the
@ -389,6 +403,17 @@ impl Carrier {
} }
Task::none() 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 // Results pass regardless of the busy flag: each one clears the
// operation it belongs to, so swallowing it would deadlock. // operation it belongs to, so swallowing it would deadlock.
Message::PinOffered(result) => { Message::PinOffered(result) => {
@ -974,3 +999,39 @@ mod tests {
assert_eq!(app.busy.as_deref(), Some("fetching")); 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);
}
}

View file

@ -20,6 +20,7 @@ fn main() -> iced::Result {
} }
iced::application("carrier", Carrier::update, view::view) iced::application("carrier", Carrier::update, view::view)
.theme(|_| theme::theme()) .theme(|_| theme::theme())
.subscription(Carrier::subscription)
.window_size(iced::Size::new(1080.0, 640.0)) .window_size(iced::Size::new(1080.0, 640.0))
.run_with(move || { .run_with(move || {
let (state, task) = Carrier::boot(dir); let (state, task) = Carrier::boot(dir);

View file

@ -198,9 +198,12 @@ fn status<'a>(app: &'a Carrier) -> Element<'a, Message> {
let mut line = row![].spacing(10).align_y(Alignment::Center); let mut line = row![].spacing(10).align_y(Alignment::Center);
match (&app.busy, &app.status) { match (&app.busy, &app.status) {
(Some(busy), _) => { (Some(busy), _) => {
line = line.push(muted(format!("{busy}…")));
if busy == "fetching" { 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)); line = line.push(quiet_action("Cancel", Message::FetchCancelled));
} else {
line = line.push(muted(format!("{busy}…")));
} }
} }
(None, Some(status)) => { (None, Some(status)) => {