split notedeck into crates

This splits notedeck into crates, separating the browser chrome and
individual apps:

* notedeck: binary file, browser chrome
* notedeck_columns: our columns app
* enostr: same as before

We still need to do more work to cleanly separate the chrome apis
from the app apis. Soon I will create notedeck-notebook to see what
makes sense to be shared between the apps.

Some obvious ones that come to mind:

1. ImageCache

We will likely want to move this to the notedeck crate, as most apps
will want some kind of image cache. In web browsers, web pages do not
need to worry about this, so we will likely have to do something similar

2. Ndb

Since NdbRef is threadsafe and Ndb is an Arc<NdbRef>, it can be safely
copied to each app. This will simplify things. In the future we might
want to create an abstraction over this? Maybe each app shouldn't have
access to the same database... we assume the data in DBs are all public
anyways, but if we have unwrapped giftwraps that could be a problem.

3. RelayPool / Subscription Manager

The browser should probably maintain these. Then apps can use ken's
high level subscription manager api and not have to worry about
connection pool details

4. Accounts

Accounts and key management should be handled by the chrome. Apps should
only have a simple signer interface.

That's all for now, just something to think about!

Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
William Casarin
2024-12-11 02:53:05 -08:00
parent 10cbdf15f0
commit 74c5f0c748
156 changed files with 194 additions and 252 deletions

View File

@@ -0,0 +1,208 @@
use crate::error::{Error, FilterError};
use crate::filter;
use crate::filter::FilterState;
use crate::timeline::Timeline;
use enostr::{Filter, Pubkey};
use nostrdb::{Ndb, Transaction};
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, fmt::Display};
use tracing::{error, warn};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PubkeySource {
Explicit(Pubkey),
DeckAuthor,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ListKind {
Contact(PubkeySource),
}
impl PubkeySource {
pub fn to_pubkey<'a>(&'a self, deck_author: &'a Pubkey) -> &'a Pubkey {
match self {
PubkeySource::Explicit(pk) => pk,
PubkeySource::DeckAuthor => deck_author,
}
}
}
impl ListKind {
pub fn pubkey_source(&self) -> Option<&PubkeySource> {
match self {
ListKind::Contact(pk_src) => Some(pk_src),
}
}
}
///
/// What kind of timeline is it?
/// - Follow List
/// - Notifications
/// - DM
/// - filter
/// - ... etc
///
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimelineKind {
List(ListKind),
Notifications(PubkeySource),
Profile(PubkeySource),
Universe,
/// Generic filter
Generic,
Hashtag(String),
}
impl Display for TimelineKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimelineKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"),
TimelineKind::Generic => f.write_str("Timeline"),
TimelineKind::Notifications(_) => f.write_str("Notifications"),
TimelineKind::Profile(_) => f.write_str("Profile"),
TimelineKind::Universe => f.write_str("Universe"),
TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
}
}
}
impl TimelineKind {
pub fn pubkey_source(&self) -> Option<&PubkeySource> {
match self {
TimelineKind::List(list_kind) => list_kind.pubkey_source(),
TimelineKind::Notifications(pk_src) => Some(pk_src),
TimelineKind::Profile(pk_src) => Some(pk_src),
TimelineKind::Universe => None,
TimelineKind::Generic => None,
TimelineKind::Hashtag(_ht) => None,
}
}
pub fn contact_list(pk: PubkeySource) -> Self {
TimelineKind::List(ListKind::Contact(pk))
}
pub fn is_contacts(&self) -> bool {
matches!(self, TimelineKind::List(ListKind::Contact(_)))
}
pub fn profile(pk: PubkeySource) -> Self {
TimelineKind::Profile(pk)
}
pub fn is_notifications(&self) -> bool {
matches!(self, TimelineKind::Notifications(_))
}
pub fn notifications(pk: PubkeySource) -> Self {
TimelineKind::Notifications(pk)
}
pub fn into_timeline(self, ndb: &Ndb, default_user: Option<&[u8; 32]>) -> Option<Timeline> {
match self {
TimelineKind::Universe => Some(Timeline::new(
TimelineKind::Universe,
FilterState::ready(vec![Filter::new()
.kinds([1])
.limit(filter::default_limit())
.build()]),
)),
TimelineKind::Generic => {
warn!("you can't convert a TimelineKind::Generic to a Timeline");
None
}
TimelineKind::Profile(pk_src) => {
let pk = match &pk_src {
PubkeySource::DeckAuthor => default_user?,
PubkeySource::Explicit(pk) => pk.bytes(),
};
let filter = Filter::new()
.authors([pk])
.kinds([1])
.limit(filter::default_limit())
.build();
Some(Timeline::new(
TimelineKind::profile(pk_src),
FilterState::ready(vec![filter]),
))
}
TimelineKind::Notifications(pk_src) => {
let pk = match &pk_src {
PubkeySource::DeckAuthor => default_user?,
PubkeySource::Explicit(pk) => pk.bytes(),
};
let notifications_filter = Filter::new()
.pubkeys([pk])
.kinds([1])
.limit(crate::filter::default_limit())
.build();
Some(Timeline::new(
TimelineKind::notifications(pk_src),
FilterState::ready(vec![notifications_filter]),
))
}
TimelineKind::Hashtag(hashtag) => Some(Timeline::hashtag(hashtag)),
TimelineKind::List(ListKind::Contact(pk_src)) => {
let pk = match &pk_src {
PubkeySource::DeckAuthor => default_user?,
PubkeySource::Explicit(pk) => pk.bytes(),
};
let contact_filter = Filter::new().authors([pk]).kinds([3]).limit(1).build();
let txn = Transaction::new(ndb).expect("txn");
let results = ndb
.query(&txn, &[contact_filter.clone()], 1)
.expect("contact query failed?");
if results.is_empty() {
return Some(Timeline::new(
TimelineKind::contact_list(pk_src),
FilterState::needs_remote(vec![contact_filter.clone()]),
));
}
match Timeline::contact_list(&results[0].note, pk_src.clone()) {
Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new(
TimelineKind::contact_list(pk_src),
FilterState::needs_remote(vec![contact_filter]),
)),
Err(e) => {
error!("Unexpected error: {e}");
None
}
Ok(tl) => Some(tl),
}
}
}
}
pub fn to_title(&self) -> Cow<'static, str> {
match self {
TimelineKind::List(list_kind) => match list_kind {
ListKind::Contact(_pubkey_source) => Cow::Borrowed("Contacts"),
},
TimelineKind::Notifications(_pubkey_source) => Cow::Borrowed("Notifications"),
TimelineKind::Profile(_pubkey_source) => Cow::Borrowed("Profile"),
TimelineKind::Universe => Cow::Borrowed("Universe"),
TimelineKind::Generic => Cow::Borrowed("Custom"),
TimelineKind::Hashtag(hashtag) => Cow::Owned(format!("#{}", hashtag)),
}
}
}

View File

@@ -0,0 +1,695 @@
use crate::{
column::Columns,
decks::DecksCache,
error::{Error, FilterError},
filter::{self, FilterState, FilterStates},
muted::MuteFun,
note::NoteRef,
notecache::{CachedNote, NoteCache},
subscriptions::{self, SubKind, Subscriptions},
unknowns::UnknownIds,
Result,
};
use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use egui_virtual_list::VirtualList;
use enostr::{Relay, RelayPool};
use nostrdb::{Filter, Ndb, Note, Subscription, Transaction};
use std::cell::RefCell;
use std::hash::Hash;
use std::rc::Rc;
use tracing::{debug, error, info, warn};
pub mod kind;
pub mod route;
pub use kind::{PubkeySource, TimelineKind};
pub use route::TimelineRoute;
#[derive(Debug, Hash, Copy, Clone, Eq, PartialEq)]
pub struct TimelineId(u32);
impl TimelineId {
pub fn new(id: u32) -> Self {
TimelineId(id)
}
}
impl fmt::Display for TimelineId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "TimelineId({})", self.0)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub enum ViewFilter {
Notes,
#[default]
NotesAndReplies,
}
impl ViewFilter {
pub fn name(&self) -> &'static str {
match self {
ViewFilter::Notes => "Notes",
ViewFilter::NotesAndReplies => "Notes & Replies",
}
}
pub fn index(&self) -> usize {
match self {
ViewFilter::Notes => 0,
ViewFilter::NotesAndReplies => 1,
}
}
pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool {
!cache.reply.borrow(note.tags()).is_reply()
}
fn identity(_cache: &CachedNote, _note: &Note) -> bool {
true
}
pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool {
match self {
ViewFilter::Notes => ViewFilter::filter_notes,
ViewFilter::NotesAndReplies => ViewFilter::identity,
}
}
}
/// A timeline view is a filtered view of notes in a timeline. Two standard views
/// are "Notes" and "Notes & Replies". A timeline is associated with a Filter,
/// but a TimelineTab is a further filtered view of this Filter that can't
/// be captured by a Filter itself.
#[derive(Default, Debug)]
pub struct TimelineTab {
pub notes: Vec<NoteRef>,
pub selection: i32,
pub filter: ViewFilter,
pub list: Rc<RefCell<VirtualList>>,
}
impl TimelineTab {
pub fn new(filter: ViewFilter) -> Self {
TimelineTab::new_with_capacity(filter, 1000)
}
pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self {
let selection = 0i32;
let mut list = VirtualList::new();
list.hide_on_resize(None);
list.over_scan(1000.0);
let list = Rc::new(RefCell::new(list));
let notes: Vec<NoteRef> = Vec::with_capacity(cap);
TimelineTab {
notes,
selection,
filter,
list,
}
}
pub fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
if new_refs.is_empty() {
return;
}
let num_prev_items = self.notes.len();
let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs);
self.notes = notes;
let new_items = self.notes.len() - num_prev_items;
// TODO: technically items could have been added inbetween
if new_items > 0 {
let mut list = self.list.borrow_mut();
match merge_kind {
// TODO: update egui_virtual_list to support spliced inserts
MergeKind::Spliced => {
debug!(
"spliced when inserting {} new notes, resetting virtual list",
new_refs.len()
);
list.reset();
}
MergeKind::FrontInsert => {
// only run this logic if we're reverse-chronological
// reversed in this case means chronological, since the
// default is reverse-chronological. yeah it's confusing.
if !reversed {
list.items_inserted_at_start(new_items);
}
}
}
}
}
pub fn select_down(&mut self) {
debug!("select_down {}", self.selection + 1);
if self.selection + 1 > self.notes.len() as i32 {
return;
}
self.selection += 1;
}
pub fn select_up(&mut self) {
debug!("select_up {}", self.selection - 1);
if self.selection - 1 < 0 {
return;
}
self.selection -= 1;
}
}
/// A column in a deck. Holds navigation state, loaded notes, column kind, etc.
#[derive(Debug)]
pub struct Timeline {
pub id: TimelineId,
pub kind: TimelineKind,
// We may not have the filter loaded yet, so let's make it an option so
// that codepaths have to explicitly handle it
pub filter: FilterStates,
pub views: Vec<TimelineTab>,
pub selected_view: i32,
/// Our nostrdb subscription
pub subscription: Option<Subscription>,
}
impl Timeline {
/// Create a timeline from a contact list
pub fn contact_list(contact_list: &Note, pk_src: PubkeySource) -> Result<Self> {
let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
Ok(Timeline::new(
TimelineKind::contact_list(pk_src),
FilterState::ready(filter),
))
}
pub fn hashtag(hashtag: String) -> Self {
let filter = Filter::new()
.kinds([1])
.limit(filter::default_limit())
.tags([hashtag.clone()], 't')
.build();
Timeline::new(
TimelineKind::Hashtag(hashtag),
FilterState::ready(vec![filter]),
)
}
pub fn make_view_id(id: TimelineId, selected_view: i32) -> egui::Id {
egui::Id::new((id, selected_view))
}
pub fn view_id(&self) -> egui::Id {
Timeline::make_view_id(self.id, self.selected_view)
}
pub fn new(kind: TimelineKind, filter_state: FilterState) -> Self {
// global unique id for all new timelines
static UIDS: AtomicU32 = AtomicU32::new(0);
let filter = FilterStates::new(filter_state);
let subscription: Option<Subscription> = None;
let notes = TimelineTab::new(ViewFilter::Notes);
let replies = TimelineTab::new(ViewFilter::NotesAndReplies);
let views = vec![notes, replies];
let selected_view = 0;
let id = TimelineId::new(UIDS.fetch_add(1, Ordering::Relaxed));
Timeline {
id,
kind,
filter,
views,
subscription,
selected_view,
}
}
pub fn current_view(&self) -> &TimelineTab {
&self.views[self.selected_view as usize]
}
pub fn current_view_mut(&mut self) -> &mut TimelineTab {
&mut self.views[self.selected_view as usize]
}
pub fn notes(&self, view: ViewFilter) -> &[NoteRef] {
&self.views[view.index()].notes
}
pub fn view(&self, view: ViewFilter) -> &TimelineTab {
&self.views[view.index()]
}
pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab {
&mut self.views[view.index()]
}
pub fn poll_notes_into_view(
timeline_idx: usize,
mut timelines: Vec<&mut Timeline>,
ndb: &Ndb,
txn: &Transaction,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
is_muted: &MuteFun,
) -> Result<()> {
let timeline = timelines
.get_mut(timeline_idx)
.ok_or(Error::TimelineNotFound)?;
let sub = timeline.subscription.ok_or(Error::no_active_sub())?;
let new_note_ids = ndb.poll_for_notes(sub, 500);
if new_note_ids.is_empty() {
return Ok(());
} else {
debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
}
let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
for key in new_note_ids {
let note = if let Ok(note) = ndb.get_note_by_key(txn, key) {
note
} else {
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;
};
if is_muted(&note) {
continue;
}
UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note);
let created_at = note.created_at();
new_refs.push((note, NoteRef { key, created_at }));
}
// We're assuming reverse-chronological here (timelines). This
// flag ensures we trigger the items_inserted_at_start
// optimization in VirtualList. We need this flag because we can
// insert notes into chronological order sometimes, and this
// optimization doesn't make sense in those situations.
let reversed = false;
// ViewFilter::NotesAndReplies
{
let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
let reversed = false;
timeline
.view_mut(ViewFilter::NotesAndReplies)
.insert(&refs, reversed);
}
//
// handle the filtered case (ViewFilter::Notes, no replies)
//
// TODO(jb55): this is mostly just copied from above, let's just use a loop
// I initially tried this but ran into borrow checker issues
{
let mut filtered_refs = Vec::with_capacity(new_refs.len());
for (note, nr) in &new_refs {
let cached_note = note_cache.cached_note_or_insert(nr.key, note);
if ViewFilter::filter_notes(cached_note, note) {
filtered_refs.push(*nr);
}
}
timeline
.view_mut(ViewFilter::Notes)
.insert(&filtered_refs, reversed);
}
Ok(())
}
}
pub enum MergeKind {
FrontInsert,
Spliced,
}
pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> (Vec<T>, MergeKind) {
let mut merged = Vec::with_capacity(vec1.len() + vec2.len());
let mut i = 0;
let mut j = 0;
let mut result: Option<MergeKind> = None;
while i < vec1.len() && j < vec2.len() {
if vec1[i] <= vec2[j] {
if result.is_none() && j < vec2.len() {
// if we're pushing from our large list and still have
// some left in vec2, then this is a splice
result = Some(MergeKind::Spliced);
}
merged.push(vec1[i]);
i += 1;
} else {
merged.push(vec2[j]);
j += 1;
}
}
// Append any remaining elements from either vector
if i < vec1.len() {
merged.extend_from_slice(&vec1[i..]);
}
if j < vec2.len() {
merged.extend_from_slice(&vec2[j..]);
}
(merged, result.unwrap_or(MergeKind::FrontInsert))
}
/// When adding a new timeline, we may have a situation where the
/// FilterState is NeedsRemote. This can happen if we don't yet have the
/// contact list, etc. For these situations, we query all of the relays
/// with the same sub_id. We keep track of this sub_id and update the
/// filter with the latest version of the returned filter (ie contact
/// list) when they arrive.
///
/// We do this by maintaining this sub_id in the filter state, even when
/// in the ready state. See: [`FilterReady`]
pub fn setup_new_timeline(
timeline: &mut Timeline,
ndb: &Ndb,
subs: &mut Subscriptions,
pool: &mut RelayPool,
note_cache: &mut NoteCache,
since_optimize: bool,
is_muted: &MuteFun,
) {
// if we're ready, setup local subs
if is_timeline_ready(ndb, pool, note_cache, timeline, is_muted) {
if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline, is_muted) {
error!("setup_new_timeline: {err}");
}
}
for relay in &mut pool.relays {
send_initial_timeline_filter(ndb, since_optimize, subs, &mut relay.relay, timeline);
}
}
/// Send initial filters for a specific relay. This typically gets called
/// when we first connect to a new relay for the first time. For
/// situations where you are adding a new timeline, use
/// setup_new_timeline.
pub fn send_initial_timeline_filters(
ndb: &Ndb,
since_optimize: bool,
columns: &mut Columns,
subs: &mut Subscriptions,
pool: &mut RelayPool,
relay_id: &str,
) -> Option<()> {
info!("Sending initial filters to {}", relay_id);
let relay = &mut pool
.relays
.iter_mut()
.find(|r| r.relay.url == relay_id)?
.relay;
for timeline in columns.timelines_mut() {
send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline);
}
Some(())
}
pub fn send_initial_timeline_filter(
ndb: &Ndb,
can_since_optimize: bool,
subs: &mut Subscriptions,
relay: &mut Relay,
timeline: &mut Timeline,
) {
let filter_state = timeline.filter.get(&relay.url);
match filter_state {
FilterState::Broken(err) => {
error!(
"FetchingRemote state in broken state when sending initial timeline filter? {err}"
);
}
FilterState::FetchingRemote(_unisub) => {
error!("FetchingRemote state when sending initial timeline filter?");
}
FilterState::GotRemote(_sub) => {
error!("GotRemote state when sending initial timeline filter?");
}
FilterState::Ready(filter) => {
let filter = filter.to_owned();
let new_filters = filter.into_iter().map(|f| {
// limit the size of remote filters
let default_limit = filter::default_remote_limit();
let mut lim = f.limit().unwrap_or(default_limit);
let mut filter = f;
if lim > default_limit {
lim = default_limit;
filter = filter.limit_mut(lim);
}
let notes = timeline.notes(ViewFilter::NotesAndReplies);
// Should we since optimize? Not always. For example
// if we only have a few notes locally. One way to
// determine this is by looking at the current filter
// and seeing what its limit is. If we have less
// notes than the limit, we might want to backfill
// older notes
if can_since_optimize && filter::should_since_optimize(lim, notes.len()) {
filter = filter::since_optimize_filter(filter, notes);
} else {
warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", filter);
}
filter
}).collect();
//let sub_id = damus.gen_subid(&SubKind::Initial);
let sub_id = subscriptions::new_sub_id();
subs.subs.insert(sub_id.clone(), SubKind::Initial);
relay.subscribe(sub_id, new_filters);
}
// we need some data first
FilterState::NeedsRemote(filter) => {
fetch_contact_list(filter.to_owned(), ndb, subs, relay, timeline)
}
}
}
fn fetch_contact_list(
filter: Vec<Filter>,
ndb: &Ndb,
subs: &mut Subscriptions,
relay: &mut Relay,
timeline: &mut Timeline,
) {
let sub_kind = SubKind::FetchingContactList(timeline.id);
let sub_id = subscriptions::new_sub_id();
let local_sub = ndb.subscribe(&filter).expect("sub");
timeline.filter.set_relay_state(
relay.url.clone(),
FilterState::fetching_remote(sub_id.clone(), local_sub),
);
subs.subs.insert(sub_id.clone(), sub_kind);
info!("fetching contact list from {}", &relay.url);
relay.subscribe(sub_id, filter);
}
fn setup_initial_timeline(
ndb: &Ndb,
timeline: &mut Timeline,
note_cache: &mut NoteCache,
filters: &[Filter],
is_muted: &MuteFun,
) -> Result<()> {
timeline.subscription = Some(ndb.subscribe(filters)?);
let txn = Transaction::new(ndb)?;
debug!(
"querying nostrdb sub {:?} {:?}",
timeline.subscription, timeline.filter
);
let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32;
let notes = ndb
.query(&txn, filters, lim)?
.into_iter()
.map(NoteRef::from_query_result)
.collect();
copy_notes_into_timeline(timeline, &txn, ndb, note_cache, notes, is_muted);
Ok(())
}
pub fn copy_notes_into_timeline(
timeline: &mut Timeline,
txn: &Transaction,
ndb: &Ndb,
note_cache: &mut NoteCache,
notes: Vec<NoteRef>,
is_muted: &MuteFun,
) {
let filters = {
let views = &timeline.views;
let filters: Vec<fn(&CachedNote, &Note) -> bool> =
views.iter().map(|v| v.filter.filter()).collect();
filters
};
for note_ref in notes {
for (view, filter) in filters.iter().enumerate() {
if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) {
if is_muted(&note) {
continue;
}
if filter(
note_cache.cached_note_or_insert_mut(note_ref.key, &note),
&note,
) {
timeline.views[view].notes.push(note_ref)
}
}
}
}
}
pub fn setup_initial_nostrdb_subs(
ndb: &Ndb,
note_cache: &mut NoteCache,
decks_cache: &mut DecksCache,
is_muted: &MuteFun,
) -> Result<()> {
for decks in decks_cache.get_all_decks_mut() {
for deck in decks.decks_mut() {
for timeline in deck.columns_mut().timelines_mut() {
if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline, is_muted) {
error!("setup_initial_nostrdb_subs: {err}");
}
}
}
}
Ok(())
}
fn setup_timeline_nostrdb_sub(
ndb: &Ndb,
note_cache: &mut NoteCache,
timeline: &mut Timeline,
is_muted: &MuteFun,
) -> Result<()> {
let filter_state = timeline
.filter
.get_any_ready()
.ok_or(Error::empty_contact_list())?
.to_owned();
setup_initial_timeline(ndb, timeline, note_cache, &filter_state, is_muted)?;
Ok(())
}
/// Check our timeline filter and see if we have any filter data ready.
/// Our timelines may require additional data before it is functional. For
/// example, when we have to fetch a contact list before we do the actual
/// following list query.
pub fn is_timeline_ready(
ndb: &Ndb,
pool: &mut RelayPool,
note_cache: &mut NoteCache,
timeline: &mut Timeline,
is_muted: &MuteFun,
) -> bool {
// TODO: we should debounce the filter states a bit to make sure we have
// seen all of the different contact lists from each relay
if let Some(_f) = timeline.filter.get_any_ready() {
return true;
}
let (relay_id, sub) = if let Some((relay_id, sub)) = timeline.filter.get_any_gotremote() {
(relay_id.to_string(), sub)
} else {
return false;
};
// We got at least one eose for our filter request. Let's see
// if nostrdb is done processing it yet.
let res = ndb.poll_for_notes(sub, 1);
if res.is_empty() {
debug!(
"check_timeline_filter_state: no notes found (yet?) for timeline {:?}",
timeline
);
return false;
}
info!("notes found for contact timeline after GotRemote!");
let note_key = res[0];
let filter = {
let txn = Transaction::new(ndb).expect("txn");
let note = ndb.get_note_by_key(&txn, note_key).expect("note");
filter::filter_from_tags(&note).map(|f| f.into_follow_filter())
};
// TODO: into_follow_filter is hardcoded to contact lists, let's generalize
match filter {
Err(Error::Filter(e)) => {
error!("got broken when building filter {e}");
timeline
.filter
.set_relay_state(relay_id, FilterState::broken(e));
false
}
Err(err) => {
error!("got broken when building filter {err}");
timeline
.filter
.set_relay_state(relay_id, FilterState::broken(FilterError::EmptyContactList));
false
}
Ok(filter) => {
// we just switched to the ready state, we should send initial
// queries and setup the local subscription
info!("Found contact list! Setting up local and remote contact list query");
setup_initial_timeline(ndb, timeline, note_cache, &filter, is_muted)
.expect("setup init");
timeline
.filter
.set_relay_state(relay_id, FilterState::ready(filter.clone()));
//let ck = &timeline.kind;
//let subid = damus.gen_subid(&SubKind::Column(ck.clone()));
let subid = subscriptions::new_sub_id();
pool.subscribe(subid, filter);
true
}
}
}

View File

@@ -0,0 +1,182 @@
use crate::{
accounts::Accounts,
column::Columns,
draft::Drafts,
imgcache::ImageCache,
muted::MuteFun,
nav::RenderNavAction,
notecache::NoteCache,
notes_holder::NotesHolderStorage,
profile::Profile,
thread::Thread,
timeline::{TimelineId, TimelineKind},
ui::{
self,
note::{NoteOptions, QuoteRepostView},
profile::ProfileView,
},
unknowns::UnknownIds,
};
use enostr::{NoteId, Pubkey};
use nostrdb::{Ndb, Transaction};
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum TimelineRoute {
Timeline(TimelineId),
Thread(NoteId),
Profile(Pubkey),
Reply(NoteId),
Quote(NoteId),
}
#[allow(clippy::too_many_arguments)]
pub fn render_timeline_route(
ndb: &Ndb,
columns: &mut Columns,
drafts: &mut Drafts,
img_cache: &mut ImageCache,
unknown_ids: &mut UnknownIds,
note_cache: &mut NoteCache,
threads: &mut NotesHolderStorage<Thread>,
profiles: &mut NotesHolderStorage<Profile>,
accounts: &mut Accounts,
route: TimelineRoute,
col: usize,
textmode: bool,
ui: &mut egui::Ui,
) -> Option<RenderNavAction> {
match route {
TimelineRoute::Timeline(timeline_id) => {
let note_options = {
let is_universe = if let Some(timeline) = columns.find_timeline(timeline_id) {
timeline.kind == TimelineKind::Universe
} else {
false
};
let mut options = NoteOptions::new(is_universe);
options.set_textmode(textmode);
options
};
let note_action = ui::TimelineView::new(
timeline_id,
columns,
ndb,
note_cache,
img_cache,
note_options,
)
.ui(ui);
note_action.map(RenderNavAction::NoteAction)
}
TimelineRoute::Thread(id) => ui::ThreadView::new(
threads,
ndb,
note_cache,
unknown_ids,
img_cache,
id.bytes(),
textmode,
)
.id_source(egui::Id::new(("threadscroll", col)))
.ui(ui, &accounts.mutefun())
.map(Into::into),
TimelineRoute::Reply(id) => {
let txn = if let Ok(txn) = Transaction::new(ndb) {
txn
} else {
ui.label("Reply to unknown note");
return None;
};
let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) {
note
} else {
ui.label("Reply to unknown note");
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
let poster = accounts.selected_or_first_nsec()?;
let action = {
let draft = drafts.reply_mut(note.id());
let response = egui::ScrollArea::vertical().show(ui, |ui| {
ui::PostReplyView::new(ndb, poster, draft, note_cache, img_cache, &note)
.id_source(id)
.show(ui)
});
response.inner.action
};
action.map(Into::into)
}
TimelineRoute::Profile(pubkey) => render_profile_route(
&pubkey,
ndb,
profiles,
img_cache,
note_cache,
col,
ui,
&accounts.mutefun(),
),
TimelineRoute::Quote(id) => {
let txn = Transaction::new(ndb).expect("txn");
let note = if let Ok(note) = ndb.get_note_by_id(&txn, id.bytes()) {
note
} else {
ui.label("Quote of unknown note");
return None;
};
let id = egui::Id::new(("post", col, note.key().unwrap()));
let poster = accounts.selected_or_first_nsec()?;
let draft = drafts.quote_mut(note.id());
let response = egui::ScrollArea::vertical().show(ui, |ui| {
QuoteRepostView::new(ndb, poster, note_cache, img_cache, draft, &note)
.id_source(id)
.show(ui)
});
response.inner.action.map(Into::into)
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn render_profile_route(
pubkey: &Pubkey,
ndb: &Ndb,
profiles: &mut NotesHolderStorage<Profile>,
img_cache: &mut ImageCache,
note_cache: &mut NoteCache,
col: usize,
ui: &mut egui::Ui,
is_muted: &MuteFun,
) -> Option<RenderNavAction> {
let note_action = ProfileView::new(
pubkey,
col,
profiles,
ndb,
note_cache,
img_cache,
NoteOptions::default(),
)
.ui(ui, is_muted);
note_action.map(RenderNavAction::NoteAction)
}