Compare commits

..

4 Commits

Author SHA1 Message Date
xeruf 5e6b274fe3 feat: requeue events with missing references 2024-09-14 17:14:51 +03:00
xeruf cb75a5749f style(tasks): some bits of documentation 2024-09-14 16:17:30 +03:00
xeruf 0744e86922 feat: properly include tasks with hashtags in filters 2024-09-14 16:13:41 +03:00
xeruf ddb68f7107 feat: recognize hashtags in task name 2024-09-14 15:53:27 +03:00
5 changed files with 81 additions and 34 deletions

View File

@ -166,9 +166,6 @@ Considering to use Calendar: https://github.com/nostr-protocol/nips/blob/master/
- Remove status filter when moving up?
- Task markdown support? - colored
- Time tracking: Ability to postpone task and add planned timestamps (calendar entry)
- Parse Hashtag tags from task name
- Unified Filter object
-> include subtasks of matched tasks
- Speedup: Offline caching & Expiry (no need to fetch potential years of history)
+ Fetch most recent tasks first
+ Relay: compress tracked time for old tasks, filter closed tasks

View File

@ -1,9 +1,10 @@
use itertools::Itertools;
use log::info;
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
use nostr_sdk::TagStandard::Hashtag;
use nostr_sdk::{Alphabet, EventBuilder, EventId, Kind, Tag, TagStandard};
use std::collections::HashSet;
use crate::task::{MARKER_PARENT, State};
use crate::task::{State, MARKER_PARENT};
pub const TASK_KIND: Kind = Kind::GitIssue;
pub const PROCEDURE_KIND_ID: u16 = 1639;
@ -82,21 +83,33 @@ pub(crate) fn build_prop(
)
}
/// Expects sanitized input
/// Return Hashtags embedded in the string.
pub(crate) fn extract_hashtags(input: &str) -> impl Iterator<Item=Tag> + '_ {
input.split_ascii_whitespace()
.filter(|s| s.starts_with('#'))
.map(|s| s.trim_start_matches('#'))
.map(to_hashtag)
}
/// Extracts everything after a ": " as a list of tags.
///
/// Expects sanitized input.
pub(crate) fn extract_tags(input: &str) -> (&str, Vec<Tag>) {
match input.split_once(": ") {
None => (input, vec![]),
Some(s) => {
let tags = s
.1
.split_ascii_whitespace()
.map(|t| Hashtag(t.to_string()).into())
None => (input, extract_hashtags(input).collect_vec()),
Some((name, tags)) => {
let tags = extract_hashtags(name)
.chain(tags.split_ascii_whitespace().map(to_hashtag))
.collect();
(s.0, tags)
(name, tags)
}
}
}
fn to_hashtag(tag: &str) -> Tag {
Hashtag(tag.to_string()).into()
}
fn format_tag(tag: &Tag) -> String {
match tag.as_standardized() {
Some(TagStandard::Event {
@ -123,3 +136,9 @@ pub(crate) fn is_hashtag(tag: &Tag) -> bool {
.is_some_and(|letter| letter.character == Alphabet::T)
}
#[test]
fn test_extract_tags() {
assert_eq!(extract_tags("Hello from #mars with #greetings: yeah done-it"),
("Hello from #mars with #greetings", ["mars", "greetings", "yeah", "done-it"].into_iter().map(to_hashtag).collect()))
}

View File

@ -361,6 +361,8 @@ async fn main() -> Result<()> {
}
if count > 0 {
info!("Received {count} Updates");
} else {
relays.values_mut().for_each(|tasks| tasks.process_overflow());
}
let mut iter = input.chars();

View File

@ -78,11 +78,14 @@ impl Task {
/// Trimmed event content or stringified id
pub(crate) fn get_title(&self) -> String {
Some(self.event.content.trim().to_string())
.filter(|s| !s.is_empty())
some_non_empty(self.event.content.trim())
.unwrap_or_else(|| self.get_id().to_string())
}
pub(crate) fn get_filter_title(&self) -> String {
self.event.content.trim().trim_start_matches('#').to_string()
}
pub(crate) fn description_events(&self) -> impl Iterator<Item=&Event> + '_ {
self.props.iter().filter(|event| event.kind == Kind::TextNote)
}

View File

@ -6,18 +6,18 @@ use std::ops::{Div, Rem};
use std::str::FromStr;
use std::time::Duration;
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
use crate::kinds::*;
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
use crate::{EventSender, MostrMessage};
use colored::Colorize;
use itertools::{Either, Itertools};
use log::{debug, error, info, trace, warn};
use nostr_sdk::prelude::Marker;
use nostr_sdk::{Event, EventBuilder, EventId, JsonUtil, Keys, Kind, Metadata, PublicKey, Tag, TagStandard, Timestamp, UncheckedUrl, Url};
use tokio::sync::mpsc::Sender;
use TagStandard::Hashtag;
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, CHARACTER_THRESHOLD};
use crate::kinds::*;
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT};
use crate::{EventSender, MostrMessage};
const MAX_OFFSET: u64 = 9;
fn now() -> Timestamp {
Timestamp::now() + MAX_OFFSET
@ -54,6 +54,7 @@ pub(crate) struct Tasks {
state: StateFilter,
sender: EventSender,
overflow: VecDeque<Event>,
}
#[derive(Clone, Debug, Default)]
@ -105,7 +106,7 @@ impl Display for StateFilter {
impl Tasks {
pub(crate) fn from(
url: Option<Url>,
tx: &tokio::sync::mpsc::Sender<MostrMessage>,
tx: &Sender<MostrMessage>,
keys: &Keys,
metadata: Option<Metadata>,
) -> Self {
@ -142,10 +143,25 @@ impl Tasks {
tags_excluded: Default::default(),
state: Default::default(),
depth: 1,
sender,
overflow: Default::default(),
}
}
pub(crate) fn process_overflow(&mut self) {
let elements = self.overflow.len();
for _ in 0..elements {
if let Some(event) = self.overflow.pop_front() {
if let Some(event) = self.add_prop(event) {
warn!("Unable to sort Event {:?}", event);
//self.overflow.push_back(event);
}
}
}
info!("Reprocessed {elements} Updates {}", self.sender.url.clone().map(|url| format!(" from {url}")).unwrap_or_default());
}
// Accessors
#[inline]
@ -201,9 +217,10 @@ impl Tasks {
last = new;
}
}
// TODO show history for active tags
("Your Time-Tracking History:".to_string(), Box::from(full.into_iter()))
} else {
("You have nothing tracked yet".to_string(), Box::from(empty()))
("You have nothing time-tracked yet".to_string(), Box::from(empty()))
}
}
Some(id) => {
@ -670,20 +687,21 @@ impl Tasks {
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
for task in self.filtered_tasks(position, false) {
let lowercase = task.event.content.to_ascii_lowercase();
let content = task.get_filter_title();
let lowercase = content.to_ascii_lowercase();
if lowercase == lowercase_arg {
return vec![task.event.id];
} else if task.event.content.starts_with(arg) {
} else if content.starts_with(arg) {
filtered.push(task.event.id)
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.starts_with(&lowercase_arg)) } {
} else if if has_space { lowercase.starts_with(&lowercase_arg) } else { lowercase.split_ascii_whitespace().any(|word| word.trim_start_matches('#').starts_with(&lowercase_arg)) } {
filtered_fuzzy.push(task.event.id)
}
}
for task in self.tasks.values() {
// Find global exact match
if task.event.content.to_ascii_lowercase() == lowercase_arg &&
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
for task in self.tasks.values() {
if task.get_filter_title().to_ascii_lowercase() == lowercase_arg &&
// exclude closed tasks and their subtasks
!self.traverse_up_from(Some(*task.get_id())).any(|t| t.pure_state() == State::Closed) {
return vec![task.event.id];
}
}
@ -700,7 +718,10 @@ impl Tasks {
filtered
}
/// Finds out what to do with the given string.
/// Finds out what to do with the given string, one of:
/// - filtering the visible tasks
/// - entering the only matching task
/// - creating a new task
/// Returns an EventId if a new Task was created.
pub(crate) fn filter_or_create(&mut self, position: Option<&EventId>, arg: &str) -> Option<EventId> {
let filtered = self.get_matching(position, arg);
@ -908,7 +929,10 @@ impl Tasks {
None => { self.history.insert(event.pubkey, BTreeMap::from([(event.created_at, event)])); }
}
} else {
self.add_prop(event)
if let Some(event) = self.add_prop(event) {
debug!("Requeueing unknown Event {:?}", event);
self.overflow.push_back(event);
}
}
}
}
@ -927,17 +951,19 @@ impl Tasks {
}
}
fn add_prop(&mut self, event: Event) {
/// Add event as prop, returning it if not processable
fn add_prop(&mut self, event: Event) -> Option<Event> {
let found = self.referenced_tasks(&event, |t| {
t.props.insert(event.clone());
});
if !found {
if event.kind.as_u16() == 1 {
self.add_task(event);
return;
} else {
return Some(event)
}
warn!("Unknown event {:?}", event)
}
None
}
fn get_own_history(&self) -> Option<&BTreeMap<Timestamp, Event>> {