Merge rewrite deck serialization, timeline cache, add algo timelines #712

William Casarin (19):
      algos: introduce last_n_per_pubkey_from_tags
      wip algo timelines
      Initial token parser combinator
      token_parser: unify parsing and serialization
      token_serializer: introduce TokenWriter
      token_parser: simplify AddColumnRoute serialization
      tokens: add a more advanced tokens parser
      tokens: add AccountsRoute token serializer
      tokens: add PubkeySource and ListKinds token serializer
      tokens: add TimelineRoute token serializer
      tokens: initial Route token serializer
      add tokenator crate
      note_id: add hex helpers for root notes
      tokens: add token serialization for AlgoTimeline
      tokens: add token serialization for TimelineKind
      tokens: switch over to using token serialization
      Switch to unified timeline cache via TimelineKinds
      hashtags: click hashtags to open them
This commit is contained in:
William Casarin
2025-02-05 18:45:22 -08:00
46 changed files with 2256 additions and 2309 deletions

View File

@@ -414,6 +414,12 @@ impl Accounts {
.or_else(|| self.accounts.iter().find_map(|a| a.to_full()))
}
/// Get the selected account's pubkey as bytes. Common operation so
/// we make it a helper here.
pub fn selected_account_pubkey_bytes(&self) -> Option<&[u8; 32]> {
self.get_selected_account().map(|kp| kp.pubkey.bytes())
}
pub fn get_selected_account(&self) -> Option<&UserAccount> {
if let Some(account_index) = self.currently_selected_account {
if let Some(account) = self.get_account(account_index) {

View File

@@ -35,6 +35,9 @@ impl From<String> for Error {
pub enum FilterError {
#[error("empty contact list")]
EmptyContactList,
#[error("filter not ready")]
FilterNotReady,
}
#[derive(Debug, Eq, PartialEq, Copy, Clone, thiserror::Error)]

View File

@@ -1,6 +1,5 @@
use crate::error::{Error, FilterError};
use crate::note::NoteRef;
use crate::Result;
use nostrdb::{Filter, FilterBuilder, Note, Subscription};
use std::collections::HashMap;
use tracing::{debug, warn};
@@ -24,7 +23,7 @@ pub struct FilterStates {
}
impl FilterStates {
pub fn get(&mut self, relay: &str) -> &FilterState {
pub fn get_mut(&mut self, relay: &str) -> &FilterState {
// if our initial state is ready, then just use that
if let FilterState::Ready(_) = self.initial_state {
&self.initial_state
@@ -190,13 +189,67 @@ impl FilteredTags {
}
}
/// Create a "last N notes per pubkey" query.
pub fn last_n_per_pubkey_from_tags(
note: &Note,
kind: u64,
notes_per_pubkey: u64,
) -> Result<Vec<Filter>, Error> {
let mut filters: Vec<Filter> = vec![];
for tag in note.tags() {
// TODO: fix arbitrary MAX_FILTER limit in nostrdb
if filters.len() == 15 {
break;
}
if tag.count() < 2 {
continue;
}
let t = if let Some(t) = tag.get_unchecked(0).variant().str() {
t
} else {
continue;
};
if t == "p" {
let author = if let Some(author) = tag.get_unchecked(1).variant().id() {
author
} else {
continue;
};
let mut filter = Filter::new();
filter.start_authors_field()?;
filter.add_id_element(author)?;
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
} else if t == "t" {
let hashtag = if let Some(hashtag) = tag.get_unchecked(1).variant().str() {
hashtag
} else {
continue;
};
let mut filter = Filter::new();
filter.start_tags_field('t')?;
filter.add_str_element(hashtag)?;
filter.end_field();
filters.push(filter.kinds([kind]).limit(notes_per_pubkey).build());
}
}
Ok(filters)
}
/// Create a filter from tags. This can be used to create a filter
/// from a contact list
pub fn filter_from_tags(
note: &Note,
add_pubkey: Option<&[u8; 32]>,
with_hashtags: bool,
) -> Result<FilteredTags> {
) -> Result<FilteredTags, Error> {
let mut author_filter = Filter::new();
let mut hashtag_filter = Filter::new();
let mut author_res: Option<FilterBuilder> = None;
@@ -284,3 +337,11 @@ pub fn filter_from_tags(
hashtags: hashtag_res,
})
}
pub fn make_filters_since(raw: &[Filter], since: u64) -> Vec<Filter> {
let mut filters = Vec::with_capacity(raw.len());
for builder in raw {
filters.push(Filter::copy_from(builder).since(since).build());
}
filters
}

View File

@@ -3,6 +3,7 @@ use enostr::NoteId;
use nostrdb::{Ndb, Note, NoteKey, QueryResult, Transaction};
use std::borrow::Borrow;
use std::cmp::Ordering;
use std::fmt;
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
pub struct NoteRef {
@@ -10,9 +11,15 @@ pub struct NoteRef {
pub created_at: u64,
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
#[derive(Clone, Copy, Eq, PartialEq, Hash)]
pub struct RootNoteIdBuf([u8; 32]);
impl fmt::Debug for RootNoteIdBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "RootNoteIdBuf({})", self.hex())
}
}
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
pub struct RootNoteId<'a>(&'a [u8; 32]);
@@ -34,6 +41,10 @@ impl RootNoteIdBuf {
root_note_id_from_selected_id(ndb, note_cache, txn, id).map(|rnid| Self(*rnid.bytes()))
}
pub fn hex(&self) -> String {
hex::encode(self.bytes())
}
pub fn new_unsafe(id: [u8; 32]) -> Self {
Self(id)
}
@@ -52,6 +63,10 @@ impl<'a> RootNoteId<'a> {
self.0
}
pub fn hex(&self) -> String {
hex::encode(self.bytes())
}
pub fn to_owned(&self) -> RootNoteIdBuf {
RootNoteIdBuf::new_unsafe(*self.bytes())
}