commit 00091c508845a3e5a1beec322e9e4de9ac932ad8
parent 4379466d1db434387be5569f61e375059d634df7
Author: William Casarin <jb55@jb55.com>
Date: Tue, 10 Sep 2024 15:27:54 -0700
Switch to Columns
Also refactor damus app usage to only pass in things that we need in views.
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
30 files changed, 1595 insertions(+), 1098 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1116,7 +1116,7 @@ dependencies = [
[[package]]
name = "egui_nav"
version = "0.1.0"
-source = "git+https://github.com/damus-io/egui-nav?branch=egui-0.28#a8dd95d2ae2a9a5c5251d47407320ad0eb074953"
+source = "git+https://github.com/damus-io/egui-nav?rev=b19742503329a13df660ac8c5a3ada4a25b7cc53#b19742503329a13df660ac8c5a3ada4a25b7cc53"
dependencies = [
"egui",
"egui_extras",
diff --git a/Cargo.toml b/Cargo.toml
@@ -18,7 +18,7 @@ eframe = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da
egui_extras = { git = "https://github.com/emilk/egui", rev = "fcb7764e48ce00f8f8e58da10f937410d65b0bfb", package = "egui_extras", features = ["all_loaders"] }
ehttp = "0.2.0"
egui_tabs = { git = "https://github.com/damus-io/egui-tabs", branch = "egui-0.28" }
-egui_nav = { git = "https://github.com/damus-io/egui-nav", branch = "egui-0.28" }
+egui_nav = { git = "https://github.com/damus-io/egui-nav", rev = "b19742503329a13df660ac8c5a3ada4a25b7cc53" }
egui_virtual_list = { git = "https://github.com/jb55/hello_egui", branch = "egui-0.28", package = "egui_virtual_list" }
reqwest = { version = "0.12.4", default-features = false, features = [ "rustls-tls-native-roots" ] }
image = { version = "0.25", features = ["jpeg", "png", "webp"] }
diff --git a/enostr/src/keypair.rs b/enostr/src/keypair.rs
@@ -32,11 +32,11 @@ impl Keypair {
}
}
- pub fn to_full(self) -> Option<FullKeypair> {
- if let Some(secret_key) = self.secret_key {
- Some(FullKeypair {
- pubkey: self.pubkey,
- secret_key,
+ pub fn to_full<'a>(&'a self) -> Option<FilledKeypair<'a>> {
+ if let Some(secret_key) = &self.secret_key {
+ Some(FilledKeypair {
+ pubkey: &self.pubkey,
+ secret_key: secret_key,
})
} else {
None
@@ -44,17 +44,40 @@ impl Keypair {
}
}
-#[derive(Debug, Eq, PartialEq)]
+#[derive(Debug, Eq, PartialEq, Clone)]
pub struct FullKeypair {
pub pubkey: Pubkey,
pub secret_key: SecretKey,
}
+#[derive(Debug, Eq, PartialEq, Clone, Copy)]
+pub struct FilledKeypair<'a> {
+ pub pubkey: &'a Pubkey,
+ pub secret_key: &'a SecretKey,
+}
+
+impl<'a> FilledKeypair<'a> {
+ pub fn new(pubkey: &'a Pubkey, secret_key: &'a SecretKey) -> Self {
+ FilledKeypair { pubkey, secret_key }
+ }
+
+ pub fn to_full(&self) -> FullKeypair {
+ FullKeypair {
+ pubkey: self.pubkey.to_owned(),
+ secret_key: self.secret_key.to_owned(),
+ }
+ }
+}
+
impl FullKeypair {
pub fn new(pubkey: Pubkey, secret_key: SecretKey) -> Self {
FullKeypair { pubkey, secret_key }
}
+ pub fn to_filled<'a>(&'a self) -> FilledKeypair<'a> {
+ FilledKeypair::new(&self.pubkey, &self.secret_key)
+ }
+
pub fn generate() -> Self {
let mut rng = nostr::secp256k1::rand::rngs::OsRng;
let (secret_key, _) = &nostr::SECP256K1.generate_keypair(&mut rng);
diff --git a/enostr/src/lib.rs b/enostr/src/lib.rs
@@ -11,7 +11,7 @@ pub use client::ClientMessage;
pub use error::Error;
pub use ewebsock;
pub use filter::Filter;
-pub use keypair::{FullKeypair, Keypair, SerializableKeypair};
+pub use keypair::{FilledKeypair, FullKeypair, Keypair, SerializableKeypair};
pub use nostr::SecretKey;
pub use note::{Note, NoteId};
pub use profile::Profile;
diff --git a/src/account_manager.rs b/src/account_manager.rs
@@ -1,6 +1,6 @@
use std::cmp::Ordering;
-use enostr::Keypair;
+use enostr::{FilledKeypair, Keypair};
use crate::key_storage::{KeyStorage, KeyStorageResponse, KeyStorageType};
pub use crate::user_account::UserAccount;
@@ -88,6 +88,12 @@ impl AccountManager {
self.currently_selected_account
}
+ pub fn selected_or_first_nsec(&self) -> Option<FilledKeypair<'_>> {
+ self.get_selected_account()
+ .and_then(|kp| kp.to_full())
+ .or_else(|| self.accounts.iter().find_map(|a| a.to_full()))
+ }
+
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) {
diff --git a/src/actionbar.rs b/src/actionbar.rs
@@ -1,11 +1,12 @@
use crate::{
+ column::Column,
note::NoteRef,
+ notecache::NoteCache,
route::Route,
- thread::{Thread, ThreadResult},
- Damus,
+ thread::{Thread, ThreadResult, Threads},
};
-use enostr::NoteId;
-use nostrdb::Transaction;
+use enostr::{NoteId, RelayPool};
+use nostrdb::{Ndb, Transaction};
use tracing::{error, info};
use uuid::Uuid;
@@ -30,26 +31,28 @@ pub enum BarResult {
/// the thread view. We don't have a concept of model/view/controller etc
/// in egui, but this is the closest thing to that.
fn open_thread(
- app: &mut Damus,
+ ndb: &Ndb,
txn: &Transaction,
- timeline: usize,
+ column: &mut Column,
+ note_cache: &mut NoteCache,
+ pool: &mut RelayPool,
+ threads: &mut Threads,
selected_note: &[u8; 32],
) -> Option<BarResult> {
{
- let timeline = &mut app.timelines[timeline];
- timeline
- .routes
+ column
+ .routes_mut()
.push(Route::Thread(NoteId::new(selected_note.to_owned())));
- timeline.navigating = true;
+ column.navigating = true;
}
- let root_id = crate::note::root_note_id_from_selected_id(app, txn, selected_note);
- let thread_res = app.threads.thread_mut(&app.ndb, txn, root_id);
+ let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note);
+ let thread_res = threads.thread_mut(ndb, txn, root_id);
let (thread, result) = match thread_res {
ThreadResult::Stale(thread) => {
// The thread is stale, let's update it
- let notes = Thread::new_notes(&thread.view.notes, root_id, txn, &app.ndb);
+ let notes = Thread::new_notes(&thread.view.notes, root_id, txn, ndb);
let bar_result = if notes.is_empty() {
None
} else {
@@ -76,14 +79,14 @@ fn open_thread(
// an active subscription for this thread.
if thread.subscription().is_none() {
let filters = Thread::filters(root_id);
- *thread.subscription_mut() = app.ndb.subscribe(&filters).ok();
+ *thread.subscription_mut() = ndb.subscribe(&filters).ok();
if thread.remote_subscription().is_some() {
error!("Found active remote subscription when it was not expected");
} else {
let subid = Uuid::new_v4().to_string();
*thread.remote_subscription_mut() = Some(subid.clone());
- app.pool.subscribe(subid, filters);
+ pool.subscribe(subid, filters);
}
match thread.subscription() {
@@ -91,7 +94,7 @@ fn open_thread(
thread.subscribers += 1;
info!(
"Locally/remotely subscribing to thread. {} total active subscriptions, {} on this thread",
- app.ndb.subscription_count(),
+ ndb.subscription_count(),
thread.subscribers,
);
}
@@ -104,7 +107,7 @@ fn open_thread(
thread.subscribers += 1;
info!(
"Re-using existing thread subscription. {} total active subscriptions, {} on this thread",
- app.ndb.subscription_count(),
+ ndb.subscription_count(),
thread.subscribers,
)
}
@@ -113,24 +116,29 @@ fn open_thread(
}
impl BarAction {
+ #[allow(clippy::too_many_arguments)]
pub fn execute(
self,
- app: &mut Damus,
- timeline: usize,
+ ndb: &Ndb,
+ column: &mut Column,
+ threads: &mut Threads,
+ note_cache: &mut NoteCache,
+ pool: &mut RelayPool,
replying_to: &[u8; 32],
txn: &Transaction,
) -> Option<BarResult> {
match self {
BarAction::Reply => {
- let timeline = &mut app.timelines[timeline];
- timeline
- .routes
+ column
+ .routes_mut()
.push(Route::Reply(NoteId::new(replying_to.to_owned())));
- timeline.navigating = true;
+ column.navigating = true;
None
}
- BarAction::OpenThread => open_thread(app, txn, timeline, replying_to),
+ BarAction::OpenThread => {
+ open_thread(ndb, txn, column, note_cache, pool, threads, replying_to)
+ }
}
}
}
diff --git a/src/app.rs b/src/app.rs
@@ -3,7 +3,7 @@ use crate::actionbar::BarResult;
use crate::app_creation::setup_cc;
use crate::app_style::user_requested_visuals_change;
use crate::args::Args;
-use crate::column::ColumnKind;
+use crate::column::{Column, ColumnKind, Columns};
use crate::draft::Drafts;
use crate::error::{Error, FilterError};
use crate::filter::FilterState;
@@ -16,7 +16,7 @@ use crate::relay_pool_manager::RelayPoolManager;
use crate::route::Route;
use crate::subscriptions::{SubKind, Subscriptions};
use crate::thread::{DecrementResult, Threads};
-use crate::timeline::{Timeline, TimelineSource, ViewFilter};
+use crate::timeline::{Timeline, TimelineKind, TimelineSource, ViewFilter};
use crate::ui::note::PostAction;
use crate::ui::{self, AccountSelectionWidget, DesktopGlobalPopup};
use crate::ui::{DesktopSidePanel, RelayView, View};
@@ -24,8 +24,6 @@ use crate::unknowns::UnknownIds;
use crate::{filter, Result};
use egui_nav::{Nav, NavAction};
use enostr::{ClientMessage, RelayEvent, RelayMessage, RelayPool};
-use std::cell::RefCell;
-use std::rc::Rc;
use uuid::Uuid;
use egui::{Context, Frame, Style};
@@ -53,15 +51,13 @@ pub struct Damus {
/// global navigation for account management popups, etc.
pub global_nav: Vec<Route>,
- pub timelines: Vec<Timeline>,
- pub selected_timeline: i32,
-
+ pub columns: Columns,
pub ndb: Ndb,
pub unknown_ids: UnknownIds,
pub drafts: Drafts,
pub threads: Threads,
pub img_cache: ImageCache,
- pub account_manager: AccountManager,
+ pub accounts: AccountManager,
pub subscriptions: Subscriptions,
frame_history: crate::frame_history::FrameHistory,
@@ -99,10 +95,15 @@ fn relay_setup(pool: &mut RelayPool, ctx: &egui::Context) {
}
}
-fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) {
- let can_since_optimize = damus.since_optimize;
-
- let filter_state = damus.timelines[timeline].filter.clone();
+fn send_initial_timeline_filter(
+ ndb: &Ndb,
+ can_since_optimize: bool,
+ subs: &mut Subscriptions,
+ pool: &mut RelayPool,
+ timeline: &mut Timeline,
+ to: &str,
+) {
+ let filter_state = timeline.filter.clone();
match filter_state {
FilterState::Broken(err) => {
@@ -131,7 +132,7 @@ fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) {
filter = filter.limit_mut(lim);
}
- let notes = damus.timelines[timeline].notes(ViewFilter::NotesAndReplies);
+ 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
@@ -148,38 +149,41 @@ fn send_initial_timeline_filter(damus: &mut Damus, timeline: usize, to: &str) {
filter
}).collect();
- let sub_id = damus.gen_subid(&SubKind::Initial);
- damus
- .subscriptions()
- .insert(sub_id.clone(), SubKind::Initial);
+ //let sub_id = damus.gen_subid(&SubKind::Initial);
+ let sub_id = Uuid::new_v4().to_string();
+ subs.subs.insert(sub_id.clone(), SubKind::Initial);
let cmd = ClientMessage::req(sub_id, new_filters);
- damus.pool.send_to(&cmd, to);
+ pool.send_to(&cmd, to);
}
// we need some data first
FilterState::NeedsRemote(filter) => {
- let uid = damus.timelines[timeline].uid;
- let sub_kind = SubKind::FetchingContactList(uid);
- let sub_id = damus.gen_subid(&sub_kind);
- let local_sub = damus.ndb.subscribe(&filter).expect("sub");
+ let sub_kind = SubKind::FetchingContactList(timeline.id);
+ //let sub_id = damus.gen_subid(&sub_kind);
+ let sub_id = Uuid::new_v4().to_string();
+ let local_sub = ndb.subscribe(&filter).expect("sub");
- damus.timelines[timeline].filter =
- FilterState::fetching_remote(sub_id.clone(), local_sub);
+ timeline.filter = FilterState::fetching_remote(sub_id.clone(), local_sub);
- damus.subscriptions().insert(sub_id.clone(), sub_kind);
+ subs.subs.insert(sub_id.clone(), sub_kind);
- damus.pool.subscribe(sub_id, filter.to_owned());
+ pool.subscribe(sub_id, filter.to_owned());
}
}
}
fn send_initial_filters(damus: &mut Damus, relay_url: &str) {
info!("Sending initial filters to {}", relay_url);
- let timelines = damus.timelines.len();
-
- for i in 0..timelines {
- send_initial_timeline_filter(damus, i, relay_url);
+ for timeline in damus.columns.timelines_mut() {
+ send_initial_timeline_filter(
+ &damus.ndb,
+ damus.since_optimize,
+ &mut damus.subscriptions,
+ &mut damus.pool,
+ timeline,
+ relay_url,
+ );
}
}
@@ -190,7 +194,7 @@ enum ContextAction {
fn handle_key_events(
input: &egui::InputState,
pixels_per_point: f32,
- damus: &mut Damus,
+ columns: &mut Columns,
) -> Option<ContextAction> {
let amount = 0.2;
@@ -213,16 +217,16 @@ fn handle_key_events(
Some(ContextAction::SetPixelsPerPoint(pixels_per_point - amount));
}
egui::Key::J => {
- damus.select_down();
+ columns.select_down();
}
egui::Key::K => {
- damus.select_up();
+ columns.select_up();
}
egui::Key::H => {
- damus.select_left();
+ columns.select_left();
}
egui::Key::L => {
- damus.select_left();
+ columns.select_left();
}
_ => {}
}
@@ -234,7 +238,7 @@ fn handle_key_events(
fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
let ppp = ctx.pixels_per_point();
- let res = ctx.input(|i| handle_key_events(i, ppp, damus));
+ let res = ctx.input(|i| handle_key_events(i, ppp, &mut damus.columns));
if let Some(action) = res {
match action {
ContextAction::SetPixelsPerPoint(amt) => {
@@ -263,12 +267,27 @@ fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
}
}
- for timeline in 0..damus.timelines.len() {
- let src = TimelineSource::column(timeline);
+ let n_cols = damus.columns.columns().len();
+ for col_ind in 0..n_cols {
+ let timeline =
+ if let ColumnKind::Timeline(timeline) = damus.columns.column_mut(col_ind).kind_mut() {
+ timeline
+ } else {
+ continue;
+ };
- if let Ok(true) = is_timeline_ready(damus, timeline) {
+ if let Ok(true) =
+ is_timeline_ready(&damus.ndb, &mut damus.pool, &mut damus.note_cache, timeline)
+ {
let txn = Transaction::new(&damus.ndb).expect("txn");
- if let Err(err) = src.poll_notes_into_view(&txn, damus) {
+ if let Err(err) = TimelineSource::column(timeline.id).poll_notes_into_view(
+ &txn,
+ &damus.ndb,
+ &mut damus.columns,
+ &mut damus.threads,
+ &mut damus.unknown_ids,
+ &mut damus.note_cache,
+ ) {
error!("poll_notes_into_view: {err}");
}
} else {
@@ -298,8 +317,13 @@ fn unknown_id_send(damus: &mut Damus) {
/// 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.
-fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result<bool> {
- let sub = match &damus.timelines[timeline].filter {
+fn is_timeline_ready(
+ ndb: &Ndb,
+ pool: &mut RelayPool,
+ note_cache: &mut NoteCache,
+ timeline: &mut Timeline,
+) -> Result<bool> {
+ let sub = match &timeline.filter {
FilterState::GotRemote(sub) => *sub,
FilterState::Ready(_f) => return Ok(true),
_ => return Ok(false),
@@ -307,9 +331,12 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result<bool> {
// We got at least one eose for our filter request. Let's see
// if nostrdb is done processing it yet.
- let res = damus.ndb.poll_for_notes(sub, 1);
+ let res = ndb.poll_for_notes(sub, 1);
if res.is_empty() {
- debug!("check_timeline_filter_state: no notes found (yet?) for timeline {timeline}");
+ debug!(
+ "check_timeline_filter_state: no notes found (yet?) for timeline {:?}",
+ timeline
+ );
return Ok(false);
}
@@ -318,8 +345,8 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result<bool> {
let note_key = res[0];
let filter = {
- let txn = Transaction::new(&damus.ndb).expect("txn");
- let note = damus.ndb.get_note_by_key(&txn, note_key).expect("note");
+ let txn = Transaction::new(ndb).expect("txn");
+ let note = ndb.get_note_by_key(&txn, note_key).expect("note");
filter::filter_from_tags(¬e).map(|f| f.into_follow_filter())
};
@@ -327,23 +354,24 @@ fn is_timeline_ready(damus: &mut Damus, timeline: usize) -> Result<bool> {
match filter {
Err(Error::Filter(e)) => {
error!("got broken when building filter {e}");
- damus.timelines[timeline].filter = FilterState::broken(e);
+ timeline.filter = FilterState::broken(e);
}
Err(err) => {
error!("got broken when building filter {err}");
- damus.timelines[timeline].filter = FilterState::broken(FilterError::EmptyContactList);
+ timeline.filter = FilterState::broken(FilterError::EmptyContactList);
return Err(err);
}
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(damus, timeline, &filter).expect("setup init");
- damus.timelines[timeline].filter = FilterState::ready(filter.clone());
+ setup_initial_timeline(ndb, timeline, note_cache, &filter).expect("setup init");
+ timeline.filter = FilterState::ready(filter.clone());
- let ck = &damus.timelines[timeline].kind;
- let subid = damus.gen_subid(&SubKind::Column(ck.clone()));
- damus.pool.subscribe(subid, filter)
+ //let ck = &timeline.kind;
+ //let subid = damus.gen_subid(&SubKind::Column(ck.clone()));
+ let subid = Uuid::new_v4().to_string();
+ pool.subscribe(subid, filter)
}
}
@@ -355,18 +383,23 @@ fn setup_profiling() {
puffin::set_scopes_on(true); // tell puffin to collect data
}
-fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter]) -> Result<()> {
- damus.timelines[timeline].subscription = Some(damus.ndb.subscribe(filters)?);
- let txn = Transaction::new(&damus.ndb)?;
+fn setup_initial_timeline(
+ ndb: &Ndb,
+ timeline: &mut Timeline,
+ note_cache: &mut NoteCache,
+ filters: &[Filter],
+) -> Result<()> {
+ timeline.subscription = Some(ndb.subscribe(filters)?);
+ let txn = Transaction::new(ndb)?;
debug!(
"querying nostrdb sub {:?} {:?}",
- damus.timelines[timeline].subscription, damus.timelines[timeline].filter
+ timeline.subscription, timeline.filter
);
let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32;
- let results = damus.ndb.query(&txn, filters, lim)?;
+ let results = ndb.query(&txn, filters, lim)?;
let filters = {
- let views = &damus.timelines[timeline].views;
+ let views = &timeline.views;
let filters: Vec<fn(&CachedNote, &Note) -> bool> =
views.iter().map(|v| v.filter.filter()).collect();
filters
@@ -375,12 +408,10 @@ fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter]
for result in results {
for (view, filter) in filters.iter().enumerate() {
if filter(
- damus
- .note_cache_mut()
- .cached_note_or_insert_mut(result.note_key, &result.note),
+ note_cache.cached_note_or_insert_mut(result.note_key, &result.note),
&result.note,
) {
- damus.timelines[timeline].views[view].notes.push(NoteRef {
+ timeline.views[view].notes.push(NoteRef {
key: result.note_key,
created_at: result.note.created_at(),
})
@@ -391,12 +422,16 @@ fn setup_initial_timeline(damus: &mut Damus, timeline: usize, filters: &[Filter]
Ok(())
}
-fn setup_initial_nostrdb_subs(damus: &mut Damus) -> Result<()> {
- let timelines = damus.timelines.len();
- for i in 0..timelines {
- let filter = damus.timelines[i].filter.clone();
- match filter {
- FilterState::Ready(filters) => setup_initial_timeline(damus, i, &filters)?,
+fn setup_initial_nostrdb_subs(
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ columns: &mut Columns,
+) -> Result<()> {
+ for timeline in columns.timelines_mut() {
+ match &timeline.filter {
+ FilterState::Ready(filters) => {
+ { setup_initial_timeline(ndb, timeline, note_cache, &filters.clone()) }?
+ }
FilterState::Broken(err) => {
error!("FetchingRemote state broken in setup_initial_nostr_subs: {err}")
@@ -427,7 +462,8 @@ fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
damus
.subscriptions()
.insert("unknownids".to_string(), SubKind::OneShot);
- setup_initial_nostrdb_subs(damus).expect("home subscription failed");
+ setup_initial_nostrdb_subs(&damus.ndb, &mut damus.note_cache, &mut damus.columns)
+ .expect("home subscription failed");
}
if let Err(err) = try_process_event(damus, ctx) {
@@ -458,12 +494,18 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
};
match *sub_kind {
- SubKind::Column(_) => {
- // eose on column? whatevs
+ SubKind::Timeline(_) => {
+ // eose on timeline? whatevs
}
SubKind::Initial => {
let txn = Transaction::new(&damus.ndb)?;
- UnknownIds::update(&txn, damus);
+ UnknownIds::update(
+ &txn,
+ &mut damus.unknown_ids,
+ &damus.columns,
+ &damus.ndb,
+ &mut damus.note_cache,
+ );
// this is possible if this is the first time
if damus.unknown_ids.ready_to_send() {
unknown_id_send(damus);
@@ -477,8 +519,8 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
}
SubKind::FetchingContactList(timeline_uid) => {
- let timeline_ind = if let Some(i) = damus.find_timeline(timeline_uid) {
- i
+ let timeline = if let Some(tl) = damus.columns.find_timeline_mut(timeline_uid) {
+ tl
} else {
error!(
"timeline uid:{} not found for FetchingContactList",
@@ -487,35 +529,25 @@ fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
return Ok(());
};
- let local_sub = if let FilterState::FetchingRemote(unisub) =
- &damus.timelines[timeline_ind].filter
- {
+ // If this request was fetching a contact list, our filter
+ // state should be "FetchingRemote". We look at the local
+ // subscription for that filter state and get the subscription id
+ let local_sub = if let FilterState::FetchingRemote(unisub) = &timeline.filter {
unisub.local
} else {
// TODO: we could have multiple contact list results, we need
// to check to see if this one is newer and use that instead
warn!(
"Expected timeline to have FetchingRemote state but was {:?}",
- damus.timelines[timeline_ind].filter
+ timeline.filter
);
return Ok(());
};
- damus.timelines[timeline_ind].filter = FilterState::got_remote(local_sub);
-
- /*
- // see if we're fast enough to catch a processed contact list
- let note_keys = damus.ndb.poll_for_notes(local_sub, 1);
- if !note_keys.is_empty() {
- debug!("fast! caught contact list from {relay_url} right away");
- let txn = Transaction::new(&damus.ndb)?;
- let note_key = note_keys[0];
- let nr = damus.ndb.get_note_by_key(&txn, note_key)?;
- let filter = filter::filter_from_tags(&nr)?.into_follow_filter();
- setup_initial_timeline(damus, timeline, &filter)
- damus.timelines[timeline_ind].filter = FilterState::ready(filter);
- }
- */
+ // We take the subscription id and pass it to the new state of
+ // "GotRemote". This will let future frames know that it can try
+ // to look for the contact list in nostrdb.
+ timeline.filter = FilterState::got_remote(local_sub);
}
}
@@ -593,7 +625,7 @@ impl Damus {
let mut config = Config::new();
config.set_ingester_threads(4);
- let mut account_manager = AccountManager::new(
+ let mut accounts = AccountManager::new(
// TODO: should pull this from settings
None,
// TODO: use correct KeyStorage mechanism for current OS arch
@@ -602,12 +634,12 @@ impl Damus {
for key in parsed_args.keys {
info!("adding account: {}", key.pubkey);
- account_manager.add_account(key);
+ accounts.add_account(key);
}
// TODO: pull currently selected account from settings
- if account_manager.num_accounts() > 0 {
- account_manager.select_account(0);
+ if accounts.num_accounts() > 0 {
+ accounts.select_account(0);
}
// setup relays if we have them
@@ -629,27 +661,27 @@ impl Damus {
pool
};
- let account = account_manager
+ let account = accounts
.get_selected_account()
.as_ref()
.map(|a| a.pubkey.bytes());
let ndb = Ndb::new(&dbpath, &config).expect("ndb");
- let mut timelines: Vec<Timeline> = Vec::with_capacity(parsed_args.columns.len());
+ let mut columns: Vec<Column> = Vec::with_capacity(parsed_args.columns.len());
for col in parsed_args.columns {
if let Some(timeline) = col.into_timeline(&ndb, account) {
- timelines.push(timeline);
+ columns.push(Column::timeline(timeline));
}
}
let debug = parsed_args.debug;
- if timelines.is_empty() {
+ if columns.is_empty() {
let filter = Filter::from_json(include_str!("../queries/timeline.json")).unwrap();
- timelines.push(Timeline::new(
- ColumnKind::Generic,
+ columns.push(Column::timeline(Timeline::new(
+ TimelineKind::Generic,
FilterState::ready(vec![filter]),
- ));
+ )));
}
Self {
@@ -663,11 +695,10 @@ impl Damus {
state: DamusState::Initializing,
img_cache: ImageCache::new(imgcache_dir.into()),
note_cache: NoteCache::default(),
- selected_timeline: 0,
- timelines,
+ columns: Columns::new(columns),
textmode: parsed_args.textmode,
ndb,
- account_manager,
+ accounts,
frame_history: FrameHistory::default(),
show_account_switcher: false,
show_global_popup: false,
@@ -684,12 +715,12 @@ impl Damus {
}
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
- let mut timelines: Vec<Timeline> = vec![];
+ let mut columns: Vec<Column> = vec![];
let filter = Filter::from_json(include_str!("../queries/global.json")).unwrap();
- timelines.push(Timeline::new(
- ColumnKind::Universe,
+ columns.push(Column::timeline(Timeline::new(
+ TimelineKind::Universe,
FilterState::ready(vec![filter]),
- ));
+ )));
let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir());
let _ = std::fs::create_dir_all(imgcache_dir.clone());
@@ -708,11 +739,10 @@ impl Damus {
pool: RelayPool::new(),
img_cache: ImageCache::new(imgcache_dir),
note_cache: NoteCache::default(),
- selected_timeline: 0,
- timelines,
+ columns: Columns::new(columns),
textmode: false,
ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"),
- account_manager: AccountManager::new(None, KeyStorageType::None),
+ accounts: AccountManager::new(None, KeyStorageType::None),
frame_history: FrameHistory::default(),
show_account_switcher: false,
show_global_popup: true,
@@ -720,16 +750,6 @@ impl Damus {
}
}
- pub fn find_timeline(&self, uid: u32) -> Option<usize> {
- for (i, timeline) in self.timelines.iter().enumerate() {
- if timeline.uid == uid {
- return Some(i);
- }
- }
-
- None
- }
-
pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> {
&mut self.subscriptions.subs
}
@@ -741,32 +761,6 @@ impl Damus {
pub fn note_cache(&self) -> &NoteCache {
&self.note_cache
}
-
- pub fn selected_timeline(&mut self) -> &mut Timeline {
- &mut self.timelines[self.selected_timeline as usize]
- }
-
- pub fn select_down(&mut self) {
- self.selected_timeline().current_view_mut().select_down();
- }
-
- pub fn select_up(&mut self) {
- self.selected_timeline().current_view_mut().select_up();
- }
-
- pub fn select_left(&mut self) {
- if self.selected_timeline - 1 < 0 {
- return;
- }
- self.selected_timeline -= 1;
- }
-
- pub fn select_right(&mut self) {
- if self.selected_timeline + 1 >= self.timelines.len() as i32 {
- return;
- }
- self.selected_timeline += 1;
- }
}
/*
@@ -797,7 +791,7 @@ fn top_panel(ctx: &egui::Context) -> egui::TopBottomPanel {
.show_separator_line(false)
}
-fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) {
+fn render_panel(ctx: &egui::Context, app: &mut Damus) {
top_panel(ctx).show(ctx, |ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
ui.visuals_mut().button_frame = false;
@@ -843,26 +837,34 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) {
app.frame_history.mean_frame_time() * 1e3
));
- if !app.timelines.is_empty() {
+ /*
+ if !app.timelines().count().is_empty() {
ui.weak(format!(
"{} notes",
- &app.timelines[timeline_ind]
+ &app.timelines()
.notes(ViewFilter::NotesAndReplies)
.len()
));
}
+ */
}
});
});
}
/// Local thread unsubscribe
-fn thread_unsubscribe(app: &mut Damus, id: &[u8; 32]) {
+fn thread_unsubscribe(
+ ndb: &Ndb,
+ threads: &mut Threads,
+ pool: &mut RelayPool,
+ note_cache: &mut NoteCache,
+ id: &[u8; 32],
+) {
let (unsubscribe, remote_subid) = {
- let txn = Transaction::new(&app.ndb).expect("txn");
- let root_id = crate::note::root_note_id_from_selected_id(app, &txn, id);
+ let txn = Transaction::new(ndb).expect("txn");
+ let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id);
- let thread = app.threads.thread_mut(&app.ndb, &txn, root_id).get_ptr();
+ let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr();
let unsub = thread.decrement_sub();
let mut remote_subid: Option<String> = None;
@@ -877,30 +879,30 @@ fn thread_unsubscribe(app: &mut Damus, id: &[u8; 32]) {
match unsubscribe {
Ok(DecrementResult::LastSubscriber(sub)) => {
- if let Err(e) = app.ndb.unsubscribe(sub) {
+ if let Err(e) = ndb.unsubscribe(sub) {
error!(
"failed to unsubscribe from thread: {e}, subid:{}, {} active subscriptions",
sub.id(),
- app.ndb.subscription_count()
+ ndb.subscription_count()
);
} else {
info!(
"Unsubscribed from thread subid:{}. {} active subscriptions",
sub.id(),
- app.ndb.subscription_count()
+ ndb.subscription_count()
);
}
// unsub from remote
if let Some(subid) = remote_subid {
- app.pool.unsubscribe(subid);
+ pool.unsubscribe(subid);
}
}
Ok(DecrementResult::ActiveSubscribers) => {
info!(
"Keeping thread subscription. {} active subscriptions.",
- app.ndb.subscription_count()
+ ndb.subscription_count()
);
// do nothing
}
@@ -909,25 +911,49 @@ fn thread_unsubscribe(app: &mut Damus, id: &[u8; 32]) {
// something is wrong!
error!(
"Thread unsubscribe error: {e}. {} active subsciptions.",
- app.ndb.subscription_count()
+ ndb.subscription_count()
);
}
}
}
-fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut egui::Ui) {
- let navigating = app.timelines[timeline_ind].navigating;
- let returning = app.timelines[timeline_ind].returning;
- let app_ctx = Rc::new(RefCell::new(app));
+fn render_nav(show_postbox: bool, col: usize, app: &mut Damus, ui: &mut egui::Ui) {
+ let navigating = app.columns.column(col).navigating;
+ let returning = app.columns.column(col).returning;
- let nav_response = Nav::new(routes)
+ let nav_response = Nav::new(app.columns.column(col).routes().to_vec())
.navigating(navigating)
.returning(returning)
.title(false)
- .show(ui, |ui, nav| match nav.top() {
+ .show_mut(ui, |ui, nav| match nav.top() {
Route::Timeline(_n) => {
- let app = &mut app_ctx.borrow_mut();
- ui::TimelineView::new(app, timeline_ind).ui(ui);
+ let column = app.columns.column_mut(col);
+ if column.kind().timeline().is_some() {
+ if show_postbox {
+ if let Some(kp) = app.accounts.selected_or_first_nsec() {
+ ui::timeline::postbox_view(
+ &app.ndb,
+ kp,
+ &mut app.pool,
+ &mut app.drafts,
+ &mut app.img_cache,
+ ui,
+ );
+ }
+ }
+ ui::TimelineView::new(
+ &app.ndb,
+ column,
+ &mut app.note_cache,
+ &mut app.img_cache,
+ &mut app.threads,
+ &mut app.pool,
+ app.textmode,
+ )
+ .ui(ui);
+ } else {
+ ui.label("no timeline for this column?");
+ }
None
}
@@ -937,15 +963,25 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
}
Route::Relays => {
- let pool = &mut app_ctx.borrow_mut().pool;
- let manager = RelayPoolManager::new(pool);
+ let manager = RelayPoolManager::new(&mut app.pool);
RelayView::new(manager).ui(ui);
None
}
Route::Thread(id) => {
- let app = &mut app_ctx.borrow_mut();
- let result = ui::ThreadView::new(app, timeline_ind, id.bytes()).ui(ui);
+ let result = ui::ThreadView::new(
+ col,
+ &mut app.columns,
+ &mut app.threads,
+ &app.ndb,
+ &mut app.note_cache,
+ &mut app.img_cache,
+ &mut app.unknown_ids,
+ &mut app.pool,
+ app.textmode,
+ id.bytes(),
+ )
+ .ui(ui);
if let Some(bar_result) = result {
match bar_result {
@@ -960,8 +996,6 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
}
Route::Reply(id) => {
- let mut app = app_ctx.borrow_mut();
-
let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
txn
} else {
@@ -976,32 +1010,55 @@ fn render_nav(routes: Vec<Route>, timeline_ind: usize, app: &mut Damus, ui: &mut
return None;
};
- let id = egui::Id::new(("post", timeline_ind, note.key().unwrap()));
- let response = egui::ScrollArea::vertical().show(ui, |ui| {
- ui::PostReplyView::new(&mut app, ¬e)
+ let id = egui::Id::new((
+ "post",
+ app.columns.column(col).view_id(),
+ note.key().unwrap(),
+ ));
+
+ if let Some(poster) = app.accounts.selected_or_first_nsec() {
+ let response = egui::ScrollArea::vertical().show(ui, |ui| {
+ ui::PostReplyView::new(
+ &app.ndb,
+ poster,
+ &mut app.pool,
+ &mut app.drafts,
+ &mut app.note_cache,
+ &mut app.img_cache,
+ ¬e,
+ )
.id_source(id)
.show(ui)
- });
+ });
- Some(response)
+ Some(response)
+ } else {
+ None
+ }
}
});
- let mut app = app_ctx.borrow_mut();
+ let column = app.columns.column_mut(col);
if let Some(reply_response) = nav_response.inner {
if let Some(PostAction::Post(_np)) = reply_response.inner.action {
- app.timelines[timeline_ind].returning = true;
+ column.returning = true;
}
}
if let Some(NavAction::Returned) = nav_response.action {
- let popped = app.timelines[timeline_ind].routes.pop();
+ let popped = column.routes_mut().pop();
if let Some(Route::Thread(id)) = popped {
- thread_unsubscribe(&mut app, id.bytes());
+ thread_unsubscribe(
+ &app.ndb,
+ &mut app.threads,
+ &mut app.pool,
+ &mut app.note_cache,
+ id.bytes(),
+ );
}
- app.timelines[timeline_ind].returning = false;
+ column.returning = false;
} else if let Some(NavAction::Navigated) = nav_response.action {
- app.timelines[timeline_ind].navigating = false;
+ column.navigating = false;
}
}
@@ -1014,7 +1071,9 @@ fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
//let routes = app.timelines[0].routes.clone();
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
- render_nav(app.timelines[0].routes.clone(), 0, app, ui);
+ if !app.columns.columns().is_empty() {
+ render_nav(false, 0, app, ui);
+ }
});
}
@@ -1033,12 +1092,12 @@ fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel {
}
fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
- render_panel(ctx, app, 0);
+ render_panel(ctx, app);
#[cfg(feature = "profiling")]
puffin::profile_function!();
let screen_size = ctx.screen_rect().width();
- let calc_panel_width = (screen_size / app.timelines.len() as f32) - 30.0;
+ let calc_panel_width = (screen_size / app.columns.columns().len() as f32) - 30.0;
let min_width = 320.0;
let need_scroll = calc_panel_width < min_width;
let panel_sizes = if need_scroll {
@@ -1053,18 +1112,18 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
DesktopGlobalPopup::show(app.global_nav.clone(), app, ui);
if need_scroll {
egui::ScrollArea::horizontal().show(ui, |ui| {
- timelines_view(ui, panel_sizes, app, app.timelines.len());
+ timelines_view(ui, panel_sizes, app, app.columns.columns().len());
});
} else {
- timelines_view(ui, panel_sizes, app, app.timelines.len());
+ timelines_view(ui, panel_sizes, app, app.columns.columns().len());
}
});
}
-fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: usize) {
+fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, columns: usize) {
StripBuilder::new(ui)
.size(Size::exact(40.0))
- .sizes(sizes, timelines)
+ .sizes(sizes, columns)
.clip(true)
.horizontal(|mut strip| {
strip.cell(|ui| {
@@ -1085,15 +1144,17 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: us
);
});
- for timeline_ind in 0..timelines {
+ let n_cols = app.columns.columns().len();
+ let mut first = true;
+ for column_ind in 0..n_cols {
strip.cell(|ui| {
let rect = ui.available_rect_before_wrap();
- render_nav(
- app.timelines[timeline_ind].routes.clone(),
- timeline_ind,
- app,
- ui,
- );
+ let show_postbox =
+ first && app.columns.column(column_ind).kind().timeline().is_some();
+ if show_postbox {
+ first = false
+ }
+ render_nav(show_postbox, column_ind, app, ui);
// vertical line
ui.painter().vline(
diff --git a/src/args.rs b/src/args.rs
@@ -1,6 +1,5 @@
-use crate::column::{ColumnKind, PubkeySource};
use crate::filter::FilterState;
-use crate::timeline::Timeline;
+use crate::timeline::{PubkeySource, Timeline, TimelineKind};
use enostr::{Filter, Keypair, Pubkey, SecretKey};
use nostrdb::Ndb;
use tracing::{debug, error, info};
@@ -137,22 +136,24 @@ impl Args {
if let Some(rest) = column_name.strip_prefix("contacts:") {
if let Ok(pubkey) = Pubkey::parse(rest) {
info!("contact column for user {}", pubkey.hex());
- res.columns.push(ArgColumn::Column(ColumnKind::contact_list(
- PubkeySource::Explicit(pubkey),
- )))
+ res.columns
+ .push(ArgColumn::Timeline(TimelineKind::contact_list(
+ PubkeySource::Explicit(pubkey),
+ )))
} else {
error!("error parsing contacts pubkey {}", rest);
continue;
}
} else if column_name == "contacts" {
- res.columns.push(ArgColumn::Column(ColumnKind::contact_list(
- PubkeySource::DeckAuthor,
- )))
+ res.columns
+ .push(ArgColumn::Timeline(TimelineKind::contact_list(
+ PubkeySource::DeckAuthor,
+ )))
} else if let Some(notif_pk_str) = column_name.strip_prefix("notifications:") {
if let Ok(pubkey) = Pubkey::parse(notif_pk_str) {
info!("got notifications column for user {}", pubkey.hex());
res.columns
- .push(ArgColumn::Column(ColumnKind::notifications(
+ .push(ArgColumn::Timeline(TimelineKind::notifications(
PubkeySource::Explicit(pubkey),
)))
} else {
@@ -162,21 +163,22 @@ impl Args {
} else if column_name == "notifications" {
debug!("got notification column for default user");
res.columns
- .push(ArgColumn::Column(ColumnKind::notifications(
+ .push(ArgColumn::Timeline(TimelineKind::notifications(
PubkeySource::DeckAuthor,
)))
} else if column_name == "profile" {
debug!("got profile column for default user");
- res.columns.push(ArgColumn::Column(ColumnKind::profile(
+ res.columns.push(ArgColumn::Timeline(TimelineKind::profile(
PubkeySource::DeckAuthor,
)))
} else if column_name == "universe" {
debug!("got universe column");
- res.columns.push(ArgColumn::Column(ColumnKind::Universe))
+ res.columns
+ .push(ArgColumn::Timeline(TimelineKind::Universe))
} else if let Some(profile_pk_str) = column_name.strip_prefix("profile:") {
if let Ok(pubkey) = Pubkey::parse(profile_pk_str) {
info!("got profile column for user {}", pubkey.hex());
- res.columns.push(ArgColumn::Column(ColumnKind::profile(
+ res.columns.push(ArgColumn::Timeline(TimelineKind::profile(
PubkeySource::Explicit(pubkey),
)))
} else {
@@ -214,9 +216,9 @@ impl Args {
}
if res.columns.is_empty() {
- let ck = ColumnKind::contact_list(PubkeySource::DeckAuthor);
+ let ck = TimelineKind::contact_list(PubkeySource::DeckAuthor);
info!("No columns set, setting up defaults: {:?}", ck);
- res.columns.push(ArgColumn::Column(ck));
+ res.columns.push(ArgColumn::Timeline(ck));
}
res
@@ -226,7 +228,7 @@ impl Args {
/// A way to define columns from the commandline. Can be column kinds or
/// generic queries
pub enum ArgColumn {
- Column(ColumnKind),
+ Timeline(TimelineKind),
Generic(Vec<Filter>),
}
@@ -234,10 +236,10 @@ impl ArgColumn {
pub fn into_timeline(self, ndb: &Ndb, user: Option<&[u8; 32]>) -> Option<Timeline> {
match self {
ArgColumn::Generic(filters) => Some(Timeline::new(
- ColumnKind::Generic,
+ TimelineKind::Generic,
FilterState::ready(filters),
)),
- ArgColumn::Column(ck) => ck.into_timeline(ndb, user),
+ ArgColumn::Timeline(tk) => tk.into_timeline(ndb, user),
}
}
}
diff --git a/src/column.rs b/src/column.rs
@@ -1,152 +1,170 @@
-use crate::error::FilterError;
-use crate::filter;
-use crate::filter::FilterState;
-use crate::{timeline::Timeline, Error};
-use enostr::Pubkey;
-use nostrdb::{Filter, Ndb, Transaction};
-use std::fmt::Display;
-use tracing::{error, warn};
-
-#[derive(Clone, Debug)]
-pub enum PubkeySource {
- Explicit(Pubkey),
- DeckAuthor,
-}
+use crate::route::Route;
+use crate::timeline::{Timeline, TimelineId};
+use std::iter::Iterator;
+use tracing::warn;
+
+pub struct Column {
+ kind: ColumnKind,
+ routes: Vec<Route>,
-#[derive(Debug, Clone)]
-pub enum ListKind {
- Contact(PubkeySource),
+ pub navigating: bool,
+ pub returning: bool,
}
-///
-/// What kind of column is it?
-/// - Follow List
-/// - Notifications
-/// - DM
-/// - filter
-/// - ... etc
-#[derive(Debug, Clone)]
-pub enum ColumnKind {
- List(ListKind),
+impl Column {
+ pub fn timeline(timeline: Timeline) -> Self {
+ let routes = vec![Route::Timeline(format!("{}", &timeline.kind))];
+ let kind = ColumnKind::Timeline(timeline);
+ Column::new(kind, routes)
+ }
- Notifications(PubkeySource),
+ pub fn kind(&self) -> &ColumnKind {
+ &self.kind
+ }
- Profile(PubkeySource),
+ pub fn kind_mut(&mut self) -> &mut ColumnKind {
+ &mut self.kind
+ }
- Universe,
+ pub fn view_id(&self) -> egui::Id {
+ self.kind.view_id()
+ }
- /// Generic filter
- Generic,
+ pub fn routes(&self) -> &[Route] {
+ &self.routes
+ }
+
+ pub fn routes_mut(&mut self) -> &mut Vec<Route> {
+ &mut self.routes
+ }
+
+ pub fn new(kind: ColumnKind, routes: Vec<Route>) -> Self {
+ let navigating = false;
+ let returning = false;
+ Column {
+ kind,
+ routes,
+ navigating,
+ returning,
+ }
+ }
}
-impl Display for ColumnKind {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- ColumnKind::List(ListKind::Contact(_src)) => f.write_str("Contacts"),
- ColumnKind::Generic => f.write_str("Timeline"),
- ColumnKind::Notifications(_) => f.write_str("Notifications"),
- ColumnKind::Profile(_) => f.write_str("Profile"),
- ColumnKind::Universe => f.write_str("Universe"),
+pub struct Columns {
+ columns: Vec<Column>,
+
+ /// The selected column for key navigation
+ selected: i32,
+}
+
+impl Columns {
+ pub fn columns_mut(&mut self) -> &mut Vec<Column> {
+ &mut self.columns
+ }
+
+ pub fn column(&self, ind: usize) -> &Column {
+ &self.columns()[ind]
+ }
+
+ pub fn columns(&self) -> &[Column] {
+ &self.columns
+ }
+
+ pub fn new(columns: Vec<Column>) -> Self {
+ let selected = -1;
+ Columns { columns, selected }
+ }
+
+ pub fn selected(&mut self) -> &mut Column {
+ &mut self.columns[self.selected as usize]
+ }
+
+ pub fn timelines_mut(&mut self) -> impl Iterator<Item = &mut Timeline> {
+ self.columns
+ .iter_mut()
+ .filter_map(|c| c.kind_mut().timeline_mut())
+ }
+
+ pub fn timelines(&self) -> impl Iterator<Item = &Timeline> {
+ self.columns.iter().filter_map(|c| c.kind().timeline())
+ }
+
+ pub fn find_timeline_mut(&mut self, id: TimelineId) -> Option<&mut Timeline> {
+ self.timelines_mut().find(|tl| tl.id == id)
+ }
+
+ pub fn find_timeline(&self, id: TimelineId) -> Option<&Timeline> {
+ self.timelines().find(|tl| tl.id == id)
+ }
+
+ pub fn column_mut(&mut self, ind: usize) -> &mut Column {
+ &mut self.columns[ind]
+ }
+
+ pub fn select_down(&mut self) {
+ self.selected().kind_mut().select_down();
+ }
+
+ pub fn select_up(&mut self) {
+ self.selected().kind_mut().select_up();
+ }
+
+ pub fn select_left(&mut self) {
+ if self.selected - 1 < 0 {
+ return;
+ }
+ self.selected -= 1;
+ }
+
+ pub fn select_right(&mut self) {
+ if self.selected + 1 >= self.columns.len() as i32 {
+ return;
}
+ self.selected += 1;
}
}
+/// What type of column is it?
+#[derive(Debug)]
+pub enum ColumnKind {
+ Timeline(Timeline),
+
+ ManageAccount,
+}
+
impl ColumnKind {
- pub fn contact_list(pk: PubkeySource) -> Self {
- ColumnKind::List(ListKind::Contact(pk))
+ pub fn timeline_mut(&mut self) -> Option<&mut Timeline> {
+ match self {
+ ColumnKind::Timeline(tl) => Some(tl),
+ _ => None,
+ }
+ }
+
+ pub fn timeline(&self) -> Option<&Timeline> {
+ match self {
+ ColumnKind::Timeline(tl) => Some(tl),
+ _ => None,
+ }
}
- pub fn profile(pk: PubkeySource) -> Self {
- ColumnKind::Profile(pk)
+ pub fn view_id(&self) -> egui::Id {
+ match self {
+ ColumnKind::Timeline(timeline) => timeline.view_id(),
+ ColumnKind::ManageAccount => egui::Id::new("manage_account"),
+ }
}
- pub fn notifications(pk: PubkeySource) -> Self {
- ColumnKind::Notifications(pk)
+ pub fn select_down(&mut self) {
+ match self {
+ ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(),
+ ColumnKind::ManageAccount => warn!("todo: manage account select_down"),
+ }
}
- pub fn into_timeline(self, ndb: &Ndb, default_user: Option<&[u8; 32]>) -> Option<Timeline> {
+ pub fn select_up(&mut self) {
match self {
- ColumnKind::Universe => Some(Timeline::new(
- ColumnKind::Universe,
- FilterState::ready(vec![Filter::new()
- .kinds([1])
- .limit(filter::default_limit())
- .build()]),
- )),
-
- ColumnKind::Generic => {
- warn!("you can't convert a ColumnKind::Generic to a Timeline");
- None
- }
-
- ColumnKind::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(
- ColumnKind::profile(pk_src),
- FilterState::ready(vec![filter]),
- ))
- }
-
- ColumnKind::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(filter::default_limit())
- .build();
-
- Some(Timeline::new(
- ColumnKind::notifications(pk_src),
- FilterState::ready(vec![notifications_filter]),
- ))
- }
-
- ColumnKind::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(
- ColumnKind::contact_list(pk_src),
- FilterState::needs_remote(vec![contact_filter.clone()]),
- ));
- }
-
- match Timeline::contact_list(&results[0].note) {
- Err(Error::Filter(FilterError::EmptyContactList)) => Some(Timeline::new(
- ColumnKind::contact_list(pk_src),
- FilterState::needs_remote(vec![contact_filter]),
- )),
- Err(e) => {
- error!("Unexpected error: {e}");
- None
- }
- Ok(tl) => Some(tl),
- }
- }
+ ColumnKind::Timeline(tl) => tl.current_view_mut().select_down(),
+ ColumnKind::ManageAccount => warn!("todo: manage account select_down"),
}
}
}
diff --git a/src/draft.rs b/src/draft.rs
@@ -7,16 +7,21 @@ pub struct Draft {
#[derive(Default)]
pub struct Drafts {
- pub replies: HashMap<[u8; 32], Draft>,
- pub compose: Draft,
+ replies: HashMap<[u8; 32], Draft>,
+ compose: Draft,
}
impl Drafts {
- pub fn clear(&mut self, source: DraftSource) {
- source.draft(self).buffer = "".to_string();
+ pub fn compose_mut(&mut self) -> &mut Draft {
+ &mut self.compose
+ }
+
+ pub fn reply_mut(&mut self, id: &[u8; 32]) -> &mut Draft {
+ self.replies.entry(*id).or_default()
}
}
+/*
pub enum DraftSource<'a> {
Compose,
Reply(&'a [u8; 32]), // note id
@@ -25,14 +30,19 @@ pub enum DraftSource<'a> {
impl<'a> DraftSource<'a> {
pub fn draft(&self, drafts: &'a mut Drafts) -> &'a mut Draft {
match self {
- DraftSource::Compose => &mut drafts.compose,
- DraftSource::Reply(id) => drafts.replies.entry(**id).or_default(),
+ DraftSource::Compose => drafts.compose_mut(),
+ DraftSource::Reply(id) => drafts.reply_mut(id),
}
}
}
+*/
impl Draft {
pub fn new() -> Self {
Draft::default()
}
+
+ pub fn clear(&mut self) {
+ self.buffer = "".to_string();
+ }
}
diff --git a/src/note.rs b/src/note.rs
@@ -1,5 +1,5 @@
-use crate::Damus;
-use nostrdb::{NoteKey, QueryResult, Transaction};
+use crate::notecache::NoteCache;
+use nostrdb::{Ndb, NoteKey, QueryResult, Transaction};
use std::cmp::Ordering;
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
@@ -38,12 +38,12 @@ impl PartialOrd for NoteRef {
}
pub fn root_note_id_from_selected_id<'a>(
- app: &mut Damus,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
txn: &'a Transaction,
selected_note_id: &'a [u8; 32],
) -> &'a [u8; 32] {
- let selected_note_key = if let Ok(key) = app
- .ndb
+ let selected_note_key = if let Ok(key) = ndb
.get_notekey_by_id(txn, selected_note_id)
.map(NoteKey::new)
{
@@ -52,13 +52,13 @@ pub fn root_note_id_from_selected_id<'a>(
return selected_note_id;
};
- let note = if let Ok(note) = app.ndb.get_note_by_key(txn, selected_note_key) {
+ let note = if let Ok(note) = ndb.get_note_by_key(txn, selected_note_key) {
note
} else {
return selected_note_id;
};
- app.note_cache_mut()
+ note_cache
.cached_note_or_insert(selected_note_key, ¬e)
.reply
.borrow(note.tags())
diff --git a/src/post.rs b/src/post.rs
@@ -1,12 +1,17 @@
+use enostr::FullKeypair;
use nostrdb::{Note, NoteBuilder, NoteReply};
use std::collections::HashSet;
pub struct NewPost {
pub content: String,
- pub account: usize,
+ pub account: FullKeypair,
}
impl NewPost {
+ pub fn new(content: String, account: FullKeypair) -> Self {
+ NewPost { content, account }
+ }
+
pub fn to_note(&self, seckey: &[u8; 32]) -> Note {
NoteBuilder::new()
.kind(1)
diff --git a/src/subscriptions.rs b/src/subscriptions.rs
@@ -1,4 +1,4 @@
-use crate::column::ColumnKind;
+use crate::timeline::{TimelineId, TimelineKind};
use std::collections::HashMap;
#[derive(Debug, Clone)]
@@ -10,12 +10,12 @@ pub enum SubKind {
/// One shot requests, we can just close after we receive EOSE
OneShot,
- Column(ColumnKind),
+ Timeline(TimelineKind),
/// We are fetching a contact list so that we can use it for our follows
/// Filter.
// TODO: generalize this to any list?
- FetchingContactList(u32),
+ FetchingContactList(TimelineId),
}
/// Subscriptions that need to be tracked at various stages. Sometimes we
diff --git a/src/test_data.rs b/src/test_data.rs
@@ -55,6 +55,7 @@ const TEST_PROFILE_DATA: [u8; 448] = [
0x0c, 0x00, 0x24, 0x00, 0x04, 0x00, 0x0c, 0x00, 0x1c, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
];
+/*
const TEST_PUBKEY: [u8; 32] = [
0x32, 0xe1, 0x82, 0x76, 0x35, 0x45, 0x0e, 0xbb, 0x3c, 0x5a, 0x7d, 0x12, 0xc1, 0xf8, 0xe7, 0xb2,
0xb5, 0x14, 0x43, 0x9a, 0xc1, 0x0a, 0x67, 0xee, 0xf3, 0xd9, 0xfd, 0x9c, 0x5c, 0x68, 0xe2, 0x45,
@@ -63,6 +64,7 @@ const TEST_PUBKEY: [u8; 32] = [
pub fn test_pubkey() -> &'static [u8; 32] {
&TEST_PUBKEY
}
+*/
pub fn test_profile_record() -> ProfileRecord<'static> {
ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap()
@@ -99,7 +101,7 @@ pub fn test_app() -> Damus {
let accounts = get_test_accounts();
for account in accounts {
- app.account_manager.add_account(account);
+ app.accounts.add_account(account);
}
app
diff --git a/src/timeline.rs b/src/timeline.rs
@@ -1,379 +0,0 @@
-use crate::column::{ColumnKind, PubkeySource};
-use crate::error::Error;
-use crate::note::NoteRef;
-use crate::notecache::CachedNote;
-use crate::unknowns::UnknownIds;
-use crate::{filter, filter::FilterState};
-use crate::{Damus, Result};
-use std::sync::atomic::{AtomicU32, Ordering};
-
-use crate::route::Route;
-
-use egui_virtual_list::VirtualList;
-use enostr::Pubkey;
-use nostrdb::{Note, Subscription, Transaction};
-use std::cell::RefCell;
-use std::rc::Rc;
-
-use tracing::{debug, error};
-
-#[derive(Debug, Copy, Clone)]
-pub enum TimelineSource<'a> {
- Column { ind: usize },
- Thread(&'a [u8; 32]),
-}
-
-impl<'a> TimelineSource<'a> {
- pub fn column(ind: usize) -> Self {
- TimelineSource::Column { ind }
- }
-
- pub fn view<'b>(
- self,
- app: &'b mut Damus,
- txn: &Transaction,
- filter: ViewFilter,
- ) -> &'b mut TimelineTab {
- match self {
- TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter),
- TimelineSource::Thread(root_id) => {
- // TODO: replace all this with the raw entry api eventually
-
- let thread = if app.threads.root_id_to_thread.contains_key(root_id) {
- app.threads.thread_expected_mut(root_id)
- } else {
- app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr()
- };
-
- &mut thread.view
- }
- }
- }
-
- pub fn sub(self, app: &mut Damus, txn: &Transaction) -> Option<Subscription> {
- match self {
- TimelineSource::Column { ind, .. } => app.timelines[ind].subscription,
- TimelineSource::Thread(root_id) => {
- // TODO: replace all this with the raw entry api eventually
-
- let thread = if app.threads.root_id_to_thread.contains_key(root_id) {
- app.threads.thread_expected_mut(root_id)
- } else {
- app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr()
- };
-
- thread.subscription()
- }
- }
- }
-
- /// Check local subscriptions for new notes and insert them into
- /// timelines (threads, columns)
- pub fn poll_notes_into_view(&self, txn: &Transaction, app: &mut Damus) -> Result<()> {
- let sub = if let Some(sub) = self.sub(app, txn) {
- sub
- } else {
- return Err(Error::no_active_sub());
- };
-
- let new_note_ids = app.ndb.poll_for_notes(sub, 100);
- 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) = app.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;
- };
-
- UnknownIds::update_from_note(txn, app, ¬e);
-
- 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;
- self.view(app, txn, 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 = app.note_cache_mut().cached_note_or_insert(nr.key, note);
-
- if ViewFilter::filter_notes(cached_note, note) {
- filtered_refs.push(*nr);
- }
- }
-
- self.view(app, txn, ViewFilter::Notes)
- .insert(&filtered_refs, reversed);
- }
-
- Ok(())
- }
-}
-
-#[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)]
-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.
-pub struct Timeline {
- pub uid: u32,
- pub kind: ColumnKind,
- // 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: FilterState,
- pub views: Vec<TimelineTab>,
- pub selected_view: i32,
- pub routes: Vec<Route>,
- pub navigating: bool,
- pub returning: bool,
-
- /// Our nostrdb subscription
- pub subscription: Option<Subscription>,
-}
-
-impl Timeline {
- /// Create a timeline from a contact list
- pub fn contact_list(contact_list: &Note) -> Result<Self> {
- let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
- let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey()));
-
- Ok(Timeline::new(
- ColumnKind::contact_list(pk_src),
- FilterState::ready(filter),
- ))
- }
-
- pub fn new(kind: ColumnKind, filter: FilterState) -> Self {
- // global unique id for all new timelines
- static UIDS: AtomicU32 = AtomicU32::new(0);
-
- 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 routes = vec![Route::Timeline(format!("{}", kind))];
- let navigating = false;
- let returning = false;
- let uid = UIDS.fetch_add(1, Ordering::Relaxed);
-
- Timeline {
- uid,
- kind,
- navigating,
- returning,
- filter,
- views,
- subscription,
- selected_view,
- routes,
- }
- }
-
- 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 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))
-}
diff --git a/src/timeline/kind.rs b/src/timeline/kind.rs
@@ -0,0 +1,153 @@
+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 std::fmt::Display;
+use tracing::{error, warn};
+
+#[derive(Clone, Debug)]
+pub enum PubkeySource {
+ Explicit(Pubkey),
+ DeckAuthor,
+}
+
+#[derive(Debug, Clone)]
+pub enum ListKind {
+ Contact(PubkeySource),
+}
+
+///
+/// What kind of timeline is it?
+/// - Follow List
+/// - Notifications
+/// - DM
+/// - filter
+/// - ... etc
+///
+#[derive(Debug, Clone)]
+pub enum TimelineKind {
+ List(ListKind),
+
+ Notifications(PubkeySource),
+
+ Profile(PubkeySource),
+
+ Universe,
+
+ /// Generic filter
+ Generic,
+}
+
+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"),
+ }
+ }
+}
+
+impl TimelineKind {
+ pub fn contact_list(pk: PubkeySource) -> Self {
+ TimelineKind::List(ListKind::Contact(pk))
+ }
+
+ pub fn profile(pk: PubkeySource) -> Self {
+ TimelineKind::Profile(pk)
+ }
+
+ 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::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) {
+ 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),
+ }
+ }
+ }
+ }
+}
diff --git a/src/timeline/mod.rs b/src/timeline/mod.rs
@@ -0,0 +1,419 @@
+use crate::column::Columns;
+use crate::error::Error;
+use crate::note::NoteRef;
+use crate::notecache::{CachedNote, NoteCache};
+use crate::thread::Threads;
+use crate::unknowns::UnknownIds;
+use crate::Result;
+use crate::{filter, filter::FilterState};
+use std::fmt;
+use std::sync::atomic::{AtomicU32, Ordering};
+
+use egui_virtual_list::VirtualList;
+use enostr::Pubkey;
+use nostrdb::{Ndb, Note, Subscription, Transaction};
+use std::cell::RefCell;
+use std::hash::Hash;
+use std::rc::Rc;
+
+use tracing::{debug, error};
+
+mod kind;
+
+pub use kind::{PubkeySource, TimelineKind};
+
+#[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(Debug, Copy, Clone)]
+pub enum TimelineSource<'a> {
+ Column(TimelineId),
+ Thread(&'a [u8; 32]),
+}
+
+impl<'a> TimelineSource<'a> {
+ pub fn column(id: TimelineId) -> Self {
+ TimelineSource::Column(id)
+ }
+
+ pub fn view<'b>(
+ self,
+ ndb: &Ndb,
+ columns: &'b mut Columns,
+ threads: &'b mut Threads,
+ txn: &Transaction,
+ filter: ViewFilter,
+ ) -> &'b mut TimelineTab {
+ match self {
+ TimelineSource::Column(tid) => columns
+ .find_timeline_mut(tid)
+ .expect("timeline")
+ .view_mut(filter),
+
+ TimelineSource::Thread(root_id) => {
+ // TODO: replace all this with the raw entry api eventually
+
+ let thread = if threads.root_id_to_thread.contains_key(root_id) {
+ threads.thread_expected_mut(root_id)
+ } else {
+ threads.thread_mut(ndb, txn, root_id).get_ptr()
+ };
+
+ &mut thread.view
+ }
+ }
+ }
+
+ fn sub(
+ self,
+ ndb: &Ndb,
+ columns: &Columns,
+ txn: &Transaction,
+ threads: &mut Threads,
+ ) -> Option<Subscription> {
+ match self {
+ TimelineSource::Column(tid) => columns.find_timeline(tid).expect("thread").subscription,
+ TimelineSource::Thread(root_id) => {
+ // TODO: replace all this with the raw entry api eventually
+
+ let thread = if threads.root_id_to_thread.contains_key(root_id) {
+ threads.thread_expected_mut(root_id)
+ } else {
+ threads.thread_mut(ndb, txn, root_id).get_ptr()
+ };
+
+ thread.subscription()
+ }
+ }
+ }
+
+ /// Check local subscriptions for new notes and insert them into
+ /// timelines (threads, columns)
+ pub fn poll_notes_into_view(
+ &self,
+ txn: &Transaction,
+ ndb: &Ndb,
+ columns: &mut Columns,
+ threads: &mut Threads,
+ unknown_ids: &mut UnknownIds,
+ note_cache: &mut NoteCache,
+ ) -> Result<()> {
+ let sub = if let Some(sub) = self.sub(ndb, columns, txn, threads) {
+ sub
+ } else {
+ return Err(Error::no_active_sub());
+ };
+
+ let new_note_ids = ndb.poll_for_notes(sub, 100);
+ 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;
+ };
+
+ UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, ¬e);
+
+ 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;
+ self.view(ndb, columns, threads, txn, 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);
+ }
+ }
+
+ self.view(ndb, columns, threads, txn, ViewFilter::Notes)
+ .insert(&filtered_refs, reversed);
+ }
+
+ Ok(())
+ }
+}
+
+#[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: FilterState,
+ 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) -> Result<Self> {
+ let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
+ let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey()));
+
+ Ok(Timeline::new(
+ TimelineKind::contact_list(pk_src),
+ FilterState::ready(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: FilterState) -> Self {
+ // global unique id for all new timelines
+ static UIDS: AtomicU32 = AtomicU32::new(0);
+
+ 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 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))
+}
diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs
@@ -40,7 +40,7 @@ impl AccountManagementView {
let maybe_remove =
profile_preview_controller::set_profile_previews(app, ui, account_card_ui());
- Self::maybe_remove_accounts(&mut app.account_manager, maybe_remove);
+ Self::maybe_remove_accounts(&mut app.accounts, maybe_remove);
}
fn show_accounts_mobile(app: &mut Damus, ui: &mut egui::Ui) {
@@ -56,7 +56,7 @@ impl AccountManagementView {
);
// remove all account indicies user requested
- Self::maybe_remove_accounts(&mut app.account_manager, maybe_remove);
+ Self::maybe_remove_accounts(&mut app.accounts, maybe_remove);
},
);
}
@@ -95,8 +95,8 @@ impl AccountManagementView {
// Layout::right_to_left(egui::Align::Center),
// |ui| {
// if ui.add(logout_all_button()).clicked() {
- // for index in (0..self.account_manager.num_accounts()).rev() {
- // self.account_manager.remove_account(index);
+ // for index in (0..self.accounts.num_accounts()).rev() {
+ // self.accounts.remove_account(index);
// }
// }
// },
diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs
@@ -49,12 +49,10 @@ impl AccountSelectionWidget {
fn perform_action(app: &mut Damus, action: AccountSelectAction) {
match action {
- AccountSelectAction::RemoveAccount { _index } => {
- app.account_manager.remove_account(_index)
- }
+ AccountSelectAction::RemoveAccount { _index } => app.accounts.remove_account(_index),
AccountSelectAction::SelectAccount { _index } => {
app.show_account_switcher = false;
- app.account_manager.select_account(_index);
+ app.accounts.select_account(_index);
}
AccountSelectAction::OpenAccountManagement => {
app.show_account_switcher = false;
@@ -66,7 +64,7 @@ impl AccountSelectionWidget {
fn show(app: &mut Damus, ui: &mut egui::Ui) -> (AccountSelectResponse, egui::Response) {
let mut res = AccountSelectResponse::default();
- let mut selected_index = app.account_manager.get_selected_account_index();
+ let mut selected_index = app.accounts.get_selected_account_index();
let response = Frame::none()
.outer_margin(8.0)
@@ -83,7 +81,7 @@ impl AccountSelectionWidget {
ui.add(add_account_button());
if let Some(_index) = selected_index {
- if let Some(account) = app.account_manager.get_account(_index) {
+ if let Some(account) = app.accounts.get_account(_index) {
ui.add_space(8.0);
if Self::handle_sign_out(&app.ndb, ui, account) {
res.action = Some(AccountSelectAction::RemoveAccount { _index })
diff --git a/src/ui/mention.rs b/src/ui/mention.rs
@@ -1,8 +1,9 @@
-use crate::{colors, ui, Damus};
-use nostrdb::Transaction;
+use crate::{colors, imgcache::ImageCache, ui};
+use nostrdb::{Ndb, Transaction};
pub struct Mention<'a> {
- app: &'a mut Damus,
+ ndb: &'a Ndb,
+ img_cache: &'a mut ImageCache,
txn: &'a Transaction,
pk: &'a [u8; 32],
selectable: bool,
@@ -10,11 +11,17 @@ pub struct Mention<'a> {
}
impl<'a> Mention<'a> {
- pub fn new(app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32]) -> Self {
+ pub fn new(
+ ndb: &'a Ndb,
+ img_cache: &'a mut ImageCache,
+ txn: &'a Transaction,
+ pk: &'a [u8; 32],
+ ) -> Self {
let size = 16.0;
let selectable = true;
Mention {
- app,
+ ndb,
+ img_cache,
txn,
pk,
selectable,
@@ -35,12 +42,21 @@ impl<'a> Mention<'a> {
impl<'a> egui::Widget for Mention<'a> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
- mention_ui(self.app, self.txn, self.pk, ui, self.size, self.selectable)
+ mention_ui(
+ self.ndb,
+ self.img_cache,
+ self.txn,
+ self.pk,
+ ui,
+ self.size,
+ self.selectable,
+ )
}
}
fn mention_ui(
- app: &mut Damus,
+ ndb: &Ndb,
+ img_cache: &mut ImageCache,
txn: &Transaction,
pk: &[u8; 32],
ui: &mut egui::Ui,
@@ -51,7 +67,7 @@ fn mention_ui(
puffin::profile_function!();
ui.horizontal(|ui| {
- let profile = app.ndb.get_profile_by_pubkey(txn, pk).ok();
+ let profile = ndb.get_profile_by_pubkey(txn, pk).ok();
let name: String =
if let Some(name) = profile.as_ref().and_then(crate::profile::get_profile_name) {
@@ -68,7 +84,7 @@ fn mention_ui(
if let Some(rec) = profile.as_ref() {
resp.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
- ui.add(ui::ProfilePreview::new(rec, &mut app.img_cache));
+ ui.add(ui::ProfilePreview::new(rec, img_cache));
});
}
})
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -31,7 +31,9 @@ pub use username::Username;
use egui::Margin;
/// This is kind of like the Widget trait but is meant for larger top-level
-/// views that are typically stateful. The Widget trait forces us to add mutable
+/// views that are typically stateful.
+///
+/// The Widget trait forces us to add mutable
/// implementations at the type level, which screws us when generating Previews
/// for a Widget. I would have just Widget instead of making this Trait otherwise.
///
diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs
@@ -1,14 +1,17 @@
use crate::images::ImageType;
use crate::imgcache::ImageCache;
+use crate::notecache::NoteCache;
use crate::ui::note::NoteOptions;
use crate::ui::ProfilePic;
-use crate::{colors, ui, Damus};
+use crate::{colors, ui};
use egui::{Color32, Hyperlink, Image, RichText};
-use nostrdb::{BlockType, Mention, Note, NoteKey, Transaction};
+use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
use tracing::warn;
pub struct NoteContents<'a> {
- damus: &'a mut Damus,
+ ndb: &'a Ndb,
+ img_cache: &'a mut ImageCache,
+ note_cache: &'a mut NoteCache,
txn: &'a Transaction,
note: &'a Note<'a>,
note_key: NoteKey,
@@ -17,14 +20,18 @@ pub struct NoteContents<'a> {
impl<'a> NoteContents<'a> {
pub fn new(
- damus: &'a mut Damus,
+ ndb: &'a Ndb,
+ img_cache: &'a mut ImageCache,
+ note_cache: &'a mut NoteCache,
txn: &'a Transaction,
note: &'a Note,
note_key: NoteKey,
options: ui::note::NoteOptions,
) -> Self {
NoteContents {
- damus,
+ ndb,
+ img_cache,
+ note_cache,
txn,
note,
note_key,
@@ -37,7 +44,9 @@ impl egui::Widget for NoteContents<'_> {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
render_note_contents(
ui,
- self.damus,
+ self.ndb,
+ self.img_cache,
+ self.note_cache,
self.txn,
self.note,
self.note_key,
@@ -51,7 +60,9 @@ impl egui::Widget for NoteContents<'_> {
/// notes are references within a note
fn render_note_preview(
ui: &mut egui::Ui,
- app: &mut Damus,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ img_cache: &mut ImageCache,
txn: &Transaction,
id: &[u8; 32],
_id_str: &str,
@@ -59,7 +70,7 @@ fn render_note_preview(
#[cfg(feature = "profiling")]
puffin::profile_function!();
- let note = if let Ok(note) = app.ndb.get_note_by_id(txn, id) {
+ let note = if let Ok(note) = ndb.get_note_by_id(txn, id) {
// TODO: support other preview kinds
if note.kind() == 1 {
note
@@ -92,7 +103,7 @@ fn render_note_preview(
ui.visuals().noninteractive().bg_stroke.color,
))
.show(ui, |ui| {
- ui::NoteView::new(app, ¬e)
+ ui::NoteView::new(ndb, note_cache, img_cache, ¬e)
.actionbar(false)
.small_pfp(true)
.wide(true)
@@ -102,9 +113,12 @@ fn render_note_preview(
.response
}
+#[allow(clippy::too_many_arguments)]
fn render_note_contents(
ui: &mut egui::Ui,
- damus: &mut Damus,
+ ndb: &Ndb,
+ img_cache: &mut ImageCache,
+ note_cache: &mut NoteCache,
txn: &Transaction,
note: &Note,
note_key: NoteKey,
@@ -118,7 +132,7 @@ fn render_note_contents(
let mut inline_note: Option<(&[u8; 32], &str)> = None;
let resp = ui.horizontal_wrapped(|ui| {
- let blocks = if let Ok(blocks) = damus.ndb.get_blocks_by_key(txn, note_key) {
+ let blocks = if let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) {
blocks
} else {
warn!("missing note content blocks? '{}'", note.content());
@@ -132,11 +146,11 @@ fn render_note_contents(
match block.blocktype() {
BlockType::MentionBech32 => match block.as_mention().unwrap() {
Mention::Profile(profile) => {
- ui.add(ui::Mention::new(damus, txn, profile.pubkey()));
+ ui.add(ui::Mention::new(ndb, img_cache, txn, profile.pubkey()));
}
Mention::Pubkey(npub) => {
- ui.add(ui::Mention::new(damus, txn, npub.pubkey()));
+ ui.add(ui::Mention::new(ndb, img_cache, txn, npub.pubkey()));
}
Mention::Note(note) if options.has_note_previews() => {
@@ -186,13 +200,13 @@ fn render_note_contents(
});
if let Some((id, block_str)) = inline_note {
- render_note_preview(ui, damus, txn, id, block_str);
+ render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str);
}
- if !images.is_empty() && !damus.textmode {
+ if !images.is_empty() && !options.has_textmode() {
ui.add_space(2.0);
let carousel_id = egui::Id::new(("carousel", note.key().expect("expected tx note")));
- image_carousel(ui, &mut damus.img_cache, images, carousel_id);
+ image_carousel(ui, img_cache, images, carousel_id);
ui.add_space(2.0);
}
diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs
@@ -8,12 +8,21 @@ pub use options::NoteOptions;
pub use post::{PostAction, PostResponse, PostView};
pub use reply::PostReplyView;
-use crate::{actionbar::BarAction, colors, notecache::CachedNote, ui, ui::View, Damus};
+use crate::{
+ actionbar::BarAction,
+ colors,
+ imgcache::ImageCache,
+ notecache::{CachedNote, NoteCache},
+ ui,
+ ui::View,
+};
use egui::{Label, RichText, Sense};
-use nostrdb::{Note, NoteKey, NoteReply, Transaction};
+use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction};
pub struct NoteView<'a> {
- app: &'a mut Damus,
+ ndb: &'a Ndb,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
note: &'a nostrdb::Note<'a>,
flags: NoteOptions,
}
@@ -29,7 +38,13 @@ impl<'a> View for NoteView<'a> {
}
}
-fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: &mut Damus) {
+fn reply_desc(
+ ui: &mut egui::Ui,
+ txn: &Transaction,
+ note_reply: &NoteReply,
+ ndb: &Ndb,
+ img_cache: &mut ImageCache,
+) {
#[cfg(feature = "profiling")]
puffin::profile_function!();
@@ -51,7 +66,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
return;
};
- let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) {
+ let reply_note = if let Ok(reply_note) = ndb.get_note_by_id(txn, reply.id) {
reply_note
} else {
ui.add(
@@ -68,7 +83,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
if note_reply.is_reply_to_root() {
// We're replying to the root, let's show this
ui.add(
- ui::Mention::new(app, txn, reply_note.pubkey())
+ ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey())
.size(size)
.selectable(selectable),
);
@@ -83,11 +98,11 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
} else if let Some(root) = note_reply.root() {
// replying to another post in a thread, not the root
- if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) {
+ if let Ok(root_note) = ndb.get_note_by_id(txn, root.id) {
if root_note.pubkey() == reply_note.pubkey() {
// simply "replying to bob's note" when replying to bob in his thread
ui.add(
- ui::Mention::new(app, txn, reply_note.pubkey())
+ ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey())
.size(size)
.selectable(selectable),
);
@@ -103,7 +118,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
// replying to bob in alice's thread
ui.add(
- ui::Mention::new(app, txn, reply_note.pubkey())
+ ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey())
.size(size)
.selectable(selectable),
);
@@ -112,7 +127,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
.selectable(selectable),
);
ui.add(
- ui::Mention::new(app, txn, root_note.pubkey())
+ ui::Mention::new(ndb, img_cache, txn, root_note.pubkey())
.size(size)
.selectable(selectable),
);
@@ -127,7 +142,7 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
}
} else {
ui.add(
- ui::Mention::new(app, txn, reply_note.pubkey())
+ ui::Mention::new(ndb, img_cache, txn, reply_note.pubkey())
.size(size)
.selectable(selectable),
);
@@ -144,9 +159,25 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app:
}
impl<'a> NoteView<'a> {
- pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self {
+ pub fn new(
+ ndb: &'a Ndb,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ note: &'a nostrdb::Note<'a>,
+ ) -> Self {
let flags = NoteOptions::actionbar | NoteOptions::note_previews;
- Self { app, note, flags }
+ Self {
+ ndb,
+ note_cache,
+ img_cache,
+ note,
+ flags,
+ }
+ }
+
+ pub fn textmode(mut self, enable: bool) -> Self {
+ self.options_mut().set_textmode(enable);
+ self
}
pub fn actionbar(mut self, enable: bool) -> Self {
@@ -192,14 +223,13 @@ impl<'a> NoteView<'a> {
let txn = self.note.txn().expect("todo: implement non-db notes");
ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| {
- let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
+ let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
//ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
let cached_note = self
- .app
- .note_cache_mut()
+ .note_cache
.cached_note_or_insert_mut(note_key, self.note);
let (_id, rect) = ui.allocate_space(egui::vec2(50.0, 20.0));
@@ -218,7 +248,13 @@ impl<'a> NoteView<'a> {
});
ui.add(NoteContents::new(
- self.app, txn, self.note, note_key, self.flags,
+ self.ndb,
+ self.img_cache,
+ self.note_cache,
+ txn,
+ self.note,
+ note_key,
+ self.flags,
));
//});
})
@@ -255,33 +291,26 @@ impl<'a> NoteView<'a> {
let profile_key = profile.as_ref().unwrap().record().note_key();
let note_key = note_key.as_u64();
- if ui::is_narrow(ui.ctx()) {
- ui.add(ui::ProfilePic::new(&mut self.app.img_cache, pic));
- } else {
- let (rect, size, _resp) = ui::anim::hover_expand(
- ui,
- egui::Id::new((profile_key, note_key)),
- pfp_size,
- ui::NoteView::expand_size(),
- anim_speed,
- );
-
- ui.put(
- rect,
- ui::ProfilePic::new(&mut self.app.img_cache, pic).size(size),
- )
+ let (rect, size, _resp) = ui::anim::hover_expand(
+ ui,
+ egui::Id::new((profile_key, note_key)),
+ pfp_size,
+ ui::NoteView::expand_size(),
+ anim_speed,
+ );
+
+ ui.put(rect, ui::ProfilePic::new(self.img_cache, pic).size(size))
.on_hover_ui_at_pointer(|ui| {
ui.set_max_width(300.0);
ui.add(ui::ProfilePreview::new(
profile.as_ref().unwrap(),
- &mut self.app.img_cache,
+ self.img_cache,
));
});
- }
}
None => {
ui.add(
- ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url())
+ ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url())
.size(pfp_size),
);
}
@@ -289,7 +318,7 @@ impl<'a> NoteView<'a> {
}
pub fn show(&mut self, ui: &mut egui::Ui) -> NoteResponse {
- if self.app.textmode {
+ if self.options().has_textmode() {
NoteResponse {
response: self.textmode_ui(ui),
action: None,
@@ -301,7 +330,7 @@ impl<'a> NoteView<'a> {
fn note_header(
ui: &mut egui::Ui,
- app: &mut Damus,
+ note_cache: &mut NoteCache,
note: &Note,
profile: &Result<nostrdb::ProfileRecord<'_>, nostrdb::Error>,
) -> egui::Response {
@@ -311,9 +340,7 @@ impl<'a> NoteView<'a> {
ui.spacing_mut().item_spacing.x = 2.0;
ui.add(ui::Username::new(profile.as_ref().ok(), note.pubkey()).abbreviated(20));
- let cached_note = app
- .note_cache_mut()
- .cached_note_or_insert_mut(note_key, note);
+ let cached_note = note_cache.cached_note_or_insert_mut(note_key, note);
render_reltime(ui, cached_note, true);
})
.response
@@ -325,7 +352,7 @@ impl<'a> NoteView<'a> {
let note_key = self.note.key().expect("todo: support non-db notes");
let txn = self.note.txn().expect("todo: support non-db notes");
let mut note_action: Option<BarAction> = None;
- let profile = self.app.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
+ let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey());
// wide design
let response = if self.options().has_wide() {
@@ -336,28 +363,29 @@ impl<'a> NoteView<'a> {
ui.vertical(|ui| {
ui.add_sized([size.x, self.options().pfp_size()], |ui: &mut egui::Ui| {
ui.horizontal_centered(|ui| {
- NoteView::note_header(ui, self.app, self.note, &profile);
+ NoteView::note_header(ui, self.note_cache, self.note, &profile);
})
.response
});
let note_reply = self
- .app
- .note_cache_mut()
+ .note_cache
.cached_note_or_insert_mut(note_key, self.note)
.reply
.borrow(self.note.tags());
if note_reply.reply().is_some() {
ui.horizontal(|ui| {
- reply_desc(ui, txn, ¬e_reply, self.app);
+ reply_desc(ui, txn, ¬e_reply, self.ndb, self.img_cache);
});
}
});
});
let resp = ui.add(NoteContents::new(
- self.app,
+ self.ndb,
+ self.img_cache,
+ self.note_cache,
txn,
self.note,
note_key,
@@ -375,25 +403,26 @@ impl<'a> NoteView<'a> {
self.pfp(note_key, &profile, ui);
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
- NoteView::note_header(ui, self.app, self.note, &profile);
+ NoteView::note_header(ui, self.note_cache, self.note, &profile);
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 2.0;
let note_reply = self
- .app
- .note_cache_mut()
+ .note_cache
.cached_note_or_insert_mut(note_key, self.note)
.reply
.borrow(self.note.tags());
if note_reply.reply().is_some() {
- reply_desc(ui, txn, ¬e_reply, self.app);
+ reply_desc(ui, txn, ¬e_reply, self.ndb, self.img_cache);
}
});
ui.add(NoteContents::new(
- self.app,
+ self.ndb,
+ self.img_cache,
+ self.note_cache,
txn,
self.note,
note_key,
diff --git a/src/ui/note/options.rs b/src/ui/note/options.rs
@@ -12,6 +12,7 @@ bitflags! {
const medium_pfp = 0b00001000;
const wide = 0b00010000;
const selectable_text = 0b00100000;
+ const textmode = 0b01000000;
}
}
@@ -33,6 +34,7 @@ impl NoteOptions {
create_setter!(set_medium_pfp, medium_pfp);
create_setter!(set_note_previews, note_previews);
create_setter!(set_selectable_text, selectable_text);
+ create_setter!(set_textmode, textmode);
create_setter!(set_actionbar, actionbar);
#[inline]
@@ -46,6 +48,11 @@ impl NoteOptions {
}
#[inline]
+ pub fn has_textmode(self) -> bool {
+ (self & NoteOptions::textmode) == NoteOptions::textmode
+ }
+
+ #[inline]
pub fn has_note_previews(self) -> bool {
(self & NoteOptions::note_previews) == NoteOptions::note_previews
}
diff --git a/src/ui/note/post.rs b/src/ui/note/post.rs
@@ -1,16 +1,17 @@
-use crate::app::Damus;
-use crate::draft::{Draft, DraftSource};
+use crate::draft::Draft;
+use crate::imgcache::ImageCache;
use crate::post::NewPost;
use crate::ui;
use crate::ui::{Preview, PreviewConfig, View};
use egui::widgets::text_edit::TextEdit;
-use nostrdb::Transaction;
-
-pub struct PostView<'app, 'd> {
- app: &'app mut Damus,
- /// account index
- poster: usize,
- draft_source: DraftSource<'d>,
+use enostr::{FilledKeypair, FullKeypair};
+use nostrdb::{Config, Ndb, Transaction};
+
+pub struct PostView<'a> {
+ ndb: &'a Ndb,
+ draft: &'a mut Draft,
+ img_cache: &'a mut ImageCache,
+ poster: FilledKeypair<'a>,
id_source: Option<egui::Id>,
}
@@ -23,14 +24,20 @@ pub struct PostResponse {
pub edit_response: egui::Response,
}
-impl<'app, 'd> PostView<'app, 'd> {
- pub fn new(app: &'app mut Damus, draft_source: DraftSource<'d>, poster: usize) -> Self {
+impl<'a> PostView<'a> {
+ pub fn new(
+ ndb: &'a Ndb,
+ draft: &'a mut Draft,
+ img_cache: &'a mut ImageCache,
+ poster: FilledKeypair<'a>,
+ ) -> Self {
let id_source: Option<egui::Id> = None;
PostView {
- id_source,
- app,
+ ndb,
+ draft,
+ img_cache,
poster,
- draft_source,
+ id_source,
}
}
@@ -39,47 +46,30 @@ impl<'app, 'd> PostView<'app, 'd> {
self
}
- fn draft(&mut self) -> &mut Draft {
- self.draft_source.draft(&mut self.app.drafts)
- }
-
fn editbox(&mut self, txn: &nostrdb::Transaction, ui: &mut egui::Ui) -> egui::Response {
ui.spacing_mut().item_spacing.x = 12.0;
let pfp_size = 24.0;
- let poster_pubkey = self
- .app
- .account_manager
- .get_account(self.poster)
- .map(|acc| acc.pubkey.bytes())
- .unwrap_or(crate::test_data::test_pubkey());
-
// TODO: refactor pfp control to do all of this for us
let poster_pfp = self
- .app
.ndb
- .get_profile_by_pubkey(txn, poster_pubkey)
+ .get_profile_by_pubkey(txn, self.poster.pubkey.bytes())
.as_ref()
.ok()
- .and_then(|p| {
- Some(ui::ProfilePic::from_profile(&mut self.app.img_cache, p)?.size(pfp_size))
- });
+ .and_then(|p| Some(ui::ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size)));
if let Some(pfp) = poster_pfp {
ui.add(pfp);
} else {
ui.add(
- ui::ProfilePic::new(&mut self.app.img_cache, ui::ProfilePic::no_pfp_url())
- .size(pfp_size),
+ ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()).size(pfp_size),
);
}
- let buffer = &mut self.draft_source.draft(&mut self.app.drafts).buffer;
-
let response = ui.add_sized(
ui.available_size(),
- TextEdit::multiline(buffer)
+ TextEdit::multiline(&mut self.draft.buffer)
.hint_text(egui::RichText::new("Write a banger note here...").weak())
.frame(false),
);
@@ -144,10 +134,10 @@ impl<'app, 'd> PostView<'app, 'd> {
.add_sized([91.0, 32.0], egui::Button::new("Post now"))
.clicked()
{
- Some(PostAction::Post(NewPost {
- content: self.draft().buffer.clone(),
- account: self.poster,
- }))
+ Some(PostAction::Post(NewPost::new(
+ self.draft.buffer.clone(),
+ self.poster.to_full(),
+ )))
} else {
None
}
@@ -167,28 +157,41 @@ impl<'app, 'd> PostView<'app, 'd> {
mod preview {
use super::*;
- use crate::test_data;
pub struct PostPreview {
- app: Damus,
+ ndb: Ndb,
+ img_cache: ImageCache,
+ draft: Draft,
+ poster: FullKeypair,
}
impl PostPreview {
fn new() -> Self {
+ let ndb = Ndb::new(".", &Config::new()).expect("ndb");
+
PostPreview {
- app: test_data::test_app(),
+ ndb,
+ img_cache: ImageCache::new(".".into()),
+ draft: Draft::new(),
+ poster: FullKeypair::generate(),
}
}
}
impl View for PostPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
- let txn = Transaction::new(&self.app.ndb).unwrap();
- PostView::new(&mut self.app, DraftSource::Compose, 0).ui(&txn, ui);
+ let txn = Transaction::new(&self.ndb).expect("txn");
+ PostView::new(
+ &self.ndb,
+ &mut self.draft,
+ &mut self.img_cache,
+ self.poster.to_filled(),
+ )
+ .ui(&txn, ui);
}
}
- impl<'app, 'p> Preview for PostView<'app, 'p> {
+ impl<'a> Preview for PostView<'a> {
type Prev = PostPreview;
fn preview(_cfg: PreviewConfig) -> Self::Prev {
diff --git a/src/ui/note/reply.rs b/src/ui/note/reply.rs
@@ -1,21 +1,43 @@
-use crate::draft::DraftSource;
+use crate::draft::Drafts;
+use crate::imgcache::ImageCache;
+use crate::notecache::NoteCache;
+use crate::ui;
use crate::ui::note::{PostAction, PostResponse};
-use crate::{ui, Damus};
+use enostr::{FilledKeypair, RelayPool};
+use nostrdb::Ndb;
use tracing::info;
pub struct PostReplyView<'a> {
- app: &'a mut Damus,
- id_source: Option<egui::Id>,
+ ndb: &'a Ndb,
+ poster: FilledKeypair<'a>,
+ pool: &'a mut RelayPool,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ drafts: &'a mut Drafts,
note: &'a nostrdb::Note<'a>,
+ id_source: Option<egui::Id>,
}
impl<'a> PostReplyView<'a> {
- pub fn new(app: &'a mut Damus, note: &'a nostrdb::Note<'a>) -> Self {
+ pub fn new(
+ ndb: &'a Ndb,
+ poster: FilledKeypair<'a>,
+ pool: &'a mut RelayPool,
+ drafts: &'a mut Drafts,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ note: &'a nostrdb::Note<'a>,
+ ) -> Self {
let id_source: Option<egui::Id> = None;
PostReplyView {
- app,
- id_source,
+ ndb,
+ poster,
+ pool,
+ drafts,
note,
+ note_cache,
+ img_cache,
+ id_source,
}
}
@@ -46,7 +68,7 @@ impl<'a> PostReplyView<'a> {
egui::Frame::none()
.outer_margin(egui::Margin::same(note_offset))
.show(ui, |ui| {
- ui::NoteView::new(self.app, self.note)
+ ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note)
.actionbar(false)
.medium_pfp(true)
.show(ui);
@@ -54,43 +76,26 @@ impl<'a> PostReplyView<'a> {
let id = self.id();
let replying_to = self.note.id();
- let draft_source = DraftSource::Reply(replying_to);
- let poster = self
- .app
- .account_manager
- .get_selected_account_index()
- .unwrap_or(0);
let rect_before_post = ui.min_rect();
- let post_response = ui::PostView::new(self.app, draft_source, poster)
- .id_source(id)
- .ui(self.note.txn().unwrap(), ui);
-
- if self
- .app
- .account_manager
- .get_selected_account()
- .map_or(false, |a| a.secret_key.is_some())
- {
- if let Some(action) = &post_response.action {
- match action {
- PostAction::Post(np) => {
- let seckey = self
- .app
- .account_manager
- .get_account(poster)
- .unwrap()
- .secret_key
- .as_ref()
- .unwrap()
- .to_secret_bytes();
-
- let note = np.to_reply(&seckey, self.note);
-
- let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
- info!("sending {}", raw_msg);
- self.app.pool.send(&enostr::ClientMessage::raw(raw_msg));
- self.app.drafts.clear(DraftSource::Reply(replying_to));
- }
+
+ let post_response = {
+ let draft = self.drafts.reply_mut(replying_to);
+ ui::PostView::new(self.ndb, draft, self.img_cache, self.poster)
+ .id_source(id)
+ .ui(self.note.txn().unwrap(), ui)
+ };
+
+ if let Some(action) = &post_response.action {
+ match action {
+ PostAction::Post(np) => {
+ let seckey = self.poster.secret_key.to_secret_bytes();
+
+ let note = np.to_reply(&seckey, self.note);
+
+ let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
+ info!("sending {}", raw_msg);
+ self.pool.send(&enostr::ClientMessage::raw(raw_msg));
+ self.drafts.reply_mut(replying_to).clear();
}
}
}
diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs
@@ -33,8 +33,8 @@ pub fn set_profile_previews(
return None;
};
- for i in 0..app.account_manager.num_accounts() {
- let account = if let Some(account) = app.account_manager.get_account(i) {
+ for i in 0..app.accounts.num_accounts() {
+ let account = if let Some(account) = app.accounts.get_account(i) {
account
} else {
continue;
@@ -47,7 +47,7 @@ pub fn set_profile_previews(
let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache);
- let is_selected = if let Some(selected) = app.account_manager.get_selected_account_index() {
+ let is_selected = if let Some(selected) = app.accounts.get_selected_account_index() {
i == selected
} else {
false
@@ -66,7 +66,7 @@ pub fn set_profile_previews(
}
to_remove.as_mut().unwrap().push(i);
}
- ProfilePreviewOp::SwitchTo => app.account_manager.select_account(i),
+ ProfilePreviewOp::SwitchTo => app.accounts.select_account(i),
}
}
@@ -92,8 +92,8 @@ pub fn view_profile_previews(
return None;
};
- for i in 0..app.account_manager.num_accounts() {
- let account = if let Some(account) = app.account_manager.get_account(i) {
+ for i in 0..app.accounts.num_accounts() {
+ let account = if let Some(account) = app.accounts.get_account(i) {
account
} else {
continue;
@@ -106,7 +106,7 @@ pub fn view_profile_previews(
let preview = SimpleProfilePreview::new(profile.as_ref(), &mut app.img_cache);
- let is_selected = if let Some(selected) = app.account_manager.get_selected_account_index() {
+ let is_selected = if let Some(selected) = app.accounts.get_selected_account_index() {
i == selected
} else {
false
@@ -136,7 +136,7 @@ pub fn show_with_selected_pfp(
ui: &mut egui::Ui,
ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
) -> Option<egui::Response> {
- let selected_account = app.account_manager.get_selected_account();
+ let selected_account = app.accounts.get_selected_account();
if let Some(selected_account) = selected_account {
if let Ok(txn) = Transaction::new(&app.ndb) {
let profile = app
diff --git a/src/ui/thread.rs b/src/ui/thread.rs
@@ -1,28 +1,57 @@
-use crate::{actionbar::BarResult, timeline::TimelineSource, ui, Damus};
-use nostrdb::{NoteKey, Transaction};
+use crate::{
+ actionbar::BarResult, column::Columns, imgcache::ImageCache, notecache::NoteCache,
+ thread::Threads, timeline::TimelineSource, ui, unknowns::UnknownIds,
+};
+use enostr::RelayPool;
+use nostrdb::{Ndb, NoteKey, Transaction};
use tracing::{error, warn};
pub struct ThreadView<'a> {
- app: &'a mut Damus,
- timeline: usize,
+ column: usize,
+ columns: &'a mut Columns,
+ threads: &'a mut Threads,
+ ndb: &'a Ndb,
+ pool: &'a mut RelayPool,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ unknown_ids: &'a mut UnknownIds,
selected_note_id: &'a [u8; 32],
+ textmode: bool,
}
impl<'a> ThreadView<'a> {
- pub fn new(app: &'a mut Damus, timeline: usize, selected_note_id: &'a [u8; 32]) -> Self {
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ column: usize,
+ columns: &'a mut Columns,
+ threads: &'a mut Threads,
+ ndb: &'a Ndb,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ unknown_ids: &'a mut UnknownIds,
+ pool: &'a mut RelayPool,
+ textmode: bool,
+ selected_note_id: &'a [u8; 32],
+ ) -> Self {
ThreadView {
- app,
- timeline,
+ column,
+ columns,
+ threads,
+ ndb,
+ note_cache,
+ img_cache,
+ textmode,
selected_note_id,
+ unknown_ids,
+ pool,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<BarResult> {
- let txn = Transaction::new(&self.app.ndb).expect("txn");
+ let txn = Transaction::new(self.ndb).expect("txn");
let mut result: Option<BarResult> = None;
let selected_note_key = if let Ok(key) = self
- .app
.ndb
.get_notekey_by_id(&txn, self.selected_note_id)
.map(NoteKey::new)
@@ -33,12 +62,13 @@ impl<'a> ThreadView<'a> {
return None;
};
- let scroll_id = egui::Id::new((
- "threadscroll",
- self.app.timelines[self.timeline].selected_view,
- self.timeline,
- selected_note_key,
- ));
+ let scroll_id = {
+ egui::Id::new((
+ "threadscroll",
+ self.columns.column(self.column).view_id(),
+ selected_note_key,
+ ))
+ };
ui.label(
egui::RichText::new("Threads ALPHA! It's not done. Things will be broken.")
@@ -51,7 +81,7 @@ impl<'a> ThreadView<'a> {
.auto_shrink([false, false])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
.show(ui, |ui| {
- let note = if let Ok(note) = self.app.ndb.get_note_by_key(&txn, selected_note_key) {
+ let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) {
note
} else {
return;
@@ -59,8 +89,7 @@ impl<'a> ThreadView<'a> {
let root_id = {
let cached_note = self
- .app
- .note_cache_mut()
+ .note_cache
.cached_note_or_insert(selected_note_key, ¬e);
cached_note
@@ -71,17 +100,19 @@ impl<'a> ThreadView<'a> {
};
// poll for new notes and insert them into our existing notes
- if let Err(e) = TimelineSource::Thread(root_id).poll_notes_into_view(&txn, self.app)
- {
+ if let Err(e) = TimelineSource::Thread(root_id).poll_notes_into_view(
+ &txn,
+ self.ndb,
+ self.columns,
+ self.threads,
+ self.unknown_ids,
+ self.note_cache,
+ ) {
error!("Thread::poll_notes_into_view: {e}");
}
let (len, list) = {
- let thread = self
- .app
- .threads
- .thread_mut(&self.app.ndb, &txn, root_id)
- .get_ptr();
+ let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr();
let len = thread.view.notes.len();
(len, &mut thread.view.list)
@@ -95,15 +126,11 @@ impl<'a> ThreadView<'a> {
let ind = len - 1 - start_index;
let note_key = {
- let thread = self
- .app
- .threads
- .thread_mut(&self.app.ndb, &txn, root_id)
- .get_ptr();
+ let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr();
thread.view.notes[ind].key
};
- let note = if let Ok(note) = self.app.ndb.get_note_by_key(&txn, note_key) {
+ let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, note_key) {
note
} else {
warn!("failed to query note {:?}", note_key);
@@ -111,13 +138,22 @@ impl<'a> ThreadView<'a> {
};
ui::padding(8.0, ui, |ui| {
- let textmode = self.app.textmode;
- let resp = ui::NoteView::new(self.app, ¬e)
- .note_previews(!textmode)
- .show(ui);
+ let resp =
+ ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e)
+ .note_previews(!self.textmode)
+ .textmode(self.textmode)
+ .show(ui);
if let Some(action) = resp.action {
- let br = action.execute(self.app, self.timeline, note.id(), &txn);
+ let br = action.execute(
+ self.ndb,
+ self.columns.column_mut(self.column),
+ self.threads,
+ self.note_cache,
+ self.pool,
+ note.id(),
+ &txn,
+ );
if br.is_some() {
result = br;
}
diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs
@@ -1,28 +1,67 @@
-use crate::{actionbar::BarResult, draft::DraftSource, ui, ui::note::PostAction, Damus};
+use crate::{
+ actionbar::BarAction,
+ actionbar::BarResult,
+ column::{Column, ColumnKind},
+ draft::Drafts,
+ imgcache::ImageCache,
+ notecache::NoteCache,
+ thread::Threads,
+ ui,
+ ui::note::PostAction,
+};
use egui::containers::scroll_area::ScrollBarVisibility;
use egui::{Direction, Layout};
use egui_tabs::TabColor;
-use nostrdb::Transaction;
+use enostr::{FilledKeypair, RelayPool};
+use nostrdb::{Ndb, Note, Transaction};
use tracing::{debug, info, warn};
pub struct TimelineView<'a> {
- app: &'a mut Damus,
+ ndb: &'a Ndb,
+ column: &'a mut Column,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ threads: &'a mut Threads,
+ pool: &'a mut RelayPool,
+ textmode: bool,
reverse: bool,
- timeline: usize,
}
impl<'a> TimelineView<'a> {
- pub fn new(app: &'a mut Damus, timeline: usize) -> TimelineView<'a> {
+ pub fn new(
+ ndb: &'a Ndb,
+ column: &'a mut Column,
+ note_cache: &'a mut NoteCache,
+ img_cache: &'a mut ImageCache,
+ threads: &'a mut Threads,
+ pool: &'a mut RelayPool,
+ textmode: bool,
+ ) -> TimelineView<'a> {
let reverse = false;
TimelineView {
- app,
- timeline,
+ ndb,
+ column,
+ note_cache,
+ img_cache,
+ threads,
+ pool,
reverse,
+ textmode,
}
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
- timeline_ui(ui, self.app, self.timeline, self.reverse);
+ timeline_ui(
+ ui,
+ self.ndb,
+ self.column,
+ self.note_cache,
+ self.img_cache,
+ self.threads,
+ self.pool,
+ self.reverse,
+ self.textmode,
+ );
}
pub fn reversed(mut self) -> Self {
@@ -31,39 +70,61 @@ impl<'a> TimelineView<'a> {
}
}
-fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bool) {
+#[allow(clippy::too_many_arguments)]
+fn timeline_ui(
+ ui: &mut egui::Ui,
+ ndb: &Ndb,
+ column: &mut Column,
+ note_cache: &mut NoteCache,
+ img_cache: &mut ImageCache,
+ threads: &mut Threads,
+ pool: &mut RelayPool,
+ reversed: bool,
+ textmode: bool,
+) {
//padding(4.0, ui, |ui| ui.heading("Notifications"));
/*
let font_id = egui::TextStyle::Body.resolve(ui.style());
let row_height = ui.fonts(|f| f.row_height(&font_id)) + ui.spacing().item_spacing.y;
+
*/
- if timeline == 0 {
- postbox_view(app, ui);
- }
+ {
+ let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() {
+ timeline
+ } else {
+ return;
+ };
- app.timelines[timeline].selected_view = tabs_ui(ui);
+ timeline.selected_view = tabs_ui(ui);
- // need this for some reason??
- ui.add_space(3.0);
+ // need this for some reason??
+ ui.add_space(3.0);
+ }
- let scroll_id = egui::Id::new(("tlscroll", app.timelines[timeline].selected_view, timeline));
+ let scroll_id = egui::Id::new(("tlscroll", column.view_id()));
egui::ScrollArea::vertical()
.id_source(scroll_id)
.animated(false)
.auto_shrink([false, false])
.scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
.show(ui, |ui| {
- let view = app.timelines[timeline].current_view();
+ let timeline = if let ColumnKind::Timeline(timeline) = column.kind_mut() {
+ timeline
+ } else {
+ return 0;
+ };
+
+ let view = timeline.current_view();
let len = view.notes.len();
- let mut bar_result: Option<BarResult> = None;
- let txn = if let Ok(txn) = Transaction::new(&app.ndb) {
+ let txn = if let Ok(txn) = Transaction::new(ndb) {
txn
} else {
warn!("failed to create transaction");
return 0;
};
+ let mut bar_action: Option<(BarAction, Note)> = None;
view.list
.clone()
.borrow_mut()
@@ -77,9 +138,9 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo
start_index
};
- let note_key = app.timelines[timeline].current_view().notes[ind].key;
+ let note_key = timeline.current_view().notes[ind].key;
- let note = if let Ok(note) = app.ndb.get_note_by_key(&txn, note_key) {
+ let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) {
note
} else {
warn!("failed to query note {:?}", note_key);
@@ -87,17 +148,13 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo
};
ui::padding(8.0, ui, |ui| {
- let textmode = app.textmode;
- let resp = ui::NoteView::new(app, ¬e)
+ let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e)
.note_previews(!textmode)
.selectable_text(false)
.show(ui);
- if let Some(action) = resp.action {
- let br = action.execute(app, timeline, note.id(), &txn);
- if br.is_some() {
- bar_result = br;
- }
+ if let Some(ba) = resp.action {
+ bar_action = Some((ba, note));
} else if resp.response.clicked() {
debug!("clicked note");
}
@@ -109,15 +166,19 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo
1
});
- if let Some(br) = bar_result {
- match br {
- // update the thread for next render if we have new notes
- BarResult::NewThreadNotes(new_notes) => {
- let thread = app
- .threads
- .thread_mut(&app.ndb, &txn, new_notes.root_id.bytes())
- .get_ptr();
- new_notes.process(thread);
+ // handle any actions from the virtual list
+ if let Some((action, note)) = bar_action {
+ if let Some(br) =
+ action.execute(ndb, column, threads, note_cache, pool, note.id(), &txn)
+ {
+ match br {
+ // update the thread for next render if we have new notes
+ BarResult::NewThreadNotes(new_notes) => {
+ let thread = threads
+ .thread_mut(ndb, &txn, new_notes.root_id.bytes())
+ .get_ptr();
+ new_notes.process(thread);
+ }
}
}
}
@@ -126,38 +187,27 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo
});
}
-fn postbox_view(app: &mut Damus, ui: &mut egui::Ui) {
+pub fn postbox_view<'a>(
+ ndb: &'a Ndb,
+ key: FilledKeypair<'a>,
+ pool: &'a mut RelayPool,
+ drafts: &'a mut Drafts,
+ img_cache: &'a mut ImageCache,
+ ui: &'a mut egui::Ui,
+) {
// show a postbox in the first timeline
-
- if let Some(account) = app.account_manager.get_selected_account_index() {
- if app
- .account_manager
- .get_selected_account()
- .map_or(false, |a| a.secret_key.is_some())
- {
- if let Ok(txn) = Transaction::new(&app.ndb) {
- let response = ui::PostView::new(app, DraftSource::Compose, account).ui(&txn, ui);
-
- if let Some(action) = response.action {
- match action {
- PostAction::Post(np) => {
- let seckey = app
- .account_manager
- .get_account(account)
- .unwrap()
- .secret_key
- .as_ref()
- .unwrap()
- .to_secret_bytes();
-
- let note = np.to_note(&seckey);
- let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
- info!("sending {}", raw_msg);
- app.pool.send(&enostr::ClientMessage::raw(raw_msg));
- app.drafts.clear(DraftSource::Compose);
- }
- }
- }
+ let txn = Transaction::new(ndb).expect("txn");
+ let response = ui::PostView::new(ndb, drafts.compose_mut(), img_cache, key).ui(&txn, ui);
+
+ if let Some(action) = response.action {
+ match action {
+ PostAction::Post(np) => {
+ let seckey = key.secret_key.to_secret_bytes();
+ let note = np.to_note(&seckey);
+ let raw_msg = format!("[\"EVENT\",{}]", note.json().unwrap());
+ info!("sending {}", raw_msg);
+ pool.send(&enostr::ClientMessage::raw(raw_msg));
+ drafts.compose_mut().clear();
}
}
}
diff --git a/src/unknowns.rs b/src/unknowns.rs
@@ -1,6 +1,7 @@
-use crate::notecache::CachedNote;
+use crate::column::Columns;
+use crate::notecache::{CachedNote, NoteCache};
use crate::timeline::ViewFilter;
-use crate::{Damus, Result};
+use crate::Result;
use enostr::{Filter, NoteId, Pubkey};
use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction};
use std::collections::HashSet;
@@ -63,37 +64,45 @@ impl UnknownIds {
self.last_updated = Some(now);
}
- pub fn update_from_note(txn: &Transaction, app: &mut Damus, note: &Note) -> bool {
- let before = app.unknown_ids.ids().len();
+ pub fn update_from_note(
+ txn: &Transaction,
+ ndb: &Ndb,
+ unknown_ids: &mut UnknownIds,
+ note_cache: &mut NoteCache,
+ note: &Note,
+ ) -> bool {
+ let before = unknown_ids.ids().len();
let key = note.key().expect("note key");
- let cached_note = app
- .note_cache_mut()
- .cached_note_or_insert(key, note)
- .clone();
- if let Err(e) =
- get_unknown_note_ids(&app.ndb, &cached_note, txn, note, app.unknown_ids.ids_mut())
- {
+ //let cached_note = note_cache.cached_note_or_insert(key, note).clone();
+ let cached_note = note_cache.cached_note_or_insert(key, note);
+ if let Err(e) = get_unknown_note_ids(ndb, cached_note, txn, note, unknown_ids.ids_mut()) {
error!("UnknownIds::update_from_note {e}");
}
- let after = app.unknown_ids.ids().len();
+ let after = unknown_ids.ids().len();
if before != after {
- app.unknown_ids.mark_updated();
+ unknown_ids.mark_updated();
true
} else {
false
}
}
- pub fn update(txn: &Transaction, app: &mut Damus) -> bool {
- let before = app.unknown_ids.ids().len();
- if let Err(e) = get_unknown_ids(txn, app) {
+ pub fn update(
+ txn: &Transaction,
+ unknown_ids: &mut UnknownIds,
+ columns: &Columns,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+ ) -> bool {
+ let before = unknown_ids.ids().len();
+ if let Err(e) = get_unknown_ids(txn, unknown_ids, columns, ndb, note_cache) {
error!("UnknownIds::update {e}");
}
- let after = app.unknown_ids.ids().len();
+ let after = unknown_ids.ids().len();
if before != after {
- app.unknown_ids.mark_updated();
+ unknown_ids.mark_updated();
true
} else {
false
@@ -211,17 +220,23 @@ pub fn get_unknown_note_ids<'a>(
Ok(())
}
-fn get_unknown_ids(txn: &Transaction, damus: &mut Damus) -> Result<()> {
+fn get_unknown_ids(
+ txn: &Transaction,
+ unknown_ids: &mut UnknownIds,
+ columns: &Columns,
+ ndb: &Ndb,
+ note_cache: &mut NoteCache,
+) -> Result<()> {
#[cfg(feature = "profiling")]
puffin::profile_function!();
let mut new_cached_notes: Vec<(NoteKey, CachedNote)> = vec![];
- for timeline in &damus.timelines {
+ for timeline in columns.timelines() {
for noteref in timeline.notes(ViewFilter::NotesAndReplies) {
- let note = damus.ndb.get_note_by_key(txn, noteref.key)?;
+ let note = ndb.get_note_by_key(txn, noteref.key)?;
let note_key = note.key().unwrap();
- let cached_note = damus.note_cache().cached_note(noteref.key);
+ let cached_note = note_cache.cached_note(noteref.key);
let cached_note = if let Some(cn) = cached_note {
cn.clone()
} else {
@@ -230,20 +245,14 @@ fn get_unknown_ids(txn: &Transaction, damus: &mut Damus) -> Result<()> {
new_cached_note
};
- let _ = get_unknown_note_ids(
- &damus.ndb,
- &cached_note,
- txn,
- ¬e,
- damus.unknown_ids.ids_mut(),
- );
+ let _ = get_unknown_note_ids(ndb, &cached_note, txn, ¬e, unknown_ids.ids_mut());
}
}
// This is mainly done to avoid the double mutable borrow that would happen
// if we tried to update the note_cache mutably in the loop above
for (note_key, note) in new_cached_notes {
- damus.note_cache_mut().cache_mut().insert(note_key, note);
+ note_cache.cache_mut().insert(note_key, note);
}
Ok(())