forked from janek/mostr
1
0
Fork 0

feat: automatically add tags from task properties

This commit is contained in:
xeruf 2024-11-12 23:03:53 +01:00
parent 44feea9894
commit 2cec689bf1
4 changed files with 37 additions and 27 deletions

View File

@ -146,10 +146,10 @@ pub(crate) fn to_prio_tag(value: Prio) -> Tag {
#[test] #[test]
fn test_extract_tags() { fn test_extract_tags() {
assert_eq!(extract_tags("Hello from #mars with #greetings *4 # # yeah done-it"), assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"),
("Hello from #mars with #greetings".to_string(), ("Hello from #mars with #greetings #yeah".to_string(),
["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag) std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))
.chain(std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.to_string()]))).collect())); .chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag)).collect()));
assert_eq!(extract_tags("So tagless #"), assert_eq!(extract_tags("So tagless #"),
("So tagless".to_string(), vec![])); ("So tagless".to_string(), vec![]));
} }

View File

@ -677,7 +677,7 @@ async fn main() -> Result<()> {
let filtered = let filtered =
tasks.get_filtered(pos, |t| { tasks.get_filtered(pos, |t| {
transform(&t.event.content).contains(&remaining) || 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))) |tag| tag.content().is_some_and(|s| transform(s).contains(&remaining)))
}); });
if filtered.len() == 1 { if filtered.len() == 1 {

View File

@ -10,7 +10,7 @@ use colored::{ColoredString, Colorize};
use itertools::Either::{Left, Right}; use itertools::Either::{Left, Right};
use itertools::Itertools; use itertools::Itertools;
use log::{debug, error, info, trace, warn}; 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::helpers::{format_timestamp_local, some_non_empty};
use crate::kinds::{is_hashtag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND}; 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 { pub(crate) struct Task {
/// Event that defines this task /// Event that defines this task
pub(crate) event: Event, pub(crate) event: Event,
/// Cached sorted tags of the event with references remove - do not modify! /// Cached sorted tags of the event with references removed
pub(crate) tags: Option<BTreeSet<Tag>>, tags: Option<BTreeSet<Tag>>,
/// Task references derived from the event tags /// Task references derived from the event tags
refs: Vec<(String, EventId)>, refs: Vec<(String, EventId)>,
/// Events belonging to this task, such as state updates and notes /// Events belonging to this task, such as state updates and notes
@ -172,16 +172,26 @@ impl Task {
} }
} }
fn filter_tags<P>(&self, predicate: P) -> Option<String> pub(crate) fn get_hashtags(&self) -> impl Iterator<Item=&Tag> {
self.tags().filter(|t| is_hashtag(t))
}
fn tags(&self) -> impl Iterator<Item=&Tag> {
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<P>(&self, predicate: P) -> String
where where
P: FnMut(&&Tag) -> bool, P: FnMut(&&Tag) -> bool,
{ {
self.tags.as_ref().map(|tags| { self.tags()
tags.iter()
.filter(predicate) .filter(predicate)
.map(|t| t.content().unwrap().to_string()) .map(|t| t.content().unwrap().to_string())
.sorted_unstable()
.dedup()
.join(" ") .join(" ")
})
} }
pub(crate) fn get(&self, property: &str) -> Option<String> { pub(crate) fn get(&self, property: &str) -> Option<String> {
@ -198,8 +208,8 @@ impl Task {
"status" => self.state_label().map(|c| c.to_string()), "status" => self.state_label().map(|c| c.to_string()),
"desc" => self.descriptions().last().cloned(), "desc" => self.descriptions().last().cloned(),
"description" => Some(self.descriptions().join(" ")), "description" => Some(self.descriptions().join(" ")),
"hashtags" => self.filter_tags(|tag| { is_hashtag(tag) }), "hashtags" => Some(self.join_tags(|tag| { is_hashtag(tag) })),
"tags" => self.filter_tags(|_| true), "tags" => Some(self.join_tags(|_| true)),
"alltags" => Some(format!("{:?}", self.tags)), "alltags" => Some(format!("{:?}", self.tags)),
"refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())), "refs" => Some(format!("{:?}", self.refs.iter().map(|re| format!("{}: {}", re.0, re.1)).collect_vec())),
"props" => Some(format!( "props" => Some(format!(

View File

@ -241,8 +241,7 @@ impl TasksRelay {
pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> { pub(crate) fn all_hashtags(&self) -> impl Iterator<Item=&str> {
self.tasks.values() self.tasks.values()
.filter(|t| t.pure_state() != State::Closed) .filter(|t| t.pure_state() != State::Closed)
.filter_map(|t| t.tags.as_ref()).flatten() .flat_map(|t| t.get_hashtags())
.filter(|tag| is_hashtag(tag))
.filter_map(|tag| tag.content().map(|s| s.trim())) .filter_map(|tag| tag.content().map(|s| s.trim()))
.sorted_unstable() .sorted_unstable()
.dedup() .dedup()
@ -449,14 +448,11 @@ impl TasksRelay {
self.priority.is_none_or(|prio| { self.priority.is_none_or(|prio| {
task.priority().unwrap_or(DEFAULT_PRIO) >= prio task.priority().unwrap_or(DEFAULT_PRIO) >= prio
}) && }) &&
task.tags.as_ref().map_or(true, |tags| { !task.get_hashtags().any(|tag| self.tags_excluded.contains(tag)) &&
!tags.iter().any(|tag| self.tags_excluded.contains(tag)) (self.tags.is_empty() || {
}) && let mut iter = task.get_hashtags().sorted_unstable();
(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)) 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> { 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)); tasks.move_to(Some(parent));
let sub = tasks.make_task("sub # tag2"); let sub = tasks.make_task("sub # tag2");
assert_eq!(tasks.all_hashtags().collect_vec(), vec!["tag1", "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); tasks.update_state("Closing Down", State::Closed);
assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed); assert_eq!(tasks.get_by_id(&sub).unwrap().pure_state(), State::Closed);
assert_eq!(tasks.all_hashtags().next(), None); assert_eq!(tasks.all_hashtags().next(), None);