use std::hash::{DefaultHasher, Hash, Hasher}; use async_graphql::{ connection::{self, Connection, Edge}, Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject, }; use log::info; use notmuch::Notmuch; pub struct QueryRoot; /// # Number of seconds since the Epoch pub type UnixTime = isize; /// # Thread ID, sans "thread:" pub type ThreadId = String; #[derive(Debug, SimpleObject)] pub struct ThreadSummary { pub thread: ThreadId, pub timestamp: UnixTime, /// user-friendly timestamp pub date_relative: String, /// number of matched messages pub matched: isize, /// total messages in thread pub total: isize, /// comma-separated names with | between matched and unmatched pub authors: String, pub subject: String, pub tags: Vec, } #[derive(SimpleObject)] struct Tag { name: String, fg_color: String, bg_color: String, } #[Object] impl QueryRoot { async fn count<'ctx>(&self, ctx: &Context<'ctx>, query: String) -> Result { let nm = ctx.data_unchecked::(); Ok(nm.count(&query)?) } async fn search<'ctx>( &self, ctx: &Context<'ctx>, after: Option, before: Option, first: Option, last: Option, query: String, ) -> Result, Error> { let nm = ctx.data_unchecked::(); connection::query( after, before, first, last, |after, before, first, last| async move { info!("{after:?} {before:?} {first:?} {last:?} {query}"); let mut start = 0usize; let total = nm.count(&query)?; let page_size = first.unwrap_or(20); if let Some(after) = after { if after >= total { return Ok(Connection::new(false, false)); } start = after + 1; } let mut end = start + page_size; if end > total { end = total; } // TODO(wathiede): handle last/end. if let Some(before) = before { if before == 0 { return Ok(Connection::new(false, false)); } end = before; } let slice: Vec = nm .search(&query, start, first.unwrap_or(20))? .0 .into_iter() .map(|ts| ThreadSummary { thread: ts.thread, timestamp: ts.timestamp, date_relative: ts.date_relative, matched: ts.matched, total: ts.total, authors: ts.authors, subject: ts.subject, tags: ts.tags, }) .collect(); //let mut slice = &res[..]; /* if let Some(first) = first { slice = &slice[..first.min(slice.len())]; end -= first.min(slice.len()); } else if let Some(last) = last { slice = &slice[slice.len() - last.min(slice.len())..]; start = end - last.min(slice.len()); } */ let mut connection = Connection::new(start > 0, end < total); connection.edges.extend( slice .into_iter() .enumerate() .map(|(idx, item)| Edge::new(start + idx, item)), ); Ok::<_, Error>(connection) }, ) .await } async fn tags<'ctx>(&self, ctx: &Context<'ctx>) -> FieldResult> { let nm = ctx.data_unchecked::(); Ok(nm .tags()? .into_iter() .map(|tag| { let mut hasher = DefaultHasher::new(); tag.hash(&mut hasher); let hex = format!("#{:06x}", hasher.finish() % (1 << 24)); Tag { name: tag, fg_color: "white".to_string(), bg_color: hex, } }) .collect()) } } pub type GraphqlSchema = Schema;