diff --git a/src/kinds.rs b/src/kinds.rs index 8804980..f96b5a3 100644 --- a/src/kinds.rs +++ b/src/kinds.rs @@ -146,10 +146,10 @@ pub(crate) fn to_prio_tag(value: Prio) -> Tag { #[test] fn test_extract_tags() { - assert_eq!(extract_tags("Hello from #mars with #greetings *4 # # yeah done-it"), - ("Hello from #mars with #greetings".to_string(), - ["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag) - .chain(std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))).collect())); + assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"), + ("Hello from #mars with #greetings #yeah".to_string(), + std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()])) + .chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag)).collect())); assert_eq!(extract_tags("So tagless #"), ("So tagless".to_string(), vec![])); } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5dcc712..ada0310 100644 --- a/src/main.rs +++ b/src/main.rs @@ -677,7 +677,7 @@ async fn main() -> Result<()> { let filtered = tasks.get_filtered(pos, |t| { transform(&t.event.content).contains(&remaining) || - t.tags.iter().flatten().any( + t.get_hashtags().any( |tag| tag.content().is_some_and(|s| transform(s).contains(&remaining))) }); if filtered.len() == 1 { diff --git a/src/task.rs b/src/task.rs index b13740a..fbea130 100644 --- a/src/task.rs +++ b/src/task.rs @@ -10,7 +10,7 @@ use colored::{ColoredString, Colorize}; use itertools::Either::{Left, Right}; use itertools::Itertools; use log::{debug, error, info, trace, warn}; -use nostr_sdk::{Event, EventId, Kind, Tag, TagStandard, Timestamp}; +use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, TagStandard, Timestamp}; use crate::helpers::{format_timestamp_local, some_non_empty}; use crate::kinds::{is_hashtag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND}; @@ -23,8 +23,8 @@ pub static MARKER_PROPERTY: &str = "property"; pub(crate) struct Task { /// Event that defines this task pub(crate) event: Event, - /// Cached sorted tags of the event with references remove - do not modify! - pub(crate) tags: Option>, + /// Cached sorted tags of the event with references removed + tags: Option>, /// Task references derived from the event tags refs: Vec<(String, EventId)>, /// Events belonging to this task, such as state updates and notes @@ -172,16 +172,26 @@ impl Task { } } - fn filter_tags

(&self, predicate: P) -> Option + pub(crate) fn get_hashtags(&self) -> impl Iterator { + self.tags().filter(|t| is_hashtag(t)) + } + + fn tags(&self) -> impl Iterator { + self.props.iter().flat_map(|e| e.tags.iter() + .filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E))) + .chain(self.tags.iter().flatten()) + } + + fn join_tags

(&self, predicate: P) -> String where P: FnMut(&&Tag) -> bool, { - self.tags.as_ref().map(|tags| { - tags.iter() - .filter(predicate) - .map(|t| t.content().unwrap().to_string()) - .join(" ") - }) + self.tags() + .filter(predicate) + .map(|t| t.content().unwrap().to_string()) + .sorted_unstable() + .dedup() + .join(" ") } pub(crate) fn get(&self, property: &str) -> Option { @@ -198,8 +208,8 @@ impl Task { "status" => self.state_label().map(|c| c.to_string()), "desc" => self.descriptions().last().cloned(), "description" => Some(self.descriptions().join(" ")), - "hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }), - "tags" => self.filter_tags(|_| true), + "hashtags" => Some(self.join_tags(|tag| { is_hashtag(tag) })), + "tags" => Some(self.join_tags(|_| true)), "alltags" => Some(format!("{:?}", self.tags)), "refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())), "props" => Some(format!( diff --git a/src/tasks.rs b/src/tasks.rs index b80d120..59c7c32 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -241,8 +241,7 @@ impl TasksRelay { pub(crate) fn all_hashtags(&self) -> impl Iterator { self.tasks.values() .filter(|t| t.pure_state() != State::Closed) - .filter_map(|t| t.tags.as_ref()).flatten() - .filter(|tag| is_hashtag(tag)) + .flat_map(|t| t.get_hashtags()) .filter_map(|tag| tag.content().map(|s| s.trim())) .sorted_unstable() .dedup() @@ -449,14 +448,11 @@ impl TasksRelay { self.priority.is_none_or(|prio| { task.priority().unwrap_or(DEFAULT_PRIO) >= prio }) && - task.tags.as_ref().map_or(true, |tags| { - !tags.iter().any(|tag| self.tags_excluded.contains(tag)) - }) && - (self.tags.is_empty() || - task.tags.as_ref().map_or(false, |tags| { - let mut iter = tags.iter(); - self.tags.iter().all(|tag| iter.any(|t| t == tag)) - })) + !task.get_hashtags().any(|tag| self.tags_excluded.contains(tag)) && + (self.tags.is_empty() || { + let mut iter = task.get_hashtags().sorted_unstable(); + self.tags.iter().all(|tag| iter.any(|t| t == tag)) + }) } pub(crate) fn filtered_tasks<'a>(&'a self, position: Option<&'a EventId>, sparse: bool) -> Vec<&'a Task> { @@ -1654,6 +1650,10 @@ mod tasks_test { tasks.move_to(Some(parent)); let sub = tasks.make_task("sub # tag2"); assert_eq!(tasks.all_hashtags().collect_vec(), vec!["tag1", "tag2"]); + tasks.make_note("note with #tag3 # yeah"); + assert_eq!(tasks.all_hashtags().collect_vec(), vec!["tag1", "tag2", "tag3", "yeah"]); + tasks.update_state("Done #yei", State::Done); + // TODO assert_eq!(tasks.all_hashtags().collect_vec(), vec!["tag1", "tag2", "tag3", "yeah", "yei"]); tasks.update_state("Closing Down", State::Closed); assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed); assert_eq!(tasks.all_hashtags().next(), None);