Compare commits
7 commits
a8fb3f919d
...
a3eeb38e5f
Author | SHA1 | Date | |
---|---|---|---|
|
a3eeb38e5f | ||
|
3a4588b45d | ||
|
6ef5c47e98 | ||
|
87392fccb6 | ||
|
78438696ac | ||
|
654f273ad9 | ||
|
cb15fbaac5 |
5 changed files with 224 additions and 123 deletions
43
src/kinds.rs
43
src/kinds.rs
|
@ -1,7 +1,8 @@
|
|||
use crate::task::MARKER_PARENT;
|
||||
use crate::tasks::nostr_users::NostrUsers;
|
||||
use crate::tasks::HIGH_PRIO;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::{EventBuilder, EventId, Kind, Tag, TagKind, TagStandard};
|
||||
use nostr_sdk::{EventBuilder, EventId, Kind, PublicKey, Tag, TagKind, TagStandard};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub const TASK_KIND: Kind = Kind::GitIssue;
|
||||
|
@ -99,22 +100,27 @@ pub(crate) fn extract_hashtags(input: &str) -> impl Iterator<Item=Tag> + '_ {
|
|||
/// as well as various embedded tags.
|
||||
///
|
||||
/// Expects sanitized input.
|
||||
pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
||||
pub(crate) fn extract_tags(input: &str, users: &NostrUsers) -> (String, Vec<Tag>) {
|
||||
let words = input.split_ascii_whitespace();
|
||||
let mut prio = None;
|
||||
let mut tags = Vec::with_capacity(4);
|
||||
let result = words.filter(|s| {
|
||||
if s.starts_with('*') {
|
||||
if s.len() == 1 {
|
||||
prio = Some(HIGH_PRIO);
|
||||
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;
|
||||
}
|
||||
return match s[1..].parse::<Prio>() {
|
||||
Ok(num) => {
|
||||
prio = Some(num * (if s.len() > 2 { 1 } else { 10 }));
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
} else if s.starts_with('*') {
|
||||
if s.len() == 1 {
|
||||
tags.push(to_prio_tag(HIGH_PRIO));
|
||||
return false;
|
||||
}
|
||||
if let Ok(num) = s[1..].parse::<Prio>() {
|
||||
tags.push(to_prio_tag(num * (if s.len() > 2 { 1 } else { 10 })));
|
||||
return false
|
||||
}
|
||||
}
|
||||
true
|
||||
}).collect_vec();
|
||||
|
@ -122,7 +128,7 @@ pub(crate) fn extract_tags(input: &str) -> (String, Vec<Tag>) {
|
|||
let main = split.next().unwrap().join(" ");
|
||||
let mut tags = extract_hashtags(&main)
|
||||
.chain(split.flatten().map(|s| to_hashtag_tag(&s)))
|
||||
.chain(prio.map(|p| to_prio_tag(p)))
|
||||
.chain(tags)
|
||||
.collect_vec();
|
||||
tags.sort();
|
||||
tags.dedup();
|
||||
|
@ -161,10 +167,11 @@ pub fn to_prio_tag(value: Prio) -> Tag {
|
|||
|
||||
#[test]
|
||||
fn test_extract_tags() {
|
||||
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it"),
|
||||
assert_eq!(extract_tags("Hello from #mars with #greetings #yeah *4 # # yeah done-it", &Default::default()),
|
||||
("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_tag)).collect()));
|
||||
assert_eq!(extract_tags("So tagless #"),
|
||||
("So tagless".to_string(), vec![]));
|
||||
.chain(["done-it", "greetings", "mars", "yeah"].into_iter().map(to_hashtag_tag))
|
||||
.collect()));
|
||||
assert_eq!(extract_tags("So tagless @hewo #", &Default::default()),
|
||||
("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::kinds::{format_tag_basic, match_event_tag, Prio, BASIC_KINDS, PROPERTY_COLUMNS, PROP_KINDS};
|
||||
use crate::task::{State, Task, TaskState, MARKER_PROPERTY};
|
||||
use crate::tasks::{PropertyCollection, StateFilter, TasksRelay};
|
||||
use crate::tasks::{referenced_event, PropertyCollection, StateFilter, TasksRelay};
|
||||
use chrono::Local;
|
||||
use colored::Colorize;
|
||||
use directories::ProjectDirs;
|
||||
|
@ -438,14 +438,36 @@ async fn main() -> Result<()> {
|
|||
Some('&') => {
|
||||
match arg {
|
||||
None => tasks.undo(),
|
||||
Some(text) => match text.parse::<u8>() {
|
||||
Ok(int) => {
|
||||
tasks.move_back_by(int as usize);
|
||||
Some(text) => {
|
||||
if text == "&" {
|
||||
println!(
|
||||
"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;
|
||||
}
|
||||
_ => {
|
||||
if !tasks.move_back_to(text) {
|
||||
warn!("Did not find a match in history for \"{text}\"");
|
||||
continue 'repl;
|
||||
match text.parse::<u8>() {
|
||||
Ok(int) => {
|
||||
tasks.move_back_by(int as usize);
|
||||
}
|
||||
_ => {
|
||||
if !tasks.move_back_to(text) {
|
||||
warn!("Did not find a match in history for \"{text}\"");
|
||||
continue 'repl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -464,7 +486,7 @@ async fn main() -> Result<()> {
|
|||
Some(arg) => {
|
||||
if arg == "@" {
|
||||
tasks.reset_key_filter()
|
||||
} else if let Some((key, name)) = tasks.find_user_with_displayname(arg) {
|
||||
} else if let Some((key, name)) = tasks.find_user(arg) {
|
||||
info!("Showing {}'s tasks", name);
|
||||
tasks.set_key_filter(key)
|
||||
} else {
|
||||
|
|
21
src/task.rs
21
src/task.rs
|
@ -4,13 +4,14 @@ use std::collections::BTreeSet;
|
|||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::once;
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
use colored::{ColoredString, Colorize};
|
||||
use itertools::Either::{Left, Right};
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nostr_sdk::{Alphabet, Event, EventId, Kind, Tag, Timestamp};
|
||||
use nostr_sdk::{Alphabet, Event, EventId, Kind, PublicKey, SingleLetterTag, Tag, TagKind, Timestamp};
|
||||
use crate::hashtag::{is_hashtag, Hashtag};
|
||||
use crate::helpers::{format_timestamp_local, some_non_empty};
|
||||
use crate::kinds::{match_event_tag, Prio, PRIO, PROCEDURE_KIND, PROCEDURE_KIND_ID, TASK_KIND};
|
||||
|
@ -70,6 +71,15 @@ impl Task {
|
|||
&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> {
|
||||
self.refs.iter().filter_map(move |(str, id)| Some(id).filter(|_| str == marker))
|
||||
}
|
||||
|
@ -184,11 +194,12 @@ impl Task {
|
|||
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> {
|
||||
self.tags.iter().flatten().chain(
|
||||
self.props.iter().flat_map(|e| e.tags.iter()
|
||||
.filter(|t| t.single_letter_tag().is_none_or(|s| s.character != Alphabet::E)))
|
||||
)
|
||||
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
|
||||
|
|
180
src/tasks.rs
180
src/tasks.rs
|
@ -1,3 +1,5 @@
|
|||
pub(crate) mod nostr_users;
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::iter::{empty, once, FusedIterator};
|
||||
|
@ -5,11 +7,12 @@ use std::ops::{Div, Rem};
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::hashtag::Hashtag;
|
||||
use crate::event_sender::{EventSender, MostrMessage};
|
||||
use crate::hashtag::Hashtag;
|
||||
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::task::{State, Task, TaskState, MARKER_DEPENDS, MARKER_PARENT, MARKER_PROPERTY};
|
||||
use crate::tasks::nostr_users::NostrUsers;
|
||||
use colored::Colorize;
|
||||
use itertools::Itertools;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
@ -54,7 +57,7 @@ pub(crate) struct TasksRelay {
|
|||
/// History of active tasks by PubKey
|
||||
history: HashMap<PublicKey, BTreeMap<Timestamp, Event>>,
|
||||
/// Index of known users with metadata
|
||||
users: HashMap<PublicKey, Metadata>,
|
||||
users: NostrUsers,
|
||||
/// Own pinned tasks
|
||||
bookmarks: Vec<EventId>,
|
||||
|
||||
|
@ -156,7 +159,7 @@ impl TasksRelay {
|
|||
bookmarks: Default::default(),
|
||||
|
||||
properties: [
|
||||
"author",
|
||||
"owner",
|
||||
"prio",
|
||||
"state",
|
||||
"rtime",
|
||||
|
@ -167,7 +170,7 @@ impl TasksRelay {
|
|||
sorting: [
|
||||
"priority",
|
||||
"status",
|
||||
"author",
|
||||
"owner",
|
||||
"hashtags",
|
||||
"rtime",
|
||||
"name",
|
||||
|
@ -253,51 +256,60 @@ impl TasksRelay {
|
|||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
key: &PublicKey,
|
||||
) -> (String, Box<dyn DoubleEndedIterator<Item=String>>) {
|
||||
) -> (String, Box<dyn DoubleEndedIterator<Item=String> + '_>) {
|
||||
match self.get_position() {
|
||||
None => {
|
||||
if let Some(hist) = self.history.get(key) {
|
||||
let mut last = None;
|
||||
let mut full = Vec::with_capacity(hist.len());
|
||||
for event in hist.values() {
|
||||
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 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()),
|
||||
)
|
||||
match self.history_for(key) {
|
||||
Some(hist) =>
|
||||
(
|
||||
format!("Time-Tracking History for {}:", self.users.get_displayname(&key)),
|
||||
Box::from(hist),
|
||||
),
|
||||
None =>
|
||||
(
|
||||
"Nothing time-tracked yet".to_string(),
|
||||
Box::from(empty()),
|
||||
)
|
||||
}
|
||||
}
|
||||
Some(id) => {
|
||||
// TODO show current recursive with pubkey
|
||||
let ids = [id];
|
||||
let history =
|
||||
let mut history =
|
||||
self.history.iter().flat_map(|(key, set)| {
|
||||
let mut vec = Vec::with_capacity(set.len() / 2);
|
||||
let mut iter = timestamps(set.values(), &ids).tuples();
|
||||
|
@ -308,7 +320,7 @@ impl TasksRelay {
|
|||
"{} - {} by {}",
|
||||
format_timestamp_local(start),
|
||||
format_timestamp_relative_to(end, start),
|
||||
self.get_displayname(key)
|
||||
self.users.get_displayname(key)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -316,14 +328,17 @@ impl TasksRelay {
|
|||
vec.push(format!(
|
||||
"{} started by {}",
|
||||
format_timestamp_local(stamp),
|
||||
self.get_displayname(key)
|
||||
self.users.get_displayname(key)
|
||||
))
|
||||
});
|
||||
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)),
|
||||
Box::from(history),
|
||||
Box::from(history.into_iter()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +408,7 @@ impl TasksRelay {
|
|||
Some(key) =>
|
||||
if key != self.sender.pubkey() {
|
||||
prompt.push_str(" @");
|
||||
prompt.push_str(&self.get_username(&key))
|
||||
prompt.push_str(&self.users.get_username(&key))
|
||||
},
|
||||
}
|
||||
for tag in self.tags.iter() {
|
||||
|
@ -494,7 +509,7 @@ impl TasksRelay {
|
|||
|
||||
fn filter(&self, task: &Task) -> bool {
|
||||
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| {
|
||||
task.priority().unwrap_or(DEFAULT_PRIO) >= prio
|
||||
}) &&
|
||||
|
@ -646,7 +661,8 @@ impl TasksRelay {
|
|||
}
|
||||
"progress" => prog_string.clone(),
|
||||
|
||||
"author" | "creator" => format!("{:.6}", self.get_username(&task.event.pubkey)), // FIXME temporary until proper column alignment
|
||||
"owner" => format!("{:.6}", self.users.get_username(&task.get_owner())),
|
||||
"author" | "creator" => format!("{:.6}", self.users.get_username(&task.event.pubkey)), // FIXME temporary until proper column alignment
|
||||
"prio" => self
|
||||
.traverse_up_from(Some(task.event.id))
|
||||
.find_map(Task::priority_raw)
|
||||
|
@ -667,35 +683,8 @@ impl TasksRelay {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_user_with_displayname(&self, term: &str) -> Option<(PublicKey, String)> {
|
||||
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()))
|
||||
pub(super) fn find_user(&self, name: &str) -> Option<(PublicKey, String)> {
|
||||
self.users.find_user_with_displayname(name)
|
||||
}
|
||||
|
||||
// Movement and Selection
|
||||
|
@ -870,8 +859,8 @@ impl TasksRelay {
|
|||
return vec![id];
|
||||
}
|
||||
let lowercase_arg = arg.to_ascii_lowercase();
|
||||
// TODO apply regex to all matching
|
||||
let regex = Regex::new(&format!(r"\b{}", lowercase_arg)).unwrap();
|
||||
// TODO apply regex to all matching, parse as plain match
|
||||
let regex = Regex::new(&format!(r"\b{}", lowercase_arg));
|
||||
|
||||
let mut filtered: Vec<EventId> = Vec::with_capacity(32);
|
||||
let mut filtered_fuzzy: Vec<EventId> = Vec::with_capacity(32);
|
||||
|
@ -882,7 +871,9 @@ impl TasksRelay {
|
|||
return vec![task.event.id];
|
||||
} else if content.starts_with(arg) {
|
||||
filtered.push(task.event.id)
|
||||
} else if regex.is_match(lowercase.as_bytes()) {
|
||||
} else if regex.as_ref()
|
||||
.map(|r| r.is_match(lowercase.as_bytes()))
|
||||
.unwrap_or_else(|_| lowercase.starts_with(&lowercase_arg)) {
|
||||
filtered_fuzzy.push(task.event.id)
|
||||
}
|
||||
}
|
||||
|
@ -951,6 +942,11 @@ impl TasksRelay {
|
|||
}
|
||||
|
||||
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();
|
||||
let pos = self.get_position();
|
||||
if target == pos {
|
||||
|
@ -1065,7 +1061,7 @@ impl TasksRelay {
|
|||
///
|
||||
/// Sanitizes input
|
||||
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());
|
||||
let (input, input_tags) = extract_tags(input.trim(), &self.users);
|
||||
let prio =
|
||||
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));
|
||||
|
@ -1156,9 +1152,7 @@ impl TasksRelay {
|
|||
|
||||
pub(crate) fn add(&mut self, event: Event) {
|
||||
let author = event.pubkey;
|
||||
if !self.users.contains_key(&author) {
|
||||
self.users.insert(author, Metadata::new());
|
||||
}
|
||||
self.users.create(author);
|
||||
match event.kind {
|
||||
Kind::GitIssue => self.add_task(event),
|
||||
Kind::Metadata => match Metadata::from_json(event.content.as_str()) {
|
||||
|
@ -1221,25 +1215,29 @@ impl TasksRelay {
|
|||
}
|
||||
|
||||
fn get_own_events_history(&self) -> impl DoubleEndedIterator<Item=&Event> + '_ {
|
||||
self.history.get(&self.sender.pubkey())
|
||||
self.get_own_history()
|
||||
.into_iter()
|
||||
.flat_map(|t| t.values())
|
||||
}
|
||||
|
||||
fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
||||
pub(super) fn history_before_now(&self) -> impl Iterator<Item=&Event> {
|
||||
self.get_own_history().into_iter().flat_map(|hist| {
|
||||
let now = now();
|
||||
hist.values().rev().skip_while(move |e| e.created_at > now)
|
||||
hist.values().rev()
|
||||
.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 {
|
||||
let lower = str.to_ascii_lowercase();
|
||||
let found = self.history_before_now().find(|e| {
|
||||
referenced_event(e)
|
||||
.and_then(|id| self.get_by_id(&id))
|
||||
.is_some_and(|t| t.event.content.to_ascii_lowercase().contains(&lower))
|
||||
});
|
||||
let found =
|
||||
self.history_before_now()
|
||||
.find(|e| {
|
||||
referenced_event(e)
|
||||
.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 {
|
||||
self.move_to(referenced_event(event));
|
||||
return true;
|
||||
|
@ -1287,7 +1285,7 @@ impl TasksRelay {
|
|||
} else {
|
||||
vec![id]
|
||||
};
|
||||
let (desc, tags) = extract_tags(comment);
|
||||
let (desc, tags) = extract_tags(comment, &self.users);
|
||||
let prop =
|
||||
EventBuilder::new(state.into(), desc)
|
||||
.tags(ids.into_iter()
|
||||
|
@ -1315,7 +1313,7 @@ impl TasksRelay {
|
|||
/// Creates a note or activity, depending on whether the parent is a task.
|
||||
/// Sanitizes Input.
|
||||
pub(crate) fn make_note(&mut self, note: &str) -> EventId {
|
||||
let (name, tags) = extract_tags(note.trim());
|
||||
let (name, tags) = extract_tags(note.trim(), &self.users);
|
||||
let format = format!("\"{name}\" with tags [{}]", join_tags(&tags));
|
||||
let mut prop =
|
||||
EventBuilder::new(Kind::TextNote, name)
|
||||
|
@ -1527,7 +1525,7 @@ fn referenced_events(event: &Event) -> impl Iterator<Item=EventId> + '_ {
|
|||
event.tags.iter().filter_map(|tag| match_event_tag(tag).map(|t| t.id))
|
||||
}
|
||||
|
||||
fn referenced_event(event: &Event) -> Option<EventId> {
|
||||
pub fn referenced_event(event: &Event) -> Option<EventId> {
|
||||
referenced_events(event).next()
|
||||
}
|
||||
|
||||
|
@ -1828,7 +1826,7 @@ mod tasks_test {
|
|||
|
||||
tasks.custom_time = Some(Timestamp::now());
|
||||
tasks.update_state("Finished #YeaH # oi", State::Done);
|
||||
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["tag1", "YeaH", "oi", "tag3", "yeah"].map(Hashtag::from));
|
||||
assert_eq!(tasks.get_by_id(&parent).unwrap().list_hashtags().collect_vec(), ["YeaH", "oi", "tag3", "yeah", "tag1"].map(Hashtag::from));
|
||||
assert_eq!(tasks.all_hashtags(), all_tags);
|
||||
|
||||
tasks.custom_time = Some(now());
|
||||
|
|
63
src/tasks/nostr_users.rs
Normal file
63
src/tasks/nostr_users.rs
Normal file
|
@ -0,0 +1,63 @@
|
|||
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