From 31b2b5c950591536263f3218bf4a11e8f6b59ec1 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Mon, 27 May 2024 12:22:40 -0700 Subject: [PATCH] initial refactor in preparation for routing Signed-off-by: William Casarin --- enostr/src/keypair.rs | 6 +- src/account_manager.rs | 49 ++---- src/app.rs | 47 ++---- src/key_parsing.rs | 6 +- src/key_storage.rs | 10 +- src/lib.rs | 1 + src/macos_key_storage.rs | 28 ++-- src/relay_generation.rs | 42 +---- src/route.rs | 7 + src/test_data.rs | 22 +-- src/ui/account_management.rs | 90 +++++----- src/ui/account_switcher.rs | 144 +++++++++------- src/ui/global_popup.rs | 168 ------------------- src/ui/mod.rs | 3 - src/ui/profile/profile_preview_controller.rs | 140 +++++++++------- src/ui/side_panel.rs | 95 ++++++----- src/ui/state_in_memory.rs | 43 ----- src/ui_preview/main.rs | 5 +- src/user_account.rs | 13 +- 19 files changed, 332 insertions(+), 587 deletions(-) create mode 100644 src/route.rs delete mode 100644 src/ui/global_popup.rs delete mode 100644 src/ui/state_in_memory.rs diff --git a/enostr/src/keypair.rs b/enostr/src/keypair.rs index 6d0c4a2..44929e8 100644 --- a/enostr/src/keypair.rs +++ b/enostr/src/keypair.rs @@ -8,7 +8,7 @@ pub struct Keypair { } impl Keypair { - pub fn new(secret_key: SecretKey) -> Self { + pub fn from_secret(secret_key: SecretKey) -> Self { let cloned_secret_key = secret_key.clone(); let nostr_keys = nostr::Keys::new(secret_key); Keypair { @@ -17,6 +17,10 @@ impl Keypair { } } + pub fn new(pubkey: Pubkey, secret_key: Option) -> Self { + Keypair { pubkey, secret_key } + } + pub fn only_pubkey(pubkey: Pubkey) -> Self { Keypair { pubkey, diff --git a/src/account_manager.rs b/src/account_manager.rs index e610394..91077c1 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -1,9 +1,9 @@ use std::cmp::Ordering; -use enostr::FullKeypair; +use enostr::Keypair; +use crate::key_storage::KeyStorage; pub use crate::user_account::UserAccount; -use crate::{key_storage::KeyStorage, relay_generation::RelayGenerator}; /// The interface for managing the user's accounts. /// Represents all user-facing operations related to account management. @@ -11,34 +11,16 @@ pub struct AccountManager { currently_selected_account: Option, accounts: Vec, key_store: KeyStorage, - relay_generator: RelayGenerator, } impl AccountManager { - pub fn new( - currently_selected_account: Option, - key_store: KeyStorage, - // TODO: right now, there is only one way of generating relays for all accounts. In the future - // each account should have the option of generating relays differently - relay_generator: RelayGenerator, - wakeup: impl Fn() + Send + Sync + Clone + 'static, - ) -> Self { - let accounts = if let Ok(keys) = key_store.get_keys() { - keys.into_iter() - .map(|key| { - let relays = relay_generator.generate_relays_for(&key.pubkey, wakeup.clone()); - UserAccount { key, relays } - }) - .collect() - } else { - Vec::new() - }; + pub fn new(currently_selected_account: Option, key_store: KeyStorage) -> Self { + let accounts = key_store.get_keys().unwrap_or_default(); AccountManager { currently_selected_account, accounts, key_store, - relay_generator, } } @@ -46,13 +28,17 @@ impl AccountManager { &self.accounts } - pub fn get_account(&self, index: usize) -> Option<&UserAccount> { - self.accounts.get(index) + pub fn get_account(&self, ind: usize) -> Option<&UserAccount> { + self.accounts.get(ind) + } + + pub fn find_account(&self, pk: &[u8; 32]) -> Option<&UserAccount> { + self.accounts.iter().find(|acc| acc.pubkey.bytes() == pk) } pub fn remove_account(&mut self, index: usize) { if let Some(account) = self.accounts.get(index) { - let _ = self.key_store.remove_key(&account.key); + let _ = self.key_store.remove_key(account); self.accounts.remove(index); if let Some(selected_index) = self.currently_selected_account { @@ -69,17 +55,8 @@ impl AccountManager { } } - pub fn add_account( - &mut self, - key: FullKeypair, - wakeup: impl Fn() + Send + Sync + Clone + 'static, - ) { - let _ = self.key_store.add_key(&key); - let relays = self - .relay_generator - .generate_relays_for(&key.pubkey, wakeup); - let account = UserAccount { key, relays }; - + pub fn add_account(&mut self, account: Keypair) { + let _ = self.key_store.add_key(&account); self.accounts.push(account) } diff --git a/src/app.rs b/src/app.rs index 891d75d..2d48a83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,11 +5,11 @@ use crate::error::Error; use crate::frame_history::FrameHistory; use crate::imgcache::ImageCache; use crate::notecache::{CachedNote, NoteCache}; -use crate::relay_pool_manager::create_wakeup; +use crate::route::Route; use crate::timeline; use crate::timeline::{NoteRef, Timeline, ViewFilter}; use crate::ui::profile::SimpleProfilePreviewController; -use crate::ui::{is_mobile, DesktopGlobalPopup, DesktopSidePanel, View}; +use crate::ui::{is_mobile, DesktopSidePanel}; use crate::Result; use egui::{Context, Frame, Style}; @@ -39,6 +39,8 @@ pub struct Damus { note_cache: NoteCache, pool: RelayPool, + /// global navigation for account management popups, etc. + nav: Vec, pub textmode: bool, pub timelines: Vec, @@ -645,6 +647,7 @@ impl Damus { img_cache: ImageCache::new(imgcache_dir), note_cache: NoteCache::default(), selected_timeline: 0, + nav: Vec::with_capacity(6), timelines, textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), @@ -653,9 +656,6 @@ impl Damus { None, // TODO: use correct KeyStorage mechanism for current OS arch crate::key_storage::KeyStorage::None, - // TODO: setting for relay generator - crate::relay_generation::RelayGenerator::Constant, - create_wakeup(&cc.egui_ctx), ), //compose: "".to_string(), frame_history: FrameHistory::default(), @@ -680,14 +680,10 @@ impl Damus { note_cache: NoteCache::default(), selected_timeline: 0, timelines, + nav: vec![], textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), - account_manager: AccountManager::new( - None, - crate::key_storage::KeyStorage::None, - crate::relay_generation::RelayGenerator::Constant, - || {}, - ), + account_manager: AccountManager::new(None, crate::key_storage::KeyStorage::None), frame_history: FrameHistory::default(), } } @@ -852,25 +848,8 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) { Size::remainder() }; - if app.timelines.len() == 1 { - DesktopSidePanel::panel().show(ctx, |ui| { - DesktopSidePanel::new( - &mut app.account_manager, - SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache), - ) - .inner(ui); - }); - main_panel(&ctx.style()).show(ctx, |ui| { - DesktopGlobalPopup::new(app).ui(ui); - timeline::timeline_view(ui, app, 0); - }); - - return; - } - main_panel(&ctx.style()).show(ctx, |ui| { ui.spacing_mut().item_spacing.x = 0.0; - DesktopGlobalPopup::new(app).ui(ui); if need_scroll { egui::ScrollArea::horizontal().show(ui, |ui| { timelines_view(ui, panel_sizes, app, app.timelines.len()); @@ -888,11 +867,17 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: us .clip(true) .horizontal(|mut strip| { strip.cell(|ui| { - DesktopSidePanel::new( - &mut app.account_manager, + if DesktopSidePanel::new( + app.account_manager + .get_selected_account() + .map(|a| a.pubkey.bytes()), SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache), ) - .inner(ui) + .show(ui) + .clicked() + { + // clicked pfp + } }); for timeline_ind in 0..timelines { diff --git a/src/key_parsing.rs b/src/key_parsing.rs index 59d7842..a6968fa 100644 --- a/src/key_parsing.rs +++ b/src/key_parsing.rs @@ -125,7 +125,7 @@ pub async fn get_login_key(key: &str) -> Result { } else if let Ok(pubkey) = Pubkey::try_from_hex_str_with_verify(tmp_key) { Ok(Keypair::only_pubkey(pubkey)) } else if let Ok(secret_key) = SecretKey::from_str(tmp_key) { - Ok(Keypair::new(secret_key)) + Ok(Keypair::from_secret(secret_key)) } else { Err(LoginError::InvalidKey) } @@ -181,7 +181,7 @@ mod tests { promise_assert!( assert_eq, - Ok(Keypair::new(expected_privkey)), + Ok(Keypair::from_secret(expected_privkey)), &login_key_result ); } @@ -194,7 +194,7 @@ mod tests { promise_assert!( assert_eq, - Ok(Keypair::new(expected_privkey)), + Ok(Keypair::from_secret(expected_privkey)), &login_key_result ); } diff --git a/src/key_storage.rs b/src/key_storage.rs index 06d1814..0503dba 100644 --- a/src/key_storage.rs +++ b/src/key_storage.rs @@ -1,4 +1,4 @@ -use enostr::FullKeypair; +use enostr::Keypair; #[cfg(target_os = "macos")] use crate::macos_key_storage::MacOSKeyStorage; @@ -17,15 +17,15 @@ pub enum KeyStorage { } impl KeyStorage { - pub fn get_keys(&self) -> Result, KeyStorageError> { + pub fn get_keys(&self) -> Result, KeyStorageError> { match self { Self::None => Ok(Vec::new()), #[cfg(target_os = "macos")] - Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_fullkeypairs()), + Self::MacOS => Ok(MacOSKeyStorage::new(SERVICE_NAME).get_all_keypairs()), } } - pub fn add_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> { + pub fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { let _ = key; match self { Self::None => Ok(()), @@ -34,7 +34,7 @@ impl KeyStorage { } } - pub fn remove_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> { + pub fn remove_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { let _ = key; match self { Self::None => Ok(()), diff --git a/src/lib.rs b/src/lib.rs index bb33cb6..f10c945 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ mod profile; mod relay_generation; pub mod relay_pool_manager; mod result; +mod route; mod test_data; mod time; mod timecache; diff --git a/src/macos_key_storage.rs b/src/macos_key_storage.rs index e47a1c2..5c96017 100644 --- a/src/macos_key_storage.rs +++ b/src/macos_key_storage.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "macos")] -use enostr::{FullKeypair, Pubkey, SecretKey}; +use enostr::{Keypair, Pubkey, SecretKey}; use security_framework::item::{ItemClass, ItemSearchOptions, Limit, SearchResult}; use security_framework::passwords::{delete_generic_password, set_generic_password}; @@ -16,11 +16,13 @@ impl<'a> MacOSKeyStorage<'a> { MacOSKeyStorage { service_name } } - pub fn add_key(&self, key: &FullKeypair) -> Result<(), KeyStorageError> { + pub fn add_key(&self, key: &Keypair) -> Result<(), KeyStorageError> { match set_generic_password( self.service_name, key.pubkey.hex().as_str(), - key.secret_key.as_secret_bytes(), + key.secret_key + .as_ref() + .map_or_else(|| &[] as &[u8], |sc| sc.as_secret_bytes()), ) { Ok(_) => Ok(()), Err(_) => Err(KeyStorageError::Addition(key.pubkey.hex())), @@ -82,12 +84,12 @@ impl<'a> MacOSKeyStorage<'a> { } } - pub fn get_all_fullkeypairs(&self) -> Vec { + pub fn get_all_keypairs(&self) -> Vec { self.get_pubkeys() .iter() - .filter_map(|pubkey| { + .map(|pubkey| { let maybe_secret = self.get_secret_key_for_pubkey(pubkey); - maybe_secret.map(|secret| FullKeypair::new(pubkey.clone(), secret)) + Keypair::new(pubkey.clone(), maybe_secret) }) .collect() } @@ -106,6 +108,8 @@ impl<'a> MacOSKeyStorage<'a> { #[cfg(test)] mod tests { use super::*; + use enostr::FullKeypair; + static TEST_SERVICE_NAME: &str = "NOTEDECKTEST"; static STORAGE: MacOSKeyStorage = MacOSKeyStorage { service_name: TEST_SERVICE_NAME, @@ -119,7 +123,7 @@ mod tests { fn add_and_remove_test_pubkey_only() { let num_keys_before_test = STORAGE.get_pubkeys().len(); - let keypair = FullKeypair::generate(); + let keypair = FullKeypair::generate().to_keypair(); let add_result = STORAGE.add_key(&keypair); assert_eq!(add_result, Ok(())); @@ -134,18 +138,20 @@ mod tests { } fn add_and_remove_full_n(n: usize) { - let num_keys_before_test = STORAGE.get_all_fullkeypairs().len(); + let num_keys_before_test = STORAGE.get_all_keypairs().len(); // there must be zero keys in storage for the test to work as intended assert_eq!(num_keys_before_test, 0); - let expected_keypairs: Vec = (0..n).map(|_| FullKeypair::generate()).collect(); + let expected_keypairs: Vec = (0..n) + .map(|_| FullKeypair::generate().to_keypair()) + .collect(); expected_keypairs.iter().for_each(|keypair| { let add_result = STORAGE.add_key(keypair); assert_eq!(add_result, Ok(())); }); - let asserted_keypairs = STORAGE.get_all_fullkeypairs(); + let asserted_keypairs = STORAGE.get_all_keypairs(); assert_eq!(expected_keypairs, asserted_keypairs); expected_keypairs.iter().for_each(|keypair| { @@ -153,7 +159,7 @@ mod tests { assert_eq!(remove_result, Ok(())); }); - let num_keys_after_test = STORAGE.get_all_fullkeypairs().len(); + let num_keys_after_test = STORAGE.get_all_keypairs().len(); assert_eq!(num_keys_after_test, 0); } diff --git a/src/relay_generation.rs b/src/relay_generation.rs index 8bd33a6..7308abe 100644 --- a/src/relay_generation.rs +++ b/src/relay_generation.rs @@ -1,45 +1,7 @@ -use enostr::{Pubkey, RelayPool}; +use enostr::RelayPool; use tracing::error; -pub enum RelayGenerator { - GossipModel, - Nip65, - Constant, -} - -impl RelayGenerator { - pub fn generate_relays_for( - &self, - key: &Pubkey, - wakeup: impl Fn() + Send + Sync + Clone + 'static, - ) -> RelayPool { - match self { - Self::GossipModel => generate_relays_gossip(key, wakeup), - Self::Nip65 => generate_relays_nip65(key, wakeup), - Self::Constant => generate_constant_relays(wakeup), - } - } -} - -fn generate_relays_gossip( - key: &Pubkey, - wakeup: impl Fn() + Send + Sync + Clone + 'static, -) -> RelayPool { - let _ = wakeup; - let _ = key; - todo!() -} - -fn generate_relays_nip65( - key: &Pubkey, - wakeup: impl Fn() + Send + Sync + Clone + 'static, -) -> RelayPool { - let _ = wakeup; - let _ = key; - todo!() -} - -fn generate_constant_relays(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> RelayPool { +fn test_relay_pool(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> RelayPool { let mut pool = RelayPool::new(); if let Err(e) = pool.add_url("ws://localhost:8080".to_string(), wakeup.clone()) { diff --git a/src/route.rs b/src/route.rs new file mode 100644 index 0000000..35006f6 --- /dev/null +++ b/src/route.rs @@ -0,0 +1,7 @@ +use nostrdb::NoteKey; + +/// App routing. These describe different places you can go inside Notedeck. +pub enum Route { + ManageAccount, + Thread(NoteKey), +} diff --git a/src/test_data.rs b/src/test_data.rs index f99bd59..2cb19ab 100644 --- a/src/test_data.rs +++ b/src/test_data.rs @@ -7,7 +7,6 @@ use crate::{ account_manager::{AccountManager, UserAccount}, imgcache::ImageCache, key_storage::KeyStorage, - relay_generation::RelayGenerator, }; #[allow(unused_must_use)] @@ -81,26 +80,19 @@ pub fn get_test_accounts() -> Vec { TEN_ACCOUNT_HEXES .iter() .map(|account_hex| { - let key = FullKeypair::new( - Pubkey::from_hex(account_hex).unwrap(), - FullKeypair::generate().secret_key, - ); - - UserAccount { - key, - relays: sample_pool(), - } + let mut kp = FullKeypair::generate().to_keypair(); + kp.pubkey = Pubkey::from_hex(account_hex).unwrap(); + kp }) .collect() } pub fn get_accmgr_and_ndb_and_imgcache() -> (AccountManager, Ndb, ImageCache) { - let mut account_manager = - AccountManager::new(None, KeyStorage::None, RelayGenerator::Constant, || {}); + let mut account_manager = AccountManager::new(None, KeyStorage::None); let accounts = get_test_accounts(); - accounts - .into_iter() - .for_each(|acc| account_manager.add_account(acc.key, || {})); + for account in accounts { + account_manager.add_account(account); + } let mut config = Config::new(); config.set_ingester_threads(2); diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs index 68918f2..2fe22d7 100644 --- a/src/ui/account_management.rs +++ b/src/ui/account_management.rs @@ -1,5 +1,4 @@ use crate::colors::PINK; -use crate::ui::global_popup::FromApp; use crate::{ account_manager::AccountManager, app_style::NotedeckTextStyle, @@ -7,7 +6,6 @@ use crate::{ }; use egui::{Align, Button, Frame, Image, Layout, RichText, ScrollArea, Vec2}; -use super::global_popup::GlobalPopupType; use super::profile::preview::SimpleProfilePreview; use super::profile::{ProfilePreviewOp, SimpleProfilePreviewController}; @@ -39,7 +37,7 @@ impl<'a> AccountManagementView<'a> { fn show(&mut self, ui: &mut egui::Ui) { Frame::none().outer_margin(24.0).show(ui, |ui| { - ui.add(self.top_section_buttons_widget()); + self.top_section_buttons_widget(ui); ui.add_space(8.0); scroll_area().show(ui, |ui| { self.show_accounts(ui); @@ -86,8 +84,9 @@ impl<'a> AccountManagementView<'a> { fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { egui::CentralPanel::default() .show(ui.ctx(), |ui| { - ui.add(mobile_title()); - ui.add(self.top_section_buttons_widget()); + mobile_title(ui); + self.top_section_buttons_widget(ui); + ui.add_space(8.0); scroll_area().show(ui, |ui| { self.show_accounts_mobile(ui); @@ -96,34 +95,32 @@ impl<'a> AccountManagementView<'a> { .response } - fn top_section_buttons_widget(&mut self) -> impl egui::Widget + '_ { - |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::left_to_right(egui::Align::Center), - |ui| { - if ui.add(add_account_button()).clicked() { - // TODO: route to AccountLoginView - } - }, - ); + fn top_section_buttons_widget(&mut self, ui: &mut egui::Ui) -> egui::Response { + ui.horizontal(|ui| { + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| { + if ui.add(add_account_button()).clicked() { + // TODO: route to AccountLoginView + } + }, + ); - // UNCOMMENT FOR LOGOUTALL BUTTON - // ui.allocate_ui_with_layout( - // Vec2::new(ui.available_size_before_wrap().x, 32.0), - // Layout::right_to_left(egui::Align::Center), - // |ui| { - // if ui.add(logout_all_button()).clicked() { - // for index in (0..self.account_manager.num_accounts()).rev() { - // self.account_manager.remove_account(index); - // } - // } - // }, - // ); - }) - .response - } + // UNCOMMENT FOR LOGOUTALL BUTTON + // ui.allocate_ui_with_layout( + // Vec2::new(ui.available_size_before_wrap().x, 32.0), + // Layout::right_to_left(egui::Align::Center), + // |ui| { + // if ui.add(logout_all_button()).clicked() { + // for index in (0..self.account_manager.num_accounts()).rev() { + // self.account_manager.remove_account(index); + // } + // } + // }, + // ); + }) + .response } } @@ -166,26 +163,15 @@ fn account_card_ui() -> fn( } } -impl<'a> FromApp<'a> for AccountManagementView<'a> { - fn from_app(app: &'a mut crate::Damus) -> Self { - AccountManagementView::new( - &mut app.account_manager, - SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache), - ) - } -} - -fn mobile_title() -> impl egui::Widget { - |ui: &mut egui::Ui| { - ui.vertical_centered(|ui| { - ui.label( - RichText::new(GlobalPopupType::AccountManagement.title()) - .text_style(NotedeckTextStyle::Heading2.text_style()) - .strong(), - ); - }) - .response - } +fn mobile_title(ui: &mut egui::Ui) -> egui::Response { + ui.vertical_centered(|ui| { + ui.label( + RichText::new("Account Management") + .text_style(NotedeckTextStyle::Heading2.text_style()) + .strong(), + ); + }) + .response } fn scroll_area() -> ScrollArea { diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs index e786f2a..bae2f34 100644 --- a/src/ui/account_switcher.rs +++ b/src/ui/account_switcher.rs @@ -1,22 +1,43 @@ -use crate::{account_manager::UserAccount, colors::PINK, ui}; +use crate::{ + account_manager::{AccountManager, UserAccount}, + colors::PINK, + profile::DisplayName, + ui, Result, +}; use egui::{ Align, Button, Color32, Frame, Id, Image, Layout, Margin, RichText, Rounding, ScrollArea, Sense, Vec2, }; -use crate::account_manager::AccountManager; - -use super::{ - profile::{preview::SimpleProfilePreview, SimpleProfilePreviewController}, - state_in_memory::{STATE_ACCOUNT_MANAGEMENT, STATE_ACCOUNT_SWITCHER, STATE_SIDE_PANEL}, -}; +use super::profile::{preview::SimpleProfilePreview, SimpleProfilePreviewController}; pub struct AccountSelectionWidget<'a> { - account_manager: &'a mut AccountManager, + account_manager: &'a AccountManager, simple_preview_controller: SimpleProfilePreviewController<'a>, } +enum AccountSelectAction { + RemoveAccount { index: usize }, + SelectAccount { index: usize }, + OpenAccountManagement, +} + +#[derive(Default)] +struct AccountSelectResponse { + action: Option, +} + impl<'a> AccountSelectionWidget<'a> { + pub fn new( + account_manager: &'a AccountManager, + simple_preview_controller: SimpleProfilePreviewController<'a>, + ) -> Self { + AccountSelectionWidget { + account_manager, + simple_preview_controller, + } + } + pub fn ui(&'a mut self, ui: &mut egui::Ui) { if ui::is_mobile() { self.show_mobile(ui); @@ -25,33 +46,43 @@ impl<'a> AccountSelectionWidget<'a> { } } - fn show(&mut self, ui: &mut egui::Ui) { + fn show(&mut self, ui: &mut egui::Ui) -> AccountSelectResponse { + let mut res = AccountSelectResponse::default(); + let mut selected_index = self.account_manager.get_selected_account_index(); + Frame::none().outer_margin(8.0).show(ui, |ui| { - ui.add(top_section_widget()); + res = top_section_widget(ui); + scroll_area().show(ui, |ui| { - self.show_accounts(ui); + if let Some(index) = self.show_accounts(ui) { + selected_index = Some(index); + res.action = Some(AccountSelectAction::SelectAccount { index }); + } }); ui.add_space(8.0); ui.add(add_account_button()); - if let Some(account_index) = self.account_manager.get_selected_account_index() { - ui.add_space(8.0); - if self.handle_sign_out(ui, account_index) { - self.account_manager.remove_account(account_index); + if let Some(index) = selected_index { + if let Some(account) = self.account_manager.get_account(index) { + ui.add_space(8.0); + if self.handle_sign_out(ui, account) { + res.action = Some(AccountSelectAction::RemoveAccount { index }) + } } } ui.add_space(8.0); }); + + res } - fn handle_sign_out(&mut self, ui: &mut egui::Ui, account_index: usize) -> bool { - if let Some(account) = self.account_manager.get_account(account_index) { - if let Some(response) = self.sign_out_button(ui, account) { - return response.clicked(); - } + fn handle_sign_out(&mut self, ui: &mut egui::Ui, account: &UserAccount) -> bool { + if let Ok(response) = self.sign_out_button(ui, account) { + response.clicked() + } else { + false } - false } fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { @@ -59,19 +90,19 @@ impl<'a> AccountSelectionWidget<'a> { todo!() } - fn show_accounts(&mut self, ui: &mut egui::Ui) { + fn show_accounts(&mut self, ui: &mut egui::Ui) -> Option { self.simple_preview_controller.view_profile_previews( self.account_manager, ui, account_switcher_card_ui(), - ); + ) } - fn sign_out_button(&self, ui: &mut egui::Ui, account: &UserAccount) -> Option { + fn sign_out_button(&self, ui: &mut egui::Ui, account: &UserAccount) -> Result { self.simple_preview_controller.show_with_nickname( ui, - &account.key.pubkey, - |ui, username| { + account.pubkey.bytes(), + |ui: &mut egui::Ui, username: &DisplayName| { let img_data = egui::include_image!("../../assets/icons/signout_icon_4x.png"); let img = Image::new(img_data).fit_to_exact_size(Vec2::new(16.0, 16.0)); let button = egui::Button::image_and_text( @@ -88,18 +119,6 @@ impl<'a> AccountSelectionWidget<'a> { } } -impl<'a> AccountSelectionWidget<'a> { - pub fn new( - account_manager: &'a mut AccountManager, - simple_preview_controller: SimpleProfilePreviewController<'a>, - ) -> Self { - AccountSelectionWidget { - account_manager, - simple_preview_controller, - } - } -} - fn account_switcher_card_ui() -> fn( ui: &mut egui::Ui, preview: SimpleProfilePreview, @@ -147,32 +166,29 @@ fn selection_widget() -> impl egui::Widget { } } -fn top_section_widget() -> impl egui::Widget { - |ui: &mut egui::Ui| { - ui.horizontal(|ui| { - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::left_to_right(egui::Align::Center), - |ui| ui.add(account_switcher_title()), - ); +fn top_section_widget(ui: &mut egui::Ui) -> AccountSelectResponse { + ui.horizontal(|ui| { + let mut resp = AccountSelectResponse::default(); - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::right_to_left(egui::Align::Center), - |ui| { - if ui.add(manage_accounts_button()).clicked() { - STATE_ACCOUNT_SWITCHER.set_state(ui.ctx(), false); - STATE_SIDE_PANEL.set_state( - ui.ctx(), - Some(ui::global_popup::GlobalPopupType::AccountManagement), - ); - STATE_ACCOUNT_MANAGEMENT.set_state(ui.ctx(), true); - } - }, - ); - }) - .response - } + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| ui.add(account_switcher_title()), + ); + + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::right_to_left(egui::Align::Center), + |ui| { + if ui.add(manage_accounts_button()).clicked() { + resp.action = Some(AccountSelectAction::OpenAccountManagement); + } + }, + ); + + resp + }) + .inner } fn manage_accounts_button() -> egui::Button<'static> { @@ -227,7 +243,7 @@ mod previews { impl View for AccountSelectionPreview { fn ui(&mut self, ui: &mut egui::Ui) { AccountSelectionWidget::new( - &mut self.account_manager, + &self.account_manager, SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache), ) .ui(ui); diff --git a/src/ui/global_popup.rs b/src/ui/global_popup.rs deleted file mode 100644 index f1bd505..0000000 --- a/src/ui/global_popup.rs +++ /dev/null @@ -1,168 +0,0 @@ -use egui::{Align2, CentralPanel, RichText, Vec2, Window}; - -use crate::Damus; - -use super::{ - profile::SimpleProfilePreviewController, - state_in_memory::{STATE_ACCOUNT_MANAGEMENT, STATE_ACCOUNT_SWITCHER, STATE_SIDE_PANEL}, - AccountManagementView, AccountSelectionWidget, View, -}; - -#[derive(Clone, Copy, Debug)] -pub enum GlobalPopupType { - AccountManagement, - AccountSwitcher, -} - -static ACCOUNT_MANAGEMENT_TITLE: &str = "Manage accounts"; -static ACCOUNT_SWITCHER_TITLE: &str = "Account switcher"; - -impl GlobalPopupType { - pub fn title(&self) -> &'static str { - match self { - Self::AccountManagement => ACCOUNT_MANAGEMENT_TITLE, - Self::AccountSwitcher => ACCOUNT_SWITCHER_TITLE, - } - } -} - -pub trait FromApp<'a> { - fn from_app(app: &'a mut crate::Damus) -> Self - where - Self: Sized; -} - -fn title(title_str: &'static str) -> RichText { - RichText::new(title_str).size(24.0) -} - -fn overlay_window<'a>( - open: &'a mut bool, - window_size: Vec2, - title_str: &'static str, -) -> Window<'a> { - egui::Window::new(title(title_str)) - .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .collapsible(false) - .auto_sized() - .movable(false) - .open(open) - .default_size(window_size) -} - -fn account_switcher_window(open: &'_ mut bool) -> Window<'_> { - egui::Window::new("account switcher") - .title_bar(false) - .collapsible(false) - .anchor(Align2::LEFT_BOTTOM, Vec2::new(0.0, -52.0)) - .fixed_size(Vec2::new(360.0, 406.0)) - .open(open) - .movable(false) -} - -static MARGIN: Vec2 = Vec2 { x: 100.0, y: 100.0 }; - -pub struct DesktopGlobalPopup<'a> { - app: &'a mut Damus, -} - -impl<'a> View for DesktopGlobalPopup<'a> { - fn ui(&mut self, ui: &mut egui::Ui) { - DesktopGlobalPopup::global_popup(self.app, ui.ctx()) - } -} - -impl<'a> DesktopGlobalPopup<'a> { - pub fn new(app: &'a mut Damus) -> Self { - DesktopGlobalPopup { app } - } - pub fn global_popup(app: &mut Damus, ctx: &egui::Context) { - CentralPanel::default().show(ctx, |ui| { - if let Some(popup) = STATE_SIDE_PANEL.get_state(ctx) { - match popup { - GlobalPopupType::AccountManagement => { - Self::account_management(app, ctx, ui, popup.title()); - } - GlobalPopupType::AccountSwitcher => { - let mut show_account_switcher = STATE_ACCOUNT_SWITCHER.get_state(ctx); - if show_account_switcher { - STATE_ACCOUNT_MANAGEMENT.set_state(ctx, false); - account_switcher_window(&mut show_account_switcher).show(ctx, |ui| { - AccountSelectionWidget::new( - &mut app.account_manager, - SimpleProfilePreviewController::new( - &app.ndb, - &mut app.img_cache, - ), - ) - .ui(ui); - }); - } - } - } - } - }); - } - - fn account_management( - app: &mut Damus, - ctx: &egui::Context, - ui: &mut egui::Ui, - title: &'static str, - ) { - let available_size = ui.available_size(); - let window_size = available_size - MARGIN; - let mut show_account_management = STATE_ACCOUNT_MANAGEMENT.get_state(ctx); - if show_account_management { - overlay_window(&mut show_account_management, window_size, title).show(ctx, |ui| { - AccountManagementView::from_app(app).ui(ui); - }); - // user could have closed the window, set the new state in egui memory - STATE_ACCOUNT_MANAGEMENT.set_state(ctx, show_account_management); - } - } -} - -mod preview { - use crate::{ - test_data, - ui::{profile::SimpleProfilePreviewController, DesktopSidePanel, Preview, View}, - Damus, - }; - - use super::DesktopGlobalPopup; - - pub struct GlobalPopupPreview { - app: Damus, - } - - impl<'a> Preview for DesktopGlobalPopup<'a> { - type Prev = GlobalPopupPreview; - - fn preview() -> Self::Prev { - GlobalPopupPreview::new() - } - } - - impl GlobalPopupPreview { - fn new() -> Self { - let mut app = Damus::mock("."); - let accounts = test_data::get_test_accounts(); - accounts - .into_iter() - .for_each(|acc| app.account_manager.add_account(acc.key, || {})); - GlobalPopupPreview { app } - } - } - - impl View for GlobalPopupPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - let mut panel = DesktopSidePanel::new( - &mut self.app.account_manager, - SimpleProfilePreviewController::new(&self.app.ndb, &mut self.app.img_cache), - ); - DesktopSidePanel::panel().show(ui.ctx(), |ui| panel.ui(ui)); - DesktopGlobalPopup::new(&mut self.app).ui(ui); - } - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d4bd5e4..c45644e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,19 +2,16 @@ pub mod account_login_view; pub mod account_management; pub mod account_switcher; pub mod anim; -pub mod global_popup; pub mod mention; pub mod note; pub mod preview; pub mod profile; pub mod relay; pub mod side_panel; -pub mod state_in_memory; pub mod username; pub use account_management::AccountManagementView; pub use account_switcher::AccountSelectionWidget; -pub use global_popup::DesktopGlobalPopup; pub use mention::Mention; pub use note::Note; pub use preview::{Preview, PreviewApp}; diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs index f154bad..2a556a3 100644 --- a/src/ui/profile/profile_preview_controller.rs +++ b/src/ui/profile/profile_preview_controller.rs @@ -1,10 +1,6 @@ -use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use crate::{ - account_manager::AccountManager, imgcache::ImageCache, - ui::state_in_memory::STATE_ACCOUNT_SWITCHER, DisplayName, -}; +use crate::{account_manager::AccountManager, imgcache::ImageCache, DisplayName, Result}; use super::{ preview::{get_display_name, get_profile_url, SimpleProfilePreview}, @@ -42,36 +38,48 @@ impl<'a> SimpleProfilePreviewController<'a> { let width = ui.available_width(); + let txn = if let Ok(txn) = Transaction::new(self.ndb) { + txn + } else { + return None; + }; + for i in 0..account_manager.num_accounts() { - if let Some(account) = account_manager.get_account(i) { - if let Ok(txn) = Transaction::new(self.ndb) { - let profile = self - .ndb - .get_profile_by_pubkey(&txn, account.key.pubkey.bytes()); + let account = if let Some(account) = account_manager.get_account(i) { + account + } else { + continue; + }; - if let Ok(profile) = profile { - let preview = SimpleProfilePreview::new(&profile, self.img_cache); + let profile = + if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, account.pubkey.bytes()) { + profile + } else { + continue; + }; - let is_selected = - if let Some(selected) = account_manager.get_selected_account_index() { - i == selected - } else { - false - }; + let preview = SimpleProfilePreview::new(&profile, self.img_cache); - if let Some(op) = add_preview_ui(ui, preview, width, is_selected) { - match op { - ProfilePreviewOp::RemoveAccount => { - if to_remove.is_none() { - to_remove = Some(Vec::new()); - } - to_remove.as_mut().unwrap().push(i); - } - ProfilePreviewOp::SwitchTo => account_manager.select_account(i), - } - } - }; + let is_selected = if let Some(selected) = account_manager.get_selected_account_index() { + i == selected + } else { + false + }; + + let op = if let Some(op) = add_preview_ui(ui, preview, width, is_selected) { + op + } else { + continue; + }; + + match op { + ProfilePreviewOp::RemoveAccount => { + if to_remove.is_none() { + to_remove = Some(Vec::new()); + } + to_remove.as_mut().unwrap().push(i); } + ProfilePreviewOp::SwitchTo => account_manager.select_account(i), } } @@ -80,7 +88,7 @@ impl<'a> SimpleProfilePreviewController<'a> { pub fn view_profile_previews( &mut self, - account_manager: &mut AccountManager, + account_manager: &AccountManager, ui: &mut egui::Ui, add_preview_ui: fn( ui: &mut egui::Ui, @@ -89,60 +97,64 @@ impl<'a> SimpleProfilePreviewController<'a> { is_selected: bool, index: usize, ) -> bool, - ) { + ) -> Option { let width = ui.available_width(); + let txn = if let Ok(txn) = Transaction::new(self.ndb) { + txn + } else { + return None; + }; + for i in 0..account_manager.num_accounts() { - if let Some(account) = account_manager.get_account(i) { - if let Ok(txn) = Transaction::new(self.ndb) { - let profile = self - .ndb - .get_profile_by_pubkey(&txn, account.key.pubkey.bytes()); + let account = if let Some(account) = account_manager.get_account(i) { + account + } else { + continue; + }; - if let Ok(profile) = profile { - let preview = SimpleProfilePreview::new(&profile, self.img_cache); + let profile = + if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, account.pubkey.bytes()) { + profile + } else { + continue; + }; - let is_selected = - if let Some(selected) = account_manager.get_selected_account_index() { - i == selected - } else { - false - }; + let preview = SimpleProfilePreview::new(&profile, self.img_cache); - if add_preview_ui(ui, preview, width, is_selected, i) { - account_manager.select_account(i); - STATE_ACCOUNT_SWITCHER.set_state(ui.ctx(), false); - } - } - } + let is_selected = if let Some(selected) = account_manager.get_selected_account_index() { + i == selected + } else { + false + }; + + if add_preview_ui(ui, preview, width, is_selected, i) { + return Some(i); } } + + None } pub fn show_with_nickname( &self, ui: &mut egui::Ui, - key: &Pubkey, + key: &[u8; 32], ui_element: fn(ui: &mut egui::Ui, username: &DisplayName) -> egui::Response, - ) -> Option { - if let Ok(txn) = Transaction::new(self.ndb) { - let profile = self.ndb.get_profile_by_pubkey(&txn, key.bytes()); - - if let Ok(profile) = profile { - return Some(ui_element(ui, &get_display_name(&profile))); - } - } - None + ) -> Result { + let txn = Transaction::new(self.ndb)?; + let profile = self.ndb.get_profile_by_pubkey(&txn, key)?; + Ok(ui_element(ui, &get_display_name(&profile))) } pub fn show_with_pfp( - &mut self, + self, ui: &mut egui::Ui, - key: &Pubkey, + key: &[u8; 32], ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response, ) -> Option { if let Ok(txn) = Transaction::new(self.ndb) { - let profile = self.ndb.get_profile_by_pubkey(&txn, key.bytes()); + let profile = self.ndb.get_profile_by_pubkey(&txn, key); if let Ok(profile) = profile { return Some(ui_element( diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs index a81b808..068f713 100644 --- a/src/ui/side_panel.rs +++ b/src/ui/side_panel.rs @@ -1,72 +1,75 @@ use egui::{Button, Layout, SidePanel, Vec2, Widget}; -use crate::{account_manager::AccountManager, ui::global_popup::GlobalPopupType}; +use crate::account_manager::AccountManager; -use super::{ - profile::SimpleProfilePreviewController, - state_in_memory::{STATE_ACCOUNT_SWITCHER, STATE_SIDE_PANEL}, - ProfilePic, View, -}; +use super::{profile::SimpleProfilePreviewController, ProfilePic, View}; pub struct DesktopSidePanel<'a> { - account_manager: &'a mut AccountManager, + selected_account: Option<&'a [u8; 32]>, simple_preview_controller: SimpleProfilePreviewController<'a>, } -static ID: &str = "left panel"; - -impl<'a> View for DesktopSidePanel<'a> { - fn ui(&mut self, ui: &mut egui::Ui) { - self.inner(ui); +impl<'a> Widget for DesktopSidePanel<'a> { + fn ui(self, ui: &mut egui::Ui) -> egui::Response { + self.show(ui) } } impl<'a> DesktopSidePanel<'a> { pub fn new( - account_manager: &'a mut AccountManager, + selected_account: Option<&'a [u8; 32]>, simple_preview_controller: SimpleProfilePreviewController<'a>, ) -> Self { DesktopSidePanel { - account_manager, + selected_account, simple_preview_controller, } } - pub fn inner(&mut self, ui: &mut egui::Ui) { - let dark_mode = ui.ctx().style().visuals.dark_mode; - let spacing_amt = 16.0; - ui.with_layout(Layout::bottom_up(egui::Align::Center), |ui| { - ui.add_space(spacing_amt); - if self.pfp_button(ui).clicked() { - STATE_SIDE_PANEL.set_state(ui.ctx(), Some(GlobalPopupType::AccountSwitcher)); - let previous_val = STATE_ACCOUNT_SWITCHER.get_state(ui.ctx()); - STATE_ACCOUNT_SWITCHER.set_state(ui.ctx(), !previous_val); - } - ui.add_space(spacing_amt); - ui.add(settings_button(dark_mode)); - ui.add_space(spacing_amt); - ui.add(add_column_button(dark_mode)); - ui.add_space(spacing_amt); - }); + pub fn panel() -> SidePanel { + egui::SidePanel::left("side_panel") + .resizable(false) + .exact_width(40.0) } - fn pfp_button(&mut self, ui: &mut egui::Ui) -> egui::Response { - if let Some(selected_account) = self.account_manager.get_selected_account() { - if let Some(response) = self.simple_preview_controller.show_with_pfp( - ui, - &selected_account.key.pubkey, - show_pfp(), - ) { + pub fn show(self, ui: &mut egui::Ui) -> egui::Response { + let dark_mode = ui.ctx().style().visuals.dark_mode; + let spacing_amt = 16.0; + + let inner = ui + .with_layout(Layout::bottom_up(egui::Align::Center), |ui| { + ui.spacing_mut().item_spacing.y = spacing_amt; + let pfp_resp = self.pfp_button(ui); + let settings_resp = ui.add(settings_button(dark_mode)); + let column_resp = ui.add(add_column_button(dark_mode)); + + if pfp_resp.clicked() || pfp_resp.hovered() { + egui::InnerResponse::new(SidePanelAction::Account, pfp_resp) + } else if settings_resp.clicked() || settings_resp.hovered() { + egui::InnerResponse::new(SidePanelAction::Settings, settings_resp) + } else if column_resp.clicked() || column_resp.hovered() { + egui::InnerResponse::new(SidePanelAction::Columns, column_resp) + } else { + egui::InnerResponse::new(SidePanelAction::Panel, pfp_resp) + } + }) + .inner; + + SidePanelResponse::new(inner.inner, inner.response) + } + + fn pfp_button(self, ui: &mut egui::Ui) -> egui::Response { + if let Some(selected_account) = self.selected_account { + if let Some(response) = + self.simple_preview_controller + .show_with_pfp(ui, selected_account, show_pfp()) + { return response; } } add_button_to_ui(ui, no_account_pfp()) } - - pub fn panel() -> SidePanel { - egui::SidePanel::left(ID).resizable(false).exact_width(40.0) - } } fn show_pfp() -> fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response { @@ -126,10 +129,16 @@ mod preview { impl View for DesktopSidePanelPreview { fn ui(&mut self, ui: &mut egui::Ui) { - let mut panel = DesktopSidePanel::new( - &mut self.account_manager, + let selected_account = self + .account_manager + .get_selected_account() + .map(|x| x.pubkey.bytes()); + + let panel = DesktopSidePanel::new( + selected_account, SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache), ); + DesktopSidePanel::panel().show(ui.ctx(), |ui| panel.ui(ui)); } } diff --git a/src/ui/state_in_memory.rs b/src/ui/state_in_memory.rs deleted file mode 100644 index 09d37f1..0000000 --- a/src/ui/state_in_memory.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::any::Any; - -use super::global_popup::GlobalPopupType; - -/// StateInMemory is a helper struct for interacting with egui memory persisted data -#[derive(Clone)] -pub struct StateInMemory { - id: &'static str, - default_state: T, -} - -impl StateInMemory { - pub fn get_state(&self, ctx: &egui::Context) -> T { - ctx.data_mut(|d| { - d.get_temp(egui::Id::new(self.id)) - .unwrap_or(self.default_state.clone()) - }) - } - - pub fn set_state(&self, ctx: &egui::Context, new_val: T) { - ctx.data_mut(|d| d.insert_temp(egui::Id::new(self.id), new_val)); - } -} - -pub static STATE_ACCOUNT_MANAGEMENT: StateInMemory = StateInMemory:: { - id: ACCOUNT_MANAGEMENT_VIEW_STATE_ID, - default_state: false, -}; - -pub static STATE_SIDE_PANEL: StateInMemory> = - StateInMemory::> { - id: SIDE_PANEL_VIEW_STATE_ID, - default_state: None, - }; - -pub static STATE_ACCOUNT_SWITCHER: StateInMemory = StateInMemory:: { - id: ACCOUNT_SWITCHER_VIEW_STATE_ID, - default_state: false, -}; - -static ACCOUNT_MANAGEMENT_VIEW_STATE_ID: &str = "account management view state"; -static SIDE_PANEL_VIEW_STATE_ID: &str = "side panel view state"; -static ACCOUNT_SWITCHER_VIEW_STATE_ID: &str = "account switcher view state"; diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs index f7c8d06..c620308 100644 --- a/src/ui_preview/main.rs +++ b/src/ui_preview/main.rs @@ -3,8 +3,8 @@ use notedeck::app_creation::{ }; use notedeck::ui::account_login_view::AccountLoginView; use notedeck::ui::{ - AccountManagementView, AccountSelectionWidget, DesktopGlobalPopup, DesktopSidePanel, Preview, - PreviewApp, ProfilePic, ProfilePreview, RelayView, + AccountManagementView, AccountSelectionWidget, DesktopSidePanel, Preview, PreviewApp, + ProfilePic, ProfilePreview, RelayView, }; use std::env; @@ -89,6 +89,5 @@ async fn main() { AccountManagementView, AccountSelectionWidget, DesktopSidePanel, - DesktopGlobalPopup, ); } diff --git a/src/user_account.rs b/src/user_account.rs index 254be32..1bb911a 100644 --- a/src/user_account.rs +++ b/src/user_account.rs @@ -1,6 +1,9 @@ -use enostr::{FullKeypair, RelayPool}; +use enostr::Keypair; -pub struct UserAccount { - pub key: FullKeypair, - pub relays: RelayPool, -} +//pub struct UserAccount { +//pub key: Keypair, +//pub relays: RelayPool, +//pub relays: Vec, +//} + +pub type UserAccount = Keypair;