Compare commits
No commits in common. "a3eeb38e5f8b8ebc0cdd193b823c1a4804830133" and "a8fb3f919dca1d48dbba6fb73d247a3308716240" have entirely different histories.
a3eeb38e5f
...
a8fb3f919d
5 changed files with 122 additions and 223 deletions
41
src/kinds.rs
41
src/kinds.rs
|
@ -1,8 +1,7 @@
|
||||||
use crate::task::MARKER_PARENT;
|
use crate::task::MARKER_PARENT;
|
||||||
use crate::tasks::nostr_users::NostrUsers;
|
|
||||||
use crate::tasks::HIGH_PRIO;
|
use crate::tasks::HIGH_PRIO;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::{EventBuilder, EventId, Kind, PublicKey, Tag, TagKind, TagStandard};
|
use nostr_sdk::{EventBuilder, EventId, Kind, Tag, TagKind, TagStandard};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
pub const TASK_KIND: Kind = Kind::GitIssue;
|
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||||
|
@ -100,27 +99,22 @@ pub(crate) fn extract_hashtags(input: &str) -> impl Iterator<Item=Tag> + '_ {
|
||||||
/// as well as various embedded tags.
|
/// as well as various embedded tags.
|
||||||
///
|
///
|
||||||
/// Expects sanitized input.
|
/// Expects sanitized input.
|
||||||
pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>) {
|
pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
||||||
let words = input.split_ascii_whitespace();
|
let words = input.split_ascii_whitespace();
|
||||||
let mut tags = Vec::with_capacity(4);
|
let mut prio = None;
|
||||||
let result = words.filter(|s| {
|
let result = words.filter(|s| {
|
||||||
if s.starts_with('@') {
|
if s.starts_with('*') {
|
||||||
if let Ok(key) = PublicKey::parse(&s[1..]) {
|
|
||||||
tags.push(Tag::public_key(key));
|
|
||||||
return false;
|
|
||||||
} else if let Some((key, _)) = users.find_user(&s[1..]) {
|
|
||||||
tags.push(Tag::public_key(*key));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if s.starts_with('*') {
|
|
||||||
if s.len() == 1 {
|
if s.len() == 1 {
|
||||||
tags.push(to_prio_tag(HIGH_PRIO));
|
prio = Some(HIGH_PRIO);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Ok(num) = s[1..].parse::<Prio>() {
|
return match s[1..].parse::<Prio>() {
|
||||||
tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 })));
|
Ok(num) => {
|
||||||
return false
|
prio = Some(num * (if s.len() > 2 { 1 } else { 10 }));
|
||||||
}
|
false
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}).collect_vec();
|
}).collect_vec();
|
||||||
|
@ -128,7 +122,7 @@ pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>
|
||||||
let main = split.next().unwrap().join(" ");
|
let main = split.next().unwrap().join(" ");
|
||||||
let mut tags = extract_hashtags(&main)
|
let mut tags = extract_hashtags(&main)
|
||||||
.chain(split.flatten().map(|s| to_hashtag_tag(&s)))
|
.chain(split.flatten().map(|s| to_hashtag_tag(&s)))
|
||||||
.chain(tags)
|
.chain(prio.map(|p| to_prio_tag(p)))
|
||||||
.collect_vec();
|
.collect_vec();
|
||||||
tags.sort();
|
tags.sort();
|
||||||
tags.dedup();
|
tags.dedup();
|
||||||
|
@ -167,11 +161,10 @@ pub 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 #yeah *4 # # yeah done-it", &Default::default()),
|
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"),
|
||||||
("Hello from #mars with #greetings #yeah".to_string(),
|
("Hello from #mars with #greetings #yeah".to_string(),
|
||||||
std::iter::once(Tag::custom(TagKind::Custom(Cow::from(PRIO)), [40.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_tag))
|
.chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag_tag)).collect()));
|
||||||
.collect()));
|
assert_eq!(extract_tags("So tagless #"),
|
||||||
assert_eq!(extract_tags("So tagless @hewo #", &Default::default()),
|
("So tagless".to_string(), vec![]));
|
||||||
("So tagless @hewo".to_string(), vec![]));
|
|
||||||
}
|
}
|
40
src/main.rs
40
src/main.rs
|
@ -12,7 +12,7 @@ use crate::event_sender::MostrMessage;
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
use crate::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
||||||
use crate::task::{State, Task, TaskState, MARKER_PROPERTY};
|
use crate::task::{State, Task, TaskState, MARKER_PROPERTY};
|
||||||
use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
|
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
@ -438,36 +438,14 @@ async fn main() -> Result<()> {
|
||||||
Some('&') => {
|
Some('&') => {
|
||||||
match arg {
|
match arg {
|
||||||
None => tasks.undo(),
|
None => tasks.undo(),
|
||||||
Some(text) => {
|
Some(text) => match text.parse::<u8>() {
|
||||||
if text == "&" {
|
Ok(int) => {
|
||||||
println!(
|
tasks.move_back_by(int as usize);
|
||||||
"My History:\n{}",
|
|
||||||
tasks.history_before_now()
|
|
||||||
.take(9)
|
|
||||||
.enumerate()
|
|
||||||
.dropping(1)
|
|
||||||
.map(|(c, e)| {
|
|
||||||
format!("({}) {}",
|
|
||||||
c,
|
|
||||||
match referenced_event(e) {
|
|
||||||
Some(target) => tasks.get_task_path(Some(target)),
|
|
||||||
None => "---".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
continue 'repl;
|
|
||||||
}
|
}
|
||||||
match text.parse::<u8>() {
|
_ => {
|
||||||
Ok(int) => {
|
if !tasks.move_back_to(text) {
|
||||||
tasks.move_back_by(int as usize);
|
warn!("Did not find a match in history for \"{text}\"");
|
||||||
}
|
continue 'repl;
|
||||||
_ => {
|
|
||||||
if !tasks.move_back_to(text) {
|
|
||||||
warn!("Did not find a match in history for \"{text}\"");
|
|
||||||
continue 'repl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -486,7 +464,7 @@ async fn main() -> Result<()> {
|
||||||
Some(arg) => {
|
Some(arg) => {
|
||||||
if arg == "@" {
|
if arg == "@" {
|
||||||
tasks.reset_key_filter()
|
tasks.reset_key_filter()
|
||||||
} else if let Some((key, name)) = tasks.find_user(arg) {
|
} else if let Some((key, name)) = tasks.find_user_with_displayname(arg) {
|
||||||
info!("Showing {}'s tasks", name);
|
info!("Showing {}'s tasks", name);
|
||||||
tasks.set_key_filter(key)
|
tasks.set_key_filter(key)
|
||||||
} else {
|
} else {
|
||||||
|
|
21
src/task.rs
21
src/task.rs
|
@ -4,14 +4,13 @@ use std::collections::BTreeSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
|
|
||||||
use colored::{ColoredString, Colorize};
|
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::{Alphabet, Event, EventId, Kind, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp};
|
use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, Timestamp};
|
||||||
use crate::hashtag::{is_hashtag, Hashtag};
|
use crate::hashtag::{is_hashtag, Hashtag};
|
||||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
use crate::helpers::{format_timestamp_local, some_non_empty};
|
||||||
use crate::kinds::{match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
use crate::kinds::{match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||||
|
@ -71,15 +70,6 @@ impl Task {
|
||||||
&self.event.id
|
&self.event.id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_owner(&self) -> PublicKey {
|
|
||||||
self.tags()
|
|
||||||
.find(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
|
|
||||||
.and_then(|t| t.content()
|
|
||||||
.and_then(|c| PublicKey::from_str(c).inspect_err(|e| warn!("Unparseable pubkey in {:?}", t)).ok()))
|
|
||||||
.unwrap_or_else(|| self.event.pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
|
pub(crate) fn find_refs<'a>(&'a self, marker: &'a str) -> impl Iterator<Item=&'a EventId> {
|
||||||
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
|
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
|
||||||
}
|
}
|
||||||
|
@ -194,12 +184,11 @@ impl Task {
|
||||||
self.tags().filter_map(|t| Hashtag::try_from(t).ok())
|
self.tags().filter_map(|t| Hashtag::try_from(t).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tags of this task that are not event references, newest to oldest
|
|
||||||
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
fn tags(&self) -> impl Iterator<Item=&Tag> {
|
||||||
self.props.iter()
|
self.tags.iter().flatten().chain(
|
||||||
.flat_map(|e| e.tags.iter()
|
self.props.iter().flat_map(|e| e.tags.iter()
|
||||||
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
.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
|
fn join_tags<P>(&self, predicate: P) -> String
|
||||||
|
|
180
src/tasks.rs
180
src/tasks.rs
|
@ -1,5 +1,3 @@
|
||||||
pub(crate) mod nostr_users;
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::iter::{empty, once, FusedIterator};
|
use std::iter::{empty, once, FusedIterator};
|
||||||
|
@ -7,12 +5,11 @@ use std::ops::{Div, Rem};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::event_sender::{EventSender, MostrMessage};
|
|
||||||
use crate::hashtag::Hashtag;
|
use crate::hashtag::Hashtag;
|
||||||
|
use crate::event_sender::{EventSender, MostrMessage};
|
||||||
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD};
|
use crate::helpers::{format_timestamp_local, format_timestamp_relative, format_timestamp_relative_to, parse_tracking_stamp, some_non_empty, to_string_or_default, CHARACTER_THRESHOLD};
|
||||||
use crate::kinds::*;
|
use crate::kinds::*;
|
||||||
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
use crate::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
||||||
use crate::tasks::nostr_users::NostrUsers;
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
|
@ -57,7 +54,7 @@ pub(crate) struct TasksRelay {
|
||||||
/// History of active tasks by PubKey
|
/// History of active tasks by PubKey
|
||||||
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
||||||
/// Index of known users with metadata
|
/// Index of known users with metadata
|
||||||
users: NostrUsers,
|
users: HashMap<PublicKey, Metadata>,
|
||||||
/// Own pinned tasks
|
/// Own pinned tasks
|
||||||
bookmarks: Vec<EventId>,
|
bookmarks: Vec<EventId>,
|
||||||
|
|
||||||
|
@ -159,7 +156,7 @@ impl TasksRelay {
|
||||||
bookmarks: Default::default(),
|
bookmarks: Default::default(),
|
||||||
|
|
||||||
properties: [
|
properties: [
|
||||||
"owner",
|
"author",
|
||||||
"prio",
|
"prio",
|
||||||
"state",
|
"state",
|
||||||
"rtime",
|
"rtime",
|
||||||
|
@ -170,7 +167,7 @@ impl TasksRelay {
|
||||||
sorting: [
|
sorting: [
|
||||||
"priority",
|
"priority",
|
||||||
"status",
|
"status",
|
||||||
"owner",
|
"author",
|
||||||
"hashtags",
|
"hashtags",
|
||||||
"rtime",
|
"rtime",
|
||||||
"name",
|
"name",
|
||||||
|
@ -256,60 +253,51 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dynamic time tracking overview for current task or current user.
|
/// Dynamic time tracking overview for current task or current user.
|
||||||
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
pub(crate) fn times_tracked(&self) -> (String, Box<dyn DoubleEndedIterator<Item=String>>) {
|
||||||
self.times_tracked_for(&self.sender.pubkey())
|
self.times_tracked_for(&self.sender.pubkey())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn history_for(
|
|
||||||
&self,
|
|
||||||
key: &PublicKey,
|
|
||||||
) -> Option<impl DoubleEndedIterator<Item=String> + '_> {
|
|
||||||
self.history.get(key).map(|hist| {
|
|
||||||
let mut last = None;
|
|
||||||
// TODO limit history to active tags
|
|
||||||
hist.values().filter_map(move |event| {
|
|
||||||
let new = some_non_empty(&event.tags.iter()
|
|
||||||
.filter_map(|t| t.content())
|
|
||||||
.map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_path(Some(id))))
|
|
||||||
.join(" "));
|
|
||||||
if new != last {
|
|
||||||
// TODO omit intervals <2min - but I think I need threeway variable tracking for that
|
|
||||||
// TODO alternate color with grey between days
|
|
||||||
last = new;
|
|
||||||
return Some(format!(
|
|
||||||
"{} {}",
|
|
||||||
format_timestamp_local(&event.created_at),
|
|
||||||
last.as_ref().unwrap_or(&"---".to_string())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn times_tracked_for(
|
pub(crate) fn times_tracked_for(
|
||||||
&self,
|
&self,
|
||||||
key: &PublicKey,
|
key: &PublicKey,
|
||||||
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
) -> (String, Box<dyn DoubleEndedIterator<Item=String>>) {
|
||||||
match self.get_position() {
|
match self.get_position() {
|
||||||
None => {
|
None => {
|
||||||
match self.history_for(key) {
|
if let Some(hist) = self.history.get(key) {
|
||||||
Some(hist) =>
|
let mut last = None;
|
||||||
(
|
let mut full = Vec::with_capacity(hist.len());
|
||||||
format!("Time-Tracking History for {}:", self.users.get_displayname(&key)),
|
for event in hist.values() {
|
||||||
Box::from(hist),
|
let new = some_non_empty(&event.tags.iter()
|
||||||
),
|
.filter_map(|t| t.content())
|
||||||
None =>
|
.map(|str| EventId::from_str(str).ok().map_or(str.to_string(), |id| self.get_task_path(Some(id))))
|
||||||
(
|
.join(" "));
|
||||||
"Nothing time-tracked yet".to_string(),
|
if new != last {
|
||||||
Box::from(empty()),
|
// TODO omit intervals <2min - but I think I need threeway for that
|
||||||
)
|
// TODO alternate color with grey between days
|
||||||
|
full.push(format!(
|
||||||
|
"{} {}",
|
||||||
|
format_timestamp_local(&event.created_at),
|
||||||
|
new.as_ref().unwrap_or(&"---".to_string())
|
||||||
|
));
|
||||||
|
last = new;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO show history for active tags
|
||||||
|
(
|
||||||
|
format!("Time-Tracking History for {}:", self.get_displayname(&key)),
|
||||||
|
Box::from(full.into_iter()),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"Nothing time-tracked yet".to_string(),
|
||||||
|
Box::from(empty()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
// TODO show current recursive with pubkey
|
// TODO show current recursive with pubkey
|
||||||
let ids = [id];
|
let ids = [id];
|
||||||
let mut history =
|
let history =
|
||||||
self.history.iter().flat_map(|(key, set)| {
|
self.history.iter().flat_map(|(key, set)| {
|
||||||
let mut vec = Vec::with_capacity(set.len() / 2);
|
let mut vec = Vec::with_capacity(set.len() / 2);
|
||||||
let mut iter = timestamps(set.values(), &ids).tuples();
|
let mut iter = timestamps(set.values(), &ids).tuples();
|
||||||
|
@ -320,7 +308,7 @@ impl TasksRelay {
|
||||||
"{} - {} by {}",
|
"{} - {} by {}",
|
||||||
format_timestamp_local(start),
|
format_timestamp_local(start),
|
||||||
format_timestamp_relative_to(end, start),
|
format_timestamp_relative_to(end, start),
|
||||||
self.users.get_displayname(key)
|
self.get_displayname(key)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -328,17 +316,14 @@ impl TasksRelay {
|
||||||
vec.push(format!(
|
vec.push(format!(
|
||||||
"{} started by {}",
|
"{} started by {}",
|
||||||
format_timestamp_local(stamp),
|
format_timestamp_local(stamp),
|
||||||
self.users.get_displayname(key)
|
self.get_displayname(key)
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
vec
|
vec
|
||||||
})
|
}).sorted_unstable(); // TODO sorting depends on timestamp format - needed to interleave different people
|
||||||
.collect_vec();
|
|
||||||
// TODO sorting depends on timestamp format - needed to interleave different people
|
|
||||||
history.sort_unstable();
|
|
||||||
(
|
(
|
||||||
format!("Times Tracked on {:?}", self.get_task_title(&id)),
|
format!("Times Tracked on {:?}", self.get_task_title(&id)),
|
||||||
Box::from(history.into_iter()),
|
Box::from(history),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -408,7 +393,7 @@ impl TasksRelay {
|
||||||
Some(key) =>
|
Some(key) =>
|
||||||
if key != self.sender.pubkey() {
|
if key != self.sender.pubkey() {
|
||||||
prompt.push_str(" @");
|
prompt.push_str(" @");
|
||||||
prompt.push_str(&self.users.get_username(&key))
|
prompt.push_str(&self.get_username(&key))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for tag in self.tags.iter() {
|
for tag in self.tags.iter() {
|
||||||
|
@ -509,7 +494,7 @@ impl TasksRelay {
|
||||||
|
|
||||||
fn filter(&self, task: &Task) -> bool {
|
fn filter(&self, task: &Task) -> bool {
|
||||||
self.state.matches(task) &&
|
self.state.matches(task) &&
|
||||||
(!task.is_task() || self.pubkey.is_none_or(|p| p == task.event.pubkey)) &&
|
(!!task.is_task() || self.pubkey.is_none_or(|p| p == task.event.pubkey)) &&
|
||||||
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
|
||||||
}) &&
|
}) &&
|
||||||
|
@ -661,8 +646,7 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
"progress" => prog_string.clone(),
|
"progress" => prog_string.clone(),
|
||||||
|
|
||||||
"owner" => format!("{:.6}", self.users.get_username(&task.get_owner())),
|
"author" | "creator" => format!("{:.6}", self.get_username(&task.event.pubkey)), // FIXME temporary until proper column alignment
|
||||||
"author" | "creator" => format!("{:.6}", self.users.get_username(&task.event.pubkey)), // FIXME temporary until proper column alignment
|
|
||||||
"prio" => self
|
"prio" => self
|
||||||
.traverse_up_from(Some(task.event.id))
|
.traverse_up_from(Some(task.event.id))
|
||||||
.find_map(Task::priority_raw)
|
.find_map(Task::priority_raw)
|
||||||
|
@ -683,8 +667,35 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn find_user(&self, name: &str) -> Option<(PublicKey, String)> {
|
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
|
||||||
self.users.find_user_with_displayname(name)
|
match PublicKey::from_str(term) {
|
||||||
|
Ok(key) => Some((key, self.get_displayname(&key))),
|
||||||
|
Err(_) => self.find_user(term).map(|(k, _)| (*k, self.get_displayname(k))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find username or key starting with the given term.
|
||||||
|
pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> {
|
||||||
|
if let Ok(key) = PublicKey::from_str(term) {
|
||||||
|
return self.users.get_key_value(&key);
|
||||||
|
}
|
||||||
|
self.users.iter().find(|(k, v)|
|
||||||
|
// TODO regex word boundary
|
||||||
|
v.name.as_ref().is_some_and(|n| n.starts_with(term)) ||
|
||||||
|
v.display_name.as_ref().is_some_and(|n| n.starts_with(term)) ||
|
||||||
|
(term.len() > 4 && k.to_string().starts_with(term)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_displayname(&self, pubkey: &PublicKey) -> String {
|
||||||
|
self.users.get(pubkey)
|
||||||
|
.and_then(|m| m.display_name.clone().or(m.name.clone()))
|
||||||
|
.unwrap_or_else(|| pubkey.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_username(&self, pubkey: &PublicKey) -> String {
|
||||||
|
self.users.get(pubkey)
|
||||||
|
.and_then(|m| m.name.clone())
|
||||||
|
.unwrap_or_else(|| format!("{:.6}", pubkey.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Movement and Selection
|
// Movement and Selection
|
||||||
|
@ -859,8 +870,8 @@ impl TasksRelay {
|
||||||
return vec![id];
|
return vec![id];
|
||||||
}
|
}
|
||||||
let lowercase_arg = arg.to_ascii_lowercase();
|
let lowercase_arg = arg.to_ascii_lowercase();
|
||||||
// TODO apply regex to all matching, parse as plain match
|
// TODO apply regex to all matching
|
||||||
let regex = Regex::new(&format!(r"\b{}", lowercase_arg));
|
let regex = Regex::new(&format!(r"\b{}", lowercase_arg)).unwrap();
|
||||||
|
|
||||||
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
||||||
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
||||||
|
@ -871,9 +882,7 @@ impl TasksRelay {
|
||||||
return vec![task.event.id];
|
return vec![task.event.id];
|
||||||
} else if content.starts_with(arg) {
|
} else if content.starts_with(arg) {
|
||||||
filtered.push(task.event.id)
|
filtered.push(task.event.id)
|
||||||
} else if regex.as_ref()
|
} else if regex.is_match(lowercase.as_bytes()) {
|
||||||
.map(|r| r.is_match(lowercase.as_bytes()))
|
|
||||||
.unwrap_or_else(|_| lowercase.starts_with(&lowercase_arg)) {
|
|
||||||
filtered_fuzzy.push(task.event.id)
|
filtered_fuzzy.push(task.event.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -942,11 +951,6 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn move_to(&mut self, target: Option<EventId>) {
|
pub(crate) fn move_to(&mut self, target: Option<EventId>) {
|
||||||
if let Some(time) = self.custom_time {
|
|
||||||
self.track_at(time, target);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.view.clear();
|
self.view.clear();
|
||||||
let pos = self.get_position();
|
let pos = self.get_position();
|
||||||
if target == pos {
|
if target == pos {
|
||||||
|
@ -1061,7 +1065,7 @@ impl TasksRelay {
|
||||||
///
|
///
|
||||||
/// Sanitizes input
|
/// Sanitizes input
|
||||||
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
|
pub(crate) fn make_task_with(&mut self, input: &str, tags: impl IntoIterator<Item=Tag>, set_state: bool) -> EventId {
|
||||||
let (input, input_tags) = extract_tags(input.trim(), &self.users);
|
let (input, input_tags) = extract_tags(input.trim());
|
||||||
let prio =
|
let prio =
|
||||||
if input_tags.iter().any(|t| t.kind().to_string() == PRIO) { None } else { self.priority.map(|p| to_prio_tag(p)) };
|
if input_tags.iter().any(|t| t.kind().to_string() == PRIO) { None } else { self.priority.map(|p| to_prio_tag(p)) };
|
||||||
info!("Created task \"{input}\" with tags [{}]", join_tags(&input_tags));
|
info!("Created task \"{input}\" with tags [{}]", join_tags(&input_tags));
|
||||||
|
@ -1152,7 +1156,9 @@ impl TasksRelay {
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, event: Event) {
|
pub(crate) fn add(&mut self, event: Event) {
|
||||||
let author = event.pubkey;
|
let author = event.pubkey;
|
||||||
self.users.create(author);
|
if !self.users.contains_key(&author) {
|
||||||
|
self.users.insert(author, Metadata::new());
|
||||||
|
}
|
||||||
match event.kind {
|
match event.kind {
|
||||||
Kind::GitIssue => self.add_task(event),
|
Kind::GitIssue => self.add_task(event),
|
||||||
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
||||||
|
@ -1215,29 +1221,25 @@ impl TasksRelay {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
||||||
self.get_own_history()
|
self.history.get(&self.sender.pubkey())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|t| t.values())
|
.flat_map(|t| t.values())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
||||||
self.get_own_history().into_iter().flat_map(|hist| {
|
self.get_own_history().into_iter().flat_map(|hist| {
|
||||||
let now = now();
|
let now = now();
|
||||||
hist.values().rev()
|
hist.values().rev().skip_while(move |e| e.created_at > now)
|
||||||
.skip_while(move |e| e.created_at > now)
|
|
||||||
.dedup_by(|e1, e2| e1.id == e2.id)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn move_back_to(&mut self, str: &str) -> bool {
|
pub(crate) fn move_back_to(&mut self, str: &str) -> bool {
|
||||||
let lower = str.to_ascii_lowercase();
|
let lower = str.to_ascii_lowercase();
|
||||||
let found =
|
let found = self.history_before_now().find(|e| {
|
||||||
self.history_before_now()
|
referenced_event(e)
|
||||||
.find(|e| {
|
.and_then(|id| self.get_by_id(&id))
|
||||||
referenced_event(e)
|
.is_some_and(|t| t.event.content.to_ascii_lowercase().contains(&lower))
|
||||||
.and_then(|id| self.get_by_id(&id))
|
});
|
||||||
.is_some_and(|t| t.event.content.to_ascii_lowercase().contains(&lower))
|
|
||||||
});
|
|
||||||
if let Some(event) = found {
|
if let Some(event) = found {
|
||||||
self.move_to(referenced_event(event));
|
self.move_to(referenced_event(event));
|
||||||
return true;
|
return true;
|
||||||
|
@ -1285,7 +1287,7 @@ impl TasksRelay {
|
||||||
} else {
|
} else {
|
||||||
vec![id]
|
vec![id]
|
||||||
};
|
};
|
||||||
let (desc, tags) = extract_tags(comment, &self.users);
|
let (desc, tags) = extract_tags(comment);
|
||||||
let prop =
|
let prop =
|
||||||
EventBuilder::new(state.into(), desc)
|
EventBuilder::new(state.into(), desc)
|
||||||
.tags(ids.into_iter()
|
.tags(ids.into_iter()
|
||||||
|
@ -1313,7 +1315,7 @@ impl TasksRelay {
|
||||||
/// Creates a note or activity, depending on whether the parent is a task.
|
/// Creates a note or activity, depending on whether the parent is a task.
|
||||||
/// Sanitizes Input.
|
/// Sanitizes Input.
|
||||||
pub(crate) fn make_note(&mut self, note: &str) -> EventId {
|
pub(crate) fn make_note(&mut self, note: &str) -> EventId {
|
||||||
let (name, tags) = extract_tags(note.trim(), &self.users);
|
let (name, tags) = extract_tags(note.trim());
|
||||||
let format = format!("\"{name}\" with tags [{}]", join_tags(&tags));
|
let format = format!("\"{name}\" with tags [{}]", join_tags(&tags));
|
||||||
let mut prop =
|
let mut prop =
|
||||||
EventBuilder::new(Kind::TextNote, name)
|
EventBuilder::new(Kind::TextNote, name)
|
||||||
|
@ -1525,7 +1527,7 @@ fn referenced_events(event: &Event) -> impl Iterator<Item=EventId> + '_ {
|
||||||
event.tags.iter().filter_map(|tag| match_event_tag(tag).map(|t| t.id))
|
event.tags.iter().filter_map(|tag| match_event_tag(tag).map(|t| t.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn referenced_event(event: &Event) -> Option<EventId> {
|
fn referenced_event(event: &Event) -> Option<EventId> {
|
||||||
referenced_events(event).next()
|
referenced_events(event).next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1826,7 +1828,7 @@ mod tasks_test {
|
||||||
|
|
||||||
tasks.custom_time = Some(Timestamp::now());
|
tasks.custom_time = Some(Timestamp::now());
|
||||||
tasks.update_state("Finished #YeaH # oi", State::Done);
|
tasks.update_state("Finished #YeaH # oi", State::Done);
|
||||||
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from));
|
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["tag1", "YeaH", "oi", "tag3", "yeah"].map(Hashtag::from));
|
||||||
assert_eq!(tasks.all_hashtags(), all_tags);
|
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||||
|
|
||||||
tasks.custom_time = Some(now());
|
tasks.custom_time = Some(now());
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use nostr_sdk::{Keys, Metadata, PublicKey, Tag};
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct NostrUsers {
|
|
||||||
users: HashMap<PublicKey, Metadata>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NostrUsers {
|
|
||||||
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
|
|
||||||
self.find_user(term)
|
|
||||||
.map(|(k, _)| (*k, self.get_displayname(k)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find username or key starting with the given term.
|
|
||||||
pub(crate) fn find_user(&self, term: &str) -> Option<(&PublicKey, &Metadata)> {
|
|
||||||
let lowered = term.trim().to_ascii_lowercase();
|
|
||||||
let term = lowered.as_str();
|
|
||||||
if term.is_empty() {
|
|
||||||
return None
|
|
||||||
}
|
|
||||||
if let Ok(key) = PublicKey::from_str(term) {
|
|
||||||
return self.users.get_key_value(&key);
|
|
||||||
}
|
|
||||||
self.users.iter().find(|(k, v)|
|
|
||||||
// TODO regex word boundary
|
|
||||||
v.name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
|
|
||||||
v.display_name.as_ref().is_some_and(|n| n.to_ascii_lowercase().starts_with(term)) ||
|
|
||||||
(term.len() > 4 && k.to_string().starts_with(term)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_displayname(&self, pubkey: &PublicKey) -> String {
|
|
||||||
self.users.get(pubkey)
|
|
||||||
.and_then(|m| m.display_name.clone().or(m.name.clone()))
|
|
||||||
.unwrap_or_else(|| pubkey.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_username(&self, pubkey: &PublicKey) -> String {
|
|
||||||
self.users.get(pubkey)
|
|
||||||
.and_then(|m| m.name.clone())
|
|
||||||
.unwrap_or_else(|| format!("{:.6}", pubkey.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn insert(&mut self, pubkey: PublicKey, metadata: Metadata) {
|
|
||||||
self.users.insert(pubkey, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn create(&mut self, pubkey: PublicKey) {
|
|
||||||
if !self.users.contains_key(&pubkey) {
|
|
||||||
self.users.insert(pubkey, Default::default());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_user_extract() {
|
|
||||||
let keys = Keys::generate();
|
|
||||||
let mut users = NostrUsers::default();
|
|
||||||
users.insert(keys.public_key, Metadata::new().display_name("Tester Jo"));
|
|
||||||
assert_eq!(crate::kinds::extract_tags("Hello @test", &users),
|
|
||||||
("Hello".to_string(), vec![Tag::public_key(keys.public_key)]));
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue