now displays messages in the message list \o/

This commit is contained in:
Michael Zhang 2021-03-27 02:14:25 -05:00
parent 1bc6776615
commit 41c05ec38a
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
9 changed files with 115 additions and 143 deletions

View file

@ -66,7 +66,8 @@ jobs:
path: | path: |
~/.cargo/registry ~/.cargo/registry
~/.cargo/git ~/.cargo/git
~/.cargo/bin
target target
key: ${{ runner.os }}-cargo key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
# vim: set sw=2 et : # vim: set sw=2 et :

2
Cargo.lock generated
View file

@ -2020,6 +2020,7 @@ dependencies = [
"futures 0.3.13", "futures 0.3.13",
"gluon", "gluon",
"hex 0.4.3", "hex 0.4.3",
"indexmap",
"inotify", "inotify",
"log", "log",
"mailparse", "mailparse",
@ -3224,6 +3225,7 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [ dependencies = [
"indexmap",
"serde", "serde",
] ]

View file

@ -44,9 +44,10 @@ tokio = { version = "1.3.0", features = ["full"] }
tokio-rustls = "0.22.0" tokio-rustls = "0.22.0"
tokio-stream = { version = "0.1.4", features = ["sync"] } tokio-stream = { version = "0.1.4", features = ["sync"] }
tokio-util = { version = "0.6.4", features = ["full"] } tokio-util = { version = "0.6.4", features = ["full"] }
toml = "0.5.8" toml = { version = "0.5.8", features = ["preserve_order"] }
webpki-roots = "0.21.0" webpki-roots = "0.21.0"
xdg = "2.2.0" xdg = "2.2.0"
indexmap = "1.6.2"
[dependencies.panorama-imap] [dependencies.panorama-imap]
path = "imap" path = "imap"

View file

@ -79,6 +79,7 @@ pub async fn sync_main(
debug!("select response: {:?}", select); debug!("select response: {:?}", select);
if let (Some(exists), Some(uidvalidity)) = (select.exists, select.uid_validity) { if let (Some(exists), Some(uidvalidity)) = (select.exists, select.uid_validity) {
// figure out which uids don't exist locally yet
let new_uids = stream::iter(1..exists).map(Ok).try_filter_map(|uid| { let new_uids = stream::iter(1..exists).map(Ok).try_filter_map(|uid| {
mail_store.try_identify_email(&acct_name, &folder, uid, uidvalidity, None) mail_store.try_identify_email(&acct_name, &folder, uid, uidvalidity, None)
// invert the option to only select uids that haven't been downloaded // invert the option to only select uids that haven't been downloaded

View file

@ -6,10 +6,12 @@ use std::path::{Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use chrono::{DateTime, Local};
use futures::{ use futures::{
future::{self, FutureExt, TryFutureExt}, future::{self, FutureExt, TryFutureExt},
stream::{StreamExt, TryStreamExt}, stream::{StreamExt, TryStreamExt},
}; };
use indexmap::IndexMap;
use panorama_imap::response::AttributeValue; use panorama_imap::response::AttributeValue;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use sqlx::{ use sqlx::{
@ -19,7 +21,7 @@ use sqlx::{
}; };
use tokio::{ use tokio::{
fs, fs,
sync::{broadcast, RwLock}, sync::{broadcast, watch, RwLock},
task::JoinHandle, task::JoinHandle,
}; };
@ -37,6 +39,10 @@ pub struct MailStore {
config: Arc<RwLock<Option<Config>>>, config: Arc<RwLock<Option<Config>>>,
inner: Arc<RwLock<Option<MailStoreInner>>>, inner: Arc<RwLock<Option<MailStoreInner>>>,
handle: Arc<JoinHandle<()>>, handle: Arc<JoinHandle<()>>,
store_out_tx: Arc<watch::Sender<Option<MailStoreUpdate>>>,
/// A receiver for listening to updates to the mail store
pub store_out_rx: watch::Receiver<Option<MailStoreUpdate>>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -44,12 +50,16 @@ pub struct MailStore {
struct MailStoreInner { struct MailStoreInner {
pool: SqlitePool, pool: SqlitePool,
mail_dir: PathBuf, mail_dir: PathBuf,
accounts: HashMap<String, Arc<AccountRef>>, accounts: IndexMap<String, Arc<AccountRef>>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
#[non_exhaustive]
/// Probably an event about new emails? i forgot /// Probably an event about new emails? i forgot
pub struct EmailUpdateInfo {} pub enum MailStoreUpdate {
/// The list of accounts has been updated (probably as a result of a config update)
AccountListUpdate(()),
}
impl MailStore { impl MailStore {
/// Creates a new MailStore /// Creates a new MailStore
@ -60,9 +70,14 @@ impl MailStore {
let inner = Arc::new(RwLock::new(None)); let inner = Arc::new(RwLock::new(None));
let inner2 = inner.clone(); let inner2 = inner.clone();
let (store_out_tx, store_out_rx) = watch::channel(None);
let store_out_tx = Arc::new(store_out_tx);
let store_out_tx2 = store_out_tx.clone();
let listener = async move { let listener = async move {
while let Ok(()) = config_watcher.changed().await { while let Ok(()) = config_watcher.changed().await {
let new_config = config_watcher.borrow().clone(); let new_config = config_watcher.borrow().clone();
let fut = future::try_join( let fut = future::try_join(
async { async {
let mut write = config2.write().await; let mut write = config2.write().await;
@ -77,13 +92,14 @@ impl MailStore {
Ok(()) Ok(())
}, },
); );
match fut.await { match fut.await {
Ok(_) => {} Ok(_) => store_out_tx2.send(Some(MailStoreUpdate::AccountListUpdate(()))),
Err(e) => { Err(e) => {
error!("during mail loop: {}", e); error!("during mail loop: {}", e);
panic!(); panic!();
} }
} };
} }
}; };
let handle = tokio::spawn(listener); let handle = tokio::spawn(listener);
@ -92,9 +108,14 @@ impl MailStore {
config, config,
inner, inner,
handle: Arc::new(handle), handle: Arc::new(handle),
store_out_tx,
store_out_rx,
} }
} }
/// Nuke all messages with an invalid UIDVALIDITY
pub async fn nuke_old_uidvalidity(&self, current: usize) {}
/// Given a UID and optional message-id try to identify a particular message /// Given a UID and optional message-id try to identify a particular message
pub async fn try_identify_email( pub async fn try_identify_email(
&self, &self,
@ -265,11 +286,11 @@ impl MailStore {
/// Return a map of the accounts that are currently being tracked as well as a reference to the /// Return a map of the accounts that are currently being tracked as well as a reference to the
/// account handles themselves /// account handles themselves
pub async fn list_accounts(&self) -> HashMap<String, Arc<AccountRef>> { pub async fn list_accounts(&self) -> IndexMap<String, Arc<AccountRef>> {
let read = self.inner.read().await; let read = self.inner.read().await;
let inner = match &*read { let inner = match read.as_ref() {
Some(v) => v, Some(v) => v,
None => return HashMap::new(), None => return IndexMap::new(),
}; };
inner.accounts.clone() inner.accounts.clone()
@ -365,6 +386,11 @@ impl AccountRef {
.bind(folder) .bind(folder)
.fetch(&self.pool) .fetch(&self.pool)
.map_ok(|(date, subject): (String, String)| EmailMetadata { .map_ok(|(date, subject): (String, String)| EmailMetadata {
date: Some(
DateTime::parse_from_rfc3339(&date)
.unwrap()
.with_timezone(&Local),
),
subject, subject,
..EmailMetadata::default() ..EmailMetadata::default()
}) })

View file

@ -1,90 +0,0 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use crate::mail::{EmailMetadata, MailEvent};
/// UI's view of the currently-known mail-related state of all accounts.
#[derive(Clone, Debug, Default)]
pub struct MailStore {
accounts: Arc<RwLock<HashMap<String, Arc<RwLock<MailAccountState>>>>>,
}
impl MailStore {
pub fn handle_mail_event(&self, evt: MailEvent) {
let acct_name = evt.acct_name().to_owned();
{
let accounts = self.accounts.read();
let contains_key = accounts.contains_key(&acct_name);
std::mem::drop(accounts);
if !contains_key {
let mut accounts = self.accounts.write();
accounts.insert(
acct_name.clone(),
Arc::new(RwLock::new(MailAccountState::default())),
);
}
}
let accounts = self.accounts.read();
if let Some(lock) = accounts.get(&acct_name) {
let mut state = lock.write();
state.update(evt);
}
}
pub fn iter_accts(&self) -> Vec<String> {
self.accounts.read().keys().cloned().collect()
}
pub fn folders_of(&self, acct_name: impl AsRef<str>) -> Option<Vec<String>> {
let accounts = self.accounts.read();
let lock = accounts.get(acct_name.as_ref())?;
let state = lock.read();
Some(state.folders.clone())
}
pub fn messages_of(&self, acct_name: impl AsRef<str>) -> Option<Vec<EmailMetadata>> {
let accounts = self.accounts.read();
let lock = accounts.get(acct_name.as_ref())?;
let state = lock.read();
let mut msgs = Vec::new();
for uid in state.message_uids.iter() {
if let Some(meta) = state.message_map.get(uid) {
msgs.push(meta.clone());
}
}
Some(msgs)
}
}
#[derive(Debug, Default)]
pub struct MailAccountState {
pub folders: Vec<String>,
pub message_uids: Vec<u32>,
pub message_map: HashMap<u32, EmailMetadata>,
}
impl MailAccountState {
pub fn update(&mut self, evt: MailEvent) {
match evt {
MailEvent::FolderList(_, new_folders) => self.folders = new_folders,
MailEvent::MessageUids(_, new_uids) => self.message_uids = new_uids,
MailEvent::UpdateUid(_, uid, attrs) => {
let meta = EmailMetadata::from_attrs(attrs);
let uid = meta.uid.unwrap_or(uid);
self.message_map.insert(uid, meta);
}
MailEvent::NewUid(_, uid) => {
debug!("new msg!");
self.message_uids.push(uid);
}
_ => {}
}
// debug!("mail store updated! {:?}", self);
}
}

View file

@ -19,9 +19,12 @@ use panorama_tui::{
widgets::*, widgets::*,
}, },
}; };
use tokio::task::JoinHandle; use tokio::{sync::RwLock, task::JoinHandle};
use crate::mail::{store::AccountRef, EmailMetadata}; use crate::mail::{
store::{AccountRef, MailStoreUpdate},
EmailMetadata,
};
use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI}; use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, UI};
@ -29,11 +32,17 @@ use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, U
/// A singular UI view of a list of mail /// A singular UI view of a list of mail
pub struct MailView { pub struct MailView {
pub mail_store: MailStore, pub mail_store: MailStore,
pub current_account: Option<Arc<AccountRef>>,
pub current_folder: Option<String>,
pub message_list: TableState, pub message_list: TableState,
pub selected: Arc<AtomicU32>, pub selected: Arc<AtomicU32>,
pub change: Arc<AtomicI8>, pub change: Arc<AtomicI8>,
current: Arc<RwLock<Option<Current>>>,
mail_store_listener: JoinHandle<()>,
}
#[derive(Debug)]
struct Current {
account: Arc<AccountRef>,
folder: Option<String>,
} }
impl HandlesInput for MailView { impl HandlesInput for MailView {
@ -60,7 +69,7 @@ impl Window for MailView {
String::from("email") String::from("email")
} }
async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI) { async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI) -> Result<()> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.margin(0) .margin(0)
@ -89,14 +98,12 @@ impl Window for MailView {
.highlight_symbol(">>"); .highlight_symbol(">>");
let mut rows = vec![]; let mut rows = vec![];
if let Some(acct_ref) = self.current_account.as_ref() { if let Some(current) = self.current.read().await.as_ref() {
let messages = acct_ref.get_newest_n_messages("INBOX", chunks[1].height as usize); let messages = current
} .account
.get_newest_n_messages("INBOX", chunks[1].height as usize)
for (acct_name, acct_ref) in accts.iter() { .await?;
let result: Option<Vec<EmailMetadata>> = None; // self.mail_store.messages_of(acct); for meta in messages.iter() {
if let Some(messages) = result {
for meta in messages {
let mut row = Row::new(vec![ let mut row = Row::new(vec![
String::from(if meta.unread { "\u{2b24}" } else { "" }), String::from(if meta.unread { "\u{2b24}" } else { "" }),
meta.uid.map(|u| u.to_string()).unwrap_or_default(), meta.uid.map(|u| u.to_string()).unwrap_or_default(),
@ -114,7 +121,6 @@ impl Window for MailView {
rows.push(row); rows.push(row);
} }
} }
}
let table = Table::new(rows) let table = Table::new(rows)
.style(Style::default().fg(Color::White)) .style(Style::default().fg(Color::White))
@ -133,6 +139,8 @@ impl Window for MailView {
f.render_widget(dirlist, chunks[0]); f.render_widget(dirlist, chunks[0]);
f.render_widget(table, chunks[1]); f.render_widget(table, chunks[1]);
Ok(())
} }
async fn update(&mut self) { async fn update(&mut self) {
@ -174,21 +182,42 @@ fn humanize_timestamp(date: DateTime<Local>) -> String {
impl MailView { impl MailView {
pub fn new(mail_store: MailStore) -> Self { pub fn new(mail_store: MailStore) -> Self {
let current = Arc::new(RwLock::new(None));
let current2 = current.clone();
let mut listener = mail_store.store_out_rx.clone();
let mail_store2 = mail_store.clone();
let mail_store_listener = tokio::spawn(async move {
while let Ok(()) = listener.changed().await {
let updated = listener.borrow().clone();
debug!("new update from mail store: {:?}", updated);
// TODO: maybe do the processing of updates somewhere else?
// in case events get missed
match updated {
Some(MailStoreUpdate::AccountListUpdate(_)) => {
// TODO: maybe have a default account?
let accounts = mail_store2.list_accounts().await;
if let Some((acct_name, acct_ref)) = accounts.iter().next() {
let mut write = current2.write().await;
*write = Some(Current {
account: acct_ref.clone(),
folder: None,
})
}
}
_ => {}
}
}
});
MailView { MailView {
mail_store, mail_store,
current_account: None, current,
current_folder: None,
message_list: TableState::default(), message_list: TableState::default(),
selected: Arc::new(AtomicU32::default()), selected: Arc::new(AtomicU32::default()),
change: Arc::new(AtomicI8::default()), change: Arc::new(AtomicI8::default()),
} mail_store_listener,
}
pub async fn set_current_account(&mut self, name: impl AsRef<str>) {
let name = name.as_ref();
let accounts = self.mail_store.list_accounts().await;
if let Some(acct_ref) = accounts.get(name) {
self.current_account = Some(acct_ref.clone());
} }
} }
@ -219,5 +248,4 @@ impl MailView {
// self.message_list.select(Some(len - 1)); // self.message_list.select(Some(len - 1));
// } // }
} }
} }

View file

@ -102,7 +102,7 @@ pub async fn run_ui2(params: UiParams) -> Result<()> {
term.pre_draw()?; term.pre_draw()?;
{ {
let mut frame = term.get_frame(); let mut frame = term.get_frame();
ui.draw(&mut frame).await; ui.draw(&mut frame).await?;
} }
term.post_draw()?; term.post_draw()?;
@ -148,7 +148,7 @@ pub struct UI {
} }
impl UI { impl UI {
async fn draw(&mut self, f: &mut FrameType<'_, '_>) { async fn draw(&mut self, f: &mut FrameType<'_, '_>) -> Result<()> {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.margin(0) .margin(0)
@ -179,10 +179,12 @@ impl UI {
let visible = self.window_layout.visible_windows(chunks[0]); let visible = self.window_layout.visible_windows(chunks[0]);
for (layout_id, area) in visible.into_iter() { for (layout_id, area) in visible.into_iter() {
if let Some(window) = self.windows.get(&layout_id) { if let Some(window) = self.windows.get(&layout_id) {
window.draw(f, area, self).await; window.draw(f, area, self).await?;
debug!("drew {:?} {:?}", layout_id, area); debug!("drew {:?} {:?}", layout_id, area);
} }
} }
Ok(())
} }
fn open_window(&mut self, window: impl Window) { fn open_window(&mut self, window: impl Window) {

View file

@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet, VecDeque}; use std::collections::{HashMap, HashSet, VecDeque};
use std::rc::Rc; use std::rc::Rc;
use anyhow::Result;
use futures::future::Future; use futures::future::Future;
use panorama_tui::tui::layout::Rect; use panorama_tui::tui::layout::Rect;
@ -12,7 +13,7 @@ pub trait Window: HandlesInput {
fn name(&self) -> String; fn name(&self) -> String;
/// Main draw function /// Main draw function
async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI); async fn draw(&self, f: &mut FrameType<'_, '_>, area: Rect, ui: &UI) -> Result<()>;
// async fn draw(&self, f: FrameType, area: Rect, ui: Rc<UI>); // async fn draw(&self, f: FrameType, area: Rect, ui: Rc<UI>);
/// Update function /// Update function