Compare commits

..

No commits in common. "d5fa89b38c136687a0bd176ebad7033e1aad9ebf" and "c76df0ef90c626f22d3ece29067c055c091d47a3" have entirely different histories.

7 changed files with 100 additions and 107 deletions

View File

@ -1,6 +1,7 @@
use std::fs; use std::fs;
use server::sanitize_html; use server::sanitize_html;
use url::Url;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1); let mut args = std::env::args().skip(1);

View File

@ -10,6 +10,7 @@ use log::{error, info, warn};
use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail}; use mailparse::{parse_mail, MailHeader, MailHeaderMap, ParsedMail};
use memmap::MmapOptions; use memmap::MmapOptions;
use notmuch::Notmuch; use notmuch::Notmuch;
use url::Url;
use crate::{ use crate::{
error::ServerError, error::ServerError,

View File

@ -3,8 +3,8 @@ use seed_hooks::{state_access::CloneState, topo, use_state};
use crate::{ use crate::{
api::urls, api::urls,
state::{Context, Model, Msg}, state::{Context, Model, Msg, Tag},
view::{self, view_header, view_search_results, view_tags}, view::{self, view_header, view_search_results},
}; };
#[topo::nested] #[topo::nested]
@ -33,9 +33,100 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
show_icon_text, show_icon_text,
), ),
}; };
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
let href = if search_unread {
urls::search(&format!("is:unread tag:{}", t.name), 0)
} else {
urls::search(&format!("tag:{}", t.name), 0)
};
li![a![
attrs! {
At::Href => href
},
(0..indent).map(|_| span![C!["tag-indent"], ""]),
i![
C!["tag-tag", "fa-solid", "fa-tag"],
style! {
//"--fa-primary-color" => t.fg_color,
St::Color => t.bg_color,
},
],
display_name,
IF!(t.unread>0 => format!(" ({})", t.unread))
]]
}
fn matches(a: &[&str], b: &[&str]) -> usize {
std::iter::zip(a.iter(), b.iter())
.take_while(|(a, b)| a == b)
.count()
}
fn view_tag_list<'a>(
tags: impl Iterator<Item = &'a Tag>,
search_unread: bool,
) -> Vec<Node<Msg>> {
let mut lis = Vec::new();
let mut last = Vec::new();
for t in tags {
let parts: Vec<_> = t.name.split('/').collect();
let mut n = matches(&last, &parts);
if n <= parts.len() - 2 && parts.len() > 1 {
// Synthesize fake tags for proper indenting.
for i in n..parts.len() - 1 {
let display_name = parts[n];
lis.push(view_tag_li(
&display_name,
n,
&Tag {
name: parts[..i + 1].join("/"),
bg_color: "#fff".to_string(),
fg_color: "#000".to_string(),
unread: 0,
},
search_unread,
));
}
n = parts.len() - 1;
}
let display_name = parts[n];
lis.push(view_tag_li(&display_name, n, t, search_unread));
last = parts;
}
lis
}
let unread = model
.tags
.as_ref()
.map(|tags| tags.iter().filter(|t| t.unread > 0).collect())
.unwrap_or(Vec::new());
let tags_open = use_state(|| false);
let force_tags_open = unread.is_empty();
div![ div![
C!["main-content"], C!["main-content"],
view_tags(model), aside![
C!["tags-menu", "menu"],
IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]),
IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter(),true)]),
p![
C!["menu-label"],
IF!(!force_tags_open =>
i![C![
"fa-solid",
if tags_open.get() {
"fa-angle-up"
} else {
"fa-angle-down"
}
]]),
" Tags",
ev(Ev::Click, move |_| {
tags_open.set(!tags_open.get());
})
],
ul![
C!["menu-list"],
IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter(),false))),
]
],
div![ div![
view_header(&model.query, &model.refreshing_state), view_header(&model.query, &model.refreshing_state),
content, content,

View File

@ -6,10 +6,7 @@ use crate::{
api::urls, api::urls,
graphql::front_page_query::*, graphql::front_page_query::*,
state::{Context, Model, Msg}, state::{Context, Model, Msg},
view::{ view::{self, human_age, pretty_authors, search_toolbar, set_title, tags_chiclet, view_header},
self, human_age, pretty_authors, search_toolbar, set_title, tags_chiclet, view_header,
view_tags,
},
}; };
pub(super) fn view(model: &Model) -> Node<Msg> { pub(super) fn view(model: &Model) -> Node<Msg> {
@ -40,7 +37,6 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
view_header(&model.query, &model.refreshing_state), view_header(&model.query, &model.refreshing_state),
content, content,
view_header(&model.query, &model.refreshing_state), view_header(&model.query, &model.refreshing_state),
view_tags(model),
] ]
} }

View File

@ -13,7 +13,7 @@ use seed_hooks::{state_access::CloneState, topo, use_state};
use crate::{ use crate::{
api::urls, api::urls,
graphql::{front_page_query::*, show_thread_query::*}, graphql::{front_page_query::*, show_thread_query::*},
state::{unread_query, Model, Msg, RefreshingState, Tag}, state::{unread_query, Model, Msg, RefreshingState},
}; };
mod desktop; mod desktop;
@ -1000,97 +1000,3 @@ pub fn view(model: &Model) -> Node<Msg> {
_ => div![C!["desktop"], desktop::view(model)], _ => div![C!["desktop"], desktop::view(model)],
},] },]
} }
pub fn view_tags(model: &Model) -> Node<Msg> {
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
let href = if search_unread {
urls::search(&format!("is:unread tag:{}", t.name), 0)
} else {
urls::search(&format!("tag:{}", t.name), 0)
};
li![a![
attrs! {
At::Href => href
},
(0..indent).map(|_| span![C!["tag-indent"], ""]),
i![
C!["tag-tag", "fa-solid", "fa-tag"],
style! {
//"--fa-primary-color" => t.fg_color,
St::Color => t.bg_color,
},
],
display_name,
IF!(t.unread>0 => format!(" ({})", t.unread))
]]
}
fn matches(a: &[&str], b: &[&str]) -> usize {
std::iter::zip(a.iter(), b.iter())
.take_while(|(a, b)| a == b)
.count()
}
fn view_tag_list<'a>(
tags: impl Iterator<Item = &'a Tag>,
search_unread: bool,
) -> Vec<Node<Msg>> {
let mut lis = Vec::new();
let mut last = Vec::new();
for t in tags {
let parts: Vec<_> = t.name.split('/').collect();
let mut n = matches(&last, &parts);
if n <= parts.len() - 2 && parts.len() > 1 {
// Synthesize fake tags for proper indenting.
for i in n..parts.len() - 1 {
let display_name = parts[n];
lis.push(view_tag_li(
&display_name,
n,
&Tag {
name: parts[..i + 1].join("/"),
bg_color: "#fff".to_string(),
fg_color: "#000".to_string(),
unread: 0,
},
search_unread,
));
}
n = parts.len() - 1;
}
let display_name = parts[n];
lis.push(view_tag_li(&display_name, n, t, search_unread));
last = parts;
}
lis
}
let unread = model
.tags
.as_ref()
.map(|tags| tags.iter().filter(|t| t.unread > 0).collect())
.unwrap_or(Vec::new());
let tags_open = use_state(|| false);
let force_tags_open = unread.is_empty();
aside![
C!["tags-menu", "menu"],
IF!(!unread.is_empty() => p![C!["menu-label"], "Unread"]),
IF!(!unread.is_empty() => ul![C!["menu-list"], view_tag_list(unread.into_iter(),true)]),
p![
C!["menu-label"],
IF!(!force_tags_open =>
i![C![
"fa-solid",
if tags_open.get() {
"fa-angle-up"
} else {
"fa-angle-down"
}
]]),
" Tags",
ev(Ev::Click, move |_| {
tags_open.set(!tags_open.get());
})
],
ul![
C!["menu-list"],
IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tag_list(tags.iter(),false))),
]
]
}

View File

@ -2,7 +2,7 @@ use seed::{prelude::*, *};
use crate::{ use crate::{
state::{Context, Model, Msg}, state::{Context, Model, Msg},
view::{self, view_header, view_search_results, view_tags}, view::{self, view_header, view_search_results},
}; };
pub(super) fn view(model: &Model) -> Node<Msg> { pub(super) fn view(model: &Model) -> Node<Msg> {
@ -36,7 +36,6 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
view_header(&model.query, &model.refreshing_state), view_header(&model.query, &model.refreshing_state),
content, content,
view_header(&model.query, &model.refreshing_state), view_header(&model.query, &model.refreshing_state),
view_tags(model),
] ]
] ]
} }

View File

@ -65,9 +65,8 @@
} }
.view-part-text-plain { .view-part-text-plain {
font-family: monospace;
overflow-wrap: break-word;
padding: 0.5em; padding: 0.5em;
overflow-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;