notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 9b784dfdf741c11275c7ca2119c3a683b4244837
parent c1d6c0f535116d02f486b505d2672fd4d6211194
Author: kernelkind <kernelkind@gmail.com>
Date:   Sun, 24 Aug 2025 23:39:51 -0400

add `TimelineUnits`

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_columns/src/timeline/mod.rs | 1+
Acrates/notedeck_columns/src/timeline/timeline_units.rs | 179+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 180 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -30,6 +30,7 @@ pub mod kind; mod note_units; pub mod route; pub mod thread; +mod timeline_units; mod unit; pub use cache::TimelineCache; diff --git a/crates/notedeck_columns/src/timeline/timeline_units.rs b/crates/notedeck_columns/src/timeline/timeline_units.rs @@ -0,0 +1,179 @@ +use std::collections::HashSet; + +use enostr::Pubkey; +use nostrdb::{Ndb, Note, NoteKey, Transaction}; +use notedeck::NoteRef; + +use crate::timeline::{ + note_units::{InsertManyResponse, NoteUnits}, + unit::{CompositeFragment, NoteUnit, NoteUnitFragment, Reaction, ReactionFragment}, +}; + +#[derive(Debug, Default)] +pub struct TimelineUnits { + pub units: NoteUnits, +} + +impl TimelineUnits { + pub fn with_capacity(cap: usize) -> Self { + Self { + units: NoteUnits::new_with_cap(cap, false), + } + } + + pub fn from_refs_single(refs: Vec<NoteRef>) -> Self { + let mut entries = TimelineUnits::default(); + refs.into_iter().for_each(|r| entries.merge_single_note(r)); + entries + } + + pub fn len(&self) -> usize { + self.units.len() + } + + pub fn is_empty(&self) -> bool { + self.units.len() == 0 + } + + /// returns number of new entries merged + pub fn merge_new_notes<'a>( + &mut self, + payloads: Vec<&'a NotePayload>, + ndb: &Ndb, + txn: &Transaction, + ) -> MergeResponse<'a> { + let mut unknown_pks = HashSet::with_capacity(payloads.len()); + let new_fragments = payloads + .into_iter() + .filter_map(|p| to_fragment(p, ndb, txn)) + .map(|f| { + if let Some(pk) = f.unknown_pk { + unknown_pks.insert(pk); + } + f.fragment + }) + .collect(); + + let tl_response = if unknown_pks.is_empty() { + None + } else { + Some(UnknownPks { unknown_pks }) + }; + + MergeResponse { + insertion_response: self.units.merge_fragments(new_fragments), + tl_response, + } + } + + pub fn latest(&self) -> Option<&NoteRef> { + self.units.latest_ref() + } + + pub fn merge_single_note(&mut self, note_ref: NoteRef) { + self.units.merge_single_unit(note_ref); + } + + /// Used in the view + pub fn get(&self, index: usize) -> Option<&NoteUnit> { + self.units.kth(index) + } +} + +pub struct MergeResponse<'a> { + pub insertion_response: InsertManyResponse, + pub tl_response: Option<UnknownPks<'a>>, +} + +pub struct UnknownPks<'a> { + pub(crate) unknown_pks: HashSet<&'a [u8; 32]>, +} + +pub struct NoteUnitFragmentResponse<'a> { + pub fragment: NoteUnitFragment, + pub unknown_pk: Option<&'a [u8; 32]>, +} + +pub struct NotePayload<'a> { + pub note: Note<'a>, + pub key: NoteKey, +} + +fn to_fragment<'a>( + payload: &'a NotePayload, + ndb: &Ndb, + txn: &Transaction, +) -> Option<NoteUnitFragmentResponse<'a>> { + match payload.note.kind() { + 1 => Some(NoteUnitFragmentResponse { + fragment: NoteUnitFragment::Single(NoteRef { + key: payload.key, + created_at: payload.note.created_at(), + }), + unknown_pk: None, + }), + 7 => to_reaction(payload, ndb, txn).map(|r| NoteUnitFragmentResponse { + fragment: NoteUnitFragment::Composite(CompositeFragment::Reaction(r.fragment)), + unknown_pk: Some(r.pk), + }), + _ => None, + } +} + +fn to_reaction<'a>( + payload: &'a NotePayload, + ndb: &Ndb, + txn: &Transaction, +) -> Option<ReactionResponse<'a>> { + let reaction = payload.note.content(); + + let mut note_reacted_to = None; + + for tag in payload.note.tags() { + if tag.count() < 2 { + continue; + } + + let Some("e") = tag.get_str(0) else { + continue; + }; + + let Some(react_to_id) = tag.get_id(1) else { + continue; + }; + + note_reacted_to = Some(react_to_id); + break; + } + + let reacted_to_noteid = note_reacted_to?; + + let reaction_note_ref = NoteRef { + key: payload.key, + created_at: payload.note.created_at(), + }; + + let reacted_to_note = ndb.get_note_by_id(txn, reacted_to_noteid).ok()?; + + let noteref_reacted_to = NoteRef { + key: reacted_to_note.key()?, + created_at: reacted_to_note.created_at(), + }; + + Some(ReactionResponse { + fragment: ReactionFragment { + noteref_reacted_to, + reaction_note_ref, + reaction: Reaction { + reaction: reaction.to_string(), + sender: Pubkey::new(*payload.note.pubkey()), + }, + }, + pk: payload.note.pubkey(), + }) +} + +pub struct ReactionResponse<'a> { + fragment: ReactionFragment, + pk: &'a [u8; 32], +}