diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index ce077450..a850bc7d 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -28,6 +28,14 @@ pub const NIP4E_ROTATION_DEFAULT_DAYS: u32 = 30; /// Built-in image shown behind chat and group panels pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg"; +fn default_onboarded() -> bool { + true +} + +fn default_shadow() -> bool { + true +} + /// The user's appearance preference, persisted with the rest of the settings. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] pub enum AppearanceMode { @@ -78,6 +86,9 @@ pub struct ThemeSettings { pub radius: Option, /// Corner radius for large elements like dialogs; follows the OS theme when unset pub radius_lg: Option, + /// Draw soft shadows on inputs, buttons and dialogs + #[serde(default = "default_shadow")] + pub shadow: bool, } impl Default for ThemeSettings { @@ -87,6 +98,7 @@ impl Default for ThemeSettings { focus_ring: false, radius: None, radius_lg: None, + shadow: true, } } } @@ -145,6 +157,12 @@ pub struct Settings { /// Hide the notice that messages in public groups are not encrypted #[serde(default)] pub hide_group_notice: bool, + /// Enable the NIP-60 wallet + #[serde(default)] + pub wallet: bool, + /// Whether the first-run onboarding has been completed + #[serde(default = "default_onboarded")] + pub onboarded: bool, /// Tuning applied on top of the native OS theme #[serde(default)] pub theme: ThemeSettings, @@ -178,10 +196,12 @@ impl Default for Settings { appearance_mode: AppearanceMode::default(), hide_avatar: false, screening: true, - wot_auto_trust: true, + wot_auto_trust: false, nip4e: false, nip4e_rotation_days: None, hide_group_notice: false, + wallet: false, + onboarded: false, theme: ThemeSettings::default(), theme_light: None, theme_dark: None, @@ -222,6 +242,9 @@ macro_rules! setting_accessors { inner.$field = value; cx.notify(); }); + // Notify the handle too, so observers of AppSettings + // react to the change + cx.notify(); }); } } @@ -257,6 +280,8 @@ setting_accessors! { pub panel_background_path: Option, pub theme_light: Option, pub theme_dark: Option, + pub wallet: bool, + pub onboarded: bool, } setting_getters! { @@ -314,6 +339,8 @@ impl AppSettings { edit(&mut inner.theme); cx.notify(); }); + // Notify the handle too, so observers of AppSettings react + cx.notify(); }); } @@ -326,6 +353,7 @@ impl AppSettings { let styled = Theme::global_mut(cx); styled.font_size = px(tuning.font_size); styled.focus_ring = tuning.focus_ring; + styled.shadow = tuning.shadow; if let Some(radius) = tuning.radius { styled.radius = px(radius); } @@ -582,6 +610,11 @@ fn persisted_content() -> Option { common::web_store::get(SETTINGS_KEY).ok().flatten() } +/// Read the persisted onboarding flag synchronously. +pub fn persisted_onboarded() -> bool { + persisted_settings().is_some_and(|settings| settings.onboarded) +} + pub fn persisted_appearance_mode() -> AppearanceMode { persisted_settings() .map(|settings| settings.appearance_mode) @@ -672,6 +705,40 @@ mod tests { assert_eq!(settings.theme, ThemeSettings::default()); assert_eq!(settings.theme_light, None); assert_eq!(settings.theme_dark, None); + + // Files written before onboarding and the wallet flag existed count + // as onboarded with the wallet off + assert!(settings.onboarded); + assert!(!settings.wallet); + } + + /// Fresh installs start un-onboarded with optional features off, and the + /// flags round-trip through serde + #[test] + fn onboarding_and_wallet_persist() { + let settings = Settings::default(); + assert!(!settings.onboarded); + assert!(!settings.wallet); + assert!(!settings.wot_auto_trust); + assert!(settings.theme.shadow); + + let settings: Settings = serde_json::from_str( + r#"{ + "appearance_mode": "Dark", + "hide_avatar": false, + "screening": true, + "wot_auto_trust": true, + "nip4e": false, + "trusted_relays": [], + "file_server": "https://nostr.download/", + "onboarded": true, + "wallet": true + }"#, + ) + .unwrap(); + + assert!(settings.onboarded); + assert!(settings.wallet); } /// The theme selection round-trips through serde. diff --git a/crates/workspace/src/dialogs/mod.rs b/crates/workspace/src/dialogs/mod.rs index f104a3bb..379b7c8e 100644 --- a/crates/workspace/src/dialogs/mod.rs +++ b/crates/workspace/src/dialogs/mod.rs @@ -2,6 +2,7 @@ pub mod import; pub mod join_group; pub mod new_chat; pub mod new_group; +pub mod onboarding; pub mod profile; pub mod relays; pub mod restore; diff --git a/crates/workspace/src/dialogs/onboarding.rs b/crates/workspace/src/dialogs/onboarding.rs new file mode 100644 index 00000000..a5535c8c --- /dev/null +++ b/crates/workspace/src/dialogs/onboarding.rs @@ -0,0 +1,477 @@ +use std::rc::Rc; + +use gpui::{ + AnyElement, App, AppContext, Context, Div, Entity, Hsla, InteractiveElement, IntoElement, + ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, + div, px, +}; +use gpui_kit::base::StyledExt as _; +use gpui_kit::component::button::{Button, ButtonVariants}; +use gpui_kit::component::dialog::{Confirm, DialogButtonProps, DialogFooter}; +use gpui_kit::component::input::{InputEvent, InputState, NumberInput}; +use gpui_kit::component::separator::Separator; +use gpui_kit::component::switch::Switch; +use gpui_kit::component::theme::{ThemeColor, try_parse_color}; +use gpui_kit::component::{ + ActiveTheme, Theme, ThemeConfig, ThemeRegistry, WindowExt, h_flex, v_flex, +}; +use gpui_kit::prelude::FluentBuilder; +use rand::seq::IndexedRandom; +use settings::AppSettings; +use smallvec::{SmallVec, smallvec}; +use ui::{settings_block, settings_row}; + +use super::settings::{FONT_SIZE_RANGE, RADIUS_RANGE}; + +/// Open the first-run onboarding dialog +pub fn open(window: &mut Window, cx: &mut App) { + let view = cx.new(|cx| Onboarding::new(window, cx)); + + window.open_dialog(cx, move |this, _window, _cx| { + this.width(px(640.)) + .close_button(false) + .overlay_closable(false) + .keyboard(false) + .title("Welcome to Coop") + .child(view.clone()) + .button_props( + DialogButtonProps::default() + .ok_text("Get started") + .show_cancel(false) + .on_ok(|_, _, cx| { + AppSettings::update_onboarded(true, cx); + // Returning true closes the dialog + true + }), + ) + .footer( + DialogFooter::new().child( + Button::new("get-started") + .label("Get started") + .primary() + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(Confirm { secondary: false }), cx); + }), + ), + ) + }); +} + +pub struct Onboarding { + font_size_input: Entity, + radius_input: Entity, + light_themes: Vec>, + dark_themes: Vec>, + _subscriptions: SmallVec<[Subscription; 3]>, +} + +impl Onboarding { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let theme = AppSettings::get_theme(cx); + let (light_themes, dark_themes) = Self::sample_themes(cx); + + let font_size_input = cx.new(|cx| { + InputState::new(window, cx) + .default_value(theme.font_size.to_string()) + .step(1.) + .min(FONT_SIZE_RANGE.0 as f64) + .max(FONT_SIZE_RANGE.1 as f64) + }); + + let radius_input = cx.new(|cx| { + InputState::new(window, cx) + .default_value( + theme + .radius + .map(|value| value.to_string()) + .unwrap_or_default(), + ) + .placeholder("System") + .step(1.) + .min(RADIUS_RANGE.0 as f64) + .max(RADIUS_RANGE.1 as f64) + }); + + let mut subscriptions = smallvec![]; + + subscriptions.push({ + let state = font_size_input.clone(); + cx.subscribe(&state, move |_, state, event: &InputEvent, cx| { + if !matches!(event, InputEvent::Change) { + return; + } + + if let Some(font_size) = Self::number_at(&state, FONT_SIZE_RANGE, cx) { + AppSettings::update_theme(|theme| theme.font_size = font_size, cx); + AppSettings::apply_theme_style(cx); + } + }) + }); + + // One radius control tunes both radii so the whole UI stays coherent + subscriptions.push({ + let state = radius_input.clone(); + cx.subscribe(&state, move |_, state, event: &InputEvent, cx| { + if !matches!(event, InputEvent::Change) { + return; + } + + let radius = Self::number_at(&state, RADIUS_RANGE, cx); + AppSettings::update_theme( + move |theme| { + theme.radius = radius; + theme.radius_lg = radius; + }, + cx, + ); + AppSettings::apply_theme_style(cx); + }) + }); + + Self { + font_size_input, + radius_input, + light_themes, + dark_themes, + _subscriptions: subscriptions, + } + } + + /// The number in a theme input, clamped to the range, empty is None + fn number_at(state: &Entity, range: (f32, f32), cx: &App) -> Option { + let (min, max) = range; + state + .read(cx) + .value() + .trim() + .parse::() + .ok() + .map(|value| value.clamp(min, max)) + } + + /// Three light and three dark bundled themes, sampled at random. + fn sample_themes(cx: &App) -> (Vec>, Vec>) { + let active = Theme::global(cx).theme_name().clone(); + let mut rng = rand::rng(); + + let (lights, darks): (Vec<_>, Vec<_>) = ThemeRegistry::global(cx) + .sorted_themes() + .into_iter() + .cloned() + .partition(|theme| !theme.mode.is_dark()); + + let mut sample = |group: &[Rc]| { + let mut picks: Vec<_> = group.sample(&mut rng, 3).cloned().collect(); + let active_config = group.iter().find(|theme| theme.name == active); + let already_sampled = picks.iter().any(|theme| theme.name == active); + + if let (Some(config), Some(slot)) = (active_config, picks.first_mut()) + && !already_sampled + { + *slot = config.clone(); + } + + picks + }; + + (sample(&lights), sample(&darks)) + } + + /// The theme picker: a demo block per sampled bundled theme + fn themes_view(&self, cx: &App) -> AnyElement { + let active = Theme::global(cx).theme_name().clone(); + + settings_block( + "Theme", + "Customize UI colors. More in Settings.", + v_flex() + .gap_3() + .child(Self::theme_group("Light", &self.light_themes, &active, cx)) + .child(Self::theme_group("Dark", &self.dark_themes, &active, cx)), + cx, + ) + .into_any_element() + } + + fn theme_group( + label: &'static str, + themes: &[Rc], + active: &SharedString, + cx: &App, + ) -> Div { + v_flex() + .gap_2() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().muted_foreground) + .child(label), + ) + .child( + div().flex().flex_wrap().gap_2().children( + themes + .iter() + .map(|config| Self::theme_card(config, active, cx)), + ), + ) + } + + /// A card painting a mini app mockup in the theme's own colors + fn theme_card(config: &Rc, active: &SharedString, cx: &App) -> impl IntoElement { + let selected = &config.name == active; + let name = config.name.clone(); + let preview = Self::theme_preview(config); + + let border = if selected { + cx.theme().list_active_border + } else { + preview.border + }; + + // Square block; the bigger inset keeps the mock window small inside it + let block = div() + .flex() + .w_full() + .aspect_square() + .p_4() + .rounded(cx.theme().radius_lg) + .overflow_hidden() + .bg(preview.background) + .border_1() + .border_color(border) + .hover(|this| this.border_color(cx.theme().border)); + + let block = if selected { block.border_2() } else { block }; + + v_flex() + .id(SharedString::from(format!("theme-{name}"))) + .flex_1() + .cursor_pointer() + .child( + block.child( + h_flex() + .size_full() + .rounded(cx.theme().radius) + .when(cx.theme().shadow, |this| this.shadow_sm()) + .overflow_hidden() + .border_1() + .border_color(preview.border) + .bg(preview.window) + .child( + v_flex() + .w_1_3() + .h_full() + .p_1() + .justify_between() + .bg(preview.sidebar) + .child( + v_flex() + .gap_1() + .child(h_flex().gap_0p5().children((0..3).map(|_| { + div().size_1().rounded_full().bg(preview.accent) + }))) + .child(Self::demo_bar(preview.bar)) + .child(Self::demo_bar(preview.bar)), + ) + .child( + h_flex() + .gap_0p5() + .items_center() + .child( + div() + .size_2() + .rounded_full() + .bg(preview.bar.opacity(0.55)), + ) + .child( + div() + .flex_1() + .h_1() + .rounded_full() + .bg(preview.bar.opacity(0.55)), + ), + ), + ) + .child( + v_flex() + .flex_1() + .h_full() + .p_1() + .gap_1() + .child(Self::demo_bar(preview.bar)) + .child( + h_flex() + .gap_1() + .child(div().flex_1().h_4().rounded_sm().bg(preview.block)) + .child(div().flex_1().h_4().rounded_sm().bg(preview.block)), + ), + ), + ), + ) + .child( + div() + .mt_1() + .text_xs() + .truncate() + .text_color(if selected { + cx.theme().foreground + } else { + cx.theme().muted_foreground + }) + .child(name), + ) + .on_click({ + let name = config.name.clone(); + move |_, _window, cx| AppSettings::select_theme(&name, cx) + }) + } + + /// A skeleton text bar for the demo block + fn demo_bar(color: Hsla) -> Div { + div().w_full().h_1().rounded_full().bg(color.opacity(0.55)) + } + + /// The demo block colors resolved from a theme config. + fn theme_preview(config: &ThemeConfig) -> ThemePreview { + let defaults = if config.mode.is_dark() { + ThemeColor::dark() + } else { + ThemeColor::light() + }; + let colors = &config.colors; + + let resolve = |value: Option<&SharedString>, fallback: Hsla| { + value + .and_then(|value| try_parse_color(value).ok()) + .unwrap_or(fallback) + }; + + ThemePreview { + background: resolve(colors.background.as_ref(), defaults.background), + border: resolve(colors.border.as_ref(), defaults.border), + window: resolve(colors.popover.as_ref(), defaults.popover), + sidebar: resolve(colors.secondary.as_ref(), defaults.secondary), + bar: resolve(colors.muted_foreground.as_ref(), defaults.muted_foreground), + block: resolve(colors.secondary.as_ref(), defaults.secondary), + accent: resolve(colors.primary.as_ref(), defaults.primary), + } + } +} + +/// The colors of a theme's demo block +struct ThemePreview { + background: Hsla, + border: Hsla, + window: Hsla, + sidebar: Hsla, + bar: Hsla, + block: Hsla, + accent: Hsla, +} + +impl Render for Onboarding { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + const INTRO: &str = + "Pick how Coop works and looks. You can change all of this later in Preferences."; + const WOT: &str = "Treat people followed by your contacts as known senders."; + const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; + #[cfg(not(target_arch = "wasm32"))] + const WALLET: &str = "Show the wallet and allow ecash payments."; + const FONT_SIZE: &str = "The base font size for the interface."; + const RADIUS: &str = + "Corner radius for UI elements, including dialogs. Empty follows the theme."; + const FOCUS_RING: &str = "Draw a ring around focused controls."; + const SHADOW: &str = "Draw soft shadows on buttons, inputs and dialogs."; + + let wot_auto_trust = AppSettings::get_wot_auto_trust(cx); + let nip4e = AppSettings::get_nip4e(cx); + let theme = AppSettings::get_theme(cx); + + let features = v_flex() + .gap_3() + .child(settings_row( + "Web of Trust", + WOT, + Switch::new("onboarding-wot") + .checked(wot_auto_trust) + .on_click(move |checked, _window, cx| { + AppSettings::update_wot_auto_trust(*checked, cx); + }), + cx, + )) + .child(settings_row( + "Decoupling Encryption Key", + NIP4E, + Switch::new("onboarding-nip4e").checked(nip4e).on_click( + move |checked, _window, cx| { + AppSettings::update_nip4e(*checked, cx); + }, + ), + cx, + )); + + #[cfg(not(target_arch = "wasm32"))] + let features = features.child(settings_row( + "Wallet", + WALLET, + Switch::new("onboarding-wallet") + .checked(AppSettings::get_wallet(cx)) + .on_click(move |checked, _window, cx| { + AppSettings::update_wallet(*checked, cx); + }), + cx, + )); + + v_flex() + .gap_4() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(INTRO), + ) + .child(features) + .child(Separator::horizontal()) + .child(self.themes_view(cx)) + .child(Separator::horizontal()) + .child( + v_flex() + .gap_3() + .child(settings_row( + "Font Size", + FONT_SIZE, + NumberInput::new(&self.font_size_input).w_24(), + cx, + )) + .child(settings_row( + "Corner Radius", + RADIUS, + NumberInput::new(&self.radius_input).w_24(), + cx, + )) + .child(settings_row( + "Focus Ring", + FOCUS_RING, + Switch::new("onboarding-focus-ring") + .checked(theme.focus_ring) + .on_click(move |checked, _window, cx| { + AppSettings::update_theme(|theme| theme.focus_ring = *checked, cx); + AppSettings::apply_theme_style(cx); + }), + cx, + )) + .child(settings_row( + "Shadows", + SHADOW, + Switch::new("onboarding-shadow") + .checked(theme.shadow) + .on_click(move |checked, _window, cx| { + AppSettings::update_theme(|theme| theme.shadow = *checked, cx); + AppSettings::apply_theme_style(cx); + }), + cx, + )), + ) + } +} diff --git a/crates/workspace/src/dialogs/settings.rs b/crates/workspace/src/dialogs/settings.rs index 725530e0..439faa1c 100644 --- a/crates/workspace/src/dialogs/settings.rs +++ b/crates/workspace/src/dialogs/settings.rs @@ -7,6 +7,8 @@ use gpui::PathPromptOptions; #[cfg(not(target_arch = "wasm32"))] use gpui::Task; use gpui::http_client::Url; +#[cfg(not(target_arch = "wasm32"))] +use gpui::prelude::FluentBuilder as _; use gpui::{ AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window, div, @@ -28,8 +30,8 @@ use speech::model::{self as speech_model}; use ui::{settings_block, settings_row}; /// Bounds for the theme number inputs -const FONT_SIZE_RANGE: (f32, f32) = (10., 32.); -const RADIUS_RANGE: (f32, f32) = (0., 24.); +pub(super) const FONT_SIZE_RANGE: (f32, f32) = (10., 32.); +pub(super) const RADIUS_RANGE: (f32, f32) = (0., 24.); pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Preferences::new(window, cx)) @@ -95,7 +97,7 @@ impl Preferences { #[cfg(not(target_arch = "wasm32"))] let mint_input = cx.new(|cx| InputState::new(window, cx).placeholder("https://mint…")); - let subscriptions = vec![ + let mut subscriptions = vec![ Self::wire_number_input(&font_size_input, FONT_SIZE_RANGE, cx, |value, cx| { if let Some(font_size) = value { AppSettings::update_theme(|theme| theme.font_size = font_size, cx); @@ -112,6 +114,12 @@ impl Preferences { }), ]; + // Keep the switches in sync with the settings they edit + subscriptions.push({ + let settings = AppSettings::global(cx); + cx.observe(&settings, |_, _, cx| cx.notify()) + }); + #[cfg(not(target_arch = "wasm32"))] let _speech_poll = cx.spawn_in(window, async move |this, cx| { let timer = Duration::from_millis(500); @@ -219,26 +227,42 @@ impl Preferences { #[cfg(not(target_arch = "wasm32"))] { const WALLET: &str = "Cashu mint for your NIP-60 wallet. Set up creates the wallet, Add extends it with another mint."; + let enabled = AppSettings::get_wallet(cx); let has_wallet = nip60::WalletRegistry::global(cx).read(cx).has_wallet(); - settings_block( - "Wallet", - WALLET, - h_flex() - .gap_1() - .child(div().flex_1().child(Input::new(&self.mint_input))) - .child( - Button::new("setup-mint") - .label(if has_wallet { "Add" } else { "Set up" }) - .ghost() - .size_8() - .on_click(cx.listener(move |this, _ev, window, cx| { - this.setup_mint(window, cx); - })), - ), - cx, - ) - .into_any_element() + v_flex() + .w_full() + .gap_3() + .child(settings_row( + "Wallet", + "Show the wallet and allow ecash payments.", + Switch::new("wallet") + .checked(enabled) + .on_click(move |checked, _window, cx| { + AppSettings::update_wallet(*checked, cx); + }), + cx, + )) + .when(enabled, |this| { + this.child(settings_block( + "Mint", + WALLET, + h_flex() + .gap_1() + .child(div().flex_1().child(Input::new(&self.mint_input))) + .child( + Button::new("setup-mint") + .label(if has_wallet { "Add" } else { "Set up" }) + .ghost() + .size_8() + .on_click(cx.listener(move |this, _ev, window, cx| { + this.setup_mint(window, cx); + })), + ), + cx, + )) + }) + .into_any_element() } } @@ -458,6 +482,7 @@ impl Render for Preferences { const RADIUS_LG: &str = "Corner radius for large elements like dialogs. Empty follows the OS theme."; const FOCUS_RING: &str = "Draw a ring around focused controls."; + const SHADOW: &str = "Draw soft shadows on buttons, inputs and dialogs."; const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; const NIP4E_ROTATION: &str = "Automatically rotate the encryption key every 30 days, keeping the previous key for messages still sealed to it."; const BACKGROUND: &str = "Show an image behind chat and group panels."; @@ -566,14 +591,22 @@ impl Render for Preferences { FOCUS_RING, Switch::new("focus-ring") .checked(theme.focus_ring) - .on_click(move |_, _window, cx| { - AppSettings::update_theme( - |theme| theme.focus_ring = !theme.focus_ring, - cx, - ); + .on_click(move |checked, _window, cx| { + AppSettings::update_theme(|theme| theme.focus_ring = *checked, cx); AppSettings::apply_theme_style(cx); }), cx, + )) + .child(settings_row( + "Shadows", + SHADOW, + Switch::new("shadow").checked(theme.shadow).on_click( + move |checked, _window, cx| { + AppSettings::update_theme(|theme| theme.shadow = *checked, cx); + AppSettings::apply_theme_style(cx); + }, + ), + cx, )), ) .child(Separator::horizontal()) diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 11f56705..da29c089 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -2,7 +2,7 @@ use std::cell::Cell; use std::rc::Rc; use std::sync::Arc; -use ::settings::{AppSettings, AppearanceMode}; +use ::settings::{AppSettings, AppearanceMode, persisted_onboarded}; use anyhow::Error; #[cfg(not(target_arch = "wasm32"))] use auto_update::AutoUpdater; @@ -30,7 +30,9 @@ use smallvec::{SmallVec, smallvec}; use state::{NostrRegistry, StateEvent}; use crate::dialogs::restore::RestoreEncryption; -use crate::dialogs::{import, join_group, new_chat, new_group, profile, relays, settings}; +use crate::dialogs::{ + import, join_group, new_chat, new_group, onboarding, profile, relays, settings, +}; use crate::panels::{backup, browse, contact_list, greeter, inbox}; use crate::sidebar::Sidebar; @@ -94,6 +96,7 @@ impl Workspace { let skin = dock::CoopDockSkin::new(cx); gpui_kit::component::dock::DockArea::new("coop", None, window, cx).with_renderer(skin) }); + let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx)); let left_sidebar = sidebar.clone(); @@ -107,10 +110,18 @@ impl Workspace { } })); + let login_nostr = nostr.clone(); subscriptions.push( // Subscribe to the nostr events cx.subscribe_in(&nostr, window, move |_, _, event, window, cx| match event { - StateEvent::SignerChanged => window.close_all_dialogs(cx), + StateEvent::SignerChanged => { + window.close_all_dialogs(cx); + + // Show the first-run onboarding once the user is signed in + if !persisted_onboarded() && login_nostr.read(cx).current_user().is_some() { + onboarding::open(window, cx); + } + } StateEvent::PassphraseRequired => import::open_unlock(window, cx), _ => {} }), @@ -265,6 +276,10 @@ impl Workspace { this.set_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx); this.set_center(center, window, cx); }); + + if !persisted_onboarded() && nostr.read(cx).current_user().is_some() { + onboarding::open(window, cx); + } }); Self { @@ -343,9 +358,10 @@ impl Workspace { let panel = browse::init(self.dock.downgrade(), window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } - Command::ShowWallet => { + Command::ShowWallet => + { #[cfg(not(target_arch = "wasm32"))] - { + if AppSettings::get_wallet(cx) { let panel = nip60_ui::wallet::init(window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); } diff --git a/crates/workspace/src/sidebar/list.rs b/crates/workspace/src/sidebar/list.rs index ec846d49..51d59380 100644 --- a/crates/workspace/src/sidebar/list.rs +++ b/crates/workspace/src/sidebar/list.rs @@ -67,17 +67,23 @@ impl Sidebar { .gap_1() .items_start() .justify_start() - .when(cfg!(not(target_arch = "wasm32")), |this| { - this.child( - Nav::new("wallet") - .prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small()) - .label("Wallet") - .text_sm() - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(crate::Command::ShowWallet), cx) - }), - ) - }) + .when( + cfg!(not(target_arch = "wasm32")) && AppSettings::get_wallet(cx), + |this| { + this.child( + Nav::new("wallet") + .prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small()) + .label("Wallet") + .text_sm() + .on_click(|_, window, cx| { + window.dispatch_action( + Box::new(crate::Command::ShowWallet), + cx, + ) + }), + ) + }, + ) .child( Nav::new("browse") .prefix(Icon::new(IconName::Compass).small())