now displays messages in the message list \o/
This commit is contained in:
parent
1bc6776615
commit
41c05ec38a
9 changed files with 115 additions and 143 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -66,7 +66,8 @@ jobs:
|
|||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin
|
||||
target
|
||||
key: ${{ runner.os }}-cargo
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
# vim: set sw=2 et :
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2020,6 +2020,7 @@ dependencies = [
|
|||
"futures 0.3.13",
|
||||
"gluon",
|
||||
"hex 0.4.3",
|
||||
"indexmap",
|
||||
"inotify",
|
||||
"log",
|
||||
"mailparse",
|
||||
|
@ -3224,6 +3225,7 @@ version = "0.5.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
|
@ -44,9 +44,10 @@ tokio = { version = "1.3.0", features = ["full"] }
|
|||
tokio-rustls = "0.22.0"
|
||||
tokio-stream = { version = "0.1.4", features = ["sync"] }
|
||||
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"
|
||||
xdg = "2.2.0"
|
||||
indexmap = "1.6.2"
|
||||
|
||||
[dependencies.panorama-imap]
|
||||
path = "imap"
|
||||
|
|
|
@ -79,6 +79,7 @@ pub async fn sync_main(
|
|||
debug!("select response: {:?}", select);
|
||||
|
||||
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| {
|
||||
mail_store.try_identify_email(&acct_name, &folder, uid, uidvalidity, None)
|
||||
// invert the option to only select uids that haven't been downloaded
|
||||
|
|
|
@ -6,10 +6,12 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Error, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
use futures::{
|
||||
future::{self, FutureExt, TryFutureExt},
|
||||
stream::{StreamExt, TryStreamExt},
|
||||
};
|
||||
use indexmap::IndexMap;
|
||||
use panorama_imap::response::AttributeValue;
|
||||
use sha2::{Digest, Sha256};
|
||||
use sqlx::{
|
||||
|
@ -19,7 +21,7 @@ use sqlx::{
|
|||
};
|
||||
use tokio::{
|
||||
fs,
|
||||
sync::{broadcast, RwLock},
|
||||
sync::{broadcast, watch, RwLock},
|
||||
task::JoinHandle,
|
||||
};
|
||||
|
||||
|
@ -37,6 +39,10 @@ pub struct MailStore {
|
|||
config: Arc<RwLock<Option<Config>>>,
|
||||
inner: Arc<RwLock<Option<MailStoreInner>>>,
|
||||
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)]
|
||||
|
@ -44,12 +50,16 @@ pub struct MailStore {
|
|||
struct MailStoreInner {
|
||||
pool: SqlitePool,
|
||||
mail_dir: PathBuf,
|
||||
accounts: HashMap<String, Arc<AccountRef>>,
|
||||
accounts: IndexMap<String, Arc<AccountRef>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
/// 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 {
|
||||
/// Creates a new MailStore
|
||||
|
@ -60,9 +70,14 @@ impl MailStore {
|
|||
let inner = Arc::new(RwLock::new(None));
|
||||
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 {
|
||||
while let Ok(()) = config_watcher.changed().await {
|
||||
let new_config = config_watcher.borrow().clone();
|
||||
|
||||
let fut = future::try_join(
|
||||
async {
|
||||
let mut write = config2.write().await;
|
||||
|
@ -77,13 +92,14 @@ impl MailStore {
|
|||
Ok(())
|
||||
},
|
||||
);
|
||||
|
||||
match fut.await {
|
||||
Ok(_) => {}
|
||||
Ok(_) => store_out_tx2.send(Some(MailStoreUpdate::AccountListUpdate(()))),
|
||||
Err(e) => {
|
||||
error!("during mail loop: {}", e);
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
let handle = tokio::spawn(listener);
|
||||
|
@ -92,9 +108,14 @@ impl MailStore {
|
|||
config,
|
||||
inner,
|
||||
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
|
||||
pub async fn try_identify_email(
|
||||
&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
|
||||
/// 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 inner = match &*read {
|
||||
let inner = match read.as_ref() {
|
||||
Some(v) => v,
|
||||
None => return HashMap::new(),
|
||||
None => return IndexMap::new(),
|
||||
};
|
||||
|
||||
inner.accounts.clone()
|
||||
|
@ -365,6 +386,11 @@ impl AccountRef {
|
|||
.bind(folder)
|
||||
.fetch(&self.pool)
|
||||
.map_ok(|(date, subject): (String, String)| EmailMetadata {
|
||||
date: Some(
|
||||
DateTime::parse_from_rfc3339(&date)
|
||||
.unwrap()
|
||||
.with_timezone(&Local),
|
||||
),
|
||||
subject,
|
||||
..EmailMetadata::default()
|
||||
})
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -19,9 +19,12 @@ use panorama_tui::{
|
|||
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};
|
||||
|
||||
|
@ -29,11 +32,17 @@ use super::{FrameType, HandlesInput, InputResult, MailStore, TermType, Window, U
|
|||
/// A singular UI view of a list of mail
|
||||
pub struct MailView {
|
||||
pub mail_store: MailStore,
|
||||
pub current_account: Option<Arc<AccountRef>>,
|
||||
pub current_folder: Option<String>,
|
||||
pub message_list: TableState,
|
||||
pub selected: Arc<AtomicU32>,
|
||||
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 {
|
||||
|
@ -60,7 +69,7 @@ impl Window for MailView {
|
|||
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()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
|
@ -89,30 +98,27 @@ impl Window for MailView {
|
|||
.highlight_symbol(">>");
|
||||
|
||||
let mut rows = vec![];
|
||||
if let Some(acct_ref) = self.current_account.as_ref() {
|
||||
let messages = acct_ref.get_newest_n_messages("INBOX", chunks[1].height as usize);
|
||||
}
|
||||
|
||||
for (acct_name, acct_ref) in accts.iter() {
|
||||
let result: Option<Vec<EmailMetadata>> = None; // self.mail_store.messages_of(acct);
|
||||
if let Some(messages) = result {
|
||||
for meta in messages {
|
||||
let mut row = Row::new(vec![
|
||||
String::from(if meta.unread { "\u{2b24}" } else { "" }),
|
||||
meta.uid.map(|u| u.to_string()).unwrap_or_default(),
|
||||
meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
||||
meta.from.clone(),
|
||||
meta.subject.clone(),
|
||||
]);
|
||||
if meta.unread {
|
||||
row = row.style(
|
||||
Style::default()
|
||||
.fg(Color::LightCyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
rows.push(row);
|
||||
if let Some(current) = self.current.read().await.as_ref() {
|
||||
let messages = current
|
||||
.account
|
||||
.get_newest_n_messages("INBOX", chunks[1].height as usize)
|
||||
.await?;
|
||||
for meta in messages.iter() {
|
||||
let mut row = Row::new(vec![
|
||||
String::from(if meta.unread { "\u{2b24}" } else { "" }),
|
||||
meta.uid.map(|u| u.to_string()).unwrap_or_default(),
|
||||
meta.date.map(|d| humanize_timestamp(d)).unwrap_or_default(),
|
||||
meta.from.clone(),
|
||||
meta.subject.clone(),
|
||||
]);
|
||||
if meta.unread {
|
||||
row = row.style(
|
||||
Style::default()
|
||||
.fg(Color::LightCyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,6 +139,8 @@ impl Window for MailView {
|
|||
|
||||
f.render_widget(dirlist, chunks[0]);
|
||||
f.render_widget(table, chunks[1]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update(&mut self) {
|
||||
|
@ -174,21 +182,42 @@ fn humanize_timestamp(date: DateTime<Local>) -> String {
|
|||
|
||||
impl MailView {
|
||||
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 {
|
||||
mail_store,
|
||||
current_account: None,
|
||||
current_folder: None,
|
||||
current,
|
||||
message_list: TableState::default(),
|
||||
selected: Arc::new(AtomicU32::default()),
|
||||
change: Arc::new(AtomicI8::default()),
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
mail_store_listener,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,5 +248,4 @@ impl MailView {
|
|||
// self.message_list.select(Some(len - 1));
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ pub async fn run_ui2(params: UiParams) -> Result<()> {
|
|||
term.pre_draw()?;
|
||||
{
|
||||
let mut frame = term.get_frame();
|
||||
ui.draw(&mut frame).await;
|
||||
ui.draw(&mut frame).await?;
|
||||
}
|
||||
term.post_draw()?;
|
||||
|
||||
|
@ -148,7 +148,7 @@ pub struct UI {
|
|||
}
|
||||
|
||||
impl UI {
|
||||
async fn draw(&mut self, f: &mut FrameType<'_, '_>) {
|
||||
async fn draw(&mut self, f: &mut FrameType<'_, '_>) -> Result<()> {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
|
@ -179,10 +179,12 @@ impl UI {
|
|||
let visible = self.window_layout.visible_windows(chunks[0]);
|
||||
for (layout_id, area) in visible.into_iter() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_window(&mut self, window: impl Window) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
use futures::future::Future;
|
||||
use panorama_tui::tui::layout::Rect;
|
||||
|
||||
|
@ -12,7 +13,7 @@ pub trait Window: HandlesInput {
|
|||
fn name(&self) -> String;
|
||||
|
||||
/// 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>);
|
||||
|
||||
/// Update function
|
||||
|
|
Loading…
Reference in a new issue