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