diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 8c11861f..15909b35 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -87,6 +87,9 @@ pub struct NostrRegistry { /// Current user's public key current_user: Option, + /// Whether the initial credential check has concluded + ready: bool, + /// Tasks for asynchronous operations tasks: Vec>>, } @@ -140,6 +143,7 @@ impl NostrRegistry { this.connect_bootstrap_relays(cx); if cfg!(target_arch = "wasm32") { + this.mark_ready(cx); cx.emit(StateEvent::NoSigner); } else if let Some(secret) = cli_key { // Use CLI-provided key -- same path as get_user_credential @@ -156,6 +160,7 @@ impl NostrRegistry { client, signer, current_user: None, + ready: false, tasks: vec![], } } @@ -175,6 +180,20 @@ impl NostrRegistry { self.current_user } + /// Whether the initial credential check has concluded + pub fn ready(&self) -> bool { + self.ready + } + + fn mark_ready(&mut self, cx: &mut Context) { + if self.ready { + return; + } + + self.ready = true; + cx.notify(); + } + /// Update the signer pub fn set_signer(&mut self, new_signer: T, cx: &mut Context) where @@ -189,6 +208,7 @@ impl NostrRegistry { this.update(cx, |this, cx| { this.signer.swap_inner(new_signer); this.current_user = Some(public_key); + this.mark_ready(cx); cx.emit(StateEvent::SignerChanged); cx.notify(); })?; @@ -275,12 +295,16 @@ impl NostrRegistry { } else if content == "proxy" { #[cfg(not(target_arch = "wasm32"))] this.update(cx, |this, cx| { + this.mark_ready(cx); this.connect_proxy(cx); })?; + } else { + this.update(cx, |this, cx| this.mark_ready(cx))?; } } _ => { - this.update(cx, |_, cx| { + this.update(cx, |this, cx| { + this.mark_ready(cx); cx.emit(StateEvent::NoSigner); })?; } diff --git a/crates/workspace/src/dialogs/import.rs b/crates/workspace/src/dialogs/import.rs index cb41d765..a43c7449 100644 --- a/crates/workspace/src/dialogs/import.rs +++ b/crates/workspace/src/dialogs/import.rs @@ -1,8 +1,8 @@ use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ - AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Task, Window, div, + App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, Window, div, px, }; use instant::Duration; use nostr_connect::prelude::*; @@ -12,6 +12,19 @@ use ui::button::{Button, ButtonVariants}; use ui::input::{Input, InputEvent, InputState}; use ui::{Disableable, StyledExt, WindowExtension, divider, v_flex}; +pub fn open(window: &mut Window, cx: &mut App) { + let import = cx.new(|cx| ImportIdentity::new(window, cx)); + + window.open_modal(cx, move |this, _window, _cx| { + this.width(px(450.)) + .show_close(false) + .overlay_closable(false) + .keyboard(false) + .title("Onboarding") + .child(import.clone()) + }); +} + #[derive(Debug)] pub struct ImportIdentity { /// Secret key input diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index 4e5ef190..1658ff45 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -24,7 +24,6 @@ use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; -use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{new_chat, new_community, settings}; use crate::panels::{ @@ -97,16 +96,10 @@ impl Workspace { subscriptions.push( // Subscribe to the nostr events - cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { - match event { - StateEvent::SignerChanged => { - window.close_all_modals(cx); - } - StateEvent::NoSigner => { - this.import_identity(window, cx); - } - _ => {} - }; + cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| { + if let StateEvent::SignerChanged = event { + window.close_all_modals(cx); + } }), ); @@ -463,19 +456,6 @@ impl Workspace { }); } - fn import_identity(&mut self, window: &mut Window, cx: &mut Context) { - let import = cx.new(|cx| ImportIdentity::new(window, cx)); - - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(450.)) - .show_close(false) - .overlay_closable(false) - .keyboard(false) - .title("Onboarding") - .child(import.clone()) - }); - } - fn theme_selector(&mut self, window: &mut Window, cx: &mut Context) { window.open_modal(cx, move |this, _window, cx| { let registry = ThemeRegistry::global(cx); diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index b38bfeab..8929b577 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -31,6 +31,7 @@ use ui::{ use crate::Command; +mod onboarding; mod tab; mod tree; @@ -44,13 +45,14 @@ pub struct Sidebar { active_tab: SidebarTab, /// Whether there are new chat requests new_requests: bool, - _subscriptions: SmallVec<[Subscription; 2]>, + _subscriptions: SmallVec<[Subscription; 3]>, } impl Sidebar { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); let communities = CommunityRegistry::global(cx); + let nostr = NostrRegistry::global(cx); let mut subscriptions = smallvec![]; @@ -74,6 +76,8 @@ impl Sidebar { }, )); + subscriptions.push(cx.observe(&nostr, |_this, _nostr, cx| cx.notify())); + Self { focus_handle: cx.focus_handle(), scroll_handles: [ @@ -441,10 +445,26 @@ impl Focusable for Sidebar { impl Render for Sidebar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let logged_in = nostr.read(cx).current_user().is_some(); + let (logged_in, ready) = { + let nostr = nostr.read(cx); + (nostr.current_user().is_some(), nostr.ready()) + }; + + if !logged_in { + if !ready { + return v_flex() + .size_full() + .bg(cx.theme().surface_background) + .border_r_1() + .border_color(cx.theme().border_variant) + .into_any_element(); + } + + return onboarding::render(window, cx).into_any_element(); + } let chat = ChatRegistry::global(cx); - let loading = chat.read(cx).loading() && logged_in; + let loading = chat.read(cx).loading(); let sidebar = cx.entity().downgrade(); let active_tab = self.active_tab; @@ -592,5 +612,6 @@ impl Render for Sidebar { ), ) }) + .into_any_element() } } diff --git a/crates/workspace/src/sidebar/onboarding.rs b/crates/workspace/src/sidebar/onboarding.rs new file mode 100644 index 00000000..abf2d9d4 --- /dev/null +++ b/crates/workspace/src/sidebar/onboarding.rs @@ -0,0 +1,74 @@ +use gpui::{App, InteractiveElement, IntoElement, ParentElement, Styled, Window, div, svg}; +use theme::{ActiveTheme, TABBAR_HEIGHT}; +use ui::button::{Button, ButtonVariants}; +use ui::{StyledExt, h_flex, title_bar_drag_handlers, v_flex}; + +use crate::dialogs::import; + +const TITLE: &str = "Welcome to Coop!"; +const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; + +pub(super) fn render(window: &mut Window, cx: &mut App) -> impl IntoElement { + v_flex() + .size_full() + .relative() + .bg(cx.theme().surface_background) + .child(title_bar_drag_handlers( + div() + .id("onboarding-drag") + .absolute() + .top_0() + .left_0() + .h(TABBAR_HEIGHT) + .w_full(), + window, + cx, + )) + .child( + v_flex() + .size_full() + .justify_end() + .gap_4() + .p_4() + .child( + h_flex() + .gap_2() + .child( + svg() + .path("brand/coop.svg") + .size_8() + .text_color(cx.theme().icon_muted), + ) + .child( + v_flex().child(div().font_semibold().child(TITLE)).child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(DESCRIPTION), + ), + ), + ) + .child( + v_flex() + .gap_2() + .w_full() + .child( + Button::new("join-now") + .label("Join now") + .primary() + .font_semibold() + .h_8() + .w_full(), + ) + .child( + Button::new("import-identity") + .label("Import identity") + .secondary() + .font_semibold() + .h_8() + .w_full() + .on_click(|_event, window, cx| import::open(window, cx)), + ), + ), + ) +} diff --git a/crates/workspace/src/sidebar/tab.rs b/crates/workspace/src/sidebar/tab.rs index 662d3b28..72f7502a 100644 --- a/crates/workspace/src/sidebar/tab.rs +++ b/crates/workspace/src/sidebar/tab.rs @@ -48,10 +48,6 @@ impl SidebarTab { } } - pub fn recents(self) -> bool { - matches!(self, Self::Recents) - } - pub fn chat(self) -> bool { matches!(self, Self::Chats) }