web: add bulk read/unerad functionality

This commit is contained in:
2024-02-20 19:24:56 -08:00
parent de3f392bd7
commit f50fe7196e
8 changed files with 484 additions and 72 deletions

View File

@@ -1,3 +1,5 @@
use std::collections::HashSet;
use graphql_client::GraphQLQuery;
use log::{error, info};
use seed::{app::subs, prelude::*, *};
@@ -224,6 +226,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
results: data.search.nodes,
count: data.count as usize,
pager: data.search.page_info,
selected_threads: HashSet::new(),
};
}
@@ -256,6 +259,54 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
Msg::ShowThreadResult(bad) => {
error!("show_thread_query error: {bad:#?}");
}
Msg::SelectionMarkAsRead => {
if let Context::SearchResult {
selected_threads, ..
} = &mut model.context
{
let threads = selected_threads
.iter()
.map(|tid| format!("thread:{tid}"))
.collect::<Vec<_>>()
.join(" ");
orders
.skip()
.perform_cmd(async move { Msg::SetUnread(threads, false) });
}
}
Msg::SelectionMarkAsUnread => {
if let Context::SearchResult {
selected_threads, ..
} = &mut model.context
{
let threads = selected_threads
.iter()
.map(|tid| format!("thread:{tid}"))
.collect::<Vec<_>>()
.join(" ");
orders
.skip()
.perform_cmd(async move { Msg::SetUnread(threads, true) });
}
}
Msg::SelectionAddThread(tid) => {
if let Context::SearchResult {
selected_threads, ..
} = &mut model.context
{
selected_threads.insert(tid);
info!("selected threads {selected_threads:?}");
}
}
Msg::SelectionRemoveThread(tid) => {
if let Context::SearchResult {
selected_threads, ..
} = &mut model.context
{
selected_threads.remove(&tid);
info!("selected threads {selected_threads:?}");
}
}
}
}
// `Model` describes our app state.
@@ -287,6 +338,7 @@ pub enum Context {
results: Vec<FrontPageQuerySearchNodes>,
count: usize,
pager: FrontPageQuerySearchPageInfo,
selected_threads: HashSet<String>,
},
ThreadResult(ShowThreadQueryThread),
}
@@ -337,4 +389,9 @@ pub enum Msg {
ShowThreadResult(
Result<graphql_client::Response<graphql::show_thread_query::ResponseData>, gloo_net::Error>,
),
SelectionMarkAsRead,
SelectionMarkAsUnread,
SelectionAddThread(String),
SelectionRemoveThread(String),
}

View File

@@ -18,7 +18,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
results,
count,
pager,
} => view_search_results(&query, results.as_slice(), *count, pager),
selected_threads,
} => view_search_results(&query, results.as_slice(), *count, pager, selected_threads),
};
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
let href = if search_unread {

View File

@@ -1,3 +1,5 @@
use std::collections::HashSet;
use seed::{prelude::*, *};
use crate::{
@@ -16,7 +18,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
results,
count,
pager,
} => search_results(&query, results.as_slice(), *count, pager),
selected_threads,
} => search_results(&query, results.as_slice(), *count, pager, selected_threads),
};
div![
view_header(&model.query, &model.refreshing_state),
@@ -30,6 +33,7 @@ fn search_results(
results: &[FrontPageQuerySearchNodes],
count: usize,
pager: &FrontPageQuerySearchPageInfo,
selected_threads: &HashSet<String>,
) -> Node<Msg> {
if query.is_empty() {
set_title("all mail");
@@ -38,20 +42,45 @@ fn search_results(
}
let rows = results.iter().map(|r| {
let tid = r.thread.clone();
let check_tid = r.thread.clone();
let datetime = human_age(r.timestamp as i64);
let unread_idx = r.tags.iter().position(|e| e == &"unread");
let mut tags = r.tags.clone();
if let Some(idx) = unread_idx {
tags.remove(idx);
};
a![
C!["has-text-light"],
IF!(unread_idx.is_some() => C!["unread"]),
attrs! {
At::Href => urls::thread(&tid)
},
div![
C!["row"],
div![
C!["row"],
label![
C!["b-checkbox", "checkbox", "is-large"],
input![attrs! {
At::Type=>"checkbox",
At::Checked=>selected_threads.contains(&tid).as_at_value(),
}],
span![C!["check"]],
ev(Ev::Input, move |e| {
if let Some(input) = e
.target()
.as_ref()
.expect("failed to get reference to target")
.dyn_ref::<web_sys::HtmlInputElement>()
{
if input.checked() {
Msg::SelectionAddThread(check_tid)
} else {
Msg::SelectionRemoveThread(check_tid)
}
} else {
Msg::Noop
}
}),
],
a![
C!["has-text-light", "summary"],
IF!(unread_idx.is_some() => C!["unread"]),
attrs! {
At::Href => urls::thread(&tid)
},
div![C!["subject"], &r.subject],
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
div![
@@ -61,10 +90,11 @@ fn search_results(
]
]
});
let show_bulk_edit = !selected_threads.is_empty();
div![
C!["search-results"],
search_toolbar(count, pager),
div![C!["index"], rows,],
search_toolbar(count, pager),
search_toolbar(count, pager, show_bulk_edit),
div![C!["index"], rows],
search_toolbar(count, pager, show_bulk_edit),
]
}

View File

@@ -112,15 +112,17 @@ fn view_search_results(
results: &[FrontPageQuerySearchNodes],
count: usize,
pager: &FrontPageQuerySearchPageInfo,
selected_threads: &HashSet<String>,
) -> Node<Msg> {
info!("pager {pager:?}");
if query.is_empty() {
set_title("all mail");
} else {
set_title(query);
}
let show_bulk_edit = !selected_threads.is_empty();
let rows = results.iter().map(|r| {
let tid = r.thread.clone();
let check_tid = r.thread.clone();
let datetime = human_age(r.timestamp as i64);
let unread_idx = r.tags.iter().position(|e| e == &"unread");
let mut tags = r.tags.clone();
@@ -129,6 +131,31 @@ fn view_search_results(
};
tr![
IF!(unread_idx.is_some() => C!["unread"]),
td![label![
C!["checkbox"],
input![
attrs! {
At::Type=>"checkbox",
At::Checked=>selected_threads.contains(&tid).as_at_value(),
},
ev(Ev::Input, move |e| {
if let Some(input) = e
.target()
.as_ref()
.expect("failed to get reference to target")
.dyn_ref::<web_sys::HtmlInputElement>()
{
if input.checked() {
Msg::SelectionAddThread(check_tid)
} else {
Msg::SelectionRemoveThread(check_tid)
}
} else {
Msg::Noop
}
}),
]
]],
td![
C!["from"],
pretty_authors(&r.authors),
@@ -144,15 +171,6 @@ fn view_search_results(
At::Href => urls::thread(&tid)
},
&r.subject,
button![
C!["mark-read-button", "button", "is-dark", "is-small"],
"Read",
ev(Ev::Click, move |e| {
e.stop_propagation();
e.prevent_default();
Msg::SetUnread(format!("thread:{tid}"), false)
}),
]
]
],
td![C!["date"], datetime]
@@ -160,7 +178,7 @@ fn view_search_results(
});
div![
search_toolbar(count, pager),
search_toolbar(count, pager, show_bulk_edit),
table![
C![
"table",
@@ -171,53 +189,83 @@ fn view_search_results(
"is-striped",
],
thead![tr![
th![C!["edit"], ""],
th![C!["from"], "From"],
th![C!["subject"], "Subject"],
th![C!["date"], "Date"]
]],
tbody![rows]
],
search_toolbar(count, pager)
search_toolbar(count, pager, show_bulk_edit)
]
}
fn search_toolbar(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node<Msg> {
fn search_toolbar(
count: usize,
pager: &FrontPageQuerySearchPageInfo,
show_bulk_edit: bool,
) -> 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
)],
C!["level"],
div![
C!["level-left"],
IF!(show_bulk_edit =>
span![
C!["level-item", "buttons"],
button![
C!["button"],
attrs!{At::Title => "Mark as read"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
ev(Ev::Click, |_| Msg::SelectionMarkAsRead),
],
button![
C!["button"],
attrs!{At::Title => "Mark as unread"},
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread),
],
]),
],
div![
C!["level-right"],
nav![
C!["level-item", "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
)],
],
]
]
]
}
@@ -558,7 +606,6 @@ fn view_footer(render_time_ms: u128) -> Node<Msg> {
// `view` describes what to display.
pub fn view(model: &Model) -> Node<Msg> {
let start = Instant::now();
info!("refreshing {:?}", model.refreshing_state);
let win = seed::window();
let w = win
.inner_width()

View File

@@ -15,7 +15,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
results,
count,
pager,
} => view_search_results(&query, results.as_slice(), *count, pager),
selected_threads,
} => view_search_results(&query, results.as_slice(), *count, pager, selected_threads),
};
div![
C!["main-content"],