feat: add onboarding dialog

This commit is contained in:
2026-11-12 13:48:51 +07:00
parent 85f2bb9885
commit 8acbbaef5f
6 changed files with 643 additions and 43 deletions
+68 -1
View File
@@ -28,6 +28,14 @@ pub const NIP4E_ROTATION_DEFAULT_DAYS: u32 = 30;
/// Built-in image shown behind chat and group panels /// Built-in image shown behind chat and group panels
pub const DEFAULT_BACKGROUND: &str = "brand/default-bg.jpg"; 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. /// The user's appearance preference, persisted with the rest of the settings.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum AppearanceMode { pub enum AppearanceMode {
@@ -78,6 +86,9 @@ pub struct ThemeSettings {
pub radius: Option<f32>, pub radius: Option<f32>,
/// Corner radius for large elements like dialogs; follows the OS theme when unset /// Corner radius for large elements like dialogs; follows the OS theme when unset
pub radius_lg: Option<f32>, pub radius_lg: Option<f32>,
/// Draw soft shadows on inputs, buttons and dialogs
#[serde(default = "default_shadow")]
pub shadow: bool,
} }
impl Default for ThemeSettings { impl Default for ThemeSettings {
@@ -87,6 +98,7 @@ impl Default for ThemeSettings {
focus_ring: false, focus_ring: false,
radius: None, radius: None,
radius_lg: None, radius_lg: None,
shadow: true,
} }
} }
} }
@@ -145,6 +157,12 @@ pub struct Settings {
/// Hide the notice that messages in public groups are not encrypted /// Hide the notice that messages in public groups are not encrypted
#[serde(default)] #[serde(default)]
pub hide_group_notice: bool, 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 /// Tuning applied on top of the native OS theme
#[serde(default)] #[serde(default)]
pub theme: ThemeSettings, pub theme: ThemeSettings,
@@ -178,10 +196,12 @@ impl Default for Settings {
appearance_mode: AppearanceMode::default(), appearance_mode: AppearanceMode::default(),
hide_avatar: false, hide_avatar: false,
screening: true, screening: true,
wot_auto_trust: true, wot_auto_trust: false,
nip4e: false, nip4e: false,
nip4e_rotation_days: None, nip4e_rotation_days: None,
hide_group_notice: false, hide_group_notice: false,
wallet: false,
onboarded: false,
theme: ThemeSettings::default(), theme: ThemeSettings::default(),
theme_light: None, theme_light: None,
theme_dark: None, theme_dark: None,
@@ -222,6 +242,9 @@ macro_rules! setting_accessors {
inner.$field = value; inner.$field = value;
cx.notify(); 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<PathBuf>, pub panel_background_path: Option<PathBuf>,
pub theme_light: Option<String>, pub theme_light: Option<String>,
pub theme_dark: Option<String>, pub theme_dark: Option<String>,
pub wallet: bool,
pub onboarded: bool,
} }
setting_getters! { setting_getters! {
@@ -314,6 +339,8 @@ impl AppSettings {
edit(&mut inner.theme); edit(&mut inner.theme);
cx.notify(); 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); let styled = Theme::global_mut(cx);
styled.font_size = px(tuning.font_size); styled.font_size = px(tuning.font_size);
styled.focus_ring = tuning.focus_ring; styled.focus_ring = tuning.focus_ring;
styled.shadow = tuning.shadow;
if let Some(radius) = tuning.radius { if let Some(radius) = tuning.radius {
styled.radius = px(radius); styled.radius = px(radius);
} }
@@ -582,6 +610,11 @@ fn persisted_content() -> Option<String> {
common::web_store::get(SETTINGS_KEY).ok().flatten() 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 { pub fn persisted_appearance_mode() -> AppearanceMode {
persisted_settings() persisted_settings()
.map(|settings| settings.appearance_mode) .map(|settings| settings.appearance_mode)
@@ -672,6 +705,40 @@ mod tests {
assert_eq!(settings.theme, ThemeSettings::default()); assert_eq!(settings.theme, ThemeSettings::default());
assert_eq!(settings.theme_light, None); assert_eq!(settings.theme_light, None);
assert_eq!(settings.theme_dark, 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. /// The theme selection round-trips through serde.
+1
View File
@@ -2,6 +2,7 @@ pub mod import;
pub mod join_group; pub mod join_group;
pub mod new_chat; pub mod new_chat;
pub mod new_group; pub mod new_group;
pub mod onboarding;
pub mod profile; pub mod profile;
pub mod relays; pub mod relays;
pub mod restore; pub mod restore;
+477
View File
@@ -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<InputState>,
radius_input: Entity<InputState>,
light_themes: Vec<Rc<ThemeConfig>>,
dark_themes: Vec<Rc<ThemeConfig>>,
_subscriptions: SmallVec<[Subscription; 3]>,
}
impl Onboarding {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<InputState>, range: (f32, f32), cx: &App) -> Option<f32> {
let (min, max) = range;
state
.read(cx)
.value()
.trim()
.parse::<f32>()
.ok()
.map(|value| value.clamp(min, max))
}
/// Three light and three dark bundled themes, sampled at random.
fn sample_themes(cx: &App) -> (Vec<Rc<ThemeConfig>>, Vec<Rc<ThemeConfig>>) {
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<ThemeConfig>]| {
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<ThemeConfig>],
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<ThemeConfig>, 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<Self>) -> 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,
)),
)
}
}
+59 -26
View File
@@ -7,6 +7,8 @@ use gpui::PathPromptOptions;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use gpui::Task; use gpui::Task;
use gpui::http_client::Url; use gpui::http_client::Url;
#[cfg(not(target_arch = "wasm32"))]
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, AnyElement, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled,
Subscription, Window, div, Subscription, Window, div,
@@ -28,8 +30,8 @@ use speech::model::{self as speech_model};
use ui::{settings_block, settings_row}; use ui::{settings_block, settings_row};
/// Bounds for the theme number inputs /// Bounds for the theme number inputs
const FONT_SIZE_RANGE: (f32, f32) = (10., 32.); pub(super) const FONT_SIZE_RANGE: (f32, f32) = (10., 32.);
const RADIUS_RANGE: (f32, f32) = (0., 24.); pub(super) const RADIUS_RANGE: (f32, f32) = (0., 24.);
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx)) cx.new(|cx| Preferences::new(window, cx))
@@ -95,7 +97,7 @@ impl Preferences {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let mint_input = cx.new(|cx| InputState::new(window, cx).placeholder("https://mint…")); 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| { Self::wire_number_input(&font_size_input, FONT_SIZE_RANGE, cx, |value, cx| {
if let Some(font_size) = value { if let Some(font_size) = value {
AppSettings::update_theme(|theme| theme.font_size = font_size, cx); 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"))] #[cfg(not(target_arch = "wasm32"))]
let _speech_poll = cx.spawn_in(window, async move |this, cx| { let _speech_poll = cx.spawn_in(window, async move |this, cx| {
let timer = Duration::from_millis(500); let timer = Duration::from_millis(500);
@@ -219,26 +227,42 @@ impl Preferences {
#[cfg(not(target_arch = "wasm32"))] #[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."; 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(); let has_wallet = nip60::WalletRegistry::global(cx).read(cx).has_wallet();
settings_block( v_flex()
"Wallet", .w_full()
WALLET, .gap_3()
h_flex() .child(settings_row(
.gap_1() "Wallet",
.child(div().flex_1().child(Input::new(&self.mint_input))) "Show the wallet and allow ecash payments.",
.child( Switch::new("wallet")
Button::new("setup-mint") .checked(enabled)
.label(if has_wallet { "Add" } else { "Set up" }) .on_click(move |checked, _window, cx| {
.ghost() AppSettings::update_wallet(*checked, cx);
.size_8() }),
.on_click(cx.listener(move |this, _ev, window, cx| { cx,
this.setup_mint(window, cx); ))
})), .when(enabled, |this| {
), this.child(settings_block(
cx, "Mint",
) WALLET,
.into_any_element() 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 = const RADIUS_LG: &str =
"Corner radius for large elements like dialogs. Empty follows the OS theme."; "Corner radius for large elements like dialogs. Empty follows the OS theme.";
const FOCUS_RING: &str = "Draw a ring around focused controls."; 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: &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 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."; const BACKGROUND: &str = "Show an image behind chat and group panels.";
@@ -566,14 +591,22 @@ impl Render for Preferences {
FOCUS_RING, FOCUS_RING,
Switch::new("focus-ring") Switch::new("focus-ring")
.checked(theme.focus_ring) .checked(theme.focus_ring)
.on_click(move |_, _window, cx| { .on_click(move |checked, _window, cx| {
AppSettings::update_theme( AppSettings::update_theme(|theme| theme.focus_ring = *checked, cx);
|theme| theme.focus_ring = !theme.focus_ring,
cx,
);
AppSettings::apply_theme_style(cx); AppSettings::apply_theme_style(cx);
}), }),
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()) .child(Separator::horizontal())
+21 -5
View File
@@ -2,7 +2,7 @@ use std::cell::Cell;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use ::settings::{AppSettings, AppearanceMode}; use ::settings::{AppSettings, AppearanceMode, persisted_onboarded};
use anyhow::Error; use anyhow::Error;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use auto_update::AutoUpdater; use auto_update::AutoUpdater;
@@ -30,7 +30,9 @@ use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent}; use state::{NostrRegistry, StateEvent};
use crate::dialogs::restore::RestoreEncryption; 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::panels::{backup, browse, contact_list, greeter, inbox};
use crate::sidebar::Sidebar; use crate::sidebar::Sidebar;
@@ -94,6 +96,7 @@ impl Workspace {
let skin = dock::CoopDockSkin::new(cx); let skin = dock::CoopDockSkin::new(cx);
gpui_kit::component::dock::DockArea::new("coop", None, window, cx).with_renderer(skin) 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 sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let left_sidebar = sidebar.clone(); let left_sidebar = sidebar.clone();
@@ -107,10 +110,18 @@ impl Workspace {
} }
})); }));
let login_nostr = nostr.clone();
subscriptions.push( subscriptions.push(
// Subscribe to the nostr events // Subscribe to the nostr events
cx.subscribe_in(&nostr, window, move |_, _, event, window, cx| match event { 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), 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_dock_size(DockPlacement::Left, SIDEBAR_WIDTH, window, cx);
this.set_center(center, window, cx); this.set_center(center, window, cx);
}); });
if !persisted_onboarded() && nostr.read(cx).current_user().is_some() {
onboarding::open(window, cx);
}
}); });
Self { Self {
@@ -343,9 +358,10 @@ impl Workspace {
let panel = browse::init(self.dock.downgrade(), window, cx); let panel = browse::init(self.dock.downgrade(), window, cx);
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
} }
Command::ShowWallet => { Command::ShowWallet =>
{
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
{ if AppSettings::get_wallet(cx) {
let panel = nip60_ui::wallet::init(window, cx); let panel = nip60_ui::wallet::init(window, cx);
self.add_panel_to_dock(panel, DockPlacement::Center, window, cx); self.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
} }
+17 -11
View File
@@ -67,17 +67,23 @@ impl Sidebar {
.gap_1() .gap_1()
.items_start() .items_start()
.justify_start() .justify_start()
.when(cfg!(not(target_arch = "wasm32")), |this| { .when(
this.child( cfg!(not(target_arch = "wasm32")) && AppSettings::get_wallet(cx),
Nav::new("wallet") |this| {
.prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small()) this.child(
.label("Wallet") Nav::new("wallet")
.text_sm() .prefix(Icon::new(gpui_kit::assets::IconName::Wallet).small())
.on_click(|_, window, cx| { .label("Wallet")
window.dispatch_action(Box::new(crate::Command::ShowWallet), cx) .text_sm()
}), .on_click(|_, window, cx| {
) window.dispatch_action(
}) Box::new(crate::Command::ShowWallet),
cx,
)
}),
)
},
)
.child( .child(
Nav::new("browse") Nav::new("browse")
.prefix(Icon::new(IconName::Compass).small()) .prefix(Icon::new(IconName::Compass).small())