From 2e526dace1d13f59abc9cb1348e2c79f62a25ad5 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Mon, 24 Feb 2025 14:37:34 -0800 Subject: [PATCH] Implement catchup mode Show original/delivered To if no xinu.tv addresses in To/CC fields --- server/src/graphql.rs | 34 ++++ server/src/lib.rs | 2 +- web/graphql/catchup.graphql | 3 + web/graphql/schema.json | 39 ++++ web/src/graphql.rs | 8 + web/src/state.rs | 121 +++++++++++- web/src/view/mod.rs | 370 ++++++++++++++++++++++++++---------- 7 files changed, 473 insertions(+), 104 deletions(-) create mode 100644 web/graphql/catchup.graphql diff --git a/server/src/graphql.rs b/server/src/graphql.rs index 79c8a96..80145ee 100644 --- a/server/src/graphql.rs +++ b/server/src/graphql.rs @@ -310,6 +310,40 @@ impl QueryRoot { info!("count {newsreader_query:?} newsreader count {newsreader_count} notmuch count {notmuch_count} tantivy count {tantivy_count} total {total}"); Ok(total) } + async fn catchup<'ctx>( + &self, + ctx: &Context<'ctx>, + query: String, + ) -> Result, Error> { + let nm = ctx.data_unchecked::(); + let pool = ctx.data_unchecked::(); + let query: Query = query.parse()?; + // TODO: implement optimized versions of fetching just IDs + let newsreader_fut = newsreader_search(pool, None, None, None, None, &query); + let notmuch_fut = notmuch_search(nm, None, None, None, None, &query); + let (newsreader_results, notmuch_results) = join!(newsreader_fut, notmuch_fut); + + let newsreader_results = newsreader_results?; + let notmuch_results = notmuch_results?; + info!( + "newsreader_results ({}) notmuch_results ({})", + newsreader_results.len(), + notmuch_results.len(), + ); + + let results: Vec<_> = newsreader_results + .into_iter() + .chain(notmuch_results) + .collect(); + let ids = results + .into_iter() + .map(|r| match r { + ThreadSummaryCursor::Newsreader(_, ts) => ts.thread, + ThreadSummaryCursor::Notmuch(_, ts) => ts.thread, + }) + .collect(); + Ok(ids) + } // TODO: this function doesn't get parallelism, possibly because notmuch is sync and blocks, // rewrite that with tokio::process:Command diff --git a/server/src/lib.rs b/server/src/lib.rs index 7a49ba1..7cbb051 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -682,7 +682,7 @@ fn compute_offset_limit( first: Option, last: Option, ) -> (i32, i32) { - let default_page_size = 100; + let default_page_size = 10000; match (after, before, first, last) { // Reasonable defaults (None, None, None, None) => (0, default_page_size), diff --git a/web/graphql/catchup.graphql b/web/graphql/catchup.graphql new file mode 100644 index 0000000..57c717d --- /dev/null +++ b/web/graphql/catchup.graphql @@ -0,0 +1,3 @@ +query CatchupQuery($query: String!) { + catchup(query: $query) +} \ No newline at end of file diff --git a/web/graphql/schema.json b/web/graphql/schema.json index 3284a65..3167f32 100644 --- a/web/graphql/schema.json +++ b/web/graphql/schema.json @@ -1292,6 +1292,45 @@ } } }, + { + "args": [ + { + "defaultValue": null, + "description": null, + "name": "query", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + ], + "deprecationReason": null, + "description": null, + "isDeprecated": false, + "name": "catchup", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + } + }, { "args": [ { diff --git a/web/src/graphql.rs b/web/src/graphql.rs index 351833c..3f6d06b 100644 --- a/web/src/graphql.rs +++ b/web/src/graphql.rs @@ -12,6 +12,14 @@ use serde::{de::DeserializeOwned, Serialize}; )] pub struct FrontPageQuery; +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.json", + query_path = "graphql/catchup.graphql", + response_derives = "Debug" +)] +pub struct CatchupQuery; + #[derive(GraphQLQuery)] #[graphql( schema_path = "graphql/schema.json", diff --git a/web/src/state.rs b/web/src/state.rs index b1e742b..71458bb 100644 --- a/web/src/state.rs +++ b/web/src/state.rs @@ -53,6 +53,7 @@ pub fn init(url: Url, orders: &mut impl Orders) -> Model { client: version, server: None, }, + catchup: None, } } @@ -182,9 +183,9 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::GoToSearchResults => { let url = urls::search(&model.query, 0); - info!("GoToSearchRestuls Start"); + info!("GoToSearchResults Start"); orders.request_url(url); - info!("GoToSearchRestuls End"); + info!("GoToSearchResults End"); } Msg::UpdateQuery(query) => model.query = query, @@ -390,6 +391,38 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { Msg::ShowThreadResult(bad) => { error!("show_thread_query error: {bad:#?}"); } + Msg::CatchupRequest { query } => { + orders.perform_cmd(async move { + Msg::CatchupResult( + send_graphql::<_, graphql::catchup_query::ResponseData>( + graphql::CatchupQuery::build_query(graphql::catchup_query::Variables { + query, + }), + ) + .await, + ) + }); + } + Msg::CatchupResult(Ok(graphql_client::Response { + data: Some(data), .. + })) => { + let items = data.catchup; + if items.is_empty() { + orders.send_msg(Msg::GoToSearchResults); + model.catchup = None; + } else { + orders.request_url(urls::thread(&items[0])); + model.catchup = Some(Catchup { + items: items + .into_iter() + .map(|id| CatchupItem { id, seen: false }) + .collect(), + }); + } + } + Msg::CatchupResult(bad) => { + error!("catchup_query error: {bad:#?}"); + } Msg::SelectionSetNone => { if let Context::SearchResult { selected_threads, .. @@ -504,7 +537,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { }); } Msg::WindowScrolled => { - info!("WindowScrolled"); if let Some(el) = model.content_el.get() { let ih = window() .inner_height() @@ -513,7 +545,6 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { .value_of(); let r = el.get_bounding_client_rect(); - info!("r {r:?} ih {ih}"); if r.height() < ih { // The whole content fits in the window, no scrollbar orders.send_msg(Msg::SetProgress(0.)); @@ -554,8 +585,67 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } model.versions.server = Some(version); } + + Msg::StartCatchup => { + let query = if model.query.contains("is:unread") { + model.query.to_string() + } else { + format!("{} is:unread", model.query) + }; + info!("starting catchup mode w/ {}", query); + orders.send_msg(Msg::CatchupRequest { query }); + } + Msg::CatchupKeepUnread => { + if let Some(thread_id) = current_thread_id(&model.context) { + orders.send_msg(Msg::SetUnread(thread_id, true)); + }; + orders.send_msg(Msg::CatchupNext); + } + Msg::CatchupMarkAsRead => { + if let Some(thread_id) = current_thread_id(&model.context) { + orders.send_msg(Msg::SetUnread(thread_id, false)); + }; + orders.send_msg(Msg::CatchupNext); + } + Msg::CatchupNext => { + let Some(catchup) = &mut model.catchup else { + orders.send_msg(Msg::GoToSearchResults); + return; + }; + let Some(idx) = catchup.items.iter().position(|i| !i.seen) else { + orders.send_msg(Msg::GoToSearchResults); + return; + }; + catchup.items[idx].seen = true; + if idx < catchup.items.len() - 1 { + orders.request_url(urls::thread(&catchup.items[idx + 1].id)); + return; + } else { + orders.send_msg(Msg::GoToSearchResults); + return; + }; + } } } + +fn current_thread_id(context: &Context) -> Option { + match context { + Context::ThreadResult { + thread: + ShowThreadQueryThread::EmailThread(ShowThreadQueryThreadOnEmailThread { + thread_id, .. + }), + .. + } => Some(thread_id.clone()), + Context::ThreadResult { + thread: + ShowThreadQueryThread::NewsPost(ShowThreadQueryThreadOnNewsPost { thread_id, .. }), + .. + } => Some(thread_id.clone()), + _ => None, + } +} + // `Model` describes our app state. pub struct Model { pub query: String, @@ -565,6 +655,7 @@ pub struct Model { pub read_completion_ratio: f64, pub content_el: ElRef, pub versions: Version, + pub catchup: Option, } #[derive(Debug)] @@ -601,6 +692,15 @@ pub enum Context { }, } +pub struct Catchup { + pub items: Vec, +} + +pub struct CatchupItem { + pub id: String, + pub seen: bool, +} + pub struct Tag { pub name: String, pub bg_color: String, @@ -651,10 +751,14 @@ pub enum Msg { ShowThreadResult( Result, gloo_net::Error>, ), + CatchupRequest { + query: String, + }, + CatchupResult( + Result, gloo_net::Error>, + ), - #[allow(dead_code)] SelectionSetNone, - #[allow(dead_code)] SelectionSetAll, SelectionAddTag(String), #[allow(dead_code)] @@ -673,4 +777,9 @@ pub enum Msg { WindowScrolled, SetProgress(f64), UpdateServerVersion(String), + + StartCatchup, + CatchupKeepUnread, + CatchupMarkAsRead, + CatchupNext, } diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs index 0fd2f52..0542c83 100644 --- a/web/src/view/mod.rs +++ b/web/src/view/mod.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::HashSet}; +use std::{cell::RefCell, cmp::Ordering, collections::HashSet}; use chrono::{DateTime, Datelike, Duration, Local, Utc}; use human_format::{Formatter, Scales}; @@ -12,7 +12,7 @@ use web_sys::{HtmlElement, HtmlInputElement}; use crate::{ api::urls, graphql::{front_page_query::*, show_thread_query::*}, - state::{unread_query, Context, Model, Msg, RefreshingState, Tag}, + state::{unread_query, CatchupItem, Context, Model, Msg, RefreshingState, Tag, Version}, }; // TODO(wathiede): create a QueryString enum that wraps single and multiple message ids and thread @@ -68,24 +68,82 @@ mod tw_classes { } pub fn view(model: &Model) -> Node { - let content = match &model.context { - Context::None => div![h1!["Loading"]], + match &model.context { + Context::None => normal_view( + div![h1!["Loading"]], + &model.versions, + &model.query, + &model.refreshing_state, + model.read_completion_ratio, + &model.tags, + ), Context::ThreadResult { thread: ShowThreadQueryThread::EmailThread(thread_data), open_messages, - } => thread(thread_data, open_messages, &model.content_el), + } => { + if let Some(catchup) = &model.catchup { + catchup_view( + thread(thread_data, open_messages, &model.content_el), + &catchup.items, + model.read_completion_ratio, + ) + } else { + normal_view( + thread(thread_data, open_messages, &model.content_el), + &model.versions, + &model.query, + &model.refreshing_state, + model.read_completion_ratio, + &model.tags, + ) + } + } Context::ThreadResult { thread: ShowThreadQueryThread::NewsPost(post), .. - } => news_post(post, &model.content_el), + } => { + if let Some(catchup) = &model.catchup { + catchup_view( + news_post(post, &model.content_el), + &catchup.items, + model.read_completion_ratio, + ) + } else { + normal_view( + news_post(post, &model.content_el), + &model.versions, + &model.query, + &model.refreshing_state, + model.read_completion_ratio, + &model.tags, + ) + } + } Context::SearchResult { query, results, count, pager, selected_threads, - } => search_results(&query, results.as_slice(), *count, pager, selected_threads), - }; + } => normal_view( + search_results(&query, results.as_slice(), *count, pager, selected_threads), + &model.versions, + &model.query, + &model.refreshing_state, + model.read_completion_ratio, + &model.tags, + ), + } +} + +fn normal_view( + content: Node, + versions: &Version, + query: &str, + refreshing_state: &RefreshingState, + read_completion_ratio: f64, + tags: &Option>, +) -> Node { div![ C![ "relative", @@ -98,19 +156,77 @@ pub fn view(model: &Model) -> Node { ], div![ C!["w-full", "lg:w-48", "flex-none", "flex", "flex-col"], - tags(model), - versions(&model.versions) + view_tags(tags), + view_versions(&versions) ], div![ // TODO: This "overflow-hidden" is a hack because I can't figure out // how to prevent the search input box on mobile for growing it's // parent wider C!["flex-auto", "flex", "flex-col", "overflow-hidden"], - view_header(&model.query, &model.refreshing_state, true), + view_header(query, refreshing_state, true), content, - view_header(&model.query, &model.refreshing_state, false), + view_header(query, refreshing_state, false), ], - reading_progress(model.read_completion_ratio), + reading_progress(read_completion_ratio), + ] +} + +fn catchup_view( + content: Node, + items: &[CatchupItem], + read_completion_ratio: f64, +) -> Node { + div![ + C!["w-full", "relative", "text-white"], + div![ + C![ + "fixed", + "top-0", + "right-0", + "left-0", + "p-4", + "border-b", + "border-gray-500", + "bg-black", + ], + div![ + C!["absolute", "right-4", "text-gray-500"], + span![i![C!["fas", "fa-x"]]], + ev(Ev::Click, move |_| Msg::GoToSearchResults) + ], + h1![ + C!["text-center"], + format!("{} left ", items.iter().filter(|i| !i.seen).count(),) + ] + ], + div![C!["mt-12", "mb-4"], content], + div![ + C![ + "fixed", + "bottom-0", + "left-0", + "right-0", + "flex", + "justify-center", + "gap-4", + "p-4", + "border-t", + "border-gray-500", + "bg-black", + ], + button![ + C![&tw_classes::BUTTON], + "Keep unread", + ev(Ev::Click, move |_| Msg::CatchupKeepUnread) + ], + button![ + C![&tw_classes::BUTTON, "bg-green-500"], + "Mark as read", + ev(Ev::Click, move |_| Msg::CatchupMarkAsRead) + ] + ], + reading_progress(read_completion_ratio) ] } @@ -322,16 +438,22 @@ fn search_toolbar( let tristate_el: ElRef = use_state(|| Default::default()).get(); let tri = el_ref(&tristate_el); if let Some(tri) = tri.get() { - info!( - "setting tristate to {indeterminate}, current {}", - tri.indeterminate() - ); tri.set_indeterminate(indeterminate); } nav![ C!["py-4", "flex", "w-full", "justify-between"], div![ - C!["gap-2", "flex", IF!(!show_bulk_edit => "invisible")], + C!["gap-2", "flex", IF!(show_bulk_edit => "hidden")], + div![button![ + C![&tw_classes::BUTTON], + attrs! {At::Title => "Mark as read"}, + span![i![C!["far", "fa-eye"]]], + span![C!["pl-2", "hidden", "md:inline"], "Catch-up"], + ev(Ev::Click, |_| Msg::StartCatchup) + ]], + ], + div![ + C!["gap-2", "flex", IF!(!show_bulk_edit => "hidden")], div![ C!["flex", "items-center", "mr-4"], input![ @@ -484,6 +606,53 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node< let is_unread = has_unread(&msg.tags); let avatar = render_avatar(photo_url, &from, true); let unknown = "UNKNOWN".to_string(); + let to_xinu = RefCell::new(false); + let to_addrs: Vec<_> = msg + .to + .iter() + .map(|to| { + let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to; + span![ + addr.as_ref().map(|addr| { + if addr.ends_with("xinu.tv") { + *to_xinu.borrow_mut() = true; + } + attrs! { + At::Title => addr + } + }), + name.as_ref() + .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), + " ", + addr.as_ref().map(|addr| copy_text_widget(&addr)), + " " + ] + }) + .collect(); + let cc_addrs: Vec<_> = msg + .cc + .iter() + .map(|cc| { + let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc; + span![ + addr.as_ref().map(|addr| { + if addr.ends_with("xinu.tv") { + *to_xinu.borrow_mut() = true; + } + attrs! { + At::Title => addr + } + }), + name.as_ref() + .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), + " ", + addr.as_ref().map(|addr| copy_text_widget(&addr)), + " " + ] + }) + .collect(); + let show_x_original_to = !*to_xinu.borrow() && msg.x_original_to.is_some(); + let show_delivered_to = !*to_xinu.borrow() && !show_x_original_to && msg.delivered_to.is_some(); div![ C!["flex", "p-4", "bg-neutral-800"], div![avatar], @@ -505,21 +674,20 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node< "To: " ], span![ - msg.to.iter().map(|to| { - let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to; - span![ - addr.as_ref().map(|addr| attrs! { - At::Title => addr - }), - name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), - " ", - addr.as_ref().map(|addr| copy_text_widget(&addr)), - " " - ] - }) + to_addrs ] ]), - IF!(msg.to.is_empty() && msg.x_original_to.is_some()=>div![ + IF!(!msg.cc.is_empty() =>div![ + C!["text-xs"], + span![ + C!["font-semibold"], + "CC: " + ], + span![ + cc_addrs + ] + ]), + IF!(show_x_original_to => div![ C!["text-xs"], span![ C!["font-semibold"], @@ -540,7 +708,7 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node< }) ] ]), - IF!(msg.to.is_empty() && msg.x_original_to.is_none() && msg.delivered_to.is_some() => div![ + IF!(show_delivered_to => div![ C!["text-xs"], span![ C!["font-semibold"], @@ -561,27 +729,6 @@ fn render_open_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Node< }) ] ]), - IF!(!msg.cc.is_empty() =>div![ - C!["text-xs"], - span![ - C!["font-semibold"], - "CC: " - ], - span![ - msg.cc.iter().map(|cc| { - let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc; - span![ - addr.as_ref().map(|addr| attrs! { - At::Title => addr - }), - name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), - " ", - addr.as_ref().map(|addr| copy_text_widget(&addr)), - " " - ] - }) - ] - ]), ], span![ C!["text-right"], @@ -623,6 +770,51 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod let is_unread = has_unread(&msg.tags); let avatar = render_avatar(photo_url, &from, false); let unknown = "UNKNOWN".to_string(); + let to_xinu = RefCell::new(false); + let to_addrs: Vec<_> = msg + .to + .iter() + .enumerate() + .map(|(i, to)| { + let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to; + span![ + addr.as_ref().map(|addr| { + if addr.ends_with("xinu.tv") { + *to_xinu.borrow_mut() = true; + } + attrs! { + At::Title => addr + } + }), + if i > 0 { ", " } else { "" }, + name.as_ref() + .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)) + ] + }) + .collect(); + let cc_addrs: Vec<_> = msg + .cc + .iter() + .enumerate() + .map(|(i, cc)| { + let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc; + span![ + addr.as_ref().map(|addr| { + if addr.ends_with("xinu.tv") { + *to_xinu.borrow_mut() = true; + } + attrs! { + At::Title => addr + } + }), + if i > 0 { ", " } else { "" }, + name.as_ref() + .unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)) + ] + }) + .collect(); + let show_x_original_to = !*to_xinu.borrow() && msg.x_original_to.is_some(); + let show_delivered_to = !*to_xinu.borrow() && !show_x_original_to && msg.delivered_to.is_some(); div![ C!["flex", "p-4", "bg-neutral-800"], div![avatar], @@ -643,20 +835,19 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod "To: " ], span![ - msg.to.iter().enumerate().map(|(i, to)| { - let ShowThreadQueryThreadOnEmailThreadMessagesTo { name, addr } = to; - span![ - addr.as_ref().map(|addr| attrs! { - At::Title => addr - }), - if i > 0 { ", " } else { "" }, - name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)) - ] - }) + to_addrs ], " " ]), - IF!(msg.to.is_empty() && msg.x_original_to.is_some()=>div![ + IF!(!msg.cc.is_empty() => div![ + C!["text-xs", "max-w-full", "overflow-clip", "text-ellipsis"], + span![ + C!["font-semibold"], + "CC: " + ], + cc_addrs + ]), + IF!(show_x_original_to => div![ C!["text-xs"], span![ C!["font-semibold"], @@ -671,13 +862,11 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod }), name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), " ", - addr.as_ref().map(|addr| copy_text_widget(&addr)), - " " ] }) ] ]), - IF!(msg.to.is_empty() && msg.x_original_to.is_none() && msg.delivered_to.is_some() => div![ + IF!(show_delivered_to => div![ C!["text-xs"], span![ C!["font-semibold"], @@ -692,29 +881,10 @@ fn render_closed_header(msg: &ShowThreadQueryThreadOnEmailThreadMessages) -> Nod }), name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)), " ", - addr.as_ref().map(|addr| copy_text_widget(&addr)), - " " ] }) ] ]), - IF!(!msg.cc.is_empty() => div![ - C!["text-xs", "max-w-full", "overflow-clip", "text-ellipsis"], - span![ - C!["font-semibold"], - "CC: " - ], - msg.cc.iter().enumerate().map(|(i,cc)| { - let ShowThreadQueryThreadOnEmailThreadMessagesCc { name, addr } = cc; - span![ - addr.as_ref().map(|addr| attrs! { - At::Title => addr - }), - if i > 0 { ", " } else { "" }, - name.as_ref().unwrap_or_else(|| addr.as_ref().unwrap_or(&unknown)) - ] - }) - ]) ], span![ C!["text-right"], @@ -1033,8 +1203,13 @@ fn view_header( ] } -pub fn tags(model: &Model) -> Node { - fn view_tag(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node { +pub fn view_tags(tags: &Option>) -> Node { + fn inner_view_tag( + display_name: &str, + indent: usize, + t: &Tag, + search_unread: bool, + ) -> Node { // Hacky, but necessary for tailwind to see all the classes we're using let indent_cls = match indent { 0 => "", @@ -1087,7 +1262,10 @@ pub fn tags(model: &Model) -> Node { .take_while(|(a, b)| a == b) .count() } - fn view_tags<'a>(tags: impl Iterator, search_unread: bool) -> Vec> { + fn inner_view_tags<'a>( + tags: impl Iterator, + search_unread: bool, + ) -> Vec> { let mut tag_els = Vec::new(); let mut last = Vec::new(); for t in tags { @@ -1097,7 +1275,7 @@ pub fn tags(model: &Model) -> Node { // Synthesize fake tags for proper indenting. for i in n..parts.len() - 1 { let display_name = parts[n]; - tag_els.push(view_tag( + tag_els.push(inner_view_tag( &display_name, n, &Tag { @@ -1111,13 +1289,12 @@ pub fn tags(model: &Model) -> Node { n = parts.len() - 1; } let display_name = parts[n]; - tag_els.push(view_tag(&display_name, n, t, search_unread)); + tag_els.push(inner_view_tag(&display_name, n, t, search_unread)); last = parts; } tag_els } - let mut unread = model - .tags + let mut unread = tags .as_ref() .map(|tags| tags.iter().filter(|t| t.unread > 0).collect()) .unwrap_or(Vec::new()); @@ -1138,7 +1315,7 @@ pub fn tags(model: &Model) -> Node { aside![ C!["p-2"], IF!(!unread.is_empty() => p![C!["uppercase", "font-bold"], "Unread"]), - IF!(!unread.is_empty() => div![C!["flex","flex-col"], view_tags(unread.into_iter(), true)]), + IF!(!unread.is_empty() => div![C!["flex","flex-col"], inner_view_tags(unread.into_iter(), true)]), p![ span![C!["uppercase", "font-bold", "pr-2"], "Tags"], IF!(!force_tags_open => @@ -1153,7 +1330,7 @@ pub fn tags(model: &Model) -> Node { tags_open.on_click(|t| *t = !*t) ], div![ - IF!(force_tags_open||tags_open.get() => model.tags.as_ref().map(|tags| view_tags(tags.iter(),false))), + IF!(force_tags_open||tags_open.get() => tags.as_ref().map(|t| inner_view_tags(t.iter(),false))), ] ] } @@ -1328,7 +1505,6 @@ fn render_news_post_header(post: &ShowThreadQueryThreadOnNewsPost) -> Node } fn reading_progress(ratio: f64) -> Node { let percent = ratio * 100.; - info!("percent {percent}"); div![ C![ "fixed", @@ -1347,7 +1523,7 @@ fn reading_progress(ratio: f64) -> Node { ] ] } -pub fn versions(versions: &crate::state::Version) -> Node { +pub fn view_versions(versions: &Version) -> Node { debug!("versions {versions:?}"); aside![ C!["p-2"],