commit e9c35960675f22006d2e2c670e4f7a0ddaaf6a26
parent 93800e0d0472c678e418aaf8ff45d2ba21507d92
Author: kernelkind <kernelkind@gmail.com>
Date: Thu, 9 May 2024 15:21:02 -0400
AccountManagementView
View used to add and remove accounts from the app
Signed-off-by: kernelkind <kernelkind@gmail.com>
Signed-off-by: William Casarin <jb55@jb55.com>
Diffstat:
9 files changed, 555 insertions(+), 3 deletions(-)
diff --git a/src/account_manager.rs b/src/account_manager.rs
@@ -0,0 +1,107 @@
+use nostr_sdk::Keys;
+use nostrdb::{Ndb, Transaction};
+
+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.public_key().to_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
+ }
+}
+
+/// 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>,
+ 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 {
+ AccountManager {
+ accounts,
+ key_store,
+ relay_generator,
+ }
+ }
+
+ pub fn get_accounts(&'a self) -> &'a Vec<UserAccount> {
+ self.accounts
+ }
+
+ pub fn get_account(&'a self, index: usize) -> Option<&'a UserAccount> {
+ self.accounts.get(index)
+ }
+
+ pub fn remove_account(&mut self, index: usize) {
+ if let Some(account) = self.accounts.get(index) {
+ self.key_store.remove_key(&account.key);
+ }
+ if index < self.accounts.len() {
+ self.accounts.remove(index);
+ }
+ }
+
+ pub fn add_account(&'a mut self, key: Keys, ctx: &egui::Context) {
+ self.key_store.add_key(&key);
+ let relays = self.relay_generator.generate_relays_for(&key, ctx);
+ let account = UserAccount { key, relays };
+
+ self.accounts.push(account)
+ }
+
+ pub fn num_accounts(&self) -> usize {
+ self.accounts.len()
+ }
+}
diff --git a/src/key_storage.rs b/src/key_storage.rs
@@ -0,0 +1,48 @@
+use nostr_sdk::Keys;
+
+pub enum KeyStorage {
+ None,
+ // TODO:
+ // Linux,
+ // Windows,
+ // Android,
+}
+
+impl KeyStorage {
+ pub fn get_keys(&self) -> Result<Vec<Keys>, KeyStorageError> {
+ match self {
+ Self::None => Ok(Vec::new()),
+ }
+ }
+
+ pub fn add_key(&self, key: &Keys) -> Result<(), KeyStorageError> {
+ match self {
+ Self::None => Ok(()),
+ }
+ }
+
+ pub fn remove_key(&self, key: &Keys) -> Result<(), KeyStorageError> {
+ match self {
+ Self::None => Ok(()),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum KeyStorageError<'a> {
+ Retrieval,
+ Addition(&'a Keys),
+ Removal(&'a Keys),
+}
+
+impl std::fmt::Display for KeyStorageError<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match self {
+ Self::Retrieval => write!(f, "Failed to retrieve keys."),
+ Self::Addition(key) => write!(f, "Failed to add key: {:?}", key.public_key()),
+ Self::Removal(key) => write!(f, "Failed to remove key: {:?}", key.public_key()),
+ }
+ }
+}
+
+impl std::error::Error for KeyStorageError<'_> {}
diff --git a/src/lib.rs b/src/lib.rs
@@ -4,6 +4,7 @@ mod error;
//mod note;
//mod block;
mod abbrev;
+pub mod account_manager;
pub mod app_creation;
mod app_style;
mod colors;
@@ -13,9 +14,11 @@ mod frame_history;
mod images;
mod imgcache;
mod key_parsing;
+mod key_storage;
pub mod login_manager;
mod notecache;
mod profile;
+mod relay_generation;
pub mod relay_pool_manager;
mod result;
mod test_data;
@@ -23,6 +26,7 @@ mod time;
mod timecache;
mod timeline;
pub mod ui;
+mod user_account;
#[cfg(test)]
#[macro_use]
diff --git a/src/relay_generation.rs b/src/relay_generation.rs
@@ -0,0 +1,54 @@
+use crate::relay_pool_manager::create_wakeup;
+use enostr::RelayPool;
+use nostr_sdk::Keys;
+use tracing::error;
+
+pub enum RelayGenerator {
+ GossipModel,
+ Nip65,
+ Constant,
+}
+
+impl RelayGenerator {
+ pub fn generate_relays_for(&self, key: &Keys, 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: &Keys, ctx: &egui::Context) -> RelayPool {
+ todo!()
+}
+
+fn generate_relays_nip65(key: &Keys, ctx: &egui::Context) -> RelayPool {
+ todo!()
+}
+
+fn generate_constant_relays(ctx: &egui::Context) -> 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)
+ }
+ if let Err(e) = pool.add_url("wss://relay.damus.io".to_string(), wakeup.clone()) {
+ error!("{:?}", e)
+ }
+ if let Err(e) = pool.add_url("wss://pyramid.fiatjaf.com".to_string(), wakeup.clone()) {
+ error!("{:?}", e)
+ }
+ if let Err(e) = pool.add_url("wss://nos.lol".to_string(), wakeup.clone()) {
+ error!("{:?}", e)
+ }
+ if let Err(e) = pool.add_url("wss://nostr.wine".to_string(), wakeup.clone()) {
+ error!("{:?}", e)
+ }
+ if let Err(e) = pool.add_url("wss://purplepag.es".to_string(), wakeup) {
+ error!("{:?}", e)
+ }
+
+ pool
+}
diff --git a/src/relay_pool_manager.rs b/src/relay_pool_manager.rs
@@ -46,7 +46,7 @@ impl<'a> RelayPoolManager<'a> {
}
}
-fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
+pub fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static {
let ctx = ctx.clone();
move || {
ctx.request_repaint();
diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs
@@ -0,0 +1,328 @@
+use egui::{Align, Align2, Button, Frame, Layout, Margin, RichText, ScrollArea, Vec2, Window};
+
+use crate::{
+ account_manager::{AccountManager, SimpleProfilePreviewController},
+ app_style::NotedeckTextStyle,
+ ui::{self, Preview, View},
+};
+
+pub struct AccountManagementView<'a> {
+ account_manager: AccountManager<'a>,
+ simple_preview_controller: SimpleProfilePreviewController<'a>,
+ edit_mode: &'a mut bool,
+}
+
+impl<'a> View for AccountManagementView<'a> {
+ fn ui(&mut self, ui: &mut egui::Ui) {
+ if ui::is_mobile(ui.ctx()) {
+ self.show_mobile(ui);
+ } else {
+ self.show(ui);
+ }
+ }
+}
+
+impl<'a> AccountManagementView<'a> {
+ pub fn new(
+ account_manager: AccountManager<'a>,
+ 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);
+ 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,
+ *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
+ },
+ );
+
+ 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>>) {
+ if let Some(to_remove) = account_indices {
+ to_remove
+ .iter()
+ .for_each(|index| self.account_manager.remove_account(*index));
+ }
+ }
+
+ 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());
+ ui.add_space(8.0);
+ 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;
+ }
+ },
+ );
+
+ 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
+ }
+ }
+}
+
+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 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 scroll_area() -> ScrollArea {
+ egui::ScrollArea::vertical()
+ .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
+ .auto_shrink([false; 2])
+}
+
+fn add_account_button() -> Button<'static> {
+ Button::new("Add Account").min_size(Vec2::new(0.0, 32.0))
+}
+
+fn edit_account_button() -> Button<'static> {
+ Button::new("Edit").min_size(Vec2::new(0.0, 32.0))
+}
+
+fn done_account_button() -> Button<'static> {
+ Button::new("Done").min_size(Vec2::new(0.0, 32.0))
+}
+
+fn delete_button(_dark_mode: bool) -> egui::Button<'static> {
+ let img_data = egui::include_image!("../../assets/icons/delete_icon_4x.png");
+
+ egui::Button::image(egui::Image::new(img_data).max_width(30.0)).frame(true)
+}
+
+// PREVIEWS
+
+mod preview {
+ use nostr_sdk::{Keys, PublicKey};
+ use nostrdb::{Config, Ndb};
+
+ use super::*;
+ use crate::key_storage::KeyStorage;
+ use crate::relay_generation::RelayGenerator;
+ use crate::{account_manager::UserAccount, imgcache::ImageCache, test_data};
+ use std::path::Path;
+
+ pub struct AccountManagementPreview {
+ accounts: Vec<UserAccount>,
+ ndb: Ndb,
+ img_cache: ImageCache,
+ edit_mode: bool,
+ }
+
+ impl AccountManagementPreview {
+ fn new() -> Self {
+ let account_hexes = [
+ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91",
+ "5c10ed0678805156d39ef1ef6d46110fe1e7e590ae04986ccf48ba1299cb53e2",
+ "4c96d763eb2fe01910f7e7220b7c7ecdbe1a70057f344b9f79c28af080c3ee30",
+ "edf16b1dd61eab353a83af470cc13557029bff6827b4cb9b7fc9bdb632a2b8e6",
+ "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
+ ];
+
+ let accounts: Vec<UserAccount> = account_hexes
+ .iter()
+ .map(|account_hex| {
+ let key = Keys::from_public_key(PublicKey::from_hex(account_hex).unwrap());
+
+ UserAccount {
+ key,
+ relays: test_data::sample_pool(),
+ }
+ })
+ .collect();
+
+ 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);
+
+ AccountManagementPreview {
+ accounts,
+ 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,
+ );
+
+ AccountManagementView::new(
+ account_manager,
+ SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache),
+ &mut self.edit_mode,
+ )
+ .ui(ui);
+ }
+ }
+
+ impl<'a> Preview for AccountManagementView<'a> {
+ type Prev = AccountManagementPreview;
+
+ fn preview() -> Self::Prev {
+ AccountManagementPreview::new()
+ }
+ }
+}
diff --git a/src/ui/mod.rs b/src/ui/mod.rs
@@ -1,4 +1,5 @@
pub mod account_login_view;
+pub mod account_management;
pub mod anim;
pub mod mention;
pub mod note;
@@ -7,6 +8,7 @@ pub mod profile;
pub mod relay;
pub mod username;
+pub use account_management::AccountManagementView;
pub use mention::Mention;
pub use note::Note;
pub use preview::{Preview, PreviewApp};
diff --git a/src/ui_preview/main.rs b/src/ui_preview/main.rs
@@ -2,7 +2,9 @@ use notedeck::app_creation::{
generate_mobile_emulator_native_options, generate_native_options, setup_cc,
};
use notedeck::ui::account_login_view::AccountLoginView;
-use notedeck::ui::{Preview, PreviewApp, ProfilePic, ProfilePreview, RelayView};
+use notedeck::ui::{
+ AccountManagementView, Preview, PreviewApp, ProfilePic, ProfilePreview, RelayView,
+};
use std::env;
struct PreviewRunner {
@@ -82,6 +84,7 @@ async fn main() {
RelayView,
AccountLoginView,
ProfilePreview,
- ProfilePic
+ ProfilePic,
+ AccountManagementView,
);
}
diff --git a/src/user_account.rs b/src/user_account.rs
@@ -0,0 +1,6 @@
+use enostr::RelayPool;
+
+pub struct UserAccount {
+ pub key: nostr_sdk::Keys,
+ pub relays: RelayPool,
+}