commit 83eab711486aa38918ea67f3b23bf8eb6a1e0727
parent 2af44641db6e9c2c63ddf7b76938b8d3168f882f
Author: William Casarin <jb55@jb55.com>
Date: Fri, 31 May 2024 01:01:05 -0500
Merge remote-tracking branch 'pr/80'
Diffstat:
26 files changed, 982 insertions(+), 496 deletions(-)
diff --git a/assets/icons/add_account_icon_4x.png b/assets/icons/add_account_icon_4x.png
Binary files differ.
diff --git a/assets/icons/add_column_dark_4x.png b/assets/icons/add_column_dark_4x.png
Binary files differ.
diff --git a/assets/icons/plus_icon_4x.png b/assets/icons/plus_icon_4x.png
Binary files differ.
diff --git a/assets/icons/select_icon_3x.png b/assets/icons/select_icon_3x.png
Binary files differ.
diff --git a/assets/icons/settings_dark_4x.png b/assets/icons/settings_dark_4x.png
Binary files differ.
diff --git a/assets/icons/signout_icon_4x.png b/assets/icons/signout_icon_4x.png
Binary files differ.
diff --git 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<SecretKey>) -> 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
@@ -1,136 +1,92 @@
-use enostr::FullKeypair;
-use nostrdb::{Ndb, Transaction};
+use std::cmp::Ordering;
-pub use crate::user_account::UserAccount;
-use crate::{
- imgcache::ImageCache, key_storage::KeyStorage, relay_generation::RelayGenerator,
- ui::profile::preview::SimpleProfilePreview,
-};
-
-pub struct SimpleProfilePreviewController<'a> {
- ndb: &'a Ndb,
- img_cache: &'a mut ImageCache,
-}
-
-impl<'a> SimpleProfilePreviewController<'a> {
- pub fn new(ndb: &'a Ndb, img_cache: &'a mut ImageCache) -> Self {
- SimpleProfilePreviewController { ndb, img_cache }
- }
-
- pub fn set_profile_previews(
- &mut self,
- account_manager: &AccountManager<'a>,
- ui: &mut egui::Ui,
- edit_mode: bool,
- add_preview_ui: fn(
- ui: &mut egui::Ui,
- preview: SimpleProfilePreview,
- edit_mode: bool,
- ) -> bool,
- ) -> Option<Vec<usize>> {
- let mut to_remove: Option<Vec<usize>> = 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());
-
- if let Ok(profile) = profile {
- let preview = SimpleProfilePreview::new(&profile, self.img_cache);
-
- if add_preview_ui(ui, preview, edit_mode) {
- if to_remove.is_none() {
- to_remove = Some(Vec::new());
- }
- to_remove.as_mut().unwrap().push(i);
- }
- };
- }
- }
- }
-
- to_remove
- }
+use enostr::Keypair;
- pub fn view_profile_previews(
- &mut self,
- account_manager: &'a AccountManager<'a>,
- ui: &mut egui::Ui,
- add_preview_ui: fn(ui: &mut egui::Ui, preview: SimpleProfilePreview, index: usize) -> bool,
- ) -> Option<usize> {
- let mut clicked_at: Option<usize> = 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());
-
- if let Ok(profile) = profile {
- let preview = SimpleProfilePreview::new(&profile, self.img_cache);
-
- if add_preview_ui(ui, preview, i) {
- clicked_at = Some(i)
- }
- }
- }
- }
- }
-
- clicked_at
- }
-}
+use crate::key_storage::KeyStorage;
+pub use crate::user_account::UserAccount;
/// The interface for managing the user's accounts.
/// Represents all user-facing operations related to account management.
-pub struct AccountManager<'a> {
- accounts: &'a mut Vec<UserAccount>,
+pub struct AccountManager {
+ currently_selected_account: Option<usize>,
+ accounts: Vec<UserAccount>,
key_store: KeyStorage,
- relay_generator: RelayGenerator,
}
-impl<'a> AccountManager<'a> {
- pub fn new(
- accounts: &'a mut Vec<UserAccount>,
- key_store: KeyStorage,
- relay_generator: RelayGenerator,
- ) -> Self {
+impl AccountManager {
+ pub fn new(currently_selected_account: Option<usize>, key_store: KeyStorage) -> Self {
+ let accounts = key_store.get_keys().unwrap_or_default();
+
AccountManager {
+ currently_selected_account,
accounts,
key_store,
- relay_generator,
}
}
- pub fn get_accounts(&'a self) -> &'a Vec<UserAccount> {
- self.accounts
+ pub fn get_accounts(&self) -> &Vec<UserAccount> {
+ &self.accounts
}
- pub fn get_account(&'a self, index: usize) -> Option<&'a 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);
- }
- if index < self.accounts.len() {
+ let _ = self.key_store.remove_key(account);
self.accounts.remove(index);
+
+ if let Some(selected_index) = self.currently_selected_account {
+ match selected_index.cmp(&index) {
+ Ordering::Greater => {
+ self.select_account(selected_index - 1);
+ }
+ Ordering::Equal => {
+ self.clear_selected_account();
+ }
+ Ordering::Less => {}
+ }
+ }
}
}
- pub fn add_account(&'a mut self, key: FullKeypair, ctx: &egui::Context) {
- let _ = self.key_store.add_key(&key);
- let relays = self.relay_generator.generate_relays_for(&key.pubkey, ctx);
- let account = UserAccount { key, relays };
-
+ pub fn add_account(&mut self, account: Keypair) {
+ let _ = self.key_store.add_key(&account);
self.accounts.push(account)
}
pub fn num_accounts(&self) -> usize {
self.accounts.len()
}
+
+ pub fn get_selected_account_index(&self) -> Option<usize> {
+ self.currently_selected_account
+ }
+
+ pub fn get_selected_account(&self) -> Option<&UserAccount> {
+ if let Some(account_index) = self.currently_selected_account {
+ if let Some(account) = self.get_account(account_index) {
+ Some(account)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ pub fn select_account(&mut self, index: usize) {
+ if self.accounts.get(index).is_some() {
+ self.currently_selected_account = Some(index)
+ }
+ }
+
+ pub fn clear_selected_account(&mut self) {
+ self.currently_selected_account = None
+ }
}
diff --git a/src/app.rs b/src/app.rs
@@ -1,12 +1,15 @@
+use crate::account_manager::AccountManager;
use crate::app_creation::setup_cc;
use crate::app_style::user_requested_visuals_change;
use crate::error::Error;
use crate::frame_history::FrameHistory;
use crate::imgcache::ImageCache;
use crate::notecache::{CachedNote, NoteCache};
+use crate::route::Route;
use crate::timeline;
use crate::timeline::{NoteRef, Timeline, ViewFilter};
-use crate::ui::is_mobile;
+use crate::ui::profile::SimpleProfilePreviewController;
+use crate::ui::{is_mobile, DesktopSidePanel};
use crate::Result;
use egui::{Context, Frame, Style};
@@ -36,6 +39,8 @@ pub struct Damus {
note_cache: NoteCache,
pool: RelayPool,
+ /// global navigation for account management popups, etc.
+ nav: Vec<Route>,
pub textmode: bool,
pub timelines: Vec<Timeline>,
@@ -43,6 +48,7 @@ pub struct Damus {
pub img_cache: ImageCache,
pub ndb: Ndb,
+ pub account_manager: AccountManager,
frame_history: crate::frame_history::FrameHistory,
}
@@ -647,14 +653,47 @@ 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"),
+ account_manager: AccountManager::new(
+ // TODO: should pull this from settings
+ None,
+ // TODO: use correct KeyStorage mechanism for current OS arch
+ crate::key_storage::KeyStorage::None,
+ ),
//compose: "".to_string(),
frame_history: FrameHistory::default(),
}
}
+ pub fn mock<P: AsRef<Path>>(data_path: P) -> Self {
+ let mut timelines: Vec<Timeline> = vec![];
+ let _initial_limit = 100;
+ let filter = serde_json::from_str(include_str!("../queries/global.json")).unwrap();
+ timelines.push(Timeline::new(filter));
+
+ let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir());
+ let _ = std::fs::create_dir_all(imgcache_dir.clone());
+
+ let mut config = Config::new();
+ config.set_ingester_threads(2);
+ Self {
+ state: DamusState::Initializing,
+ pool: RelayPool::new(),
+ img_cache: ImageCache::new(imgcache_dir),
+ 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),
+ frame_history: FrameHistory::default(),
+ }
+ }
+
pub fn note_cache_mut(&mut self) -> &mut NoteCache {
&mut self.note_cache
}
@@ -815,14 +854,6 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
Size::remainder()
};
- if app.timelines.len() == 1 {
- main_panel(&ctx.style()).show(ctx, |ui| {
- timeline::timeline_view(ui, app, 0);
- });
-
- return;
- }
-
main_panel(&ctx.style()).show(ctx, |ui| {
ui.spacing_mut().item_spacing.x = 0.0;
if need_scroll {
@@ -837,9 +868,24 @@ fn render_damus_desktop(ctx: &egui::Context, app: &mut Damus) {
fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, timelines: usize) {
StripBuilder::new(ui)
+ .size(Size::exact(40.0))
.sizes(sizes, timelines)
.clip(true)
.horizontal(|mut strip| {
+ strip.cell(|ui| {
+ let side_panel = DesktopSidePanel::new(
+ app.account_manager
+ .get_selected_account()
+ .map(|a| a.pubkey.bytes()),
+ SimpleProfilePreviewController::new(&app.ndb, &mut app.img_cache),
+ )
+ .show(ui);
+
+ if side_panel.response.clicked() {
+ info!("clicked {:?}", side_panel.action);
+ }
+ });
+
for timeline_ind in 0..timelines {
strip.cell(|ui| timeline::timeline_view(ui, app, timeline_ind));
}
diff --git a/src/colors.rs b/src/colors.rs
@@ -1,6 +1,8 @@
use egui::Color32;
pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5);
+// TODO: This should not be exposed publicly
+pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9);
//pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52);
pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A);
const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00);
diff --git a/src/key_parsing.rs b/src/key_parsing.rs
@@ -125,7 +125,7 @@ pub async fn get_login_key(key: &str) -> Result<Keypair, LoginError> {
} 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
@@ -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<Vec<FullKeypair>, KeyStorageError> {
+ pub fn get_keys(&self) -> Result<Vec<Keypair>, 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
@@ -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
@@ -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<FullKeypair> {
+ pub fn get_all_keypairs(&self) -> Vec<Keypair> {
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<FullKeypair> = (0..n).map(|_| FullKeypair::generate()).collect();
+ let expected_keypairs: Vec<Keypair> = (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
@@ -1,38 +1,8 @@
-use crate::relay_pool_manager::create_wakeup;
-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, ctx: &egui::Context) -> RelayPool {
- match self {
- Self::GossipModel => generate_relays_gossip(key, ctx),
- Self::Nip65 => generate_relays_nip65(key, ctx),
- Self::Constant => generate_constant_relays(ctx),
- }
- }
-}
-
-fn generate_relays_gossip(key: &Pubkey, ctx: &egui::Context) -> RelayPool {
- let _ = ctx;
- let _ = key;
- todo!()
-}
-
-fn generate_relays_nip65(key: &Pubkey, ctx: &egui::Context) -> RelayPool {
- let _ = ctx;
- let _ = key;
- todo!()
-}
-
-fn generate_constant_relays(ctx: &egui::Context) -> RelayPool {
+fn test_relay_pool(wakeup: impl Fn() + Send + Sync + Clone + 'static) -> RelayPool {
let mut pool = RelayPool::new();
- let wakeup = create_wakeup(ctx);
if let Err(e) = pool.add_url("ws://localhost:8080".to_string(), wakeup.clone()) {
error!("{:?}", e)
diff --git a/src/route.rs 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
@@ -1,5 +1,13 @@
-use enostr::RelayPool;
-use nostrdb::ProfileRecord;
+use std::path::Path;
+
+use enostr::{FullKeypair, Pubkey, RelayPool};
+use nostrdb::{Config, Ndb, ProfileRecord};
+
+use crate::{
+ account_manager::{AccountManager, UserAccount},
+ imgcache::ImageCache,
+ key_storage::KeyStorage,
+};
#[allow(unused_must_use)]
pub fn sample_pool() -> RelayPool {
@@ -54,3 +62,45 @@ const TEST_PROFILE_DATA: [u8; 448] = [
pub fn test_profile_record() -> ProfileRecord<'static> {
ProfileRecord::new_owned(&TEST_PROFILE_DATA).unwrap()
}
+
+const TEN_ACCOUNT_HEXES: [&str; 10] = [
+ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91",
+ "5c10ed0678805156d39ef1ef6d46110fe1e7e590ae04986ccf48ba1299cb53e2",
+ "4c96d763eb2fe01910f7e7220b7c7ecdbe1a70057f344b9f79c28af080c3ee30",
+ "edf16b1dd61eab353a83af470cc13557029bff6827b4cb9b7fc9bdb632a2b8e6",
+ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+];
+
+pub fn get_test_accounts() -> Vec<UserAccount> {
+ TEN_ACCOUNT_HEXES
+ .iter()
+ .map(|account_hex| {
+ 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);
+ let accounts = get_test_accounts();
+ for account in accounts {
+ account_manager.add_account(account);
+ }
+
+ let mut config = Config::new();
+ config.set_ingester_threads(2);
+
+ let db_dir = Path::new(".");
+ let path = db_dir.to_str().unwrap();
+ let ndb = Ndb::new(path, &config).expect("ndb");
+ let imgcache_dir = db_dir.join("cache/img");
+ let img_cache = ImageCache::new(imgcache_dir);
+ (account_manager, ndb, img_cache)
+}
diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs
@@ -1,17 +1,17 @@
-use egui::{
- Align, Align2, Button, Frame, Id, Layout, Margin, RichText, ScrollArea, Sense, Vec2, Window,
-};
-
+use crate::colors::PINK;
use crate::{
- account_manager::{AccountManager, SimpleProfilePreviewController, UserAccount},
+ account_manager::AccountManager,
app_style::NotedeckTextStyle,
ui::{self, Preview, View},
};
+use egui::{Align, Button, Frame, Image, Layout, RichText, ScrollArea, Vec2};
+
+use super::profile::preview::SimpleProfilePreview;
+use super::profile::{ProfilePreviewOp, SimpleProfilePreviewController};
pub struct AccountManagementView<'a> {
- account_manager: AccountManager<'a>,
+ account_manager: &'a mut AccountManager,
simple_preview_controller: SimpleProfilePreviewController<'a>,
- edit_mode: &'a mut bool,
}
impl<'a> View for AccountManagementView<'a> {
@@ -26,120 +26,51 @@ impl<'a> View for AccountManagementView<'a> {
impl<'a> AccountManagementView<'a> {
pub fn new(
- account_manager: AccountManager<'a>,
+ account_manager: &'a mut AccountManager,
simple_preview_controller: SimpleProfilePreviewController<'a>,
- edit_mode: &'a mut bool,
) -> Self {
AccountManagementView {
account_manager,
simple_preview_controller,
- edit_mode,
}
}
fn show(&mut self, ui: &mut egui::Ui) {
- ui.add_space(24.0);
- let screen_size = ui.ctx().screen_rect();
- let margin_amt = 128.0;
- let window_size = Vec2::new(
- screen_size.width() - margin_amt,
- screen_size.height() - margin_amt,
- );
-
- Window::new("Account Management")
- .frame(Frame::window(ui.style()))
- .collapsible(false)
- .anchor(Align2::CENTER_CENTER, [0.0, 0.0])
- .resizable(false)
- .title_bar(false)
- .default_size(window_size)
- .show(ui.ctx(), |ui| {
- ui.add(title());
- ui.add(self.buttons_widget());
- ui.add_space(8.0);
+ Frame::none().outer_margin(24.0).show(ui, |ui| {
+ self.top_section_buttons_widget(ui);
+ ui.add_space(8.0);
+ scroll_area().show(ui, |ui| {
self.show_accounts(ui);
});
+ });
}
fn show_accounts(&mut self, ui: &mut egui::Ui) {
- scroll_area().show(ui, |ui| {
- ui.horizontal_wrapped(|ui| {
+ let maybe_remove = self.simple_preview_controller.set_profile_previews(
+ self.account_manager,
+ ui,
+ account_card_ui(),
+ );
+
+ self.maybe_remove_accounts(maybe_remove);
+ }
+
+ fn show_accounts_mobile(&mut self, ui: &mut egui::Ui) {
+ ui.allocate_ui_with_layout(
+ Vec2::new(ui.available_size_before_wrap().x, 32.0),
+ Layout::top_down(egui::Align::Min),
+ |ui| {
+ // create all account 'cards' and get the indicies the user requested to remove
let maybe_remove = self.simple_preview_controller.set_profile_previews(
- &self.account_manager,
+ self.account_manager,
ui,
- *self.edit_mode,
- |ui, preview, edit_mode| {
- let mut should_remove = false;
-
- ui.add_sized(preview.dimensions(), |ui: &mut egui::Ui| {
- simple_preview_frame(ui)
- .show(ui, |ui| {
- ui.vertical_centered(|ui| {
- ui.add(preview);
- if edit_mode {
- should_remove = ui
- .add(delete_button(ui.visuals().dark_mode))
- .clicked();
- }
- });
- })
- .response
- });
- should_remove
- },
+ account_card_ui(), // closure for creating an account 'card'
);
+ // remove all account indicies user requested
self.maybe_remove_accounts(maybe_remove);
- });
- });
- }
-
- fn show_accounts_mobile(&mut self, ui: &mut egui::Ui) {
- scroll_area().show(ui, |ui| {
- ui.allocate_ui_with_layout(
- Vec2::new(ui.available_size_before_wrap().x, 32.0),
- Layout::top_down(egui::Align::Min),
- |ui| {
- let maybe_remove = self.simple_preview_controller.set_profile_previews(
- &self.account_manager,
- ui,
- *self.edit_mode,
- |ui, preview, edit_mode| {
- let mut should_remove = false;
-
- ui.add_sized(
- Vec2::new(ui.available_width(), 50.0),
- |ui: &mut egui::Ui| {
- Frame::none()
- .show(ui, |ui| {
- ui.horizontal(|ui| {
- ui.add(preview);
- if edit_mode {
- ui.with_layout(
- Layout::right_to_left(Align::Center),
- |ui| {
- should_remove = ui
- .add(delete_button(
- ui.visuals().dark_mode,
- ))
- .clicked();
- },
- );
- }
- });
- })
- .response
- },
- );
- ui.add_space(16.0);
- should_remove
- },
- );
-
- self.maybe_remove_accounts(maybe_remove);
- },
- );
- });
+ },
+ );
}
fn maybe_remove_accounts(&mut self, account_indices: Option<Vec<usize>>) {
@@ -153,66 +84,94 @@ impl<'a> AccountManagementView<'a> {
fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response {
egui::CentralPanel::default()
.show(ui.ctx(), |ui| {
- ui.add(title());
- ui.add(self.buttons_widget());
+ mobile_title(ui);
+ self.top_section_buttons_widget(ui);
+
ui.add_space(8.0);
- self.show_accounts_mobile(ui);
+ scroll_area().show(ui, |ui| {
+ self.show_accounts_mobile(ui);
+ });
})
.response
}
- fn 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 *self.edit_mode {
- if ui.add(done_account_button()).clicked() {
- *self.edit_mode = false;
- }
- } else if ui.add(edit_account_button()).clicked() {
- *self.edit_mode = true;
- }
- },
- );
+ 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
+ }
+ },
+ );
- 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(add_account_button()).clicked() {
- // TODO: route to AccountLoginView
- }
- },
- );
- })
- .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
}
}
-fn simple_preview_frame(ui: &mut egui::Ui) -> Frame {
- Frame::none()
- .rounding(ui.visuals().window_rounding)
- .fill(ui.visuals().window_fill)
- .stroke(ui.visuals().window_stroke)
- .outer_margin(Margin::same(2.0))
- .inner_margin(12.0)
+fn account_card_ui() -> fn(
+ ui: &mut egui::Ui,
+ preview: SimpleProfilePreview,
+ width: f32,
+ is_selected: bool,
+) -> Option<ProfilePreviewOp> {
+ |ui, preview, width, is_selected| {
+ let mut op: Option<ProfilePreviewOp> = None;
+
+ ui.add_sized(Vec2::new(width, 50.0), |ui: &mut egui::Ui| {
+ Frame::none()
+ .show(ui, |ui| {
+ ui.horizontal(|ui| {
+ ui.add(preview);
+
+ ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
+ if is_selected {
+ ui.add(selected_widget());
+ } else {
+ if ui
+ .add(switch_button(ui.style().visuals.dark_mode))
+ .clicked()
+ {
+ op = Some(ProfilePreviewOp::SwitchTo);
+ }
+ if ui.add(sign_out_button(ui)).clicked() {
+ op = Some(ProfilePreviewOp::RemoveAccount)
+ }
+ }
+ });
+ });
+ })
+ .response
+ });
+ ui.add_space(16.0);
+ op
+ }
}
-fn title() -> impl egui::Widget {
- |ui: &mut egui::Ui| {
- ui.vertical_centered(|ui| {
- ui.label(
- RichText::new("Accounts")
- .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 {
@@ -222,160 +181,84 @@ fn scroll_area() -> ScrollArea {
}
fn add_account_button() -> Button<'static> {
- Button::new("Add Account").min_size(Vec2::new(0.0, 32.0))
+ let img_data = egui::include_image!("../../assets/icons/add_account_icon_4x.png");
+ let img = Image::new(img_data).fit_to_exact_size(Vec2::new(48.0, 48.0));
+ Button::image_and_text(
+ img,
+ RichText::new(" Add account")
+ .size(16.0)
+ // TODO: this color should not be hard coded. Find some way to add it to the visuals
+ .color(PINK),
+ )
+ .frame(false)
}
-fn edit_account_button() -> Button<'static> {
- Button::new("Edit").min_size(Vec2::new(0.0, 32.0))
-}
+fn sign_out_button(ui: &egui::Ui) -> egui::Button<'static> {
+ 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));
-fn done_account_button() -> Button<'static> {
- Button::new("Done").min_size(Vec2::new(0.0, 32.0))
+ egui::Button::image_and_text(
+ img,
+ RichText::new("Sign out").color(ui.visuals().noninteractive().fg_stroke.color),
+ )
+ .frame(false)
}
-fn delete_button(_dark_mode: bool) -> egui::Button<'static> {
- let img_data = egui::include_image!("../../assets/icons/delete_icon_4x.png");
+fn switch_button(dark_mode: bool) -> egui::Button<'static> {
+ let _ = dark_mode;
- egui::Button::image(egui::Image::new(img_data).max_width(30.0)).frame(true)
+ egui::Button::new("Switch").min_size(Vec2::new(76.0, 32.0))
}
-pub struct AccountSelectionWidget<'a> {
- account_manager: AccountManager<'a>,
- simple_preview_controller: SimpleProfilePreviewController<'a>,
-}
-
-impl<'a> AccountSelectionWidget<'a> {
- fn ui(&'a mut self, ui: &mut egui::Ui) -> Option<&'a UserAccount> {
- let mut result: Option<&'a UserAccount> = None;
- scroll_area().show(ui, |ui| {
- ui.horizontal_wrapped(|ui| {
- let clicked_at = self.simple_preview_controller.view_profile_previews(
- &self.account_manager,
- ui,
- |ui, preview, index| {
- let resp = ui.add_sized(preview.dimensions(), |ui: &mut egui::Ui| {
- simple_preview_frame(ui)
- .show(ui, |ui| {
- ui.vertical_centered(|ui| {
- ui.add(preview);
- });
- })
- .response
- });
-
- ui.interact(resp.rect, Id::new(index), Sense::click())
- .clicked()
- },
- );
-
- if let Some(index) = clicked_at {
- result = self.account_manager.get_account(index);
- };
- });
- });
- result
+fn selected_widget() -> impl egui::Widget {
+ |ui: &mut egui::Ui| {
+ Frame::none()
+ .show(ui, |ui| {
+ ui.label(RichText::new("Selected").size(13.0).color(PINK));
+ let img_data = egui::include_image!("../../assets/icons/select_icon_3x.png");
+ let img = Image::new(img_data).max_size(Vec2::new(16.0, 16.0));
+ ui.add(img);
+ })
+ .response
}
}
-impl<'a> AccountSelectionWidget<'a> {
- pub fn new(
- account_manager: AccountManager<'a>,
- simple_preview_controller: SimpleProfilePreviewController<'a>,
- ) -> Self {
- AccountSelectionWidget {
- account_manager,
- simple_preview_controller,
- }
- }
-}
+// fn logout_all_button() -> egui::Button<'static> {
+// egui::Button::new("Logout all")
+// }
// PREVIEWS
mod preview {
- use enostr::{FullKeypair, Pubkey};
- use nostrdb::{Config, Ndb};
+ use nostrdb::Ndb;
use super::*;
- use crate::key_storage::KeyStorage;
- use crate::relay_generation::RelayGenerator;
- use crate::{imgcache::ImageCache, test_data};
- use std::path::Path;
-
- const ACCOUNT_HEXES: [&str; 10] = [
- "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
- "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
- "bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91",
- "5c10ed0678805156d39ef1ef6d46110fe1e7e590ae04986ccf48ba1299cb53e2",
- "4c96d763eb2fe01910f7e7220b7c7ecdbe1a70057f344b9f79c28af080c3ee30",
- "edf16b1dd61eab353a83af470cc13557029bff6827b4cb9b7fc9bdb632a2b8e6",
- "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
- "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
- "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
- "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
- ];
+ use crate::{imgcache::ImageCache, test_data::get_accmgr_and_ndb_and_imgcache};
pub struct AccountManagementPreview {
- accounts: Vec<UserAccount>,
+ account_manager: AccountManager,
ndb: Ndb,
img_cache: ImageCache,
- edit_mode: bool,
- }
-
- fn get_accounts() -> Vec<UserAccount> {
- ACCOUNT_HEXES
- .iter()
- .map(|account_hex| {
- let key = FullKeypair::new(
- Pubkey::from_hex(account_hex).unwrap(),
- FullKeypair::generate().secret_key,
- );
-
- UserAccount {
- key,
- relays: test_data::sample_pool(),
- }
- })
- .collect()
- }
-
- fn get_ndb_and_img_cache() -> (Ndb, ImageCache) {
- let mut config = Config::new();
- config.set_ingester_threads(2);
-
- let db_dir = Path::new(".");
- let path = db_dir.to_str().unwrap();
- let ndb = Ndb::new(path, &config).expect("ndb");
- let imgcache_dir = db_dir.join("cache/img");
- let img_cache = ImageCache::new(imgcache_dir);
- (ndb, img_cache)
}
impl AccountManagementPreview {
fn new() -> Self {
- let accounts = get_accounts();
- let (ndb, img_cache) = get_ndb_and_img_cache();
+ let (account_manager, ndb, img_cache) = get_accmgr_and_ndb_and_imgcache();
AccountManagementPreview {
- accounts,
+ account_manager,
ndb,
img_cache,
- edit_mode: false,
}
}
}
impl View for AccountManagementPreview {
fn ui(&mut self, ui: &mut egui::Ui) {
- let account_manager = AccountManager::new(
- &mut self.accounts,
- KeyStorage::None,
- RelayGenerator::Constant,
- );
-
+ ui.add_space(24.0);
AccountManagementView::new(
- account_manager,
+ &mut self.account_manager,
SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
- &mut self.edit_mode,
)
.ui(ui);
}
@@ -388,49 +271,4 @@ mod preview {
AccountManagementPreview::new()
}
}
-
- pub struct AccountSelectionPreview {
- accounts: Vec<UserAccount>,
- ndb: Ndb,
- img_cache: ImageCache,
- }
-
- impl AccountSelectionPreview {
- fn new() -> Self {
- let accounts = get_accounts();
- let (ndb, img_cache) = get_ndb_and_img_cache();
- AccountSelectionPreview {
- accounts,
- ndb,
- img_cache,
- }
- }
- }
-
- impl View for AccountSelectionPreview {
- fn ui(&mut self, ui: &mut egui::Ui) {
- let account_manager = AccountManager::new(
- &mut self.accounts,
- KeyStorage::None,
- RelayGenerator::Constant,
- );
-
- let mut widget = AccountSelectionWidget::new(
- account_manager,
- SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
- );
-
- if let Some(account) = widget.ui(ui) {
- println!("User made selection: {:?}", account.key);
- }
- }
- }
-
- impl<'a> Preview for AccountSelectionWidget<'a> {
- type Prev = AccountSelectionPreview;
-
- fn preview() -> Self::Prev {
- AccountSelectionPreview::new()
- }
- }
}
diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs
@@ -0,0 +1,260 @@
+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 super::profile::{preview::SimpleProfilePreview, SimpleProfilePreviewController};
+
+pub struct AccountSelectionWidget<'a> {
+ account_manager: &'a AccountManager,
+ simple_preview_controller: SimpleProfilePreviewController<'a>,
+}
+
+enum AccountSelectAction {
+ RemoveAccount { index: usize },
+ SelectAccount { index: usize },
+ OpenAccountManagement,
+}
+
+#[derive(Default)]
+struct AccountSelectResponse {
+ action: Option<AccountSelectAction>,
+}
+
+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);
+ } else {
+ self.show(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| {
+ res = top_section_widget(ui);
+
+ scroll_area().show(ui, |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(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: &UserAccount) -> bool {
+ if let Ok(response) = self.sign_out_button(ui, account) {
+ response.clicked()
+ } else {
+ false
+ }
+ }
+
+ fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response {
+ let _ = ui;
+ todo!()
+ }
+
+ fn show_accounts(&mut self, ui: &mut egui::Ui) -> Option<usize> {
+ 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) -> Result<egui::Response> {
+ self.simple_preview_controller.show_with_nickname(
+ ui,
+ 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(
+ img,
+ RichText::new(format!(" Sign out @{}", username.username()))
+ .color(PINK)
+ .size(16.0),
+ )
+ .frame(false);
+
+ ui.add(button)
+ },
+ )
+ }
+}
+
+fn account_switcher_card_ui() -> fn(
+ ui: &mut egui::Ui,
+ preview: SimpleProfilePreview,
+ width: f32,
+ is_selected: bool,
+ index: usize,
+) -> bool {
+ |ui, preview, width, is_selected, index| {
+ let resp = ui.add_sized(Vec2::new(width, 50.0), |ui: &mut egui::Ui| {
+ Frame::none()
+ .show(ui, |ui| {
+ ui.add_space(8.0);
+ ui.horizontal(|ui| {
+ if is_selected {
+ Frame::none()
+ .rounding(Rounding::same(8.0))
+ .inner_margin(Margin::same(8.0))
+ .fill(Color32::from_rgb(0x45, 0x1B, 0x59))
+ .show(ui, |ui| {
+ ui.add(preview);
+ ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
+ ui.add(selection_widget());
+ });
+ });
+ } else {
+ ui.add_space(8.0);
+ ui.add(preview);
+ }
+ });
+ })
+ .response
+ });
+
+ ui.interact(resp.rect, Id::new(index), Sense::click())
+ .clicked()
+ }
+}
+
+fn selection_widget() -> impl egui::Widget {
+ |ui: &mut egui::Ui| {
+ let img_data: egui::ImageSource =
+ egui::include_image!("../../assets/icons/select_icon_3x.png");
+ let img = Image::new(img_data).max_size(Vec2::new(16.0, 16.0));
+ ui.add(img)
+ }
+}
+
+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::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> {
+ Button::new(RichText::new("Manage").color(PINK).size(16.0)).frame(false)
+}
+
+fn account_switcher_title() -> impl egui::Widget {
+ |ui: &mut egui::Ui| ui.label(RichText::new("Account switcher").size(20.0).strong())
+}
+
+fn scroll_area() -> ScrollArea {
+ egui::ScrollArea::vertical()
+ .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
+ .auto_shrink([false; 2])
+}
+
+fn add_account_button() -> egui::Button<'static> {
+ let img_data = egui::include_image!("../../assets/icons/plus_icon_4x.png");
+ let img = Image::new(img_data).fit_to_exact_size(Vec2::new(16.0, 16.0));
+ Button::image_and_text(img, RichText::new(" Add account").size(16.0).color(PINK)).frame(false)
+}
+
+mod previews {
+ use nostrdb::Ndb;
+
+ use crate::{
+ account_manager::AccountManager,
+ imgcache::ImageCache,
+ test_data,
+ ui::{profile::SimpleProfilePreviewController, Preview, View},
+ };
+
+ use super::AccountSelectionWidget;
+
+ pub struct AccountSelectionPreview {
+ account_manager: AccountManager,
+ ndb: Ndb,
+ img_cache: ImageCache,
+ }
+
+ impl AccountSelectionPreview {
+ fn new() -> Self {
+ let (account_manager, ndb, img_cache) = test_data::get_accmgr_and_ndb_and_imgcache();
+ AccountSelectionPreview {
+ account_manager,
+ ndb,
+ img_cache,
+ }
+ }
+ }
+
+ impl View for AccountSelectionPreview {
+ fn ui(&mut self, ui: &mut egui::Ui) {
+ AccountSelectionWidget::new(
+ &self.account_manager,
+ SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
+ )
+ .ui(ui);
+ }
+ }
+
+ impl<'a> Preview for AccountSelectionWidget<'a> {
+ type Prev = AccountSelectionPreview;
+
+ fn preview() -> Self::Prev {
+ AccountSelectionPreview::new()
+ }
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -1,19 +1,23 @@
pub mod account_login_view;
pub mod account_management;
+pub mod account_switcher;
pub mod anim;
pub mod mention;
pub mod note;
pub mod preview;
pub mod profile;
pub mod relay;
+pub mod side_panel;
pub mod username;
-pub use account_management::{AccountManagementView, AccountSelectionWidget};
+pub use account_management::AccountManagementView;
+pub use account_switcher::AccountSelectionWidget;
pub use mention::Mention;
pub use note::Note;
pub use preview::{Preview, PreviewApp};
pub use profile::{ProfilePic, ProfilePreview};
pub use relay::RelayView;
+pub use side_panel::DesktopSidePanel;
pub use username::Username;
use egui::Margin;
diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs
@@ -1,5 +1,7 @@
pub mod picture;
pub mod preview;
+mod profile_preview_controller;
pub use picture::ProfilePic;
pub use preview::ProfilePreview;
+pub use profile_preview_controller::{ProfilePreviewOp, SimpleProfilePreviewController};
diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs
@@ -3,7 +3,7 @@ use crate::imgcache::ImageCache;
use crate::ui::ProfilePic;
use crate::{colors, images, DisplayName};
use egui::load::TexturePoll;
-use egui::{Frame, RichText, Sense, Vec2, Widget};
+use egui::{Frame, RichText, Sense, Widget};
use egui_extras::Size;
use nostrdb::ProfileRecord;
@@ -93,10 +93,6 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> {
pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self {
SimpleProfilePreview { profile, cache }
}
-
- pub fn dimensions(&self) -> Vec2 {
- Vec2::new(120.0, 150.0)
- }
}
impl<'a, 'cache> egui::Widget for SimpleProfilePreview<'a, 'cache> {
@@ -152,7 +148,7 @@ mod previews {
}
}
-fn get_display_name<'a>(profile: &'a ProfileRecord<'a>) -> DisplayName<'a> {
+pub fn get_display_name<'a>(profile: &'a ProfileRecord<'a>) -> DisplayName<'a> {
if let Some(name) = crate::profile::get_profile_name(profile) {
name
} else {
@@ -160,7 +156,7 @@ fn get_display_name<'a>(profile: &'a ProfileRecord<'a>) -> DisplayName<'a> {
}
}
-fn get_profile_url<'a>(profile: &'a ProfileRecord<'a>) -> &'a str {
+pub fn get_profile_url<'a>(profile: &'a ProfileRecord<'a>) -> &'a str {
if let Some(url) = profile.record().profile().and_then(|p| p.picture()) {
url
} else {
diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs
@@ -0,0 +1,168 @@
+use nostrdb::{Ndb, Transaction};
+
+use crate::{account_manager::AccountManager, imgcache::ImageCache, DisplayName, Result};
+
+use super::{
+ preview::{get_display_name, get_profile_url, SimpleProfilePreview},
+ ProfilePic,
+};
+
+pub struct SimpleProfilePreviewController<'a> {
+ ndb: &'a Ndb,
+ img_cache: &'a mut ImageCache,
+}
+
+#[derive(Debug)]
+pub enum ProfilePreviewOp {
+ RemoveAccount,
+ SwitchTo,
+}
+
+impl<'a> SimpleProfilePreviewController<'a> {
+ pub fn new(ndb: &'a Ndb, img_cache: &'a mut ImageCache) -> Self {
+ SimpleProfilePreviewController { ndb, img_cache }
+ }
+
+ pub fn set_profile_previews(
+ &mut self,
+ account_manager: &mut AccountManager,
+ ui: &mut egui::Ui,
+ add_preview_ui: fn(
+ ui: &mut egui::Ui,
+ preview: SimpleProfilePreview,
+ width: f32,
+ is_selected: bool,
+ ) -> Option<ProfilePreviewOp>,
+ ) -> Option<Vec<usize>> {
+ let mut to_remove: Option<Vec<usize>> = None;
+
+ 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() {
+ let account = if let Some(account) = account_manager.get_account(i) {
+ account
+ } else {
+ continue;
+ };
+
+ let profile =
+ if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, account.pubkey.bytes()) {
+ profile
+ } else {
+ continue;
+ };
+
+ let preview = SimpleProfilePreview::new(&profile, self.img_cache);
+
+ 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),
+ }
+ }
+
+ to_remove
+ }
+
+ pub fn view_profile_previews(
+ &mut self,
+ account_manager: &AccountManager,
+ ui: &mut egui::Ui,
+ add_preview_ui: fn(
+ ui: &mut egui::Ui,
+ preview: SimpleProfilePreview,
+ width: f32,
+ is_selected: bool,
+ index: usize,
+ ) -> bool,
+ ) -> Option<usize> {
+ 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() {
+ let account = if let Some(account) = account_manager.get_account(i) {
+ account
+ } else {
+ continue;
+ };
+
+ let profile =
+ if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, account.pubkey.bytes()) {
+ profile
+ } else {
+ continue;
+ };
+
+ let preview = SimpleProfilePreview::new(&profile, self.img_cache);
+
+ 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: &[u8; 32],
+ ui_element: fn(ui: &mut egui::Ui, username: &DisplayName) -> egui::Response,
+ ) -> Result<egui::Response> {
+ 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(
+ self,
+ ui: &mut egui::Ui,
+ key: &[u8; 32],
+ ui_element: fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response,
+ ) -> Option<egui::Response> {
+ if let Ok(txn) = Transaction::new(self.ndb) {
+ let profile = self.ndb.get_profile_by_pubkey(&txn, key);
+
+ if let Ok(profile) = profile {
+ return Some(ui_element(
+ ui,
+ ProfilePic::new(self.img_cache, get_profile_url(&profile)),
+ ));
+ }
+ }
+ None
+ }
+}
diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs
@@ -0,0 +1,172 @@
+use egui::{Button, Layout, SidePanel, Vec2, Widget};
+
+use crate::account_manager::AccountManager;
+
+use super::{profile::SimpleProfilePreviewController, ProfilePic, View};
+
+pub struct DesktopSidePanel<'a> {
+ selected_account: Option<&'a [u8; 32]>,
+ simple_preview_controller: SimpleProfilePreviewController<'a>,
+}
+
+#[derive(Debug, Eq, PartialEq, Clone, Copy)]
+pub enum SidePanelAction {
+ Panel,
+ Account,
+ Settings,
+ Columns,
+}
+
+pub struct SidePanelResponse {
+ pub response: egui::Response,
+ pub action: SidePanelAction,
+}
+
+impl SidePanelResponse {
+ fn new(action: SidePanelAction, response: egui::Response) -> Self {
+ SidePanelResponse { action, response }
+ }
+}
+
+impl<'a> Widget for DesktopSidePanel<'a> {
+ fn ui(self, ui: &mut egui::Ui) -> egui::Response {
+ self.show(ui).response
+ }
+}
+
+impl<'a> DesktopSidePanel<'a> {
+ pub fn new(
+ selected_account: Option<&'a [u8; 32]>,
+ simple_preview_controller: SimpleProfilePreviewController<'a>,
+ ) -> Self {
+ DesktopSidePanel {
+ selected_account,
+ simple_preview_controller,
+ }
+ }
+
+ pub fn panel() -> SidePanel {
+ egui::SidePanel::left("side_panel")
+ .resizable(false)
+ .exact_width(40.0)
+ }
+
+ pub fn show(self, ui: &mut egui::Ui) -> SidePanelResponse {
+ 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())
+ }
+}
+
+fn show_pfp() -> fn(ui: &mut egui::Ui, pfp: ProfilePic) -> egui::Response {
+ |ui, pfp| {
+ let response = pfp.ui(ui);
+ ui.allocate_rect(response.rect, egui::Sense::click())
+ }
+}
+
+fn settings_button(dark_mode: bool) -> egui::Button<'static> {
+ let _ = dark_mode;
+ let img_data = egui::include_image!("../../assets/icons/settings_dark_4x.png");
+
+ egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false)
+}
+
+fn add_button_to_ui(ui: &mut egui::Ui, button: Button) -> egui::Response {
+ ui.add_sized(Vec2::new(32.0, 32.0), button)
+}
+
+fn no_account_pfp() -> Button<'static> {
+ Button::new("A")
+ .rounding(20.0)
+ .min_size(Vec2::new(38.0, 38.0))
+}
+
+fn add_column_button(dark_mode: bool) -> egui::Button<'static> {
+ let _ = dark_mode;
+ let img_data = egui::include_image!("../../assets/icons/add_column_dark_4x.png");
+
+ egui::Button::image(egui::Image::new(img_data).max_width(32.0)).frame(false)
+}
+
+mod preview {
+ use nostrdb::Ndb;
+
+ use crate::{imgcache::ImageCache, test_data, ui::Preview};
+
+ use super::*;
+
+ pub struct DesktopSidePanelPreview {
+ account_manager: AccountManager,
+ ndb: Ndb,
+ img_cache: ImageCache,
+ }
+
+ impl DesktopSidePanelPreview {
+ fn new() -> Self {
+ let (account_manager, ndb, img_cache) = test_data::get_accmgr_and_ndb_and_imgcache();
+ DesktopSidePanelPreview {
+ account_manager,
+ ndb,
+ img_cache,
+ }
+ }
+ }
+
+ impl View for DesktopSidePanelPreview {
+ fn ui(&mut self, ui: &mut egui::Ui) {
+ 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));
+ }
+ }
+
+ impl<'a> Preview for DesktopSidePanel<'a> {
+ type Prev = DesktopSidePanelPreview;
+
+ fn preview() -> Self::Prev {
+ DesktopSidePanelPreview::new()
+ }
+ }
+}
diff --git 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, Preview, PreviewApp, ProfilePic, ProfilePreview,
- RelayView,
+ AccountManagementView, AccountSelectionWidget, DesktopSidePanel, Preview, PreviewApp,
+ ProfilePic, ProfilePreview, RelayView,
};
use std::env;
@@ -88,5 +88,6 @@ async fn main() {
ProfilePic,
AccountManagementView,
AccountSelectionWidget,
+ DesktopSidePanel,
);
}
diff --git 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<String>,
+//}
+
+pub type UserAccount = Keypair;