Merge pull request #7 from aljazceru/claude/debug-notedeck-startup-015gdey128gdKyUWDTo5X3Xd

Claude/debug notedeck startup 015gdey128gd ky uwd to5 x3 xd
This commit is contained in:
2025-11-15 11:41:56 +01:00
committed by GitHub
10 changed files with 230 additions and 184 deletions

53
STARTUP_CONFIG.md Normal file
View File

@@ -0,0 +1,53 @@
# Startup Configuration
Notedeck can be configured to automatically connect to a relay and load your account on startup using a configuration file.
## Configuration File Location
Create a file named `startup_config.json` in:
- **Linux**: `~/.local/share/notedeck/settings/startup_config.json`
- **macOS**: `~/Library/Application Support/notedeck/settings/startup_config.json`
- **Windows**: `%APPDATA%\notedeck\settings\startup_config.json`
## Configuration Format
The configuration file is a JSON file with the following format:
```json
{
"relay": "wss://relay.damus.io",
"nsec": "nsec1your_private_key_here"
}
```
### Fields
- **`relay`** (optional): The WebSocket URL of the relay you want to connect to
- Example: `"wss://relay.damus.io"`
- If not specified, the application will use default relays
- **`nsec`** (optional): Your Nostr private key in nsec format
- Example: `"nsec1..."`
- This will be used to automatically create your account on startup
- **Keep this file secure!** Your nsec is your private key and should never be shared
## Example Configuration
See `startup_config.json.example` in the root directory for a template.
## Security Notes
- **Never share your nsec** with anyone
- **Keep your startup_config.json file secure** with appropriate file permissions
- **Back up your nsec** in a safe location
- On Linux/macOS, you can set secure permissions with:
```bash
chmod 600 ~/.local/share/notedeck/settings/startup_config.json
```
## Notes
- Both fields are optional - you can specify just the relay, just the nsec, or both
- If the configuration file doesn't exist, the application will start with default settings
- The startup configuration is loaded once during application startup
- Changes to the file require restarting the application to take effect

View File

@@ -411,6 +411,7 @@ impl UrlMimes {
+ Duration::from_secs(253_402_300_799 / 2), // never expire... + Duration::from_secs(253_402_300_799 / 2), // never expire...
}, },
); );
return None;
} }
let (sender, promise) = Promise::new(); let (sender, promise) = Promise::new();
ehttp_get_mime_type(url, sender); ehttp_get_mime_type(url, sender);

View File

@@ -747,6 +747,45 @@ impl Damus {
); );
} }
// Load startup config (if exists)
if let Some(startup_config) = crate::storage::load_startup_config(app_context.path) {
info!("StartupConfig: loaded from disk");
// Add account from nsec if provided
if let Some(nsec) = &startup_config.nsec {
use enostr::SecretKey;
use std::str::FromStr;
match SecretKey::from_str(nsec.as_str()) {
Ok(secret_key) => {
let keypair = enostr::Keypair::from_secret(secret_key);
info!("StartupConfig: Adding account from nsec: {}", keypair.pubkey);
if let Some(resp) = app_context.accounts.add_account(keypair) {
let txn = nostrdb::Transaction::new(app_context.ndb).expect("txn");
resp.unk_id_action.process_action(
app_context.unknown_ids,
app_context.ndb,
&txn,
);
}
}
Err(e) => {
error!("StartupConfig: Failed to parse nsec: {}", e);
}
}
}
// Add relay from startup config if provided
if let Some(relay_url) = &startup_config.relay {
let wakeup = || {}; // Wakeup closure for relay events
if let Err(e) = app_context.pool.add_url(relay_url.clone(), wakeup) {
error!("StartupConfig: Failed to add relay {}: {}", relay_url, e);
} else {
info!("StartupConfig: Added relay from config: {}", relay_url);
}
}
}
// Load or create relay config // Load or create relay config
let relay_config = if let Some(relay_config) = crate::storage::load_relay_config( let relay_config = if let Some(relay_config) = crate::storage::load_relay_config(
app_context.path, app_context.path,
@@ -1176,12 +1215,8 @@ fn timelines_view(
// Desktop Side Panel // Desktop Side Panel
strip.cell(|ui| { strip.cell(|ui| {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
let side_panel = DesktopSidePanel::new( let side_panel =
ctx.accounts.get_selected_account(), DesktopSidePanel::new(ctx.accounts.get_selected_account()).show(ui);
&app.decks_cache,
ctx.i18n,
)
.show(ui);
if let Some(side_panel) = side_panel { if let Some(side_panel) = side_panel {
if side_panel.response.clicked() || side_panel.response.secondary_clicked() { if side_panel.response.clicked() || side_panel.response.secondary_clicked() {
@@ -1320,12 +1355,8 @@ fn timelines_view(
// Desktop Side Panel // Desktop Side Panel
strip.cell(|ui| { strip.cell(|ui| {
let rect = ui.available_rect_before_wrap(); let rect = ui.available_rect_before_wrap();
let side_panel = DesktopSidePanel::new( let side_panel =
ctx.accounts.get_selected_account(), DesktopSidePanel::new(ctx.accounts.get_selected_account()).show(ui);
&app.decks_cache,
ctx.i18n,
)
.show(ui);
if let Some(side_panel) = side_panel { if let Some(side_panel) = side_panel {
if side_panel.response.clicked() || side_panel.response.secondary_clicked() { if side_panel.response.clicked() || side_panel.response.secondary_clicked() {

View File

@@ -2,8 +2,6 @@ use egui::{FontFamily, FontId};
use notedeck::fonts::NamedFontFamily; use notedeck::fonts::NamedFontFamily;
pub static DECK_ICON_SIZE: f32 = 24.0;
pub fn deck_icon_font_sized(size: f32) -> FontId { pub fn deck_icon_font_sized(size: f32) -> FontId {
egui::FontId::new(size, emoji_font_family()) egui::FontId::new(size, emoji_font_family())
} }

View File

@@ -14,6 +14,7 @@ mod deck_state;
mod decks; mod decks;
mod draft; mod draft;
pub mod relay_config; pub mod relay_config;
pub mod startup_config;
mod key_parsing; mod key_parsing;
pub mod login_manager; pub mod login_manager;
mod media_upload; mod media_upload;

View File

@@ -0,0 +1,58 @@
use serde::{Deserialize, Serialize};
/// Startup configuration for initial relay and account setup
/// This file should be manually created by the user in ~/.local/share/notedeck/settings/startup_config.json
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StartupConfig {
/// Single relay URL to connect to (e.g., "wss://relay.damus.io")
pub relay: Option<String>,
/// Private key in nsec format (e.g., "nsec1...")
/// This will be used to create the user's account
pub nsec: Option<String>,
}
impl StartupConfig {
pub fn new() -> Self {
Self {
relay: None,
nsec: None,
}
}
pub fn with_relay(mut self, relay: String) -> Self {
self.relay = Some(relay);
self
}
pub fn with_nsec(mut self, nsec: String) -> Self {
self.nsec = Some(nsec);
self
}
pub fn is_configured(&self) -> bool {
self.relay.is_some() || self.nsec.is_some()
}
}
impl Default for StartupConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_startup_config() {
let config = StartupConfig::new()
.with_relay("wss://relay.damus.io".to_string())
.with_nsec("nsec1test".to_string());
assert!(config.is_configured());
assert_eq!(config.relay, Some("wss://relay.damus.io".to_string()));
assert_eq!(config.nsec, Some("nsec1test".to_string()));
}
}

View File

@@ -1,7 +1,9 @@
mod channels; mod channels;
mod decks; mod decks;
mod relay_config; mod relay_config;
mod startup_config;
pub use channels::{load_channels_cache, save_channels_cache, CHANNELS_CACHE_FILE}; pub use channels::{load_channels_cache, save_channels_cache, CHANNELS_CACHE_FILE};
pub use decks::{load_decks_cache, save_decks_cache, DECKS_CACHE_FILE}; pub use decks::{load_decks_cache, save_decks_cache, DECKS_CACHE_FILE};
pub use relay_config::{load_relay_config, save_relay_config, RELAY_CONFIG_FILE}; pub use relay_config::{load_relay_config, save_relay_config, RELAY_CONFIG_FILE};
pub use startup_config::{load_startup_config, save_startup_config, STARTUP_CONFIG_FILE};

View File

@@ -0,0 +1,60 @@
use notedeck::{storage, DataPath, DataPathType, Directory};
use tracing::{debug, error, info};
use crate::startup_config::StartupConfig;
pub static STARTUP_CONFIG_FILE: &str = "startup_config.json";
/// Load startup configuration from disk
/// Returns None if file doesn't exist or can't be parsed
pub fn load_startup_config(path: &DataPath) -> Option<StartupConfig> {
let data_path = path.path(DataPathType::Setting);
let config_str = match Directory::new(data_path).get_file(STARTUP_CONFIG_FILE.to_owned()) {
Ok(s) => s,
Err(e) => {
debug!(
"No startup config file found at {}: {}. This is normal for first-time setup.",
STARTUP_CONFIG_FILE, e
);
return None;
}
};
match serde_json::from_str::<StartupConfig>(&config_str) {
Ok(config) => {
info!("Loaded startup configuration from {}", STARTUP_CONFIG_FILE);
Some(config)
}
Err(e) => {
error!("Could not parse startup config: {}", e);
None
}
}
}
/// Save startup configuration to disk (optional - mainly for reference)
pub fn save_startup_config(path: &DataPath, config: &StartupConfig) {
let serialized_config = match serde_json::to_string_pretty(config) {
Ok(s) => s,
Err(e) => {
error!("Could not serialize startup config: {}", e);
return;
}
};
let data_path = path.path(DataPathType::Setting);
if let Err(e) = storage::write_file(
&data_path,
STARTUP_CONFIG_FILE.to_string(),
&serialized_config,
) {
error!(
"Could not write startup config to file {}: {}",
STARTUP_CONFIG_FILE, e
);
} else {
debug!("Successfully wrote startup config to {}", STARTUP_CONFIG_FILE);
}
}

View File

@@ -1,33 +1,22 @@
use egui::{ use egui::{vec2, CursorIcon, InnerResponse, Layout, Margin, Separator, Stroke, Widget};
vec2, CursorIcon, InnerResponse, Layout, Margin, RichText, ScrollArea, Separator, Stroke, use tracing::info;
Widget,
};
use tracing::{error, info};
use crate::{ use crate::{
accounts::AccountsRoute, accounts::AccountsRoute, app::get_active_columns_mut, decks::DecksCache,
app::{get_active_columns_mut, get_decks_mut}, nav::SwitchingAction, route::Route,
app_style::DECK_ICON_SIZE,
decks::{DecksAction, DecksCache},
nav::SwitchingAction,
route::Route,
}; };
use notedeck::{tr, Accounts, Localization, UserAccount}; use notedeck::{Accounts, Localization, UserAccount};
use notedeck_ui::{ use notedeck_ui::{
anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE}, anim::{AnimationHelper, ICON_EXPANSION_MULTIPLE},
app_images, colors, View, app_images, colors, View,
}; };
use super::configure_deck::deck_icon;
pub static SIDE_PANEL_WIDTH: f32 = 68.0; pub static SIDE_PANEL_WIDTH: f32 = 68.0;
static ICON_WIDTH: f32 = 40.0; static ICON_WIDTH: f32 = 40.0;
pub struct DesktopSidePanel<'a> { pub struct DesktopSidePanel<'a> {
selected_account: &'a UserAccount, selected_account: &'a UserAccount,
decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
} }
impl View for DesktopSidePanel<'_> { impl View for DesktopSidePanel<'_> {
@@ -42,9 +31,6 @@ pub enum SidePanelAction {
ComposeNote, ComposeNote,
Search, Search,
ExpandSidePanel, ExpandSidePanel,
NewDeck,
SwitchDeck(usize),
EditDeck(usize),
Wallet, Wallet,
Account, // Use existing Account instead of UserAccount Account, // Use existing Account instead of UserAccount
Settings, Settings,
@@ -62,16 +48,8 @@ impl SidePanelResponse {
} }
impl<'a> DesktopSidePanel<'a> { impl<'a> DesktopSidePanel<'a> {
pub fn new( pub fn new(selected_account: &'a UserAccount) -> Self {
selected_account: &'a UserAccount, Self { selected_account }
decks_cache: &'a DecksCache,
i18n: &'a mut Localization,
) -> Self {
Self {
selected_account,
decks_cache,
i18n,
}
} }
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> { pub fn show(&mut self, ui: &mut egui::Ui) -> Option<SidePanelResponse> {
@@ -124,28 +102,6 @@ impl<'a> DesktopSidePanel<'a> {
let search_resp = ui.add(search_button()); let search_resp = ui.add(search_button());
let column_resp = ui.add(add_column_button()); let column_resp = ui.add(add_column_button());
ui.add(Separator::default().horizontal().spacing(8.0).shrink(4.0));
ui.add_space(8.0);
ui.add(egui::Label::new(
RichText::new(tr!(
self.i18n,
"DECKS",
"Label for decks section in side panel"
))
.size(11.0)
.color(ui.visuals().noninteractive().fg_stroke.color),
));
ui.add_space(8.0);
let add_deck_resp = ui.add(add_deck_button(self.i18n));
let decks_inner = ScrollArea::vertical()
.max_height(ui.available_height() - (3.0 * (ICON_WIDTH + 12.0)))
.show(ui, |ui| {
show_decks(ui, self.decks_cache, self.selected_account)
})
.inner;
/* /*
if expand_resp.clicked() { if expand_resp.clicked() {
Some(InnerResponse::new( Some(InnerResponse::new(
@@ -174,27 +130,6 @@ impl<'a> DesktopSidePanel<'a> {
Some(InnerResponse::new(SidePanelAction::Search, search_resp)) Some(InnerResponse::new(SidePanelAction::Search, search_resp))
} else if column_resp.clicked() { } else if column_resp.clicked() {
Some(InnerResponse::new(SidePanelAction::Columns, column_resp)) Some(InnerResponse::new(SidePanelAction::Columns, column_resp))
} else if add_deck_resp.clicked() {
Some(InnerResponse::new(SidePanelAction::NewDeck, add_deck_resp))
} else if decks_inner.response.secondary_clicked() {
info!("decks inner secondary click");
if let Some(clicked_index) = decks_inner.inner {
Some(InnerResponse::new(
SidePanelAction::EditDeck(clicked_index),
decks_inner.response,
))
} else {
None
}
} else if decks_inner.response.clicked() {
if let Some(clicked_index) = decks_inner.inner {
Some(InnerResponse::new(
SidePanelAction::SwitchDeck(clicked_index),
decks_inner.response,
))
} else {
None
}
} else { } else {
None None
} }
@@ -217,7 +152,7 @@ impl<'a> DesktopSidePanel<'a> {
i18n: &mut Localization, i18n: &mut Localization,
) -> Option<SwitchingAction> { ) -> Option<SwitchingAction> {
let router = get_active_columns_mut(i18n, accounts, decks_cache).get_selected_router(); let router = get_active_columns_mut(i18n, accounts, decks_cache).get_selected_router();
let mut switching_response = None; let switching_response = None;
match action { match action {
SidePanelAction::Account => { SidePanelAction::Account => {
if router if router
@@ -273,38 +208,6 @@ impl<'a> DesktopSidePanel<'a> {
// TODO // TODO
info!("Clicked expand side panel button"); info!("Clicked expand side panel button");
} }
SidePanelAction::NewDeck => {
if router.routes().iter().any(|r| r == &Route::NewDeck) {
router.go_back();
} else {
router.route_to(Route::NewDeck);
}
}
SidePanelAction::SwitchDeck(index) => {
switching_response = Some(crate::nav::SwitchingAction::Decks(DecksAction::Switch(
index,
)))
}
SidePanelAction::EditDeck(index) => {
if router.routes().iter().any(|r| r == &Route::EditDeck(index)) {
router.go_back();
} else {
switching_response = Some(crate::nav::SwitchingAction::Decks(
DecksAction::Switch(index),
));
if let Some(edit_deck) = get_decks_mut(i18n, accounts, decks_cache)
.decks_mut()
.get_mut(index)
{
edit_deck
.columns_mut()
.get_selected_router()
.route_to(Route::EditDeck(index));
} else {
error!("Cannot push EditDeck route to index {}", index);
}
}
}
SidePanelAction::Wallet => 's: { SidePanelAction::Wallet => 's: {
if router if router
.routes() .routes()
@@ -399,71 +302,6 @@ pub fn search_button() -> impl Widget {
search_button_impl(colors::MID_GRAY, 1.5) search_button_impl(colors::MID_GRAY, 1.5)
} }
// TODO: convert to responsive button when expanded side panel impl is finished
fn add_deck_button<'a>(i18n: &'a mut Localization) -> impl Widget + 'a {
|ui: &mut egui::Ui| -> egui::Response {
let img_size = 40.0;
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; // max size of the widget
let img = app_images::new_deck_image().max_width(img_size);
let helper = AnimationHelper::new(ui, "new-deck-icon", vec2(max_size, max_size));
let cur_img_size = helper.scale_1d_pos(img_size);
img.paint_at(
ui,
helper
.get_animation_rect()
.shrink((max_size - cur_img_size) / 2.0),
);
helper
.take_animation_response()
.on_hover_cursor(CursorIcon::PointingHand)
.on_hover_text(tr!(
i18n,
"Add new deck",
"Tooltip text for adding a new deck button"
))
}
}
fn show_decks<'a>(
ui: &mut egui::Ui,
decks_cache: &'a DecksCache,
selected_account: &'a UserAccount,
) -> InnerResponse<Option<usize>> {
let show_decks_id = ui.id().with("show-decks");
let account_id = selected_account.key.pubkey;
let (cur_decks, account_id) = (
decks_cache.decks(&account_id),
show_decks_id.with(account_id),
);
let active_index = cur_decks.active_index();
let (_, mut resp) = ui.allocate_exact_size(vec2(0.0, 0.0), egui::Sense::click());
let mut clicked_index = None;
for (index, deck) in cur_decks.decks().iter().enumerate() {
let highlight = index == active_index;
let deck_icon_resp = ui
.add(deck_icon(
account_id.with(index),
Some(deck.icon),
DECK_ICON_SIZE,
40.0,
highlight,
))
.on_hover_text_at_pointer(&deck.name)
.on_hover_cursor(CursorIcon::PointingHand);
if deck_icon_resp.clicked() || deck_icon_resp.secondary_clicked() {
clicked_index = Some(index);
}
resp = resp.union(deck_icon_resp);
}
InnerResponse::new(clicked_index, resp)
}
fn user_account_button(user_account: &UserAccount, dark_mode: bool) -> impl Widget + '_ { fn user_account_button(user_account: &UserAccount, dark_mode: bool) -> impl Widget + '_ {
move |ui: &mut egui::Ui| -> egui::Response { move |ui: &mut egui::Ui| -> egui::Response {
let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE; let max_size = ICON_WIDTH * ICON_EXPANSION_MULTIPLE;

View File

@@ -0,0 +1,4 @@
{
"relay": "wss://relay.damus.io",
"nsec": "nsec1your_private_key_here"
}