@@ -2,10 +2,15 @@ use crate::{
|
|||||||
column::Columns,
|
column::Columns,
|
||||||
nav::{RouterAction, RouterType},
|
nav::{RouterAction, RouterType},
|
||||||
route::Route,
|
route::Route,
|
||||||
timeline::{ThreadSelection, TimelineCache, TimelineKind},
|
timeline::{
|
||||||
|
thread::{
|
||||||
|
selected_has_at_least_n_replies, InsertionResponse, NoteSeenFlags, ThreadNode, Threads,
|
||||||
|
},
|
||||||
|
ThreadSelection, TimelineCache, TimelineKind,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use enostr::{Pubkey, RelayPool};
|
use enostr::{NoteId, Pubkey, RelayPool};
|
||||||
use nostrdb::{Ndb, NoteKey, Transaction};
|
use nostrdb::{Ndb, NoteKey, Transaction};
|
||||||
use notedeck::{
|
use notedeck::{
|
||||||
get_wallet_for_mut, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction,
|
get_wallet_for_mut, note::ZapTargetAmount, Accounts, GlobalWallet, Images, NoteAction,
|
||||||
@@ -252,3 +257,103 @@ impl NewNotes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct NewThreadNotes {
|
||||||
|
pub selected_note_id: NoteId,
|
||||||
|
pub notes: Vec<NoteKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewThreadNotes {
|
||||||
|
pub fn process(
|
||||||
|
&self,
|
||||||
|
threads: &mut Threads,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
|
note_cache: &mut NoteCache,
|
||||||
|
) {
|
||||||
|
let Some(node) = threads.threads.get_mut(&self.selected_note_id.bytes()) else {
|
||||||
|
tracing::error!("Could not find thread node for {:?}", self.selected_note_id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
process_thread_notes(
|
||||||
|
&self.notes,
|
||||||
|
node,
|
||||||
|
&mut threads.seen_flags,
|
||||||
|
ndb,
|
||||||
|
txn,
|
||||||
|
unknown_ids,
|
||||||
|
note_cache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_thread_notes(
|
||||||
|
notes: &Vec<NoteKey>,
|
||||||
|
thread: &mut ThreadNode,
|
||||||
|
seen_flags: &mut NoteSeenFlags,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
|
note_cache: &mut NoteCache,
|
||||||
|
) {
|
||||||
|
if notes.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut has_spliced_resp = false;
|
||||||
|
let mut num_new_notes = 0;
|
||||||
|
for key in notes {
|
||||||
|
let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) {
|
||||||
|
note
|
||||||
|
} else {
|
||||||
|
tracing::error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that unknown ids are captured when inserting notes
|
||||||
|
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
|
||||||
|
|
||||||
|
let created_at = note.created_at();
|
||||||
|
let note_ref = notedeck::NoteRef {
|
||||||
|
key: *key,
|
||||||
|
created_at,
|
||||||
|
};
|
||||||
|
|
||||||
|
if thread.replies.contains(¬e_ref) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertion_resp = thread.replies.insert(note_ref);
|
||||||
|
if let InsertionResponse::Merged(crate::timeline::MergeKind::Spliced) = insertion_resp {
|
||||||
|
has_spliced_resp = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(insertion_resp, InsertionResponse::Merged(_)) {
|
||||||
|
num_new_notes += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !seen_flags.contains(note.id()) {
|
||||||
|
let cached_note = note_cache.cached_note_or_insert_mut(*key, ¬e);
|
||||||
|
|
||||||
|
let note_reply = cached_note.reply.borrow(note.tags());
|
||||||
|
|
||||||
|
let has_reply = if let Some(root) = note_reply.root() {
|
||||||
|
selected_has_at_least_n_replies(ndb, txn, Some(note.id()), root.id, 1)
|
||||||
|
} else {
|
||||||
|
selected_has_at_least_n_replies(ndb, txn, None, note.id(), 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
seen_flags.mark_replies(note.id(), has_reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if has_spliced_resp {
|
||||||
|
tracing::debug!(
|
||||||
|
"spliced when inserting {} new notes, resetting virtual list",
|
||||||
|
num_new_notes
|
||||||
|
);
|
||||||
|
thread.list.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use tracing::{debug, error, info, warn};
|
|||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod kind;
|
pub mod kind;
|
||||||
pub mod route;
|
pub mod route;
|
||||||
|
pub mod thread;
|
||||||
|
|
||||||
pub use cache::TimelineCache;
|
pub use cache::TimelineCache;
|
||||||
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
||||||
|
|||||||
528
crates/notedeck_columns/src/timeline/thread.rs
Normal file
528
crates/notedeck_columns/src/timeline/thread.rs
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{BTreeSet, HashSet},
|
||||||
|
hash::Hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
use egui_nav::ReturnType;
|
||||||
|
use egui_virtual_list::VirtualList;
|
||||||
|
use enostr::{NoteId, RelayPool};
|
||||||
|
use hashbrown::{hash_map::RawEntryMut, HashMap};
|
||||||
|
use nostrdb::{Filter, Ndb, Note, NoteKey, NoteReplyBuf, Transaction};
|
||||||
|
use notedeck::{NoteCache, NoteRef, UnknownIds};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actionbar::{process_thread_notes, NewThreadNotes},
|
||||||
|
multi_subscriber::ThreadSubs,
|
||||||
|
timeline::MergeKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ThreadSelection;
|
||||||
|
|
||||||
|
pub struct ThreadNode {
|
||||||
|
pub replies: HybridSet<NoteRef>,
|
||||||
|
pub prev: ParentState,
|
||||||
|
pub have_all_ancestors: bool,
|
||||||
|
pub list: VirtualList,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ParentState {
|
||||||
|
Unknown,
|
||||||
|
None,
|
||||||
|
Parent(NoteId),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affords:
|
||||||
|
/// - O(1) contains
|
||||||
|
/// - O(log n) sorted insertion
|
||||||
|
pub struct HybridSet<T> {
|
||||||
|
reversed: bool,
|
||||||
|
lookup: HashSet<T>, // fast deduplication
|
||||||
|
ordered: BTreeSet<T>, // sorted iteration
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Default for HybridSet<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
reversed: Default::default(),
|
||||||
|
lookup: Default::default(),
|
||||||
|
ordered: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum InsertionResponse {
|
||||||
|
AlreadyExists,
|
||||||
|
Merged(MergeKind),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Copy + Ord + Eq + Hash> HybridSet<T> {
|
||||||
|
pub fn insert(&mut self, val: T) -> InsertionResponse {
|
||||||
|
if !self.lookup.insert(val) {
|
||||||
|
return InsertionResponse::AlreadyExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
let front_insertion = match self.ordered.iter().next() {
|
||||||
|
Some(first) => (val >= *first) == self.reversed,
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.ordered.insert(val); // O(log n)
|
||||||
|
|
||||||
|
InsertionResponse::Merged(if front_insertion {
|
||||||
|
MergeKind::FrontInsert
|
||||||
|
} else {
|
||||||
|
MergeKind::Spliced
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Eq + Hash> HybridSet<T> {
|
||||||
|
pub fn contains(&self, val: &T) -> bool {
|
||||||
|
self.lookup.contains(val) // O(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> HybridSet<T> {
|
||||||
|
pub fn iter(&self) -> HybridIter<'_, T> {
|
||||||
|
HybridIter {
|
||||||
|
inner: self.ordered.iter(),
|
||||||
|
reversed: self.reversed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(reversed: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
reversed,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> IntoIterator for &'a HybridSet<T> {
|
||||||
|
type Item = &'a T;
|
||||||
|
type IntoIter = HybridIter<'a, T>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HybridIter<'a, T> {
|
||||||
|
inner: std::collections::btree_set::Iter<'a, T>,
|
||||||
|
reversed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Iterator for HybridIter<'a, T> {
|
||||||
|
type Item = &'a T;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.reversed {
|
||||||
|
self.inner.next_back()
|
||||||
|
} else {
|
||||||
|
self.inner.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadNode {
|
||||||
|
pub fn new(parent: ParentState) -> Self {
|
||||||
|
Self {
|
||||||
|
replies: HybridSet::new(true),
|
||||||
|
prev: parent,
|
||||||
|
have_all_ancestors: false,
|
||||||
|
list: VirtualList::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Threads {
|
||||||
|
pub threads: HashMap<NoteId, ThreadNode>,
|
||||||
|
pub subs: ThreadSubs,
|
||||||
|
|
||||||
|
pub seen_flags: NoteSeenFlags,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Threads {
|
||||||
|
/// Opening a thread.
|
||||||
|
/// Similar to [[super::cache::TimelineCache::open]]
|
||||||
|
pub fn open(
|
||||||
|
&mut self,
|
||||||
|
ndb: &mut Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
pool: &mut RelayPool,
|
||||||
|
thread: &ThreadSelection,
|
||||||
|
new_scope: bool,
|
||||||
|
col: usize,
|
||||||
|
) -> Option<NewThreadNotes> {
|
||||||
|
tracing::info!("Opening thread: {:?}", thread);
|
||||||
|
let local_sub_filter = if let Some(selected) = &thread.selected_note {
|
||||||
|
vec![direct_replies_filter_non_root(
|
||||||
|
selected.bytes(),
|
||||||
|
thread.root_id.bytes(),
|
||||||
|
)]
|
||||||
|
} else {
|
||||||
|
vec![direct_replies_filter_root(thread.root_id.bytes())]
|
||||||
|
};
|
||||||
|
|
||||||
|
let selected_note_id = thread.selected_or_root();
|
||||||
|
self.seen_flags.mark_seen(selected_note_id);
|
||||||
|
|
||||||
|
let filter = match self.threads.raw_entry_mut().from_key(&selected_note_id) {
|
||||||
|
RawEntryMut::Occupied(_entry) => {
|
||||||
|
// TODO(kernelkind): reenable this once the panic is fixed
|
||||||
|
//
|
||||||
|
// let node = entry.into_mut();
|
||||||
|
// if let Some(first) = node.replies.first() {
|
||||||
|
// &filter::make_filters_since(&local_sub_filter, first.created_at + 1)
|
||||||
|
// } else {
|
||||||
|
// &local_sub_filter
|
||||||
|
// }
|
||||||
|
&local_sub_filter
|
||||||
|
}
|
||||||
|
RawEntryMut::Vacant(entry) => {
|
||||||
|
let id = NoteId::new(*selected_note_id);
|
||||||
|
|
||||||
|
let node = ThreadNode::new(ParentState::Unknown);
|
||||||
|
entry.insert(id, node);
|
||||||
|
|
||||||
|
&local_sub_filter
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_notes = ndb.query(txn, filter, 500).ok().map(|r| {
|
||||||
|
r.into_iter()
|
||||||
|
.map(NoteRef::from_query_result)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
self.subs
|
||||||
|
.subscribe(ndb, pool, col, thread, local_sub_filter, new_scope, || {
|
||||||
|
replies_filter_remote(thread)
|
||||||
|
});
|
||||||
|
|
||||||
|
new_notes.map(|notes| NewThreadNotes {
|
||||||
|
selected_note_id: NoteId::new(*selected_note_id),
|
||||||
|
notes: notes.into_iter().map(|f| f.key).collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(
|
||||||
|
&mut self,
|
||||||
|
ndb: &mut Ndb,
|
||||||
|
pool: &mut RelayPool,
|
||||||
|
thread: &ThreadSelection,
|
||||||
|
return_type: ReturnType,
|
||||||
|
id: usize,
|
||||||
|
) {
|
||||||
|
tracing::info!("Closing thread: {:?}", thread);
|
||||||
|
self.subs.unsubscribe(ndb, pool, id, thread, return_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responsible for making sure the chain and the direct replies are up to date
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
selected: &Note<'_>,
|
||||||
|
note_cache: &mut NoteCache,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
|
col: usize,
|
||||||
|
) {
|
||||||
|
let Some(selected_key) = selected.key() else {
|
||||||
|
tracing::error!("Selected note did not have a key");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let reply = note_cache
|
||||||
|
.cached_note_or_insert_mut(selected_key, selected)
|
||||||
|
.reply;
|
||||||
|
|
||||||
|
self.fill_reply_chain_recursive(selected, &reply, note_cache, ndb, txn, unknown_ids);
|
||||||
|
let node = self
|
||||||
|
.threads
|
||||||
|
.get_mut(&selected.id())
|
||||||
|
.expect("should be guarenteed to exist from `Self::fill_reply_chain_recursive`");
|
||||||
|
|
||||||
|
let Some(sub) = self.subs.get_local(col) else {
|
||||||
|
tracing::error!("Was expecting to find local sub");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let keys = ndb.poll_for_notes(sub.sub, 10);
|
||||||
|
|
||||||
|
if keys.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("Got {} new notes", keys.len());
|
||||||
|
|
||||||
|
process_thread_notes(
|
||||||
|
&keys,
|
||||||
|
node,
|
||||||
|
&mut self.seen_flags,
|
||||||
|
ndb,
|
||||||
|
txn,
|
||||||
|
unknown_ids,
|
||||||
|
note_cache,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fill_reply_chain_recursive(
|
||||||
|
&mut self,
|
||||||
|
cur_note: &Note<'_>,
|
||||||
|
cur_reply: &NoteReplyBuf,
|
||||||
|
note_cache: &mut NoteCache,
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
unknown_ids: &mut UnknownIds,
|
||||||
|
) -> bool {
|
||||||
|
let (unknown_parent_state, mut have_all_ancestors) = self
|
||||||
|
.threads
|
||||||
|
.get(&cur_note.id())
|
||||||
|
.map(|t| (matches!(t.prev, ParentState::Unknown), t.have_all_ancestors))
|
||||||
|
.unwrap_or((true, false));
|
||||||
|
|
||||||
|
if have_all_ancestors {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_parent = None;
|
||||||
|
|
||||||
|
let note_reply = cur_reply.borrow(cur_note.tags());
|
||||||
|
|
||||||
|
let next_link = 's: {
|
||||||
|
let Some(parent) = note_reply.reply() else {
|
||||||
|
break 's NextLink::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if unknown_parent_state {
|
||||||
|
new_parent = Some(ParentState::Parent(NoteId::new(*parent.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok(reply_note) = ndb.get_note_by_id(txn, parent.id) else {
|
||||||
|
break 's NextLink::Unknown(parent.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(notekey) = reply_note.key() else {
|
||||||
|
break 's NextLink::Unknown(parent.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
NextLink::Next(reply_note, notekey)
|
||||||
|
};
|
||||||
|
|
||||||
|
match next_link {
|
||||||
|
NextLink::Unknown(parent) => {
|
||||||
|
unknown_ids.add_note_id_if_missing(ndb, txn, parent);
|
||||||
|
}
|
||||||
|
NextLink::Next(next_note, note_key) => {
|
||||||
|
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &next_note);
|
||||||
|
|
||||||
|
let cached_note = note_cache.cached_note_or_insert_mut(note_key, &next_note);
|
||||||
|
|
||||||
|
let next_reply = cached_note.reply;
|
||||||
|
if self.fill_reply_chain_recursive(
|
||||||
|
&next_note,
|
||||||
|
&next_reply,
|
||||||
|
note_cache,
|
||||||
|
ndb,
|
||||||
|
txn,
|
||||||
|
unknown_ids,
|
||||||
|
) {
|
||||||
|
have_all_ancestors = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.seen_flags.contains(next_note.id()) {
|
||||||
|
self.seen_flags.mark_replies(
|
||||||
|
next_note.id(),
|
||||||
|
selected_has_at_least_n_replies(ndb, txn, None, next_note.id(), 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NextLink::None => {
|
||||||
|
have_all_ancestors = true;
|
||||||
|
new_parent = Some(ParentState::None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.threads.raw_entry_mut().from_key(&cur_note.id()) {
|
||||||
|
RawEntryMut::Occupied(entry) => {
|
||||||
|
let node = entry.into_mut();
|
||||||
|
if let Some(parent) = new_parent {
|
||||||
|
node.prev = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if have_all_ancestors {
|
||||||
|
node.have_all_ancestors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawEntryMut::Vacant(entry) => {
|
||||||
|
let id = NoteId::new(*cur_note.id());
|
||||||
|
let parent = new_parent.unwrap_or(ParentState::Unknown);
|
||||||
|
let (_, res) = entry.insert(id, ThreadNode::new(parent));
|
||||||
|
|
||||||
|
if have_all_ancestors {
|
||||||
|
res.have_all_ancestors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
have_all_ancestors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NextLink<'a> {
|
||||||
|
Unknown(&'a [u8; 32]),
|
||||||
|
Next(Note<'a>, NoteKey),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_has_at_least_n_replies(
|
||||||
|
ndb: &Ndb,
|
||||||
|
txn: &Transaction,
|
||||||
|
selected: Option<&[u8; 32]>,
|
||||||
|
root: &[u8; 32],
|
||||||
|
n: u8,
|
||||||
|
) -> bool {
|
||||||
|
let filter = if let Some(selected) = selected {
|
||||||
|
&vec![direct_replies_filter_non_root(selected, root)]
|
||||||
|
} else {
|
||||||
|
&vec![direct_replies_filter_root(root)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(res) = ndb.query(txn, filter, n as i32) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
res.len() >= n.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn direct_replies_filter_non_root(
|
||||||
|
selected_note_id: &[u8; 32],
|
||||||
|
root_id: &[u8; 32],
|
||||||
|
) -> nostrdb::Filter {
|
||||||
|
let tmp_selected = *selected_note_id;
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.custom(move |n: nostrdb::Note<'_>| {
|
||||||
|
for tag in n.tags() {
|
||||||
|
if tag.count() < 4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some("e") = tag.get_str(0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(tagged_id) = tag.get_id(1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if *tagged_id != tmp_selected {
|
||||||
|
// NOTE: if these aren't dereferenced a segfault occurs...
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(data) = tag.get_str(3) {
|
||||||
|
if data == "reply" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.event(root_id)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom filter requirements:
|
||||||
|
/// - Do NOT capture references (e.g. `*root_id`) inside the closure
|
||||||
|
/// - Instead, copy values outside and capture them with `move`
|
||||||
|
///
|
||||||
|
/// Incorrect:
|
||||||
|
/// .custom(|_| { *root_id }) // ❌
|
||||||
|
/// Also Incorrect:
|
||||||
|
/// .custom(move |_| { *root_id }) // ❌
|
||||||
|
/// Correct:
|
||||||
|
/// let tmp = *root_id;
|
||||||
|
/// .custom(move |_| { tmp }) // ✅
|
||||||
|
fn direct_replies_filter_root(root_id: &[u8; 32]) -> nostrdb::Filter {
|
||||||
|
let tmp_root = *root_id;
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.custom(move |n: nostrdb::Note<'_>| {
|
||||||
|
let mut contains_root = false;
|
||||||
|
for tag in n.tags() {
|
||||||
|
if tag.count() < 4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some("e") = tag.get_str(0) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(s) = tag.get_str(3) {
|
||||||
|
if s == "reply" {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(tagged_id) = tag.get_id(1) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if *tagged_id != tmp_root {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = tag.get_str(3) {
|
||||||
|
if s == "root" {
|
||||||
|
contains_root = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contains_root
|
||||||
|
})
|
||||||
|
.event(root_id)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replies_filter_remote(selection: &ThreadSelection) -> Vec<Filter> {
|
||||||
|
vec![
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.kinds([1])
|
||||||
|
.event(selection.root_id.bytes())
|
||||||
|
.build(),
|
||||||
|
nostrdb::Filter::new()
|
||||||
|
.ids([selection.root_id.bytes()])
|
||||||
|
.limit(1)
|
||||||
|
.build(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents indicators that there is more content in the note to view
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct NoteSeenFlags {
|
||||||
|
// true indicates the note has replies AND it has not been read
|
||||||
|
pub flags: HashMap<NoteId, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteSeenFlags {
|
||||||
|
pub fn mark_seen(&mut self, note_id: &[u8; 32]) {
|
||||||
|
self.flags.insert(NoteId::new(*note_id), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_replies(&mut self, note_id: &[u8; 32], has_replies: bool) {
|
||||||
|
self.flags.insert(NoteId::new(*note_id), has_replies);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, note_id: &[u8; 32]) -> Option<&bool> {
|
||||||
|
self.flags.get(¬e_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, note_id: &[u8; 32]) -> bool {
|
||||||
|
self.flags.contains_key(¬e_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user