replace HybridSet with NoteUnits
This will unify the collections that hold the notes to timelines and threads and allow the notifications timeline to have grouped notifications, among other things Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
@@ -385,7 +385,7 @@ pub fn process_thread_notes(
|
||||
created_at,
|
||||
};
|
||||
|
||||
if thread.replies.contains(¬e_ref) {
|
||||
if thread.replies.contains_key(¬e_ref.key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use crate::timeline::MergeKind;
|
||||
|
||||
/// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,16 @@ use std::{rc::Rc, time::SystemTime};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
pub mod cache;
|
||||
mod hybrid_set;
|
||||
pub mod kind;
|
||||
mod note_units;
|
||||
pub mod route;
|
||||
pub mod thread;
|
||||
mod unit;
|
||||
|
||||
pub use cache::TimelineCache;
|
||||
pub use hybrid_set::{HybridSet, InsertionResponse};
|
||||
pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
|
||||
pub use note_units::{InsertionResponse, NoteUnits};
|
||||
pub use unit::{CompositeUnit, NoteUnit, ReactionUnit};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
|
||||
pub enum ViewFilter {
|
||||
|
||||
511
crates/notedeck_columns/src/timeline/note_units.rs
Normal file
511
crates/notedeck_columns/src/timeline/note_units.rs
Normal file
@@ -0,0 +1,511 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use nostrdb::NoteKey;
|
||||
use notedeck::NoteRef;
|
||||
|
||||
use crate::timeline::{
|
||||
unit::{CompositeUnit, NoteUnit, NoteUnitFragment},
|
||||
MergeKind,
|
||||
};
|
||||
|
||||
type StorageIndex = usize;
|
||||
|
||||
/// Provides efficient access to `NoteUnit`s
|
||||
/// Useful for threads and timelines
|
||||
/// when reversed=false, sorts from newest to oldest
|
||||
#[derive(Debug, Default)]
|
||||
pub struct NoteUnits {
|
||||
reversed: bool,
|
||||
storage: Vec<NoteUnit>,
|
||||
lookup: HashMap<NoteKey, StorageIndex>, // `NoteKey` to index in `NoteUnits::storage`
|
||||
order: Vec<StorageIndex>, // the sorted order of the `NoteUnit`s in `NoteUnits::storage`
|
||||
}
|
||||
|
||||
impl NoteUnits {
|
||||
pub fn values(&self) -> Values<'_> {
|
||||
Values {
|
||||
set: self,
|
||||
front: 0,
|
||||
back: self.order.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &NoteKey) -> bool {
|
||||
self.lookup.contains_key(k)
|
||||
}
|
||||
|
||||
pub fn new_with_cap(cap: usize, reversed: bool) -> Self {
|
||||
Self {
|
||||
reversed,
|
||||
storage: Vec::with_capacity(cap),
|
||||
lookup: HashMap::with_capacity(cap),
|
||||
order: Vec::with_capacity(cap),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.storage.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.storage.is_empty()
|
||||
}
|
||||
|
||||
/// Get the kth index from 0..Self::len
|
||||
pub fn kth(&self, k: usize) -> Option<&NoteUnit> {
|
||||
if k >= self.order.len() {
|
||||
return None;
|
||||
}
|
||||
let idx = if self.reversed {
|
||||
self.order[self.order.len() - 1 - k]
|
||||
} else {
|
||||
self.order[k]
|
||||
};
|
||||
Some(&self.storage[idx])
|
||||
}
|
||||
|
||||
/// Core bulk insert for already-built `NoteUnit`s
|
||||
/// Merges new `NoteUnit`s into `Self::storage`
|
||||
/// Updates `Self::order`
|
||||
fn merge_many_internal(
|
||||
&mut self,
|
||||
mut units: Vec<NoteUnit>,
|
||||
touched_indices: &[usize],
|
||||
) -> InsertManyResponse {
|
||||
units.retain(|e| !self.lookup.contains_key(&e.key()));
|
||||
if units.is_empty() && touched_indices.is_empty() {
|
||||
return InsertManyResponse::Zero;
|
||||
}
|
||||
|
||||
if !touched_indices.is_empty() {
|
||||
self.order.retain(|i| !touched_indices.contains(i));
|
||||
}
|
||||
|
||||
units.sort_unstable();
|
||||
units.dedup_by_key(|u| u.key());
|
||||
|
||||
let base = self.storage.len();
|
||||
let mut new_order = Vec::with_capacity(units.len());
|
||||
self.storage.reserve(units.len());
|
||||
for (i, unit) in units.into_iter().enumerate() {
|
||||
let idx = base + i;
|
||||
let key = unit.key();
|
||||
self.storage.push(unit);
|
||||
self.lookup.insert(key, idx);
|
||||
new_order.push(idx);
|
||||
}
|
||||
|
||||
let front_insertion = if self.order.is_empty() || new_order.is_empty() {
|
||||
true
|
||||
} else if !self.reversed {
|
||||
let first_new = *new_order.first().unwrap();
|
||||
let last_old = *self.order.last().unwrap();
|
||||
self.storage[first_new] >= self.storage[last_old]
|
||||
} else {
|
||||
let last_new = *new_order.last().unwrap();
|
||||
let first_old = *self.order.first().unwrap();
|
||||
self.storage[last_new] <= self.storage[first_old]
|
||||
};
|
||||
|
||||
let mut merged = Vec::with_capacity(self.order.len() + new_order.len());
|
||||
let (mut i, mut j) = (0, 0);
|
||||
while i < self.order.len() && j < new_order.len() {
|
||||
let index_left = self.order[i];
|
||||
let index_right = new_order[j];
|
||||
let left_item = &self.storage[index_left];
|
||||
let right_item = &self.storage[index_right];
|
||||
if left_item <= right_item {
|
||||
// left_item is newer than right_item
|
||||
merged.push(index_left);
|
||||
i += 1;
|
||||
} else {
|
||||
merged.push(index_right);
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
merged.extend_from_slice(&self.order[i..]);
|
||||
merged.extend_from_slice(&new_order[j..]);
|
||||
|
||||
for &touched_index in touched_indices {
|
||||
let pos = merged
|
||||
.binary_search_by(|&i2| self.storage[i2].cmp(&self.storage[touched_index]))
|
||||
.unwrap_or_else(|p| p);
|
||||
merged.insert(pos, touched_index);
|
||||
}
|
||||
|
||||
let inserted = merged.len() - self.order.len();
|
||||
self.order = merged;
|
||||
|
||||
if inserted == 0 {
|
||||
InsertManyResponse::Zero
|
||||
} else if front_insertion {
|
||||
InsertManyResponse::Some {
|
||||
entries_merged: inserted,
|
||||
merge_kind: MergeKind::FrontInsert,
|
||||
}
|
||||
} else {
|
||||
InsertManyResponse::Some {
|
||||
entries_merged: inserted,
|
||||
merge_kind: MergeKind::Spliced,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges `NoteUnitFragment`s
|
||||
/// `NoteUnitFragment::Single` is added normally
|
||||
/// if `NoteUnitFragment::Composite` exists already, it will fold the fragment into the `CompositeUnit`
|
||||
/// otherwise, it will generate the `NoteUnit::CompositeUnit` from the `NoteUnitFragment::Composite`
|
||||
pub fn merge_fragments(&mut self, frags: Vec<NoteUnitFragment>) -> InsertManyResponse {
|
||||
let mut to_build: HashMap<NoteKey, CompositeUnit> = HashMap::new(); // new composites by key
|
||||
let mut singles_to_build: Vec<NoteRef> = Vec::new();
|
||||
let mut singles_seen: HashSet<NoteKey> = HashSet::new();
|
||||
|
||||
let mut touched = Vec::new();
|
||||
for frag in frags {
|
||||
match frag {
|
||||
NoteUnitFragment::Single(note_ref) => {
|
||||
let key = note_ref.key;
|
||||
if self.lookup.contains_key(&key) {
|
||||
continue;
|
||||
}
|
||||
if singles_seen.insert(key) {
|
||||
singles_to_build.push(note_ref);
|
||||
}
|
||||
}
|
||||
NoteUnitFragment::Composite(c_frag) => {
|
||||
let key = c_frag.get_underlying_noteref().key;
|
||||
|
||||
if let Some(&storage_idx) = self.lookup.get(&key) {
|
||||
if let Some(NoteUnit::Composite(c_unit)) = self.storage.get_mut(storage_idx)
|
||||
{
|
||||
if c_frag.get_latest_ref() < c_unit.get_latest_ref() {
|
||||
touched.push(storage_idx);
|
||||
}
|
||||
c_frag.fold_into(c_unit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// aggregate for new composite
|
||||
use std::collections::hash_map::Entry;
|
||||
match to_build.entry(key) {
|
||||
Entry::Occupied(mut o) => {
|
||||
c_frag.fold_into(o.get_mut());
|
||||
}
|
||||
Entry::Vacant(v) => {
|
||||
v.insert(c_frag.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut items: Vec<NoteUnit> = Vec::with_capacity(singles_to_build.len() + to_build.len());
|
||||
items.extend(singles_to_build.into_iter().map(NoteUnit::Single));
|
||||
items.extend(to_build.into_values().map(NoteUnit::Composite));
|
||||
|
||||
self.merge_many_internal(items, &touched)
|
||||
}
|
||||
|
||||
/// Convienience method to merge a single note
|
||||
pub fn merge_single_unit(&mut self, note_ref: NoteRef) -> InsertionResponse {
|
||||
match self.merge_many_internal(vec![NoteUnit::Single(note_ref)], &[]) {
|
||||
InsertManyResponse::Zero => InsertionResponse::AlreadyExists,
|
||||
InsertManyResponse::Some {
|
||||
entries_merged: _,
|
||||
merge_kind,
|
||||
} => InsertionResponse::Merged(merge_kind),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_ref(&self) -> Option<&NoteRef> {
|
||||
if self.reversed {
|
||||
self.order.last().map(|&i| &self.storage[i])
|
||||
} else {
|
||||
self.order.first().map(|&i| &self.storage[i])
|
||||
}
|
||||
.map(NoteUnit::get_latest_ref)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum InsertManyResponse {
|
||||
Zero,
|
||||
Some {
|
||||
entries_merged: usize,
|
||||
merge_kind: MergeKind,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Values<'a> {
|
||||
set: &'a NoteUnits,
|
||||
front: usize,
|
||||
back: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Values<'a> {
|
||||
type Item = &'a NoteUnit;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.front >= self.back {
|
||||
return None;
|
||||
}
|
||||
let idx = if !self.set.reversed {
|
||||
let i = self.front;
|
||||
self.front += 1;
|
||||
self.set.order[i]
|
||||
} else {
|
||||
self.back -= 1;
|
||||
self.set.order[self.back]
|
||||
};
|
||||
Some(&self.set.storage[idx])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for Values<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.front >= self.back {
|
||||
return None;
|
||||
}
|
||||
let idx = if !self.set.reversed {
|
||||
self.back -= 1;
|
||||
self.set.order[self.back]
|
||||
} else {
|
||||
let i = self.front;
|
||||
self.front += 1;
|
||||
self.set.order[i]
|
||||
};
|
||||
Some(&self.set.storage[idx])
|
||||
}
|
||||
}
|
||||
|
||||
pub enum InsertionResponse {
|
||||
AlreadyExists,
|
||||
Merged(MergeKind),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use egui::ahash::HashMap;
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::NoteKey;
|
||||
use notedeck::NoteRef;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::timeline::{
|
||||
unit::{
|
||||
CompositeFragment, CompositeUnit, NoteUnit, NoteUnitFragment, Reaction,
|
||||
ReactionFragment, ReactionUnit,
|
||||
},
|
||||
NoteUnits,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct UnitBuilder {
|
||||
counter: u64,
|
||||
frags: HashMap<String, NoteUnitFragment>,
|
||||
units: NoteUnits,
|
||||
}
|
||||
|
||||
impl UnitBuilder {
|
||||
fn counter(&mut self) -> u64 {
|
||||
let res = self.counter;
|
||||
self.counter += 1;
|
||||
res
|
||||
}
|
||||
|
||||
fn random_sender(&mut self) -> Pubkey {
|
||||
let mut out = [0u8; 32];
|
||||
out[..8].copy_from_slice(&self.counter().to_le_bytes());
|
||||
|
||||
Pubkey::new(out)
|
||||
}
|
||||
|
||||
fn fragment(&mut self, reacted_to: NoteRef) -> String {
|
||||
let frag = NoteUnitFragment::Composite(CompositeFragment::Reaction(ReactionFragment {
|
||||
noteref_reacted_to: reacted_to,
|
||||
reaction_note_ref: NoteRef {
|
||||
key: NoteKey::new(self.counter()),
|
||||
created_at: self.counter(),
|
||||
},
|
||||
reaction: Reaction {
|
||||
reaction: "+".to_owned(),
|
||||
sender: self.random_sender(),
|
||||
},
|
||||
}));
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.frags.insert(id.clone(), frag.clone());
|
||||
|
||||
self.units.merge_fragments(vec![frag]);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn generate_reaction_note(&mut self) -> NoteRef {
|
||||
NoteRef {
|
||||
key: NoteKey::new(self.counter()),
|
||||
created_at: self.counter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_note(&mut self) -> String {
|
||||
let note_ref = NoteRef {
|
||||
key: NoteKey::new(self.counter()),
|
||||
created_at: self.counter(),
|
||||
};
|
||||
|
||||
let id = Uuid::new_v4().to_string();
|
||||
self.frags
|
||||
.insert(id.clone(), NoteUnitFragment::Single(note_ref.clone()));
|
||||
|
||||
self.units.merge_single_unit(note_ref);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
fn expected_reactions(&mut self, ids: Vec<&String>) -> NoteUnit {
|
||||
let mut reactions = BTreeMap::new();
|
||||
let mut reaction_id = None;
|
||||
let mut senders = HashSet::new();
|
||||
for id in ids {
|
||||
let NoteUnitFragment::Composite(CompositeFragment::Reaction(reac)) =
|
||||
self.frags.get(id).unwrap()
|
||||
else {
|
||||
panic!("got something other than reaction");
|
||||
};
|
||||
|
||||
if let Some(prev_reac_id) = reaction_id {
|
||||
if prev_reac_id != reac.noteref_reacted_to {
|
||||
panic!("internal error");
|
||||
}
|
||||
}
|
||||
|
||||
reaction_id = Some(reac.noteref_reacted_to);
|
||||
|
||||
reactions.insert(reac.reaction_note_ref, reac.reaction.clone());
|
||||
senders.insert(reac.reaction.sender);
|
||||
}
|
||||
|
||||
NoteUnit::Composite(CompositeUnit::Reaction(ReactionUnit {
|
||||
note_reacted_to: reaction_id.unwrap(),
|
||||
reactions,
|
||||
senders: senders,
|
||||
}))
|
||||
}
|
||||
|
||||
fn expected_single(&mut self, id: &String) -> NoteUnit {
|
||||
let Some(NoteUnitFragment::Single(note_ref)) = self.frags.get(id) else {
|
||||
panic!("fail");
|
||||
};
|
||||
|
||||
NoteUnit::Single(*note_ref)
|
||||
}
|
||||
|
||||
fn asserted_at(&self, index: usize) -> NoteUnit {
|
||||
self.units.kth(index).unwrap().clone()
|
||||
}
|
||||
|
||||
fn aeq(&mut self, units_kth: usize, expect: Expect) {
|
||||
assert_eq!(
|
||||
self.asserted_at(units_kth),
|
||||
match expect {
|
||||
Expect::Single(id) => self.expected_single(id),
|
||||
Expect::Reaction(items) => self.expected_reactions(items),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum Expect<'a> {
|
||||
Single(&'a String),
|
||||
Reaction(Vec<&'a String>),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let reaction_note = builder.generate_reaction_note();
|
||||
|
||||
let single0 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single0));
|
||||
|
||||
let reac1 = builder.fragment(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(1, Expect::Single(&single0));
|
||||
|
||||
let single1 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single1));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1]));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
|
||||
let reac2 = builder.fragment(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2, &reac1]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
|
||||
let single2 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single2));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac2, &reac1]));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
|
||||
let reac3 = builder.fragment(reaction_note);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1, &reac2, &reac3]));
|
||||
builder.aeq(1, Expect::Single(&single2));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test2() {
|
||||
let mut builder = UnitBuilder::default();
|
||||
let reaction_note1 = builder.generate_reaction_note();
|
||||
let reaction_note2 = builder.generate_reaction_note();
|
||||
|
||||
let single0 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single0));
|
||||
|
||||
let reac1_1 = builder.fragment(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(1, Expect::Single(&single0));
|
||||
|
||||
let reac2_1 = builder.fragment(reaction_note2);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(2, Expect::Single(&single0));
|
||||
|
||||
let single1 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single1));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(2, Expect::Reaction(vec![&reac1_1]));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
|
||||
let reac1_2 = builder.fragment(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1]));
|
||||
builder.aeq(1, Expect::Single(&single1));
|
||||
builder.aeq(2, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(3, Expect::Single(&single0));
|
||||
|
||||
let single2 = builder.insert_note();
|
||||
builder.aeq(0, Expect::Single(&single2));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1_2, &reac1_1]));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(4, Expect::Single(&single0));
|
||||
|
||||
let reac1_3 = builder.fragment(reaction_note1);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||
builder.aeq(1, Expect::Single(&single2));
|
||||
builder.aeq(2, Expect::Single(&single1));
|
||||
builder.aeq(3, Expect::Reaction(vec![&reac2_1]));
|
||||
builder.aeq(4, Expect::Single(&single0));
|
||||
|
||||
let reac2_2 = builder.fragment(reaction_note2);
|
||||
builder.aeq(0, Expect::Reaction(vec![&reac2_1, &reac2_2]));
|
||||
builder.aeq(1, Expect::Reaction(vec![&reac1_2, &reac1_1, &reac1_3]));
|
||||
builder.aeq(2, Expect::Single(&single2));
|
||||
builder.aeq(3, Expect::Single(&single1));
|
||||
builder.aeq(4, Expect::Single(&single0));
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,13 @@ use notedeck::{NoteCache, NoteRef, UnknownIds};
|
||||
use crate::{
|
||||
actionbar::{process_thread_notes, NewThreadNotes},
|
||||
multi_subscriber::ThreadSubs,
|
||||
timeline::hybrid_set::HybridSet,
|
||||
timeline::{note_units::NoteUnits, unit::NoteUnit, InsertionResponse},
|
||||
};
|
||||
|
||||
use super::ThreadSelection;
|
||||
|
||||
pub struct ThreadNode {
|
||||
pub replies: HybridSet<NoteRef>,
|
||||
pub replies: SingleNoteUnits,
|
||||
pub prev: ParentState,
|
||||
pub have_all_ancestors: bool,
|
||||
pub list: VirtualList,
|
||||
@@ -31,7 +31,7 @@ pub enum ParentState {
|
||||
impl ThreadNode {
|
||||
pub fn new(parent: ParentState) -> Self {
|
||||
Self {
|
||||
replies: HybridSet::new(true),
|
||||
replies: SingleNoteUnits::new(true),
|
||||
prev: parent,
|
||||
have_all_ancestors: false,
|
||||
list: VirtualList::new(),
|
||||
@@ -389,3 +389,34 @@ impl NoteSeenFlags {
|
||||
self.flags.contains_key(¬e_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SingleNoteUnits {
|
||||
units: NoteUnits,
|
||||
}
|
||||
|
||||
impl SingleNoteUnits {
|
||||
pub fn new(reversed: bool) -> Self {
|
||||
Self {
|
||||
units: NoteUnits::new_with_cap(0, reversed),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, note_ref: NoteRef) -> InsertionResponse {
|
||||
self.units.merge_single_unit(note_ref)
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = &NoteRef> {
|
||||
self.units.values().filter_map(|entry| {
|
||||
if let NoteUnit::Single(note_ref) = entry {
|
||||
Some(note_ref)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, k: &NoteKey) -> bool {
|
||||
self.units.contains_key(k)
|
||||
}
|
||||
}
|
||||
|
||||
205
crates/notedeck_columns/src/timeline/unit.rs
Normal file
205
crates/notedeck_columns/src/timeline/unit.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
use enostr::Pubkey;
|
||||
use nostrdb::NoteKey;
|
||||
use notedeck::NoteRef;
|
||||
|
||||
/// A `NoteUnit` represents a cohesive piece of data derived from notes
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NoteUnit {
|
||||
Single(NoteRef), // A single note
|
||||
Composite(CompositeUnit),
|
||||
}
|
||||
|
||||
impl NoteUnit {
|
||||
pub fn key(&self) -> NoteKey {
|
||||
match self {
|
||||
NoteUnit::Single(note_ref) => note_ref.key,
|
||||
NoteUnit::Composite(clustered_entry) => clustered_entry.key(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||
match self {
|
||||
NoteUnit::Single(note_ref) => note_ref,
|
||||
NoteUnit::Composite(clustered) => match clustered {
|
||||
CompositeUnit::Reaction(reaction_entry) => &reaction_entry.note_reacted_to,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
match self {
|
||||
NoteUnit::Single(note_ref) => note_ref,
|
||||
NoteUnit::Composite(composite_unit) => composite_unit.get_latest_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for NoteUnit {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.get_latest_ref().cmp(other.get_latest_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for NoteUnit {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.get_latest_ref() == other.get_latest_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for NoteUnit {}
|
||||
|
||||
impl PartialOrd for NoteUnit {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines potentially many notes into one cohesive piece of data
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompositeUnit {
|
||||
Reaction(ReactionUnit),
|
||||
}
|
||||
|
||||
impl CompositeUnit {
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeUnit::Reaction(reaction_unit) => reaction_unit.get_latest_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CompositeUnit {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Reaction(l0), Self::Reaction(r0)) => l0 == r0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositeUnit {
|
||||
pub fn key(&self) -> NoteKey {
|
||||
match self {
|
||||
CompositeUnit::Reaction(reaction_entry) => reaction_entry.note_reacted_to.key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CompositeFragment> for CompositeUnit {
|
||||
fn from(value: CompositeFragment) -> Self {
|
||||
match value {
|
||||
CompositeFragment::Reaction(reaction_fragment) => {
|
||||
CompositeUnit::Reaction(reaction_fragment.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents all the reactions to a specific note `ReactionUnit::note_reacted_to`
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ReactionUnit {
|
||||
pub note_reacted_to: NoteRef, // NOTE: this should not be modified after it's created
|
||||
pub reactions: BTreeMap<NoteRef, Reaction>,
|
||||
pub senders: HashSet<Pubkey>, // useful for making sure the same user can't add more than one reaction to a note
|
||||
}
|
||||
|
||||
impl ReactionUnit {
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
self.reactions
|
||||
.first_key_value()
|
||||
.map(|(r, _)| r)
|
||||
.unwrap_or(&self.note_reacted_to)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ReactionFragment> for ReactionUnit {
|
||||
fn from(frag: ReactionFragment) -> Self {
|
||||
let mut senders = HashSet::new();
|
||||
senders.insert(frag.reaction.sender);
|
||||
|
||||
let mut reactions = BTreeMap::new();
|
||||
reactions.insert(frag.reaction_note_ref, frag.reaction);
|
||||
|
||||
Self {
|
||||
note_reacted_to: frag.noteref_reacted_to,
|
||||
reactions,
|
||||
senders,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum NoteUnitFragment {
|
||||
Single(NoteRef),
|
||||
Composite(CompositeFragment),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CompositeFragment {
|
||||
Reaction(ReactionFragment),
|
||||
}
|
||||
|
||||
impl CompositeFragment {
|
||||
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => reaction_fragment.fold_into(unit),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(&self) -> NoteKey {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => {
|
||||
reaction_fragment.reaction_note_ref.key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_underlying_noteref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.noteref_reacted_to,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_latest_ref(&self) -> &NoteRef {
|
||||
match self {
|
||||
CompositeFragment::Reaction(reaction_fragment) => &reaction_fragment.reaction_note_ref,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A singluar reaction to a note
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReactionFragment {
|
||||
pub noteref_reacted_to: NoteRef,
|
||||
pub reaction_note_ref: NoteRef,
|
||||
pub reaction: Reaction,
|
||||
}
|
||||
|
||||
impl ReactionFragment {
|
||||
/// Add all the contents of Self into `CompositeUnit`
|
||||
pub fn fold_into(self, unit: &mut CompositeUnit) {
|
||||
match unit {
|
||||
CompositeUnit::Reaction(reaction_unit) => {
|
||||
if self.noteref_reacted_to != reaction_unit.note_reacted_to {
|
||||
return;
|
||||
}
|
||||
|
||||
if reaction_unit.senders.contains(&self.reaction.sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction_unit.senders.insert(self.reaction.sender);
|
||||
reaction_unit
|
||||
.reactions
|
||||
.insert(self.reaction_note_ref, self.reaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Reaction {
|
||||
pub reaction: String, // can't use char because some emojis are 'grapheme clusters'
|
||||
pub sender: Pubkey,
|
||||
}
|
||||
@@ -111,7 +111,7 @@ impl<'a, 'd> ThreadView<'a, 'd> {
|
||||
parent_state = ParentState::Unknown;
|
||||
}
|
||||
|
||||
for note_ref in &cur_node.replies {
|
||||
for note_ref in cur_node.replies.values() {
|
||||
if let Ok(note) = self.note_context.ndb.get_note_by_key(txn, note_ref.key) {
|
||||
note_builder.add_reply(note);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user