Add delete email with optimistic UI update
Press 'd' to delete the selected email. Removes it from the list immediately and performs the server-side delete in the background for a snappy user experience. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e871c1aab8
commit
212fd49534
2 changed files with 62 additions and 1 deletions
23
src/inbox.rs
23
src/inbox.rs
|
|
@ -170,6 +170,29 @@ pub(crate) fn fetch_body(
|
||||||
extract_plain_text(&raw)
|
extract_plain_text(&raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete an email by sequence number (flag as \Deleted + expunge).
|
||||||
|
pub(crate) fn delete_email(
|
||||||
|
session: &mut Option<ImapSession>,
|
||||||
|
seq: u32,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let s = ensure_session(session, config)?;
|
||||||
|
let range = seq.to_string();
|
||||||
|
match s {
|
||||||
|
ImapSession::Plain(s) => {
|
||||||
|
s.select("INBOX").map_err(|e| e.to_string())?;
|
||||||
|
s.store(&range, "+FLAGS (\\Deleted)").map_err(|e| e.to_string())?;
|
||||||
|
s.expunge().map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
ImapSession::Tls(s) => {
|
||||||
|
s.select("INBOX").map_err(|e| e.to_string())?;
|
||||||
|
s.store(&range, "+FLAGS (\\Deleted)").map_err(|e| e.to_string())?;
|
||||||
|
s.expunge().map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn extract_raw_body(fetches: &[imap::types::Fetch]) -> Option<Vec<u8>> {
|
fn extract_raw_body(fetches: &[imap::types::Fetch]) -> Option<Vec<u8>> {
|
||||||
fetches.first().and_then(|f| {
|
fetches.first().and_then(|f| {
|
||||||
f.body().map(|b| b.to_vec())
|
f.body().map(|b| b.to_vec())
|
||||||
|
|
|
||||||
40
src/lib.rs
40
src/lib.rs
|
|
@ -34,6 +34,7 @@ enum WorkerCmd {
|
||||||
Refresh,
|
Refresh,
|
||||||
FetchMore { oldest_seq: u32 },
|
FetchMore { oldest_seq: u32 },
|
||||||
FetchBody { seq: u32 },
|
FetchBody { seq: u32 },
|
||||||
|
Delete { seq: u32 },
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ enum WorkerResult {
|
||||||
Refreshed(Result<inbox::Inbox, String>),
|
Refreshed(Result<inbox::Inbox, String>),
|
||||||
FetchedMore(Result<(Vec<Email>, u32), String>),
|
FetchedMore(Result<(Vec<Email>, u32), String>),
|
||||||
Body { seq: u32, result: Result<String, String> },
|
Body { seq: u32, result: Result<String, String> },
|
||||||
|
Deleted(Result<(), String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn worker_loop(
|
fn worker_loop(
|
||||||
|
|
@ -64,6 +66,10 @@ fn worker_loop(
|
||||||
let result = inbox::fetch_body(&mut session, seq, &config);
|
let result = inbox::fetch_body(&mut session, seq, &config);
|
||||||
let _ = result_tx.send(WorkerResult::Body { seq, result });
|
let _ = result_tx.send(WorkerResult::Body { seq, result });
|
||||||
}
|
}
|
||||||
|
WorkerCmd::Delete { seq } => {
|
||||||
|
let result = inbox::delete_email(&mut session, seq, &config);
|
||||||
|
let _ = result_tx.send(WorkerResult::Deleted(result));
|
||||||
|
}
|
||||||
WorkerCmd::Quit => break,
|
WorkerCmd::Quit => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -155,6 +161,11 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
WorkerResult::Deleted(result) => {
|
||||||
|
if let Err(e) = result {
|
||||||
|
error = Some(format!("Delete failed: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +259,7 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
.scroll((preview_scroll, 0));
|
.scroll((preview_scroll, 0));
|
||||||
frame.render_widget(preview, layout[1]);
|
frame.render_widget(preview, layout[1]);
|
||||||
|
|
||||||
let status = Paragraph::new(" 'q' quit | 'r' refresh | ↑/↓ navigate | Tab switch pane")
|
let status = Paragraph::new(" 'q' quit | 'r' refresh | 'd' delete | ↑/↓ navigate | Tab switch pane")
|
||||||
.style(Style::default().fg(Color::DarkGray));
|
.style(Style::default().fg(Color::DarkGray));
|
||||||
frame.render_widget(status, layout[2]);
|
frame.render_widget(status, layout[2]);
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -317,6 +328,33 @@ pub fn main(config: &Config, terminal: &mut Terminal<CrosstermBackend<Stdout>>)
|
||||||
preview_scroll = preview_scroll.saturating_sub(1);
|
preview_scroll = preview_scroll.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('d') if focus == Focus::Inbox => {
|
||||||
|
if let Some(idx) = list_state.selected() {
|
||||||
|
if idx < emails.len() {
|
||||||
|
let seq = emails[idx].seq;
|
||||||
|
// Remove from UI immediately
|
||||||
|
emails.remove(idx);
|
||||||
|
if emails.is_empty() {
|
||||||
|
list_state.select(None);
|
||||||
|
preview_seq = None;
|
||||||
|
preview_body.clear();
|
||||||
|
} else {
|
||||||
|
let new_idx = idx.min(emails.len().saturating_sub(1));
|
||||||
|
list_state.select(Some(new_idx));
|
||||||
|
let new_seq = emails[new_idx].seq;
|
||||||
|
if preview_seq != Some(new_seq) {
|
||||||
|
preview_seq = Some(new_seq);
|
||||||
|
preview_body.clear();
|
||||||
|
preview_scroll = 0;
|
||||||
|
body_loading = true;
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::FetchBody { seq: new_seq });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete on server in background
|
||||||
|
let _ = cmd_tx.send(WorkerCmd::Delete { seq });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue