mirror of
https://github.com/aljazceru/notedeck.git
synced 2026-01-11 12:24:20 +01:00
862 lines
27 KiB
Rust
862 lines
27 KiB
Rust
use crate::{
|
|
accounts::{Accounts, AccountsRoute},
|
|
app_creation::setup_cc,
|
|
app_size_handler::AppSizeHandler,
|
|
args::Args,
|
|
column::Columns,
|
|
decks::{Decks, DecksCache},
|
|
draft::Drafts,
|
|
filter::FilterState,
|
|
frame_history::FrameHistory,
|
|
imgcache::ImageCache,
|
|
nav,
|
|
notecache::NoteCache,
|
|
notes_holder::NotesHolderStorage,
|
|
profile::Profile,
|
|
storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType},
|
|
subscriptions::{SubKind, Subscriptions},
|
|
support::Support,
|
|
thread::Thread,
|
|
timeline::{self, Timeline},
|
|
ui::{self, DesktopSidePanel},
|
|
unknowns::UnknownIds,
|
|
view_state::ViewState,
|
|
Result,
|
|
};
|
|
|
|
use enostr::{ClientMessage, Keypair, Pubkey, RelayEvent, RelayMessage, RelayPool};
|
|
use uuid::Uuid;
|
|
|
|
use egui::{Context, Frame, Style};
|
|
use egui_extras::{Size, StripBuilder};
|
|
|
|
use nostrdb::{Config, Ndb, Transaction};
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
use std::time::Duration;
|
|
use tracing::{error, info, trace, warn};
|
|
|
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
|
pub enum DamusState {
|
|
Initializing,
|
|
Initialized,
|
|
}
|
|
|
|
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
|
pub struct Damus {
|
|
state: DamusState,
|
|
pub note_cache: NoteCache,
|
|
pub pool: RelayPool,
|
|
|
|
pub decks_cache: DecksCache,
|
|
pub ndb: Ndb,
|
|
pub view_state: ViewState,
|
|
pub unknown_ids: UnknownIds,
|
|
pub drafts: Drafts,
|
|
pub threads: NotesHolderStorage<Thread>,
|
|
pub profiles: NotesHolderStorage<Profile>,
|
|
pub img_cache: ImageCache,
|
|
pub accounts: Accounts,
|
|
pub subscriptions: Subscriptions,
|
|
pub app_rect_handler: AppSizeHandler,
|
|
pub support: Support,
|
|
|
|
frame_history: crate::frame_history::FrameHistory,
|
|
|
|
pub path: DataPath,
|
|
// TODO: make these bitflags
|
|
pub debug: bool,
|
|
pub since_optimize: bool,
|
|
pub textmode: bool,
|
|
}
|
|
|
|
fn handle_key_events(input: &egui::InputState, _pixels_per_point: f32, columns: &mut Columns) {
|
|
for event in &input.raw.events {
|
|
if let egui::Event::Key {
|
|
key, pressed: true, ..
|
|
} = event
|
|
{
|
|
match key {
|
|
egui::Key::J => {
|
|
columns.select_down();
|
|
}
|
|
egui::Key::K => {
|
|
columns.select_up();
|
|
}
|
|
egui::Key::H => {
|
|
columns.select_left();
|
|
}
|
|
egui::Key::L => {
|
|
columns.select_left();
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_process_event(damus: &mut Damus, ctx: &egui::Context) -> Result<()> {
|
|
let ppp = ctx.pixels_per_point();
|
|
let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache);
|
|
ctx.input(|i| handle_key_events(i, ppp, current_columns));
|
|
|
|
let ctx2 = ctx.clone();
|
|
let wakeup = move || {
|
|
ctx2.request_repaint();
|
|
};
|
|
damus.pool.keepalive_ping(wakeup);
|
|
|
|
// NOTE: we don't use the while let loop due to borrow issues
|
|
#[allow(clippy::while_let_loop)]
|
|
loop {
|
|
let ev = if let Some(ev) = damus.pool.try_recv() {
|
|
ev.into_owned()
|
|
} else {
|
|
break;
|
|
};
|
|
|
|
match (&ev.event).into() {
|
|
RelayEvent::Opened => {
|
|
damus
|
|
.accounts
|
|
.send_initial_filters(&mut damus.pool, &ev.relay);
|
|
|
|
timeline::send_initial_timeline_filters(
|
|
&damus.ndb,
|
|
damus.since_optimize,
|
|
get_active_columns_mut(&damus.accounts, &mut damus.decks_cache),
|
|
&mut damus.subscriptions,
|
|
&mut damus.pool,
|
|
&ev.relay,
|
|
);
|
|
}
|
|
// TODO: handle reconnects
|
|
RelayEvent::Closed => warn!("{} connection closed", &ev.relay),
|
|
RelayEvent::Error(e) => error!("{}: {}", &ev.relay, e),
|
|
RelayEvent::Other(msg) => trace!("other event {:?}", &msg),
|
|
RelayEvent::Message(msg) => process_message(damus, &ev.relay, &msg),
|
|
}
|
|
}
|
|
|
|
let current_columns = get_active_columns_mut(&damus.accounts, &mut damus.decks_cache);
|
|
let n_timelines = current_columns.timelines().len();
|
|
for timeline_ind in 0..n_timelines {
|
|
let is_ready = {
|
|
let timeline = &mut current_columns.timelines[timeline_ind];
|
|
timeline::is_timeline_ready(
|
|
&damus.ndb,
|
|
&mut damus.pool,
|
|
&mut damus.note_cache,
|
|
timeline,
|
|
&damus.accounts.mutefun(),
|
|
)
|
|
};
|
|
|
|
if is_ready {
|
|
let txn = Transaction::new(&damus.ndb).expect("txn");
|
|
|
|
if let Err(err) = Timeline::poll_notes_into_view(
|
|
timeline_ind,
|
|
current_columns.timelines_mut(),
|
|
&damus.ndb,
|
|
&txn,
|
|
&mut damus.unknown_ids,
|
|
&mut damus.note_cache,
|
|
&damus.accounts.mutefun(),
|
|
) {
|
|
error!("poll_notes_into_view: {err}");
|
|
}
|
|
} else {
|
|
// TODO: show loading?
|
|
}
|
|
}
|
|
|
|
if damus.unknown_ids.ready_to_send() {
|
|
unknown_id_send(damus);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn unknown_id_send(damus: &mut Damus) {
|
|
let filter = damus.unknown_ids.filter().expect("filter");
|
|
info!(
|
|
"Getting {} unknown ids from relays",
|
|
damus.unknown_ids.ids().len()
|
|
);
|
|
let msg = ClientMessage::req("unknownids".to_string(), filter);
|
|
damus.unknown_ids.clear();
|
|
damus.pool.send(&msg);
|
|
}
|
|
|
|
#[cfg(feature = "profiling")]
|
|
fn setup_profiling() {
|
|
puffin::set_scopes_on(true); // tell puffin to collect data
|
|
}
|
|
|
|
fn update_damus(damus: &mut Damus, ctx: &egui::Context) {
|
|
damus.accounts.update(&damus.ndb, &mut damus.pool, ctx); // update user relay and mute lists
|
|
|
|
match damus.state {
|
|
DamusState::Initializing => {
|
|
#[cfg(feature = "profiling")]
|
|
setup_profiling();
|
|
|
|
damus.state = DamusState::Initialized;
|
|
// this lets our eose handler know to close unknownids right away
|
|
damus
|
|
.subscriptions()
|
|
.insert("unknownids".to_string(), SubKind::OneShot);
|
|
if let Err(err) = timeline::setup_initial_nostrdb_subs(
|
|
&damus.ndb,
|
|
&mut damus.note_cache,
|
|
&mut damus.decks_cache,
|
|
&damus.accounts.mutefun(),
|
|
) {
|
|
warn!("update_damus init: {err}");
|
|
}
|
|
}
|
|
|
|
DamusState::Initialized => (),
|
|
};
|
|
|
|
if let Err(err) = try_process_event(damus, ctx) {
|
|
error!("error processing event: {}", err);
|
|
}
|
|
|
|
damus.app_rect_handler.try_save_app_size(ctx);
|
|
}
|
|
|
|
fn process_event(damus: &mut Damus, _subid: &str, event: &str) {
|
|
#[cfg(feature = "profiling")]
|
|
puffin::profile_function!();
|
|
|
|
//info!("processing event {}", event);
|
|
if let Err(_err) = damus.ndb.process_event(event) {
|
|
error!("error processing event {}", event);
|
|
}
|
|
}
|
|
|
|
fn handle_eose(damus: &mut Damus, subid: &str, relay_url: &str) -> Result<()> {
|
|
let sub_kind = if let Some(sub_kind) = damus.subscriptions().get(subid) {
|
|
sub_kind
|
|
} else {
|
|
let n_subids = damus.subscriptions().len();
|
|
warn!(
|
|
"got unknown eose subid {}, {} tracked subscriptions",
|
|
subid, n_subids
|
|
);
|
|
return Ok(());
|
|
};
|
|
|
|
match *sub_kind {
|
|
SubKind::Timeline(_) => {
|
|
// eose on timeline? whatevs
|
|
}
|
|
SubKind::Initial => {
|
|
let txn = Transaction::new(&damus.ndb)?;
|
|
UnknownIds::update(
|
|
&txn,
|
|
&mut damus.unknown_ids,
|
|
get_active_columns(&damus.accounts, &damus.decks_cache),
|
|
&damus.ndb,
|
|
&mut damus.note_cache,
|
|
);
|
|
// this is possible if this is the first time
|
|
if damus.unknown_ids.ready_to_send() {
|
|
unknown_id_send(damus);
|
|
}
|
|
}
|
|
|
|
// oneshot subs just close when they're done
|
|
SubKind::OneShot => {
|
|
let msg = ClientMessage::close(subid.to_string());
|
|
damus.pool.send_to(&msg, relay_url);
|
|
}
|
|
|
|
SubKind::FetchingContactList(timeline_uid) => {
|
|
let timeline = if let Some(tl) =
|
|
get_active_columns_mut(&damus.accounts, &mut damus.decks_cache)
|
|
.find_timeline_mut(timeline_uid)
|
|
{
|
|
tl
|
|
} else {
|
|
error!(
|
|
"timeline uid:{} not found for FetchingContactList",
|
|
timeline_uid
|
|
);
|
|
return Ok(());
|
|
};
|
|
|
|
let filter_state = timeline.filter.get(relay_url);
|
|
|
|
// If this request was fetching a contact list, our filter
|
|
// state should be "FetchingRemote". We look at the local
|
|
// subscription for that filter state and get the subscription id
|
|
let local_sub = if let FilterState::FetchingRemote(unisub) = filter_state {
|
|
unisub.local
|
|
} else {
|
|
// TODO: we could have multiple contact list results, we need
|
|
// to check to see if this one is newer and use that instead
|
|
warn!(
|
|
"Expected timeline to have FetchingRemote state but was {:?}",
|
|
timeline.filter
|
|
);
|
|
return Ok(());
|
|
};
|
|
|
|
info!(
|
|
"got contact list from {}, updating filter_state to got_remote",
|
|
relay_url
|
|
);
|
|
|
|
// We take the subscription id and pass it to the new state of
|
|
// "GotRemote". This will let future frames know that it can try
|
|
// to look for the contact list in nostrdb.
|
|
timeline
|
|
.filter
|
|
.set_relay_state(relay_url.to_string(), FilterState::got_remote(local_sub));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn process_message(damus: &mut Damus, relay: &str, msg: &RelayMessage) {
|
|
match msg {
|
|
RelayMessage::Event(subid, ev) => process_event(damus, subid, ev),
|
|
RelayMessage::Notice(msg) => warn!("Notice from {}: {}", relay, msg),
|
|
RelayMessage::OK(cr) => info!("OK {:?}", cr),
|
|
RelayMessage::Eose(sid) => {
|
|
if let Err(err) = handle_eose(damus, sid, relay) {
|
|
error!("error handling eose: {}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_damus(damus: &mut Damus, ctx: &Context) {
|
|
if ui::is_narrow(ctx) {
|
|
render_damus_mobile(ctx, damus);
|
|
} else {
|
|
render_damus_desktop(ctx, damus);
|
|
}
|
|
|
|
ctx.request_repaint_after(Duration::from_secs(1));
|
|
|
|
#[cfg(feature = "profiling")]
|
|
puffin_egui::profiler_window(ctx);
|
|
}
|
|
|
|
/*
|
|
fn determine_key_storage_type() -> KeyStorageType {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
KeyStorageType::MacOS
|
|
}
|
|
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
KeyStorageType::Linux
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
|
{
|
|
KeyStorageType::None
|
|
}
|
|
}
|
|
*/
|
|
|
|
impl Damus {
|
|
/// Called once before the first frame.
|
|
pub fn new<P: AsRef<Path>>(ctx: &egui::Context, data_path: P, args: Vec<String>) -> Self {
|
|
// arg parsing
|
|
let parsed_args = Args::parse(&args);
|
|
let is_mobile = parsed_args.is_mobile.unwrap_or(ui::is_compiled_as_mobile());
|
|
|
|
// Some people have been running notedeck in debug, let's catch that!
|
|
if !cfg!(test) && cfg!(debug_assertions) && !parsed_args.debug {
|
|
println!("--- WELCOME TO DAMUS NOTEDECK! ---");
|
|
println!("It looks like are running notedeck in debug mode, unless you are a developer, this is not likely what you want.");
|
|
println!("If you are a developer, run `cargo run -- --debug` to skip this message.");
|
|
println!("For everyone else, try again with `cargo run --release`. Enjoy!");
|
|
println!("---------------------------------");
|
|
panic!();
|
|
}
|
|
|
|
setup_cc(ctx, is_mobile, parsed_args.light);
|
|
|
|
let data_path = parsed_args
|
|
.datapath
|
|
.unwrap_or(data_path.as_ref().to_str().expect("db path ok").to_string());
|
|
let path = DataPath::new(&data_path);
|
|
let dbpath_str = parsed_args
|
|
.dbpath
|
|
.unwrap_or_else(|| path.path(DataPathType::Db).to_str().unwrap().to_string());
|
|
|
|
let _ = std::fs::create_dir_all(&dbpath_str);
|
|
|
|
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
|
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
|
|
|
let mapsize = if cfg!(target_os = "windows") {
|
|
// 16 Gib on windows because it actually creates the file
|
|
1024usize * 1024usize * 1024usize * 16usize
|
|
} else {
|
|
// 1 TiB for everything else since its just virtually mapped
|
|
1024usize * 1024usize * 1024usize * 1024usize
|
|
};
|
|
|
|
let config = Config::new().set_ingester_threads(4).set_mapsize(mapsize);
|
|
|
|
let keystore = if parsed_args.use_keystore {
|
|
let keys_path = path.path(DataPathType::Keys);
|
|
let selected_key_path = path.path(DataPathType::SelectedKey);
|
|
KeyStorageType::FileSystem(FileKeyStorage::new(
|
|
Directory::new(keys_path),
|
|
Directory::new(selected_key_path),
|
|
))
|
|
} else {
|
|
KeyStorageType::None
|
|
};
|
|
|
|
let mut accounts = Accounts::new(keystore, parsed_args.relays);
|
|
|
|
let num_keys = parsed_args.keys.len();
|
|
|
|
let mut unknown_ids = UnknownIds::default();
|
|
let ndb = Ndb::new(&dbpath_str, &config).expect("ndb");
|
|
|
|
{
|
|
let txn = Transaction::new(&ndb).expect("txn");
|
|
for key in parsed_args.keys {
|
|
info!("adding account: {}", key.pubkey);
|
|
accounts
|
|
.add_account(key)
|
|
.process_action(&mut unknown_ids, &ndb, &txn);
|
|
}
|
|
}
|
|
|
|
if num_keys != 0 {
|
|
accounts.select_account(0);
|
|
}
|
|
|
|
// AccountManager will setup the pool on first update
|
|
let pool = RelayPool::new();
|
|
|
|
let account = accounts
|
|
.get_selected_account()
|
|
.as_ref()
|
|
.map(|a| a.pubkey.bytes());
|
|
|
|
let columns = if parsed_args.columns.is_empty() {
|
|
if let Some(serializable_columns) = storage::load_columns(&path) {
|
|
info!("Using columns from disk");
|
|
serializable_columns.into_columns(&ndb, account)
|
|
} else {
|
|
info!("Could not load columns from disk");
|
|
Columns::new()
|
|
}
|
|
} else {
|
|
info!(
|
|
"Using columns from command line arguments: {:?}",
|
|
parsed_args.columns
|
|
);
|
|
let mut columns: Columns = Columns::new();
|
|
for col in parsed_args.columns {
|
|
if let Some(timeline) = col.into_timeline(&ndb, account) {
|
|
columns.add_new_timeline_column(timeline);
|
|
}
|
|
}
|
|
|
|
columns
|
|
};
|
|
|
|
let mut decks_cache = {
|
|
let mut decks_cache = DecksCache::default();
|
|
|
|
let mut decks = Decks::default();
|
|
*decks.active_mut().columns_mut() = columns;
|
|
|
|
if let Some(acc) = account {
|
|
decks_cache.add_decks(Pubkey::new(*acc), decks);
|
|
}
|
|
|
|
decks_cache
|
|
};
|
|
|
|
let debug = parsed_args.debug;
|
|
|
|
if get_active_columns(&accounts, &decks_cache)
|
|
.columns()
|
|
.is_empty()
|
|
{
|
|
if accounts.get_accounts().is_empty() {
|
|
set_demo(
|
|
&path,
|
|
&ndb,
|
|
&mut accounts,
|
|
&mut decks_cache,
|
|
&mut unknown_ids,
|
|
);
|
|
} else {
|
|
get_active_columns_mut(&accounts, &mut decks_cache).new_column_picker();
|
|
}
|
|
}
|
|
|
|
let app_rect_handler = AppSizeHandler::new(&path);
|
|
let support = Support::new(&path);
|
|
|
|
Self {
|
|
pool,
|
|
debug,
|
|
unknown_ids,
|
|
subscriptions: Subscriptions::default(),
|
|
since_optimize: parsed_args.since_optimize,
|
|
threads: NotesHolderStorage::default(),
|
|
profiles: NotesHolderStorage::default(),
|
|
drafts: Drafts::default(),
|
|
state: DamusState::Initializing,
|
|
img_cache: ImageCache::new(imgcache_dir),
|
|
note_cache: NoteCache::default(),
|
|
textmode: parsed_args.textmode,
|
|
ndb,
|
|
accounts,
|
|
frame_history: FrameHistory::default(),
|
|
view_state: ViewState::default(),
|
|
path,
|
|
app_rect_handler,
|
|
support,
|
|
decks_cache,
|
|
}
|
|
}
|
|
|
|
pub fn pool_mut(&mut self) -> &mut RelayPool {
|
|
&mut self.pool
|
|
}
|
|
|
|
pub fn ndb(&self) -> &Ndb {
|
|
&self.ndb
|
|
}
|
|
|
|
pub fn drafts_mut(&mut self) -> &mut Drafts {
|
|
&mut self.drafts
|
|
}
|
|
|
|
pub fn img_cache_mut(&mut self) -> &mut ImageCache {
|
|
&mut self.img_cache
|
|
}
|
|
|
|
pub fn accounts(&self) -> &Accounts {
|
|
&self.accounts
|
|
}
|
|
|
|
pub fn accounts_mut(&mut self) -> &mut Accounts {
|
|
&mut self.accounts
|
|
}
|
|
|
|
pub fn view_state_mut(&mut self) -> &mut ViewState {
|
|
&mut self.view_state
|
|
}
|
|
|
|
pub fn columns_mut(&mut self) -> &mut Columns {
|
|
get_active_columns_mut(&self.accounts, &mut self.decks_cache)
|
|
}
|
|
|
|
pub fn columns(&self) -> &Columns {
|
|
get_active_columns(&self.accounts, &self.decks_cache)
|
|
}
|
|
|
|
pub fn gen_subid(&self, kind: &SubKind) -> String {
|
|
if self.debug {
|
|
format!("{:?}", kind)
|
|
} else {
|
|
Uuid::new_v4().to_string()
|
|
}
|
|
}
|
|
|
|
pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
|
|
let decks_cache = DecksCache::default();
|
|
|
|
let path = DataPath::new(&data_path);
|
|
let imgcache_dir = path.path(DataPathType::Cache).join(ImageCache::rel_dir());
|
|
let _ = std::fs::create_dir_all(imgcache_dir.clone());
|
|
let debug = true;
|
|
|
|
let app_rect_handler = AppSizeHandler::new(&path);
|
|
let support = Support::new(&path);
|
|
|
|
let config = Config::new().set_ingester_threads(2);
|
|
|
|
Self {
|
|
debug,
|
|
unknown_ids: UnknownIds::default(),
|
|
subscriptions: Subscriptions::default(),
|
|
since_optimize: true,
|
|
threads: NotesHolderStorage::default(),
|
|
profiles: NotesHolderStorage::default(),
|
|
drafts: Drafts::default(),
|
|
state: DamusState::Initializing,
|
|
pool: RelayPool::new(),
|
|
img_cache: ImageCache::new(imgcache_dir),
|
|
note_cache: NoteCache::default(),
|
|
textmode: false,
|
|
ndb: Ndb::new(
|
|
path.path(DataPathType::Db)
|
|
.to_str()
|
|
.expect("db path should be ok"),
|
|
&config,
|
|
)
|
|
.expect("ndb"),
|
|
accounts: Accounts::new(KeyStorageType::None, vec![]),
|
|
frame_history: FrameHistory::default(),
|
|
view_state: ViewState::default(),
|
|
path,
|
|
app_rect_handler,
|
|
support,
|
|
decks_cache,
|
|
}
|
|
}
|
|
|
|
pub fn subscriptions(&mut self) -> &mut HashMap<String, SubKind> {
|
|
&mut self.subscriptions.subs
|
|
}
|
|
|
|
pub fn note_cache_mut(&mut self) -> &mut NoteCache {
|
|
&mut self.note_cache
|
|
}
|
|
|
|
pub fn unknown_ids_mut(&mut self) -> &mut UnknownIds {
|
|
&mut self.unknown_ids
|
|
}
|
|
|
|
pub fn threads(&self) -> &NotesHolderStorage<Thread> {
|
|
&self.threads
|
|
}
|
|
|
|
pub fn threads_mut(&mut self) -> &mut NotesHolderStorage<Thread> {
|
|
&mut self.threads
|
|
}
|
|
|
|
pub fn note_cache(&self) -> &NoteCache {
|
|
&self.note_cache
|
|
}
|
|
}
|
|
|
|
/*
|
|
fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
|
|
let stroke = ui.style().interact(&response).fg_stroke;
|
|
let radius = egui::lerp(2.0..=3.0, openness);
|
|
ui.painter()
|
|
.circle_filled(response.rect.center(), radius, stroke.color);
|
|
}
|
|
*/
|
|
|
|
fn render_damus_mobile(ctx: &egui::Context, app: &mut Damus) {
|
|
#[cfg(feature = "profiling")]
|
|
puffin::profile_function!();
|
|
|
|
//let routes = app.timelines[0].routes.clone();
|
|
|
|
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
|
|
if !app.columns().columns().is_empty()
|
|
&& nav::render_nav(0, app, ui).process_render_nav_response(app)
|
|
{
|
|
storage::save_columns(&app.path, app.columns().as_serializable_columns());
|
|
}
|
|
});
|
|
}
|
|
|
|
fn main_panel(style: &Style, narrow: bool) -> egui::CentralPanel {
|
|
let inner_margin = egui::Margin {
|
|
top: if narrow { 50.0 } else { 0.0 },
|
|
left: 0.0,
|
|
right: 0.0,
|
|
bottom: 0.0,
|
|
};
|
|
egui::CentralPanel::default().frame(Frame {
|
|
inner_margin,
|
|
fill: style.visuals.panel_fill,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
|
|
#[cfg(feature = "profiling")]
|
|
puffin::profile_function!();
|
|
|
|
let screen_size = ctx.screen_rect().width();
|
|
let calc_panel_width = (screen_size
|
|
/ get_active_columns(&app.accounts, &app.decks_cache).num_columns() as f32)
|
|
- 30.0;
|
|
let min_width = 320.0;
|
|
let need_scroll = calc_panel_width < min_width;
|
|
let panel_sizes = if need_scroll {
|
|
Size::exact(min_width)
|
|
} else {
|
|
Size::remainder()
|
|
};
|
|
|
|
main_panel(&ctx.style(), ui::is_narrow(ctx)).show(ctx, |ui| {
|
|
ui.spacing_mut().item_spacing.x = 0.0;
|
|
if need_scroll {
|
|
egui::ScrollArea::horizontal().show(ui, |ui| {
|
|
timelines_view(ui, panel_sizes, app);
|
|
});
|
|
} else {
|
|
timelines_view(ui, panel_sizes, app);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus) {
|
|
StripBuilder::new(ui)
|
|
.size(Size::exact(ui::side_panel::SIDE_PANEL_WIDTH))
|
|
.sizes(
|
|
sizes,
|
|
get_active_columns(&app.accounts, &app.decks_cache).num_columns(),
|
|
)
|
|
.clip(true)
|
|
.horizontal(|mut strip| {
|
|
strip.cell(|ui| {
|
|
let rect = ui.available_rect_before_wrap();
|
|
let side_panel = DesktopSidePanel::new(
|
|
&app.ndb,
|
|
&mut app.img_cache,
|
|
app.accounts.get_selected_account(),
|
|
&app.decks_cache,
|
|
)
|
|
.show(ui);
|
|
|
|
if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
|
|
DesktopSidePanel::perform_action(
|
|
&mut app.decks_cache,
|
|
&app.accounts,
|
|
&mut app.support,
|
|
side_panel.action,
|
|
);
|
|
}
|
|
|
|
// vertical sidebar line
|
|
ui.painter().vline(
|
|
rect.right(),
|
|
rect.y_range(),
|
|
ui.visuals().widgets.noninteractive.bg_stroke,
|
|
);
|
|
});
|
|
|
|
let num_cols = app.columns().num_columns();
|
|
let mut responses = Vec::with_capacity(num_cols);
|
|
for col_index in 0..num_cols {
|
|
strip.cell(|ui| {
|
|
let rect = ui.available_rect_before_wrap();
|
|
responses.push(nav::render_nav(col_index, app, ui));
|
|
|
|
// vertical line
|
|
ui.painter().vline(
|
|
rect.right(),
|
|
rect.y_range(),
|
|
ui.visuals().widgets.noninteractive.bg_stroke,
|
|
);
|
|
});
|
|
|
|
//strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind));
|
|
}
|
|
|
|
let mut save_cols = false;
|
|
for response in responses {
|
|
let save = response.process_render_nav_response(app);
|
|
save_cols = save_cols || save;
|
|
}
|
|
|
|
if save_cols {
|
|
storage::save_columns(&app.path, app.columns().as_serializable_columns());
|
|
}
|
|
});
|
|
}
|
|
|
|
impl eframe::App for Damus {
|
|
/// Called by the frame work to save state before shutdown.
|
|
fn save(&mut self, _storage: &mut dyn eframe::Storage) {
|
|
//eframe::set_value(storage, eframe::APP_KEY, self);
|
|
}
|
|
|
|
/// Called each time the UI needs repainting, which may be many times per second.
|
|
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
|
|
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
|
self.frame_history
|
|
.on_new_frame(ctx.input(|i| i.time), frame.info().cpu_usage);
|
|
|
|
#[cfg(feature = "profiling")]
|
|
puffin::GlobalProfiler::lock().new_frame();
|
|
update_damus(self, ctx);
|
|
render_damus(self, ctx);
|
|
}
|
|
}
|
|
|
|
pub fn get_active_columns<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Columns {
|
|
get_decks(accounts, decks_cache).active().columns()
|
|
}
|
|
|
|
pub fn get_decks<'a>(accounts: &Accounts, decks_cache: &'a DecksCache) -> &'a Decks {
|
|
let key = if let Some(acc) = accounts.get_selected_account() {
|
|
&acc.pubkey
|
|
} else {
|
|
decks_cache.get_fallback_pubkey()
|
|
};
|
|
decks_cache.decks(key)
|
|
}
|
|
|
|
pub fn get_active_columns_mut<'a>(
|
|
accounts: &Accounts,
|
|
decks_cache: &'a mut DecksCache,
|
|
) -> &'a mut Columns {
|
|
get_decks_mut(accounts, decks_cache)
|
|
.active_mut()
|
|
.columns_mut()
|
|
}
|
|
|
|
pub fn get_decks_mut<'a>(accounts: &Accounts, decks_cache: &'a mut DecksCache) -> &'a mut Decks {
|
|
if let Some(acc) = accounts.get_selected_account() {
|
|
decks_cache.decks_mut(&acc.pubkey)
|
|
} else {
|
|
decks_cache.fallback_mut()
|
|
}
|
|
}
|
|
|
|
pub fn set_demo(
|
|
data_path: &DataPath,
|
|
ndb: &Ndb,
|
|
accounts: &mut Accounts,
|
|
decks_cache: &mut DecksCache,
|
|
unk_ids: &mut UnknownIds,
|
|
) {
|
|
let demo_pubkey = *decks_cache.get_fallback_pubkey();
|
|
let columns = get_active_columns_mut(accounts, decks_cache);
|
|
|
|
{
|
|
let txn = Transaction::new(ndb).expect("txn");
|
|
accounts
|
|
.add_account(Keypair::only_pubkey(demo_pubkey))
|
|
.process_action(unk_ids, ndb, &txn);
|
|
accounts.select_account(0);
|
|
}
|
|
|
|
columns.add_column(crate::column::Column::new(vec![
|
|
crate::route::Route::AddColumn(ui::add_column::AddColumnRoute::Base),
|
|
crate::route::Route::Accounts(AccountsRoute::Accounts),
|
|
]));
|
|
|
|
if let Some(timeline) =
|
|
timeline::TimelineKind::contact_list(timeline::PubkeySource::Explicit(demo_pubkey))
|
|
.into_timeline(ndb, Some(demo_pubkey.bytes()))
|
|
{
|
|
columns.add_new_timeline_column(timeline);
|
|
}
|
|
|
|
columns.add_new_timeline_column(Timeline::hashtag("introductions".to_string()));
|
|
|
|
storage::save_columns(data_path, columns.as_serializable_columns());
|
|
}
|