feat: list the store live while a fetch runs
This commit is contained in:
parent
50299547e5
commit
7ed2889966
3 changed files with 67 additions and 2 deletions
63
src/app.rs
63
src/app.rs
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue