notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 22e67c95ccb7e378b506b48b502116f3cd18d4e0
parent 6545e1ddee7d8a67bc7dabd4525939ce0f4dcad8
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 18 Nov 2024 18:03:27 -0800

refactor: rename AccountsManager to Accounts

plz stop with the managers

Diffstat:
Dsrc/account_manager.rs | 238-------------------------------------------------------------------------------
Asrc/accounts/mod.rs | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/accounts/route.rs | 13+++++++++++++
Msrc/app.rs | 19+++++++++++--------
Msrc/lib.rs | 2+-
Msrc/nav.rs | 2+-
Msrc/route.rs | 2+-
Msrc/timeline/route.rs | 4++--
Dsrc/ui/account_management.rs | 257-------------------------------------------------------------------------------
Asrc/ui/accounts.rs | 257+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/ui/mod.rs | 4++--
Msrc/ui/side_panel.rs | 2+-
Msrc/ui_preview/main.rs | 6+++---
13 files changed, 520 insertions(+), 514 deletions(-)

diff --git a/src/account_manager.rs b/src/account_manager.rs @@ -1,238 +0,0 @@ -use std::cmp::Ordering; - -use enostr::{FilledKeypair, FullKeypair, Keypair}; -use nostrdb::Ndb; -use serde::{Deserialize, Serialize}; - -use crate::{ - column::Columns, - imgcache::ImageCache, - login_manager::AcquireKeyState, - route::{Route, Router}, - storage::{KeyStorageResponse, KeyStorageType}, - ui::{ - account_login_view::{AccountLoginResponse, AccountLoginView}, - account_management::{AccountsView, AccountsViewResponse}, - }, - unknowns::SingleUnkIdAction, -}; -use tracing::{error, info}; - -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 { - currently_selected_account: Option<usize>, - accounts: Vec<UserAccount>, - key_store: KeyStorageType, -} - -// TODO(jb55): move to accounts/route.rs -pub enum AccountsRouteResponse { - Accounts(AccountsViewResponse), - AddAccount(AccountLoginResponse), -} - -#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] -pub enum AccountsRoute { - Accounts, - AddAccount, -} - -/// Render account management views from a route -#[allow(clippy::too_many_arguments)] -pub fn render_accounts_route( - ui: &mut egui::Ui, - ndb: &Ndb, - col: usize, - columns: &mut Columns, - img_cache: &mut ImageCache, - accounts: &mut AccountManager, - login_state: &mut AcquireKeyState, - route: AccountsRoute, -) -> SingleUnkIdAction { - let router = columns.column_mut(col).router_mut(); - let resp = match route { - AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) - .ui(ui) - .inner - .map(AccountsRouteResponse::Accounts), - - AccountsRoute::AddAccount => AccountLoginView::new(login_state) - .ui(ui) - .inner - .map(AccountsRouteResponse::AddAccount), - }; - - if let Some(resp) = resp { - match resp { - AccountsRouteResponse::Accounts(response) => { - process_accounts_view_response(accounts, response, router); - SingleUnkIdAction::no_action() - } - AccountsRouteResponse::AddAccount(response) => { - let action = process_login_view_response(accounts, response); - *login_state = Default::default(); - router.go_back(); - action - } - } - } else { - SingleUnkIdAction::no_action() - } -} - -pub fn process_accounts_view_response( - manager: &mut AccountManager, - response: AccountsViewResponse, - router: &mut Router<Route>, -) { - match response { - AccountsViewResponse::RemoveAccount(index) => { - manager.remove_account(index); - } - AccountsViewResponse::SelectAccount(index) => { - manager.select_account(index); - } - AccountsViewResponse::RouteToLogin => { - router.route_to(Route::add_account()); - } - } -} - -impl AccountManager { - pub fn new(key_store: KeyStorageType) -> Self { - let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { - res.unwrap_or_default() - } else { - Vec::new() - }; - - let currently_selected_account = get_selected_index(&accounts, &key_store); - AccountManager { - currently_selected_account, - accounts, - key_store, - } - } - - pub fn get_accounts(&self) -> &Vec<UserAccount> { - &self.accounts - } - - 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); - 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 has_account_pubkey(&self, pubkey: &[u8; 32]) -> bool { - for account in &self.accounts { - if account.pubkey.bytes() == pubkey { - return true; - } - } - - false - } - - #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] - pub fn add_account(&mut self, account: Keypair) -> SingleUnkIdAction { - if self.has_account_pubkey(account.pubkey.bytes()) { - info!("already have account, not adding {}", account.pubkey); - return SingleUnkIdAction::pubkey(account.pubkey); - } - - let _ = self.key_store.add_key(&account); - let pk = account.pubkey; - self.accounts.push(account); - SingleUnkIdAction::pubkey(pk) - } - - pub fn num_accounts(&self) -> usize { - self.accounts.len() - } - - pub fn get_selected_account_index(&self) -> Option<usize> { - self.currently_selected_account - } - - pub fn selected_or_first_nsec(&self) -> Option<FilledKeypair<'_>> { - self.get_selected_account() - .and_then(|kp| kp.to_full()) - .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) - } - - pub fn get_selected_account(&self) -> Option<&UserAccount> { - if let Some(account_index) = self.currently_selected_account { - if let Some(account) = self.get_account(account_index) { - Some(account) - } else { - None - } - } else { - None - } - } - - pub fn select_account(&mut self, index: usize) { - if let Some(account) = self.accounts.get(index) { - self.currently_selected_account = Some(index); - self.key_store.select_key(Some(account.pubkey)); - } - } - - pub fn clear_selected_account(&mut self) { - self.currently_selected_account = None; - self.key_store.select_key(None); - } -} - -fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> { - match keystore.get_selected_key() { - KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => { - return accounts.iter().position(|account| account.pubkey == pubkey); - } - - KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e), - KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {} - }; - - None -} - -pub fn process_login_view_response( - manager: &mut AccountManager, - response: AccountLoginResponse, -) -> SingleUnkIdAction { - let r = match response { - AccountLoginResponse::CreateNew => { - manager.add_account(FullKeypair::generate().to_keypair()) - } - AccountLoginResponse::LoginWith(keypair) => manager.add_account(keypair), - }; - manager.select_account(manager.num_accounts() - 1); - r -} diff --git a/src/accounts/mod.rs b/src/accounts/mod.rs @@ -0,0 +1,228 @@ +use std::cmp::Ordering; + +use enostr::{FilledKeypair, FullKeypair, Keypair}; +use nostrdb::Ndb; + +use crate::{ + column::Columns, + imgcache::ImageCache, + login_manager::AcquireKeyState, + route::{Route, Router}, + storage::{KeyStorageResponse, KeyStorageType}, + ui::{ + account_login_view::{AccountLoginResponse, AccountLoginView}, + accounts::{AccountsView, AccountsViewResponse}, + }, + unknowns::SingleUnkIdAction, + user_account::UserAccount, +}; +use tracing::{error, info}; + +mod route; + +pub use route::{AccountsRoute, AccountsRouteResponse}; + +/// The interface for managing the user's accounts. +/// Represents all user-facing operations related to account management. +pub struct Accounts { + currently_selected_account: Option<usize>, + accounts: Vec<UserAccount>, + key_store: KeyStorageType, +} + +/// Render account management views from a route +#[allow(clippy::too_many_arguments)] +pub fn render_accounts_route( + ui: &mut egui::Ui, + ndb: &Ndb, + col: usize, + columns: &mut Columns, + img_cache: &mut ImageCache, + accounts: &mut Accounts, + login_state: &mut AcquireKeyState, + route: AccountsRoute, +) -> SingleUnkIdAction { + let router = columns.column_mut(col).router_mut(); + let resp = match route { + AccountsRoute::Accounts => AccountsView::new(ndb, accounts, img_cache) + .ui(ui) + .inner + .map(AccountsRouteResponse::Accounts), + + AccountsRoute::AddAccount => AccountLoginView::new(login_state) + .ui(ui) + .inner + .map(AccountsRouteResponse::AddAccount), + }; + + if let Some(resp) = resp { + match resp { + AccountsRouteResponse::Accounts(response) => { + process_accounts_view_response(accounts, response, router); + SingleUnkIdAction::no_action() + } + AccountsRouteResponse::AddAccount(response) => { + let action = process_login_view_response(accounts, response); + *login_state = Default::default(); + router.go_back(); + action + } + } + } else { + SingleUnkIdAction::no_action() + } +} + +pub fn process_accounts_view_response( + manager: &mut Accounts, + response: AccountsViewResponse, + router: &mut Router<Route>, +) { + match response { + AccountsViewResponse::RemoveAccount(index) => { + manager.remove_account(index); + } + AccountsViewResponse::SelectAccount(index) => { + manager.select_account(index); + } + AccountsViewResponse::RouteToLogin => { + router.route_to(Route::add_account()); + } + } +} + +impl Accounts { + pub fn new(key_store: KeyStorageType) -> Self { + let accounts = if let KeyStorageResponse::ReceivedResult(res) = key_store.get_keys() { + res.unwrap_or_default() + } else { + Vec::new() + }; + + let currently_selected_account = get_selected_index(&accounts, &key_store); + Accounts { + currently_selected_account, + accounts, + key_store, + } + } + + pub fn get_accounts(&self) -> &Vec<UserAccount> { + &self.accounts + } + + 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); + 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 has_account_pubkey(&self, pubkey: &[u8; 32]) -> bool { + for account in &self.accounts { + if account.pubkey.bytes() == pubkey { + return true; + } + } + + false + } + + #[must_use = "UnknownIdAction's must be handled. Use .process_unknown_id_action()"] + pub fn add_account(&mut self, account: Keypair) -> SingleUnkIdAction { + if self.has_account_pubkey(account.pubkey.bytes()) { + info!("already have account, not adding {}", account.pubkey); + return SingleUnkIdAction::pubkey(account.pubkey); + } + + let _ = self.key_store.add_key(&account); + let pk = account.pubkey; + self.accounts.push(account); + SingleUnkIdAction::pubkey(pk) + } + + pub fn num_accounts(&self) -> usize { + self.accounts.len() + } + + pub fn get_selected_account_index(&self) -> Option<usize> { + self.currently_selected_account + } + + pub fn selected_or_first_nsec(&self) -> Option<FilledKeypair<'_>> { + self.get_selected_account() + .and_then(|kp| kp.to_full()) + .or_else(|| self.accounts.iter().find_map(|a| a.to_full())) + } + + pub fn get_selected_account(&self) -> Option<&UserAccount> { + if let Some(account_index) = self.currently_selected_account { + if let Some(account) = self.get_account(account_index) { + Some(account) + } else { + None + } + } else { + None + } + } + + pub fn select_account(&mut self, index: usize) { + if let Some(account) = self.accounts.get(index) { + self.currently_selected_account = Some(index); + self.key_store.select_key(Some(account.pubkey)); + } + } + + pub fn clear_selected_account(&mut self) { + self.currently_selected_account = None; + self.key_store.select_key(None); + } +} + +fn get_selected_index(accounts: &[UserAccount], keystore: &KeyStorageType) -> Option<usize> { + match keystore.get_selected_key() { + KeyStorageResponse::ReceivedResult(Ok(Some(pubkey))) => { + return accounts.iter().position(|account| account.pubkey == pubkey); + } + + KeyStorageResponse::ReceivedResult(Err(e)) => error!("Error getting selected key: {}", e), + KeyStorageResponse::Waiting | KeyStorageResponse::ReceivedResult(Ok(None)) => {} + }; + + None +} + +pub fn process_login_view_response( + manager: &mut Accounts, + response: AccountLoginResponse, +) -> SingleUnkIdAction { + let r = match response { + AccountLoginResponse::CreateNew => { + manager.add_account(FullKeypair::generate().to_keypair()) + } + AccountLoginResponse::LoginWith(keypair) => manager.add_account(keypair), + }; + manager.select_account(manager.num_accounts() - 1); + r +} diff --git a/src/accounts/route.rs b/src/accounts/route.rs @@ -0,0 +1,13 @@ +use super::{AccountLoginResponse, AccountsViewResponse}; +use serde::{Deserialize, Serialize}; + +pub enum AccountsRouteResponse { + Accounts(AccountsViewResponse), + AddAccount(AccountLoginResponse), +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] +pub enum AccountsRoute { + Accounts, + AddAccount, +} diff --git a/src/app.rs b/src/app.rs @@ -1,10 +1,10 @@ use crate::{ - account_manager::AccountManager, + accounts::{Accounts, AccountsRoute}, app_creation::setup_cc, app_size_handler::AppSizeHandler, app_style::user_requested_visuals_change, args::Args, - column::Columns, + column::{Column, Columns}, draft::Drafts, filter::FilterState, frame_history::FrameHistory, @@ -13,6 +13,7 @@ use crate::{ notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, + route::Route, storage::{self, DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageType}, subscriptions::{SubKind, Subscriptions}, support::Support, @@ -57,7 +58,7 @@ pub struct Damus { pub threads: NotesHolderStorage<Thread>, pub profiles: NotesHolderStorage<Profile>, pub img_cache: ImageCache, - pub accounts: AccountManager, + pub accounts: Accounts, pub subscriptions: Subscriptions, pub app_rect_handler: AppSizeHandler, pub support: Support, @@ -415,7 +416,7 @@ impl Damus { KeyStorageType::None }; - let mut accounts = AccountManager::new(keystore); + let mut accounts = Accounts::new(keystore); let num_keys = parsed_args.keys.len(); @@ -489,7 +490,9 @@ impl Damus { let debug = parsed_args.debug; if columns.columns().is_empty() { - columns.new_column_picker(); + columns.add_column(Column::new(vec![Route::Accounts( + AccountsRoute::AddAccount, + )])); } let app_rect_handler = AppSizeHandler::new(&path); @@ -535,11 +538,11 @@ impl Damus { &mut self.img_cache } - pub fn accounts(&self) -> &AccountManager { + pub fn accounts(&self) -> &Accounts { &self.accounts } - pub fn accounts_mut(&mut self) -> &mut AccountManager { + pub fn accounts_mut(&mut self) -> &mut Accounts { &mut self.accounts } @@ -603,7 +606,7 @@ impl Damus { &config, ) .expect("ndb"), - accounts: AccountManager::new(KeyStorageType::None), + accounts: Accounts::new(KeyStorageType::None), frame_history: FrameHistory::default(), view_state: ViewState::default(), diff --git a/src/lib.rs b/src/lib.rs @@ -4,7 +4,7 @@ mod error; //mod note; //mod block; mod abbrev; -pub mod account_manager; +pub mod accounts; mod actionbar; pub mod app_creation; mod app_size_handler; diff --git a/src/nav.rs b/src/nav.rs @@ -1,5 +1,5 @@ use crate::{ - account_manager::render_accounts_route, + accounts::render_accounts_route, app_style::{get_font_size, NotedeckTextStyle}, column::Columns, fonts::NamedFontFamily, diff --git a/src/route.rs b/src/route.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{self}; use crate::{ - account_manager::AccountsRoute, + accounts::AccountsRoute, column::Columns, timeline::{TimelineId, TimelineRoute}, ui::{ diff --git a/src/timeline/route.rs b/src/timeline/route.rs @@ -1,5 +1,5 @@ use crate::{ - account_manager::AccountManager, + accounts::Accounts, column::Columns, draft::Drafts, imgcache::ImageCache, @@ -51,7 +51,7 @@ pub fn render_timeline_route( unknown_ids: &mut UnknownIds, note_cache: &mut NoteCache, threads: &mut NotesHolderStorage<Thread>, - accounts: &mut AccountManager, + accounts: &mut Accounts, route: TimelineRoute, col: usize, textmode: bool, diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs @@ -1,257 +0,0 @@ -use crate::colors::PINK; -use crate::imgcache::ImageCache; -use crate::{ - account_manager::AccountManager, - route::{Route, Router}, - ui::{Preview, PreviewConfig, View}, - Damus, -}; -use egui::{Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, Vec2}; -use nostrdb::{Ndb, Transaction}; - -use super::profile::preview::SimpleProfilePreview; - -pub struct AccountsView<'a> { - ndb: &'a Ndb, - accounts: &'a AccountManager, - img_cache: &'a mut ImageCache, -} - -#[derive(Clone, Debug)] -pub enum AccountsViewResponse { - SelectAccount(usize), - RemoveAccount(usize), - RouteToLogin, -} - -#[derive(Debug)] -enum ProfilePreviewOp { - RemoveAccount, - SwitchTo, -} - -impl<'a> AccountsView<'a> { - pub fn new(ndb: &'a Ndb, accounts: &'a AccountManager, img_cache: &'a mut ImageCache) -> Self { - AccountsView { - ndb, - accounts, - img_cache, - } - } - - pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { - Frame::none().outer_margin(12.0).show(ui, |ui| { - if let Some(resp) = Self::top_section_buttons_widget(ui).inner { - return Some(resp); - } - - ui.add_space(8.0); - scroll_area() - .show(ui, |ui| { - Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) - }) - .inner - }) - } - - fn show_accounts( - ui: &mut Ui, - account_manager: &AccountManager, - ndb: &Ndb, - img_cache: &mut ImageCache, - ) -> Option<AccountsViewResponse> { - let mut return_op: Option<AccountsViewResponse> = None; - ui.allocate_ui_with_layout( - Vec2::new(ui.available_size_before_wrap().x, 32.0), - Layout::top_down(egui::Align::Min), - |ui| { - let txn = if let Ok(txn) = Transaction::new(ndb) { - txn - } else { - return; - }; - - for i in 0..account_manager.num_accounts() { - let account_pubkey = account_manager - .get_account(i) - .map(|account| account.pubkey.bytes()); - - let account_pubkey = if let Some(pubkey) = account_pubkey { - pubkey - } else { - continue; - }; - - let profile = ndb.get_profile_by_pubkey(&txn, account_pubkey).ok(); - let is_selected = - if let Some(selected) = account_manager.get_selected_account_index() { - i == selected - } else { - false - }; - - let profile_peview_view = { - let width = ui.available_width(); - let preview = SimpleProfilePreview::new(profile.as_ref(), img_cache); - show_profile_card(ui, preview, width, is_selected) - }; - - if let Some(op) = profile_peview_view { - return_op = Some(match op { - ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i), - ProfilePreviewOp::RemoveAccount => { - AccountsViewResponse::RemoveAccount(i) - } - }); - } - } - }, - ); - return_op - } - - fn top_section_buttons_widget( - ui: &mut egui::Ui, - ) -> InnerResponse<Option<AccountsViewResponse>> { - 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() { - Some(AccountsViewResponse::RouteToLogin) - } else { - None - } - }, - ) - } -} - -fn show_profile_card( - ui: &mut egui::Ui, - preview: SimpleProfilePreview, - width: f32, - is_selected: bool, -) -> Option<ProfilePreviewOp> { - 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 scroll_area() -> ScrollArea { - egui::ScrollArea::vertical() - .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) -} - -fn add_account_button() -> Button<'static> { - 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 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)); - - egui::Button::image_and_text( - img, - RichText::new("Sign out").color(ui.visuals().noninteractive().fg_stroke.color), - ) - .frame(false) -} - -fn switch_button(dark_mode: bool) -> egui::Button<'static> { - let _ = dark_mode; - - egui::Button::new("Switch").min_size(Vec2::new(76.0, 32.0)) -} - -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 - } -} - -// PREVIEWS -mod preview { - - use super::*; - use crate::{account_manager::process_accounts_view_response, test_data}; - - pub struct AccountsPreview { - app: Damus, - router: Router<Route>, - } - - impl AccountsPreview { - fn new() -> Self { - let app = test_data::test_app(); - let router = Router::new(vec![Route::accounts()]); - - AccountsPreview { app, router } - } - } - - impl View for AccountsPreview { - fn ui(&mut self, ui: &mut egui::Ui) { - ui.add_space(24.0); - // TODO(jb55): maybe just use render_nav here so we can step through routes - if let Some(response) = - AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) - .ui(ui) - .inner - { - process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router); - } - } - } - - impl<'a> Preview for AccountsView<'a> { - type Prev = AccountsPreview; - - fn preview(_cfg: PreviewConfig) -> Self::Prev { - AccountsPreview::new() - } - } -} diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs @@ -0,0 +1,257 @@ +use crate::colors::PINK; +use crate::imgcache::ImageCache; +use crate::{ + accounts::Accounts, + route::{Route, Router}, + ui::{Preview, PreviewConfig, View}, + Damus, +}; +use egui::{Align, Button, Frame, Image, InnerResponse, Layout, RichText, ScrollArea, Ui, Vec2}; +use nostrdb::{Ndb, Transaction}; + +use super::profile::preview::SimpleProfilePreview; + +pub struct AccountsView<'a> { + ndb: &'a Ndb, + accounts: &'a Accounts, + img_cache: &'a mut ImageCache, +} + +#[derive(Clone, Debug)] +pub enum AccountsViewResponse { + SelectAccount(usize), + RemoveAccount(usize), + RouteToLogin, +} + +#[derive(Debug)] +enum ProfilePreviewOp { + RemoveAccount, + SwitchTo, +} + +impl<'a> AccountsView<'a> { + pub fn new(ndb: &'a Ndb, accounts: &'a Accounts, img_cache: &'a mut ImageCache) -> Self { + AccountsView { + ndb, + accounts, + img_cache, + } + } + + pub fn ui(&mut self, ui: &mut Ui) -> InnerResponse<Option<AccountsViewResponse>> { + Frame::none().outer_margin(12.0).show(ui, |ui| { + if let Some(resp) = Self::top_section_buttons_widget(ui).inner { + return Some(resp); + } + + ui.add_space(8.0); + scroll_area() + .show(ui, |ui| { + Self::show_accounts(ui, self.accounts, self.ndb, self.img_cache) + }) + .inner + }) + } + + fn show_accounts( + ui: &mut Ui, + accounts: &Accounts, + ndb: &Ndb, + img_cache: &mut ImageCache, + ) -> Option<AccountsViewResponse> { + let mut return_op: Option<AccountsViewResponse> = None; + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::top_down(egui::Align::Min), + |ui| { + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + return; + }; + + for i in 0..accounts.num_accounts() { + let account_pubkey = accounts + .get_account(i) + .map(|account| account.pubkey.bytes()); + + let account_pubkey = if let Some(pubkey) = account_pubkey { + pubkey + } else { + continue; + }; + + let profile = ndb.get_profile_by_pubkey(&txn, account_pubkey).ok(); + let is_selected = if let Some(selected) = accounts.get_selected_account_index() + { + i == selected + } else { + false + }; + + let profile_peview_view = { + let width = ui.available_width(); + let preview = SimpleProfilePreview::new(profile.as_ref(), img_cache); + show_profile_card(ui, preview, width, is_selected) + }; + + if let Some(op) = profile_peview_view { + return_op = Some(match op { + ProfilePreviewOp::SwitchTo => AccountsViewResponse::SelectAccount(i), + ProfilePreviewOp::RemoveAccount => { + AccountsViewResponse::RemoveAccount(i) + } + }); + } + } + }, + ); + return_op + } + + fn top_section_buttons_widget( + ui: &mut egui::Ui, + ) -> InnerResponse<Option<AccountsViewResponse>> { + 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() { + Some(AccountsViewResponse::RouteToLogin) + } else { + None + } + }, + ) + } +} + +fn show_profile_card( + ui: &mut egui::Ui, + preview: SimpleProfilePreview, + width: f32, + is_selected: bool, +) -> Option<ProfilePreviewOp> { + 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 scroll_area() -> ScrollArea { + egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) +} + +fn add_account_button() -> Button<'static> { + 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 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)); + + egui::Button::image_and_text( + img, + RichText::new("Sign out").color(ui.visuals().noninteractive().fg_stroke.color), + ) + .frame(false) +} + +fn switch_button(dark_mode: bool) -> egui::Button<'static> { + let _ = dark_mode; + + egui::Button::new("Switch").min_size(Vec2::new(76.0, 32.0)) +} + +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 + } +} + +// PREVIEWS +mod preview { + + use super::*; + use crate::{accounts::process_accounts_view_response, test_data}; + + pub struct AccountsPreview { + app: Damus, + router: Router<Route>, + } + + impl AccountsPreview { + fn new() -> Self { + let app = test_data::test_app(); + let router = Router::new(vec![Route::accounts()]); + + AccountsPreview { app, router } + } + } + + impl View for AccountsPreview { + fn ui(&mut self, ui: &mut egui::Ui) { + ui.add_space(24.0); + // TODO(jb55): maybe just use render_nav here so we can step through routes + if let Some(response) = + AccountsView::new(&self.app.ndb, &self.app.accounts, &mut self.app.img_cache) + .ui(ui) + .inner + { + process_accounts_view_response(self.app.accounts_mut(), response, &mut self.router); + } + } + } + + impl<'a> Preview for AccountsView<'a> { + type Prev = AccountsPreview; + + fn preview(_cfg: PreviewConfig) -> Self::Prev { + AccountsPreview::new() + } + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs @@ -1,5 +1,5 @@ pub mod account_login_view; -pub mod account_management; +pub mod accounts; pub mod add_column; pub mod anim; pub mod mention; @@ -13,7 +13,7 @@ pub mod thread; pub mod timeline; pub mod username; -pub use account_management::AccountsView; +pub use accounts::AccountsView; pub use mention::Mention; pub use note::{NoteResponse, NoteView, PostReplyView, PostView}; pub use preview::{Preview, PreviewApp, PreviewConfig}; diff --git a/src/ui/side_panel.rs b/src/ui/side_panel.rs @@ -2,7 +2,7 @@ use egui::{vec2, Color32, InnerResponse, Layout, Margin, Separator, Stroke, Widg use tracing::info; use crate::{ - account_manager::AccountsRoute, + accounts::AccountsRoute, colors, column::{Column, Columns}, imgcache::ImageCache, diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs @@ -1,7 +1,7 @@ use notedeck::ui::{ - account_login_view::AccountLoginView, account_management::AccountsView, - add_column::AddColumnView, DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, - ProfilePic, ProfilePreview, RelayView, + account_login_view::AccountLoginView, accounts::AccountsView, add_column::AddColumnView, + DesktopSidePanel, PostView, Preview, PreviewApp, PreviewConfig, ProfilePic, ProfilePreview, + RelayView, }; use notedeck::{ app_creation::{generate_mobile_emulator_native_options, generate_native_options, setup_cc},