Compare commits
14 Commits
2a24a20529
...
1cdabc348b
| Author | SHA1 | Date | |
|---|---|---|---|
| 1cdabc348b | |||
| 02e16b4547 | |||
| d5a001bf03 | |||
| 0ae72b63d0 | |||
| 447a4a3387 | |||
| 0737f5aac5 | |||
| 3e3024dd5c | |||
| 24414b04bb | |||
| f7df834325 | |||
| bce2c741c4 | |||
| 1b44bc57bb | |||
| ff6675b08f | |||
| 64912be4eb | |||
| 57ccef18cb |
169
Cargo.lock
generated
169
Cargo.lock
generated
@ -66,6 +66,12 @@ version = "1.0.75"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ascii"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ascii_utils"
|
name = "ascii_utils"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@ -91,6 +97,7 @@ dependencies = [
|
|||||||
"handlebars",
|
"handlebars",
|
||||||
"http",
|
"http",
|
||||||
"indexmap 2.0.0",
|
"indexmap 2.0.0",
|
||||||
|
"log 0.4.20",
|
||||||
"mime 0.3.17",
|
"mime 0.3.17",
|
||||||
"multer",
|
"multer",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
@ -335,6 +342,16 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18e9079d1a12a2cc2bffb5db039c43661836ead4082120d5844f02555aca2d46"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"encoding_rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.31"
|
version = "0.4.31"
|
||||||
@ -349,6 +366,19 @@ dependencies = [
|
|||||||
"windows-targets",
|
"windows-targets",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "3.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
|
||||||
|
dependencies = [
|
||||||
|
"ascii",
|
||||||
|
"byteorder",
|
||||||
|
"either",
|
||||||
|
"memchr",
|
||||||
|
"unreachable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console_error_panic_hook"
|
name = "console_error_panic_hook"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@ -437,16 +467,6 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if 1.0.0",
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@ -586,6 +606,12 @@ dependencies = [
|
|||||||
"syn 2.0.29",
|
"syn 2.0.29",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbg"
|
name = "dbg"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@ -1103,6 +1129,64 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql-introspection-query"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f2a4732cf5140bd6c082434494f785a19cfb566ab07d1382c3671f5812fed6d"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql-parser"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2ebc8013b4426d5b81a4364c419a95ed0b404af2b82e2457de52d9348f0e474"
|
||||||
|
dependencies = [
|
||||||
|
"combine",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql_client"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cdf7b487d864c2939b23902291a5041bc4a84418268f25fda1c8d4e15ad8fa"
|
||||||
|
dependencies = [
|
||||||
|
"graphql_query_derive",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql_client_codegen"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a40f793251171991c4eb75bd84bc640afa8b68ff6907bc89d3b712a22f700506"
|
||||||
|
dependencies = [
|
||||||
|
"graphql-introspection-query",
|
||||||
|
"graphql-parser",
|
||||||
|
"heck",
|
||||||
|
"lazy_static",
|
||||||
|
"proc-macro2 1.0.66",
|
||||||
|
"quote 1.0.33",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "graphql_query_derive"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00bda454f3d313f909298f626115092d348bc231025699f557b27e248475f48c"
|
||||||
|
dependencies = [
|
||||||
|
"graphql_client_codegen",
|
||||||
|
"proc-macro2 1.0.66",
|
||||||
|
"syn 1.0.109",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.21"
|
version = "0.3.21"
|
||||||
@ -1470,6 +1554,7 @@ dependencies = [
|
|||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
"css-inline",
|
"css-inline",
|
||||||
|
"graphql_client",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log 0.4.20",
|
"log 0.4.20",
|
||||||
"notmuch",
|
"notmuch",
|
||||||
@ -1477,6 +1562,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
|
"thiserror",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"wasm-timer",
|
"wasm-timer",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -1540,6 +1626,17 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mailparse"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6b56570f5f8c0047260d1c8b5b331f62eb9c660b9dd4071a8c46f8c7d3f280aa"
|
||||||
|
dependencies = [
|
||||||
|
"charset",
|
||||||
|
"data-encoding",
|
||||||
|
"quoted_printable",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
@ -1575,6 +1672,16 @@ version = "2.6.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memmap"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memoffset"
|
name = "memoffset"
|
||||||
version = "0.6.5"
|
version = "0.6.5"
|
||||||
@ -2152,6 +2259,12 @@ dependencies = [
|
|||||||
"proc-macro2 1.0.66",
|
"proc-macro2 1.0.66",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quoted_printable"
|
||||||
|
version = "0.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a3866219251662ec3b26fc217e3e05bf9c4f84325234dfb96bf0bf840889e49"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@ -2235,9 +2348,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
|
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"rayon-core",
|
"rayon-core",
|
||||||
@ -2245,14 +2358,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon-core"
|
name = "rayon-core"
|
||||||
version = "1.11.0"
|
version = "1.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
|
||||||
"crossbeam-deque",
|
"crossbeam-deque",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"num_cpus",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2729,7 +2840,10 @@ dependencies = [
|
|||||||
"async-graphql-rocket",
|
"async-graphql-rocket",
|
||||||
"glog",
|
"glog",
|
||||||
"log 0.4.20",
|
"log 0.4.20",
|
||||||
|
"mailparse",
|
||||||
|
"memmap",
|
||||||
"notmuch",
|
"notmuch",
|
||||||
|
"rayon",
|
||||||
"rocket 0.5.0",
|
"rocket 0.5.0",
|
||||||
"rocket_contrib",
|
"rocket_contrib",
|
||||||
"rocket_cors",
|
"rocket_cors",
|
||||||
@ -3005,18 +3119,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.47"
|
version = "1.0.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
|
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.47"
|
version = "1.0.50"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
|
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.66",
|
"proc-macro2 1.0.66",
|
||||||
"quote 1.0.33",
|
"quote 1.0.33",
|
||||||
@ -3362,6 +3476,15 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unreachable"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||||
|
dependencies = [
|
||||||
|
"void",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -3429,6 +3552,12 @@ version = "0.9.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "void"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.3.3"
|
version = "2.3.3"
|
||||||
|
|||||||
@ -491,6 +491,8 @@ impl Notmuch {
|
|||||||
offset: usize,
|
offset: usize,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
) -> Result<SearchSummary, NotmuchError> {
|
) -> Result<SearchSummary, NotmuchError> {
|
||||||
|
let query = if query.is_empty() { "*" } else { query };
|
||||||
|
|
||||||
let res = self.run_notmuch([
|
let res = self.run_notmuch([
|
||||||
"search",
|
"search",
|
||||||
"--format=json",
|
"--format=json",
|
||||||
@ -559,7 +561,10 @@ impl Notmuch {
|
|||||||
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
|
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(wathiede): implement tags() based on "notmuch search --output=tags '*'"
|
pub fn files(&self, query: &str) -> Result<Lines<BufReader<ChildStdout>>, NotmuchError> {
|
||||||
|
let mut child = self.run_notmuch_pipe(["search", "--output=files", query])?;
|
||||||
|
Ok(BufReader::new(child.stdout.take().unwrap()).lines())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
|
fn run_notmuch<I, S>(&self, args: I) -> Result<Vec<u8>, NotmuchError>
|
||||||
where
|
where
|
||||||
|
|||||||
@ -17,9 +17,12 @@ log = "0.4.17"
|
|||||||
tokio = "1.26.0"
|
tokio = "1.26.0"
|
||||||
glog = "0.1.0"
|
glog = "0.1.0"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
async-graphql = "6.0.11"
|
async-graphql = { version = "6.0.11", features = ["log"] }
|
||||||
async-graphql-rocket = "6.0.11"
|
async-graphql-rocket = "6.0.11"
|
||||||
rocket_cors = "0.6.0"
|
rocket_cors = "0.6.0"
|
||||||
|
rayon = "1.8.0"
|
||||||
|
memmap = "0.7.0"
|
||||||
|
mailparse = "0.14.0"
|
||||||
|
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
|
|||||||
@ -167,6 +167,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
|
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
|
||||||
.data(Notmuch::default())
|
.data(Notmuch::default())
|
||||||
|
.extension(async_graphql::extensions::Logger)
|
||||||
.finish();
|
.finish();
|
||||||
|
|
||||||
let _ = rocket::build()
|
let _ = rocket::build()
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
|
};
|
||||||
|
|
||||||
use async_graphql::{
|
use async_graphql::{
|
||||||
connection::{self, Connection, Edge},
|
connection::{self, Connection, Edge},
|
||||||
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
Context, EmptyMutation, EmptySubscription, Error, FieldResult, Object, Schema, SimpleObject,
|
||||||
};
|
};
|
||||||
use log::info;
|
use log::{info, warn};
|
||||||
|
use mailparse::{parse_mail, MailHeaderMap, ParsedMail};
|
||||||
|
use memmap::MmapOptions;
|
||||||
use notmuch::Notmuch;
|
use notmuch::Notmuch;
|
||||||
|
use rayon::prelude::*;
|
||||||
|
|
||||||
pub struct QueryRoot;
|
pub struct QueryRoot;
|
||||||
|
|
||||||
@ -31,11 +37,38 @@ pub struct ThreadSummary {
|
|||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct Thread {
|
||||||
|
subject: String,
|
||||||
|
messages: Vec<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct Message {
|
||||||
|
// First From header found in email
|
||||||
|
pub from: Option<Email>,
|
||||||
|
// All To headers found in email
|
||||||
|
pub to: Vec<Email>,
|
||||||
|
// All CC headers found in email
|
||||||
|
pub cc: Vec<Email>,
|
||||||
|
// First Subject header found in email
|
||||||
|
pub subject: Option<String>,
|
||||||
|
// Parsed Date header, if found and valid
|
||||||
|
pub timestamp: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, SimpleObject)]
|
||||||
|
pub struct Email {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub addr: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(SimpleObject)]
|
#[derive(SimpleObject)]
|
||||||
struct Tag {
|
struct Tag {
|
||||||
name: String,
|
name: String,
|
||||||
fg_color: String,
|
fg_color: String,
|
||||||
bg_color: String,
|
bg_color: String,
|
||||||
|
unread: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Object]
|
#[Object]
|
||||||
@ -62,31 +95,26 @@ impl QueryRoot {
|
|||||||
last,
|
last,
|
||||||
|after, before, first, last| async move {
|
|after, before, first, last| async move {
|
||||||
info!("{after:?} {before:?} {first:?} {last:?} {query}");
|
info!("{after:?} {before:?} {first:?} {last:?} {query}");
|
||||||
let mut start = 0usize;
|
|
||||||
let total = nm.count(&query)?;
|
let total = nm.count(&query)?;
|
||||||
let page_size = first.unwrap_or(20);
|
let (first, last) = if let (None, None) = (first, last) {
|
||||||
|
info!("neither first nor last set, defaulting to 20");
|
||||||
|
(Some(20), Some(20))
|
||||||
|
} else {
|
||||||
|
(first, last)
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(after) = after {
|
let mut start = after.map(|after| after + 1).unwrap_or(0);
|
||||||
if after >= total {
|
let mut end = before.unwrap_or(total);
|
||||||
return Ok(Connection::new(false, false));
|
if let Some(first) = first {
|
||||||
|
end = (start + first).min(end);
|
||||||
}
|
}
|
||||||
start = after + 1;
|
if let Some(last) = last {
|
||||||
}
|
start = if last > end - start { end } else { end - last };
|
||||||
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 count = end - start;
|
||||||
let slice: Vec<ThreadSummary> = nm
|
let slice: Vec<ThreadSummary> = nm
|
||||||
.search(&query, start, first.unwrap_or(20))?
|
.search(&query, start, count)?
|
||||||
.0
|
.0
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|ts| ThreadSummary {
|
.map(|ts| ThreadSummary {
|
||||||
@ -100,17 +128,6 @@ impl QueryRoot {
|
|||||||
tags: ts.tags,
|
tags: ts.tags,
|
||||||
})
|
})
|
||||||
.collect();
|
.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);
|
let mut connection = Connection::new(start > 0, end < total);
|
||||||
connection.edges.extend(
|
connection.edges.extend(
|
||||||
@ -129,19 +146,117 @@ impl QueryRoot {
|
|||||||
let nm = ctx.data_unchecked::<Notmuch>();
|
let nm = ctx.data_unchecked::<Notmuch>();
|
||||||
Ok(nm
|
Ok(nm
|
||||||
.tags()?
|
.tags()?
|
||||||
.into_iter()
|
.into_par_iter()
|
||||||
.map(|tag| {
|
.map(|tag| {
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
tag.hash(&mut hasher);
|
tag.hash(&mut hasher);
|
||||||
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
|
let hex = format!("#{:06x}", hasher.finish() % (1 << 24));
|
||||||
|
let unread = if ctx.look_ahead().field("unread").exists() {
|
||||||
|
nm.count(&format!("tag:{tag} is:unread")).unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
Tag {
|
Tag {
|
||||||
name: tag,
|
name: tag,
|
||||||
fg_color: "white".to_string(),
|
fg_color: "white".to_string(),
|
||||||
bg_color: hex,
|
bg_color: hex,
|
||||||
|
unread,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
async fn thread<'ctx>(&self, ctx: &Context<'ctx>, thread_id: String) -> Result<Thread, Error> {
|
||||||
|
// TODO(wathiede): normalize all email addresses through an address book with preferred
|
||||||
|
// display names (that default to the most commonly seen name).
|
||||||
|
let nm = ctx.data_unchecked::<Notmuch>();
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
for path in nm.files(&thread_id)? {
|
||||||
|
let path = path?;
|
||||||
|
let file = File::open(&path)?;
|
||||||
|
let mmap = unsafe { MmapOptions::new().map(&file)? };
|
||||||
|
let m = parse_mail(&mmap)?;
|
||||||
|
let from = email_addresses(&path, &m, "from")?;
|
||||||
|
let from = match from.len() {
|
||||||
|
0 => None,
|
||||||
|
1 => from.into_iter().next(),
|
||||||
|
_ => {
|
||||||
|
warn!(
|
||||||
|
"Got {} from addresses in message, truncating: {:?}",
|
||||||
|
from.len(),
|
||||||
|
from
|
||||||
|
);
|
||||||
|
from.into_iter().next()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let to = email_addresses(&path, &m, "to")?;
|
||||||
|
let cc = email_addresses(&path, &m, "cc")?;
|
||||||
|
let subject = m.headers.get_first_value("subject");
|
||||||
|
let timestamp = m
|
||||||
|
.headers
|
||||||
|
.get_first_value("date")
|
||||||
|
.and_then(|d| mailparse::dateparse(&d).ok());
|
||||||
|
messages.push(Message {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
subject,
|
||||||
|
timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messages.reverse();
|
||||||
|
// Find the first subject that's set. After reversing the vec, this should be the oldest
|
||||||
|
// message.
|
||||||
|
let subject: String = messages
|
||||||
|
.iter()
|
||||||
|
.skip_while(|m| m.subject.is_none())
|
||||||
|
.next()
|
||||||
|
.and_then(|m| m.subject.clone())
|
||||||
|
.unwrap_or("(NO SUBJECT)".to_string());
|
||||||
|
Ok(Thread { subject, messages })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type GraphqlSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
|
pub type GraphqlSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>;
|
||||||
|
|
||||||
|
fn email_addresses(path: &str, m: &ParsedMail, header_name: &str) -> Result<Vec<Email>, Error> {
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
for header_value in m.headers.get_all_values(header_name) {
|
||||||
|
match mailparse::addrparse(&header_value) {
|
||||||
|
Ok(mal) => {
|
||||||
|
for ma in mal.into_inner() {
|
||||||
|
match ma {
|
||||||
|
mailparse::MailAddr::Group(gi) => {
|
||||||
|
if !gi.group_name.contains("ndisclosed") {
|
||||||
|
println!("[{path}][{header_name}] Group: {gi}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mailparse::MailAddr::Single(s) => addrs.push(Email {
|
||||||
|
name: s.display_name,
|
||||||
|
addr: Some(s.addr),
|
||||||
|
}), //println!("Single: {s}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
let v = header_value;
|
||||||
|
if v.matches('@').count() == 1 {
|
||||||
|
if v.matches('<').count() == 1 && v.ends_with('>') {
|
||||||
|
let idx = v.find('<').unwrap();
|
||||||
|
let addr = &v[idx + 1..v.len() - 1].trim();
|
||||||
|
let name = &v[..idx].trim();
|
||||||
|
addrs.push(Email {
|
||||||
|
name: Some(name.to_string()),
|
||||||
|
addr: Some(addr.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addrs.push(Email {
|
||||||
|
name: Some(v),
|
||||||
|
addr: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(addrs)
|
||||||
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@ serde_json = { version = "1.0.93", features = ["unbounded_depth"] }
|
|||||||
wasm-timer = "0.2.5"
|
wasm-timer = "0.2.5"
|
||||||
css-inline = "0.8.5"
|
css-inline = "0.8.5"
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
|
graphql_client = "0.13.0"
|
||||||
|
thiserror = "1.0.50"
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
wasm-opt = ['-Os']
|
wasm-opt = ['-Os']
|
||||||
|
|||||||
@ -9,3 +9,7 @@ port = 6758
|
|||||||
[[proxy]]
|
[[proxy]]
|
||||||
backend = "http://localhost:9345/"
|
backend = "http://localhost:9345/"
|
||||||
rewrite= "/api/"
|
rewrite= "/api/"
|
||||||
|
[[proxy]]
|
||||||
|
backend="http://localhost:9345/graphiql"
|
||||||
|
[[proxy]]
|
||||||
|
backend="http://localhost:9345/graphql"
|
||||||
|
|||||||
23
web/graphql/front_page.graphql
Normal file
23
web/graphql/front_page.graphql
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
query FrontPageQuery($query: String!, $after: String $before: String, $first: Int, $last: Int) {
|
||||||
|
count(query: $query)
|
||||||
|
search(query: $query, after: $after, before: $before, first: $first, last: $last) {
|
||||||
|
pageInfo {
|
||||||
|
hasPreviousPage
|
||||||
|
hasNextPage
|
||||||
|
startCursor
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
nodes {
|
||||||
|
thread
|
||||||
|
timestamp
|
||||||
|
subject
|
||||||
|
authors
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
bgColor
|
||||||
|
fgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
1715
web/graphql/schema.json
Normal file
1715
web/graphql/schema.json
Normal file
File diff suppressed because it is too large
Load Diff
26
web/graphql/show_thread.graphql
Normal file
26
web/graphql/show_thread.graphql
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
query ShowThreadQuery($threadId: String!) {
|
||||||
|
thread(threadId: $threadId) {
|
||||||
|
subject
|
||||||
|
messages {
|
||||||
|
subject
|
||||||
|
from {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
cc {
|
||||||
|
name
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
timestamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
name
|
||||||
|
bgColor
|
||||||
|
fgColor
|
||||||
|
}
|
||||||
|
}
|
||||||
4
web/graphql/update_schema.sh
Executable file
4
web/graphql/update_schema.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
DEV_HOST=localhost
|
||||||
|
DEV_PORT=9345
|
||||||
|
graphql-client introspect-schema http://${DEV_HOST:?}:${DEV_PORT:?}/graphql --output schema.json
|
||||||
|
git diff schema.json
|
||||||
@ -47,7 +47,7 @@ iframe {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.index .date {
|
.index .date {
|
||||||
width: 7em;
|
width: 10em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
@ -95,6 +95,11 @@ input::placeholder, .input::placeholder{
|
|||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile .search-results,
|
||||||
|
.mobile .thread {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.search-results .row {
|
.search-results .row {
|
||||||
border-bottom: 1px #444 solid;
|
border-bottom: 1px #444 solid;
|
||||||
padding-bottom: .5em;
|
padding-bottom: .5em;
|
||||||
@ -118,6 +123,26 @@ input::placeholder, .input::placeholder{
|
|||||||
.float-right {
|
.float-right {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
/* Hide quoted emails */
|
||||||
|
div[name="quote"],
|
||||||
|
blockquote[type="cite"],
|
||||||
|
.gmail_quote {
|
||||||
|
background-color: red;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.desktop-main-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 12rem 1fr;
|
||||||
|
}
|
||||||
|
.tags-menu {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.tags-menu .menu-list a {
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
43
web/src/graphql.rs
Normal file
43
web/src/graphql.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use graphql_client::GraphQLQuery;
|
||||||
|
use seed::{
|
||||||
|
fetch,
|
||||||
|
fetch::{Header, Method, Request},
|
||||||
|
};
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
|
// The paths are relative to the directory where your `Cargo.toml` is located.
|
||||||
|
// Both json and the GraphQL schema language are supported as sources for the schema
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.json",
|
||||||
|
query_path = "graphql/front_page.graphql",
|
||||||
|
response_derives = "Debug"
|
||||||
|
)]
|
||||||
|
pub struct FrontPageQuery;
|
||||||
|
|
||||||
|
#[derive(GraphQLQuery)]
|
||||||
|
#[graphql(
|
||||||
|
schema_path = "graphql/schema.json",
|
||||||
|
query_path = "graphql/show_thread.graphql",
|
||||||
|
response_derives = "Debug"
|
||||||
|
)]
|
||||||
|
pub struct ShowThreadQuery;
|
||||||
|
|
||||||
|
pub async fn send_graphql<Body, Resp>(body: Body) -> fetch::Result<graphql_client::Response<Resp>>
|
||||||
|
where
|
||||||
|
Body: Serialize,
|
||||||
|
Resp: DeserializeOwned + 'static,
|
||||||
|
{
|
||||||
|
use web_sys::RequestMode;
|
||||||
|
|
||||||
|
Request::new("/graphql/")
|
||||||
|
.method(Method::Post)
|
||||||
|
.header(Header::content_type("application/json"))
|
||||||
|
.mode(RequestMode::Cors)
|
||||||
|
.json(&body)?
|
||||||
|
.fetch()
|
||||||
|
.await?
|
||||||
|
.check_status()?
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
}
|
||||||
547
web/src/lib.rs
547
web/src/lib.rs
@ -8,14 +8,33 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
use chrono::{DateTime, Datelike, Duration, Local, Utc};
|
||||||
|
use graphql_client::GraphQLQuery;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, Level};
|
use log::{debug, error, info, Level};
|
||||||
use notmuch::{Content, Part, Thread, ThreadNode, ThreadSet};
|
use notmuch::{Content, Part, Thread, ThreadNode, ThreadSet};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use serde::de::Deserialize;
|
use serde::de::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
use wasm_timer::Instant;
|
use wasm_timer::Instant;
|
||||||
|
|
||||||
|
use crate::graphql::{front_page_query::*, send_graphql, show_thread_query::*};
|
||||||
|
|
||||||
|
mod graphql;
|
||||||
|
|
||||||
const SEARCH_RESULTS_PER_PAGE: usize = 20;
|
const SEARCH_RESULTS_PER_PAGE: usize = 20;
|
||||||
|
const USE_GRAPHQL: bool = true;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum UIError {
|
||||||
|
#[error("No error, this should never be presented to user")]
|
||||||
|
NoError,
|
||||||
|
#[error("failed to fetch {0}: {1:?}")]
|
||||||
|
FetchError(&'static str, FetchError),
|
||||||
|
#[error("{0} error decoding: {1:?}")]
|
||||||
|
FetchDecodeError(&'static str, Vec<graphql_client::Error>),
|
||||||
|
#[error("no data or errors for {0}")]
|
||||||
|
NoData(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
// ------ ------
|
// ------ ------
|
||||||
// Init
|
// Init
|
||||||
@ -34,6 +53,8 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
context: Context::None,
|
context: Context::None,
|
||||||
query: "".to_string(),
|
query: "".to_string(),
|
||||||
refreshing_state: RefreshingState::None,
|
refreshing_state: RefreshingState::None,
|
||||||
|
ui_error: UIError::NoError,
|
||||||
|
tags: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,28 +67,65 @@ fn on_url_changed(uc: subs::UrlChanged) -> Msg {
|
|||||||
);
|
);
|
||||||
let hpp = url.remaining_hash_path_parts();
|
let hpp = url.remaining_hash_path_parts();
|
||||||
match hpp.as_slice() {
|
match hpp.as_slice() {
|
||||||
["t", tid] => Msg::ShowPrettyRequest(tid.to_string()),
|
["t", tid] => {
|
||||||
|
if USE_GRAPHQL {
|
||||||
|
Msg::ShowThreadRequest {
|
||||||
|
thread_id: tid.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Msg::ShowPrettyRequest(tid.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
["s", query] => {
|
["s", query] => {
|
||||||
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
||||||
|
if USE_GRAPHQL {
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query,
|
||||||
|
after: None,
|
||||||
|
before: None,
|
||||||
|
first: None,
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Msg::SearchRequest {
|
Msg::SearchRequest {
|
||||||
query,
|
query,
|
||||||
page: 0,
|
page: 0,
|
||||||
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
["s", query, page] => {
|
["s", query, page] => {
|
||||||
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
||||||
let page = page[1..].parse().unwrap_or(0);
|
let page = page[1..].parse().unwrap_or(0);
|
||||||
|
if USE_GRAPHQL {
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query,
|
||||||
|
after: Some(page.to_string()),
|
||||||
|
before: None,
|
||||||
|
first: None,
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Msg::SearchRequest {
|
Msg::SearchRequest {
|
||||||
query,
|
query,
|
||||||
page,
|
page,
|
||||||
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
p => {
|
p => {
|
||||||
if !p.is_empty() {
|
if !p.is_empty() {
|
||||||
info!("Unhandled path '{p:?}'");
|
info!("Unhandled path '{p:?}'");
|
||||||
}
|
}
|
||||||
|
if USE_GRAPHQL {
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query: "".to_string(),
|
||||||
|
after: None,
|
||||||
|
before: None,
|
||||||
|
first: None,
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Msg::SearchRequest {
|
Msg::SearchRequest {
|
||||||
query: "".to_string(),
|
query: "".to_string(),
|
||||||
page: 0,
|
page: 0,
|
||||||
@ -76,6 +134,7 @@ fn on_url_changed(uc: subs::UrlChanged) -> Msg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod urls {
|
mod urls {
|
||||||
use seed::Url;
|
use seed::Url;
|
||||||
@ -98,7 +157,14 @@ mod urls {
|
|||||||
enum Context {
|
enum Context {
|
||||||
None,
|
None,
|
||||||
Search(shared::SearchResult),
|
Search(shared::SearchResult),
|
||||||
|
SearchResult {
|
||||||
|
query: String,
|
||||||
|
results: Vec<FrontPageQuerySearchNodes>,
|
||||||
|
count: usize,
|
||||||
|
pager: FrontPageQuerySearchPageInfo,
|
||||||
|
},
|
||||||
Thread(ThreadSet),
|
Thread(ThreadSet),
|
||||||
|
ThreadResult(ShowThreadQueryThread),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `Model` describes our app state.
|
// `Model` describes our app state.
|
||||||
@ -106,6 +172,14 @@ struct Model {
|
|||||||
query: String,
|
query: String,
|
||||||
context: Context,
|
context: Context,
|
||||||
refreshing_state: RefreshingState,
|
refreshing_state: RefreshingState,
|
||||||
|
ui_error: UIError,
|
||||||
|
tags: Option<Vec<Tag>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Tag {
|
||||||
|
name: String,
|
||||||
|
bg_color: String,
|
||||||
|
fg_color: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@ -140,6 +214,23 @@ enum Msg {
|
|||||||
ShowPrettyResult(fetch::Result<ThreadSet>),
|
ShowPrettyResult(fetch::Result<ThreadSet>),
|
||||||
NextPage,
|
NextPage,
|
||||||
PreviousPage,
|
PreviousPage,
|
||||||
|
|
||||||
|
FrontPageRequest {
|
||||||
|
query: String,
|
||||||
|
after: Option<String>,
|
||||||
|
before: Option<String>,
|
||||||
|
first: Option<i64>,
|
||||||
|
last: Option<i64>,
|
||||||
|
},
|
||||||
|
FrontPageResult(
|
||||||
|
fetch::Result<graphql_client::Response<graphql::front_page_query::ResponseData>>,
|
||||||
|
),
|
||||||
|
ShowThreadRequest {
|
||||||
|
thread_id: String,
|
||||||
|
},
|
||||||
|
ShowThreadResult(
|
||||||
|
fetch::Result<graphql_client::Response<graphql::show_thread_query::ResponseData>>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
// `update` describes how to handle each `Msg`.
|
// `update` describes how to handle each `Msg`.
|
||||||
@ -211,7 +302,21 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Context::Search(sr) => {
|
Context::Search(sr) => {
|
||||||
orders.request_url(urls::search(&sr.query, sr.page + 1));
|
orders.request_url(urls::search(&sr.query, sr.page + 1));
|
||||||
}
|
}
|
||||||
|
Context::SearchResult { query, pager, .. } => {
|
||||||
|
let query = query.to_string();
|
||||||
|
let after = pager.end_cursor.clone();
|
||||||
|
orders.perform_cmd(async move {
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query,
|
||||||
|
after,
|
||||||
|
before: None,
|
||||||
|
first: Some(SEARCH_RESULTS_PER_PAGE as i64),
|
||||||
|
last: None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Context::Thread(_) => (), // do nothing (yet?)
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
|
Context::ThreadResult(_) => (), // do nothing (yet?)
|
||||||
Context::None => (), // do nothing (yet?)
|
Context::None => (), // do nothing (yet?)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -220,10 +325,114 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Context::Search(sr) => {
|
Context::Search(sr) => {
|
||||||
orders.request_url(urls::search(&sr.query, sr.page.saturating_sub(1)));
|
orders.request_url(urls::search(&sr.query, sr.page.saturating_sub(1)));
|
||||||
}
|
}
|
||||||
|
Context::SearchResult { query, pager, .. } => {
|
||||||
|
let query = query.to_string();
|
||||||
|
let before = pager.start_cursor.clone();
|
||||||
|
orders.perform_cmd(async move {
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query,
|
||||||
|
after: None,
|
||||||
|
before,
|
||||||
|
first: None,
|
||||||
|
last: Some(SEARCH_RESULTS_PER_PAGE as i64),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Context::Thread(_) => (), // do nothing (yet?)
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
|
Context::ThreadResult(_) => (), // do nothing (yet?)
|
||||||
Context::None => (), // do nothing (yet?)
|
Context::None => (), // do nothing (yet?)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Msg::FrontPageRequest {
|
||||||
|
query,
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
} => {
|
||||||
|
info!("making FrontPageRequest: {query} after:{after:?} before:{before:?} first:{first:?} last:{last:?}");
|
||||||
|
model.query = query.clone();
|
||||||
|
orders.skip().perform_cmd(async move {
|
||||||
|
Msg::FrontPageResult(
|
||||||
|
send_graphql(graphql::FrontPageQuery::build_query(
|
||||||
|
graphql::front_page_query::Variables {
|
||||||
|
query,
|
||||||
|
after,
|
||||||
|
before,
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Msg::FrontPageResult(Err(e)) => error!("error FrontPageResult: {e:?}"),
|
||||||
|
Msg::FrontPageResult(Ok(graphql_client::Response {
|
||||||
|
data: None,
|
||||||
|
errors: None,
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
error!("FrontPageResult no data or errors, should not happen");
|
||||||
|
}
|
||||||
|
Msg::FrontPageResult(Ok(graphql_client::Response {
|
||||||
|
data: None,
|
||||||
|
errors: Some(e),
|
||||||
|
..
|
||||||
|
})) => {
|
||||||
|
error!("FrontPageResult error: {e:?}");
|
||||||
|
}
|
||||||
|
Msg::FrontPageResult(Ok(graphql_client::Response {
|
||||||
|
data: Some(data), ..
|
||||||
|
})) => {
|
||||||
|
model.tags = Some(
|
||||||
|
data.tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| Tag {
|
||||||
|
name: t.name,
|
||||||
|
bg_color: t.bg_color,
|
||||||
|
fg_color: t.fg_color,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
model.context = Context::SearchResult {
|
||||||
|
query: model.query.clone(),
|
||||||
|
results: data.search.nodes,
|
||||||
|
count: data.count as usize,
|
||||||
|
pager: data.search.page_info,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Msg::ShowThreadRequest { thread_id } => {
|
||||||
|
orders.skip().perform_cmd(async move {
|
||||||
|
Msg::ShowThreadResult(
|
||||||
|
send_graphql(graphql::ShowThreadQuery::build_query(
|
||||||
|
graphql::show_thread_query::Variables { thread_id },
|
||||||
|
))
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Msg::ShowThreadResult(Ok(graphql_client::Response {
|
||||||
|
data: Some(data), ..
|
||||||
|
})) => {
|
||||||
|
model.tags = Some(
|
||||||
|
data.tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| Tag {
|
||||||
|
name: t.name,
|
||||||
|
bg_color: t.bg_color,
|
||||||
|
fg_color: t.fg_color,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
model.context = Context::ThreadResult(data.thread);
|
||||||
|
}
|
||||||
|
Msg::ShowThreadResult(bad) => {
|
||||||
|
error!("show_thread_query error: {bad:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,13 +708,56 @@ fn human_age(timestamp: i64) -> String {
|
|||||||
ts.format("Yest. %H:%M").to_string()
|
ts.format("Yest. %H:%M").to_string()
|
||||||
} else if age < Duration::weeks(1) {
|
} else if age < Duration::weeks(1) {
|
||||||
ts.format("%a %H:%M").to_string()
|
ts.format("%a %H:%M").to_string()
|
||||||
|
} else if ts.year() == now.year() {
|
||||||
|
ts.format("%b %d %H:%M").to_string()
|
||||||
} else {
|
} else {
|
||||||
ts.format("%b %e %H:%M").to_string()
|
ts.format("%b %d, %Y %H:%M").to_string()
|
||||||
};
|
};
|
||||||
datetime
|
datetime
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult) -> Node<Msg> {
|
fn view_mobile_search_results(
|
||||||
|
query: &str,
|
||||||
|
results: &[FrontPageQuerySearchNodes],
|
||||||
|
count: usize,
|
||||||
|
pager: &FrontPageQuerySearchPageInfo,
|
||||||
|
) -> Node<Msg> {
|
||||||
|
if query.is_empty() {
|
||||||
|
set_title("all mail");
|
||||||
|
} else {
|
||||||
|
set_title(query);
|
||||||
|
}
|
||||||
|
let rows = results.iter().map(|r| {
|
||||||
|
let tid = r.thread.clone();
|
||||||
|
let datetime = human_age(r.timestamp as i64);
|
||||||
|
a![
|
||||||
|
C!["has-text-light"],
|
||||||
|
attrs! {
|
||||||
|
At::Href => urls::thread(&tid)
|
||||||
|
},
|
||||||
|
div![
|
||||||
|
C!["row"],
|
||||||
|
div![C!["subject"], &r.subject],
|
||||||
|
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
|
||||||
|
div![
|
||||||
|
span![C!["is-size-7"], tags_chiclet(&r.tags, true)],
|
||||||
|
span![C!["is-size-7", "float-right", "date"], datetime]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
div![
|
||||||
|
C!["search-results"],
|
||||||
|
view_search_pager(count, pager),
|
||||||
|
rows,
|
||||||
|
view_search_pager(count, pager),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_mobile_search_results_legacy(
|
||||||
|
query: &str,
|
||||||
|
search_results: &shared::SearchResult,
|
||||||
|
) -> Node<Msg> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_title("all mail");
|
set_title("all mail");
|
||||||
} else {
|
} else {
|
||||||
@ -528,31 +780,93 @@ fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult
|
|||||||
*/
|
*/
|
||||||
let tid = r.thread.clone();
|
let tid = r.thread.clone();
|
||||||
let datetime = human_age(r.timestamp as i64);
|
let datetime = human_age(r.timestamp as i64);
|
||||||
|
a![
|
||||||
|
C!["has-text-light"],
|
||||||
|
attrs! {
|
||||||
|
At::Href => urls::thread(&tid)
|
||||||
|
},
|
||||||
div![
|
div![
|
||||||
C!["row"],
|
C!["row"],
|
||||||
div![
|
div![C!["subject"], &r.subject],
|
||||||
C!["subject"],
|
|
||||||
&r.subject,
|
|
||||||
ev(Ev::Click, move |_| Msg::ShowPrettyRequest(tid)),
|
|
||||||
],
|
|
||||||
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
|
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
|
||||||
div![
|
div![
|
||||||
span![C!["is-size-7"], tags_chiclet(&r.tags, true)],
|
span![C!["is-size-7"], tags_chiclet(&r.tags, true)],
|
||||||
span![C!["is-size-7", "float-right", "date"], datetime]
|
span![C!["is-size-7", "float-right", "date"], datetime]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
});
|
});
|
||||||
let first = search_results.page * search_results.results_per_page;
|
let first = search_results.page * search_results.results_per_page;
|
||||||
div![
|
div![
|
||||||
C!["search-results"],
|
C!["search-results"],
|
||||||
h1!["Search results"],
|
view_search_pager_legacy(first, summaries.len(), search_results.total),
|
||||||
view_search_pager(first, summaries.len(), search_results.total),
|
|
||||||
rows,
|
rows,
|
||||||
view_search_pager(first, summaries.len(), search_results.total)
|
view_search_pager_legacy(first, summaries.len(), search_results.total)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_search_results(query: &str, search_results: &shared::SearchResult) -> Node<Msg> {
|
fn view_search_results(
|
||||||
|
query: &str,
|
||||||
|
results: &[FrontPageQuerySearchNodes],
|
||||||
|
count: usize,
|
||||||
|
pager: &FrontPageQuerySearchPageInfo,
|
||||||
|
) -> Node<Msg> {
|
||||||
|
info!("pager {pager:?}");
|
||||||
|
if query.is_empty() {
|
||||||
|
set_title("all mail");
|
||||||
|
} else {
|
||||||
|
set_title(query);
|
||||||
|
}
|
||||||
|
let rows = results.iter().map(|r| {
|
||||||
|
let tid = r.thread.clone();
|
||||||
|
let datetime = human_age(r.timestamp as i64);
|
||||||
|
tr![
|
||||||
|
td![
|
||||||
|
C!["from"],
|
||||||
|
pretty_authors(&r.authors),
|
||||||
|
// TODO(wathiede): visualize message count if more than one message is in the
|
||||||
|
// thread
|
||||||
|
//IF!(r.total>1 => small![" ", r.total.to_string()]),
|
||||||
|
],
|
||||||
|
td![
|
||||||
|
C!["subject"],
|
||||||
|
tags_chiclet(&r.tags, false),
|
||||||
|
" ",
|
||||||
|
a![
|
||||||
|
C!["has-text-light"],
|
||||||
|
attrs! {
|
||||||
|
At::Href => urls::thread(&tid)
|
||||||
|
},
|
||||||
|
&r.subject,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
td![C!["date"], datetime]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
div![
|
||||||
|
view_search_pager(count, pager),
|
||||||
|
table![
|
||||||
|
C![
|
||||||
|
"table",
|
||||||
|
"index",
|
||||||
|
"is-fullwidth",
|
||||||
|
"is-hoverable",
|
||||||
|
"is-narrow",
|
||||||
|
"is-striped",
|
||||||
|
],
|
||||||
|
thead![tr![
|
||||||
|
th![C!["from"], "From"],
|
||||||
|
th![C!["subject"], "Subject"],
|
||||||
|
th![C!["date"], "Date"]
|
||||||
|
]],
|
||||||
|
tbody![rows]
|
||||||
|
],
|
||||||
|
view_search_pager(count, pager)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_search_results_legacy(query: &str, search_results: &shared::SearchResult) -> Node<Msg> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_title("all mail");
|
set_title("all mail");
|
||||||
} else {
|
} else {
|
||||||
@ -585,7 +899,7 @@ fn view_search_results(query: &str, search_results: &shared::SearchResult) -> No
|
|||||||
});
|
});
|
||||||
let first = search_results.page * search_results.results_per_page;
|
let first = search_results.page * search_results.results_per_page;
|
||||||
div![
|
div![
|
||||||
view_search_pager(first, summaries.len(), search_results.total),
|
view_search_pager_legacy(first, summaries.len(), search_results.total),
|
||||||
table![
|
table![
|
||||||
C![
|
C![
|
||||||
"table",
|
"table",
|
||||||
@ -602,11 +916,51 @@ fn view_search_results(query: &str, search_results: &shared::SearchResult) -> No
|
|||||||
]],
|
]],
|
||||||
tbody![rows]
|
tbody![rows]
|
||||||
],
|
],
|
||||||
view_search_pager(first, summaries.len(), search_results.total)
|
view_search_pager_legacy(first, summaries.len(), search_results.total)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_search_pager(start: usize, count: usize, total: usize) -> Node<Msg> {
|
fn view_search_pager(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node<Msg> {
|
||||||
|
let start = pager
|
||||||
|
.start_cursor
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| i.parse().unwrap_or(0))
|
||||||
|
.unwrap_or(0);
|
||||||
|
nav![
|
||||||
|
C!["pagination"],
|
||||||
|
a![
|
||||||
|
C![
|
||||||
|
"pagination-previous",
|
||||||
|
"button",
|
||||||
|
//IF!(!pager.has_previous_page => "is-static"),
|
||||||
|
],
|
||||||
|
IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
|
||||||
|
"<",
|
||||||
|
IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)),
|
||||||
|
],
|
||||||
|
a![
|
||||||
|
C![
|
||||||
|
"pagination-next",
|
||||||
|
"button",
|
||||||
|
//IF!(!pager.has_next_page => "is-static")
|
||||||
|
],
|
||||||
|
IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
|
||||||
|
">",
|
||||||
|
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
|
||||||
|
],
|
||||||
|
ul![
|
||||||
|
C!["pagination-list"],
|
||||||
|
li![format!(
|
||||||
|
"{} - {} of {}",
|
||||||
|
start,
|
||||||
|
count.min(start + SEARCH_RESULTS_PER_PAGE),
|
||||||
|
count
|
||||||
|
)],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_search_pager_legacy(start: usize, count: usize, total: usize) -> Node<Msg> {
|
||||||
let is_first = start <= 0;
|
let is_first = start <= 0;
|
||||||
let is_last = (start + SEARCH_RESULTS_PER_PAGE) >= total;
|
let is_last = (start + SEARCH_RESULTS_PER_PAGE) >= total;
|
||||||
nav![
|
nav![
|
||||||
@ -630,7 +984,108 @@ fn view_search_pager(start: usize, count: usize, total: usize) -> Node<Msg> {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
|
trait Email {
|
||||||
|
fn name(&self) -> &Option<String>;
|
||||||
|
fn addr(&self) -> &Option<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Email> Email for &'_ T {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return (*self).name();
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return (*self).addr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesCc {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesFrom {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Email for ShowThreadQueryThreadMessagesTo {
|
||||||
|
fn name(&self) -> &Option<String> {
|
||||||
|
return &self.name;
|
||||||
|
}
|
||||||
|
fn addr(&self) -> &Option<String> {
|
||||||
|
return &self.addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_addresses<E: Email>(addrs: &[E]) -> Vec<Node<Msg>> {
|
||||||
|
addrs
|
||||||
|
.into_iter()
|
||||||
|
.map(|address| {
|
||||||
|
span![
|
||||||
|
C!["tag", "is-black"],
|
||||||
|
address.addr().as_ref().map(|a| attrs! {At::Title=>a}),
|
||||||
|
address
|
||||||
|
.name()
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(address.addr().as_ref().unwrap_or(&"(UNKNOWN)".to_string()))
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_thread(thread: &ShowThreadQueryThread) -> Node<Msg> {
|
||||||
|
// TODO(wathiede): show per-message subject if it changes significantly from top-level subject
|
||||||
|
set_title(&thread.subject);
|
||||||
|
let messages = thread.messages.iter().map(|msg| {
|
||||||
|
div![
|
||||||
|
C!["message"],
|
||||||
|
/* TODO(wathiede): collect all the tags and show them here. */
|
||||||
|
/* TODO(wathiede): collect all the attachments from all the subparts */
|
||||||
|
msg.from
|
||||||
|
.as_ref()
|
||||||
|
.map(|from| div![C!["header"], "From: ", view_addresses(&[from])]),
|
||||||
|
msg.timestamp
|
||||||
|
.map(|ts| div![C!["header"], "Date: ", human_age(ts)]),
|
||||||
|
IF!(!msg.to.is_empty() => div![C!["header"], "To: ", view_addresses(&msg.to)]),
|
||||||
|
IF!(!msg.cc.is_empty() => div![C!["header"], "CC: ", view_addresses(&msg.cc)]),
|
||||||
|
/*
|
||||||
|
div![
|
||||||
|
C!["body"],
|
||||||
|
match &message.body {
|
||||||
|
Some(body) => view_body(body.as_slice()),
|
||||||
|
None => div!["<no body>"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
});
|
||||||
|
div![
|
||||||
|
C!["thread"],
|
||||||
|
p![C!["is-size-4"], &thread.subject],
|
||||||
|
messages,
|
||||||
|
/* TODO(wathiede): plumb in orignal id
|
||||||
|
a![
|
||||||
|
attrs! {At::Href=>api::original(&thread_node.0.as_ref().expect("message missing").id)},
|
||||||
|
"Original"
|
||||||
|
],
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
div![
|
||||||
|
C!["debug"],
|
||||||
|
"Add zippy for debug dump",
|
||||||
|
view_debug_thread_set(thread_set)
|
||||||
|
] /* pre![format!("Thread: {:#?}", thread_set).replace(" ", " ")] */
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_thread_legacy(thread_set: &ThreadSet) -> Node<Msg> {
|
||||||
assert_eq!(thread_set.0.len(), 1);
|
assert_eq!(thread_set.0.len(), 1);
|
||||||
let thread = &thread_set.0[0];
|
let thread = &thread_set.0[0];
|
||||||
assert_eq!(thread.0.len(), 1);
|
assert_eq!(thread.0.len(), 1);
|
||||||
@ -728,7 +1183,11 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
At::Value => query,
|
At::Value => query,
|
||||||
},
|
},
|
||||||
input_ev(Ev::Input, |q| Msg::SearchRequest {
|
input_ev(Ev::Input, |q| Msg::SearchRequest {
|
||||||
query: Url::encode_uri_component(q),
|
query: Url::encode_uri_component(if q.is_empty() {
|
||||||
|
"*".to_string()
|
||||||
|
} else {
|
||||||
|
q
|
||||||
|
}),
|
||||||
page: 0,
|
page: 0,
|
||||||
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
results_per_page: SEARCH_RESULTS_PER_PAGE,
|
||||||
}),
|
}),
|
||||||
@ -758,27 +1217,64 @@ fn view_footer(render_time_ms: u128) -> Node<Msg> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn view_desktop(model: &Model) -> Node<Msg> {
|
fn view_desktop(model: &Model) -> Node<Msg> {
|
||||||
|
// Do two queries, one without `unread` so it loads fast, then a second with unread.
|
||||||
let content = match &model.context {
|
let content = match &model.context {
|
||||||
Context::None => div![h1!["Loading"]],
|
Context::None => div![h1!["Loading"]],
|
||||||
Context::Thread(thread_set) => view_thread(thread_set),
|
Context::Thread(thread_set) => view_thread_legacy(thread_set),
|
||||||
Context::Search(search_results) => view_search_results(&model.query, search_results),
|
Context::ThreadResult(thread) => view_thread(thread),
|
||||||
|
Context::Search(search_results) => view_search_results_legacy(&model.query, search_results),
|
||||||
|
Context::SearchResult {
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
count,
|
||||||
|
pager,
|
||||||
|
} => view_search_results(&query, results.as_slice(), *count, pager),
|
||||||
};
|
};
|
||||||
|
div![
|
||||||
|
C!["desktop-main-content"],
|
||||||
|
aside![
|
||||||
|
C!["tags-menu", "menu"],
|
||||||
|
p![C!["menu-label"], "Tags"],
|
||||||
|
ul![
|
||||||
|
C!["menu-list"],
|
||||||
|
model.tags.as_ref().map(|tags| tags.iter().map(|t| li![a![
|
||||||
|
attrs! {
|
||||||
|
At::Href => urls::search(&format!("tag:{}", t.name), 0)
|
||||||
|
},
|
||||||
|
style! {
|
||||||
|
St::BackgroundColor => t.bg_color,
|
||||||
|
St::Color => t.fg_color,
|
||||||
|
},
|
||||||
|
&t.name
|
||||||
|
]]))
|
||||||
|
]
|
||||||
|
],
|
||||||
div![
|
div![
|
||||||
view_header(&model.query, &model.refreshing_state),
|
view_header(&model.query, &model.refreshing_state),
|
||||||
section![C!["section"], content],
|
content,
|
||||||
view_header(&model.query, &model.refreshing_state),
|
view_header(&model.query, &model.refreshing_state),
|
||||||
]
|
]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_mobile(model: &Model) -> Node<Msg> {
|
fn view_mobile(model: &Model) -> Node<Msg> {
|
||||||
let content = match &model.context {
|
let content = match &model.context {
|
||||||
Context::None => div![h1!["Loading"]],
|
Context::None => div![h1!["Loading"]],
|
||||||
Context::Thread(thread_set) => view_thread(thread_set),
|
Context::Thread(thread_set) => view_thread_legacy(thread_set),
|
||||||
Context::Search(search_results) => view_mobile_search_results(&model.query, search_results),
|
Context::ThreadResult(thread) => view_thread(thread),
|
||||||
|
Context::Search(search_results) => {
|
||||||
|
view_mobile_search_results_legacy(&model.query, search_results)
|
||||||
|
}
|
||||||
|
Context::SearchResult {
|
||||||
|
query,
|
||||||
|
results,
|
||||||
|
count,
|
||||||
|
pager,
|
||||||
|
} => view_mobile_search_results(&query, results.as_slice(), *count, pager),
|
||||||
};
|
};
|
||||||
div![
|
div![
|
||||||
view_header(&model.query, &model.refreshing_state),
|
view_header(&model.query, &model.refreshing_state),
|
||||||
section![C!["section"], div![C!["content"], content]],
|
content,
|
||||||
view_header(&model.query, &model.refreshing_state),
|
view_header(&model.query, &model.refreshing_state),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -795,6 +1291,11 @@ fn view(model: &Model) -> Node<Msg> {
|
|||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
info!("view called");
|
info!("view called");
|
||||||
div![
|
div![
|
||||||
|
if is_mobile {
|
||||||
|
C!["mobile"]
|
||||||
|
} else {
|
||||||
|
C!["desktop"]
|
||||||
|
},
|
||||||
if is_mobile {
|
if is_mobile {
|
||||||
view_mobile(model)
|
view_mobile(model)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user