wip: startup flow
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "nostr"
|
||||
name = "client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -8,6 +8,3 @@ gpui.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
dirs.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
keyring-search = "1.2.0"
|
||||
keyring-lib = { version = "1.0.2", features = ["tokio", "derive"] }
|
||||
20
crates/client/src/lib.rs
Normal file
20
crates/client/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use gpui::Global;
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::get_client;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub struct NostrClient {
|
||||
pub client: &'static Client,
|
||||
}
|
||||
|
||||
impl Global for NostrClient {}
|
||||
|
||||
impl NostrClient {
|
||||
pub async fn init() -> Self {
|
||||
// Initialize nostr client
|
||||
let client = get_client().await;
|
||||
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
use gpui::Global;
|
||||
use keyring_search::{Limit, List, Search};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::get_client;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub mod state;
|
||||
|
||||
pub struct Nostr {
|
||||
pub client: &'static Client,
|
||||
pub accounts: HashSet<PublicKey>,
|
||||
}
|
||||
|
||||
// Set Nostr as Global State
|
||||
impl Global for Nostr {}
|
||||
|
||||
impl Nostr {
|
||||
pub async fn init() -> Self {
|
||||
// Initialize nostr client
|
||||
let client = get_client().await;
|
||||
// Get all accounts
|
||||
let accounts = Self::get_accounts();
|
||||
|
||||
Self { client, accounts }
|
||||
}
|
||||
|
||||
pub fn get_accounts() -> HashSet<PublicKey> {
|
||||
let search = Search::new().expect("Keyring not working.");
|
||||
let results = search.by_service("coop");
|
||||
let list = List::list_credentials(&results, Limit::All);
|
||||
let accounts: HashSet<PublicKey> = list
|
||||
.split_whitespace()
|
||||
.filter(|v| v.starts_with("npub1") && !v.ends_with("coop"))
|
||||
.filter_map(|i| PublicKey::from_bech32(i).ok())
|
||||
.collect();
|
||||
|
||||
accounts
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,11 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
components.workspace = true
|
||||
tokio.workspace = true
|
||||
nostr = { version = "0.1.0", path = "../nostr" }
|
||||
nostr-sdk.workspace = true
|
||||
|
||||
client = { version = "0.1.0", path = "../client" }
|
||||
|
||||
keyring-search = "1.2.0"
|
||||
keyring-lib = { version = "1.0.2", features = ["tokio", "derive"] }
|
||||
|
||||
@@ -1,31 +1,42 @@
|
||||
use client::NostrClient;
|
||||
use components::theme::{Theme, ThemeColor, ThemeMode};
|
||||
use gpui::*;
|
||||
use nostr::Nostr;
|
||||
use state::AppState;
|
||||
use views::app::AppView;
|
||||
|
||||
struct HelloWorld {
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(rgb(0xffffff))
|
||||
.flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(format!("Hello, {}!", &self.text))
|
||||
}
|
||||
}
|
||||
pub mod state;
|
||||
pub mod utils;
|
||||
pub mod views;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let nostr = Nostr::init().await;
|
||||
// Initialize nostr client
|
||||
let nostr = NostrClient::init().await;
|
||||
// Initializ app state
|
||||
let app_state = AppState::new();
|
||||
|
||||
App::new().run(move |cx| {
|
||||
// Initialize components
|
||||
components::init(cx);
|
||||
|
||||
// Set custom theme
|
||||
let mut theme = Theme::from(ThemeColor::dark());
|
||||
// TODO: support light mode
|
||||
theme.mode = ThemeMode::Dark;
|
||||
// TODO: adjust color set
|
||||
|
||||
// Set global theme
|
||||
cx.set_global(theme);
|
||||
|
||||
// Set nostr client as global state
|
||||
cx.set_global(nostr);
|
||||
cx.set_global(app_state);
|
||||
|
||||
// Rerender
|
||||
cx.refresh();
|
||||
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
// Set window size
|
||||
let bounds = Bounds::centered(None, size(px(860.0), px(650.0)), cx);
|
||||
// Set global state
|
||||
cx.set_global(nostr);
|
||||
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
@@ -38,11 +49,7 @@ async fn main() {
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|_cx| HelloWorld {
|
||||
text: "coop".into(),
|
||||
})
|
||||
},
|
||||
|cx| cx.new_view(AppView::new),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
24
crates/ui/src/state.rs
Normal file
24
crates/ui/src/state.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use gpui::Global;
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::utils::get_all_accounts_from_keyring;
|
||||
|
||||
pub struct AppState {
|
||||
pub accounts: HashSet<PublicKey>,
|
||||
}
|
||||
|
||||
impl Global for AppState {}
|
||||
|
||||
impl AppState {
|
||||
pub fn new() -> Self {
|
||||
let accounts = get_all_accounts_from_keyring();
|
||||
Self { accounts }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
16
crates/ui/src/utils.rs
Normal file
16
crates/ui/src/utils.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use keyring_search::{Limit, List, Search};
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub fn get_all_accounts_from_keyring() -> HashSet<PublicKey> {
|
||||
let search = Search::new().expect("Keyring not working.");
|
||||
let results = search.by_service("coop");
|
||||
let list = List::list_credentials(&results, Limit::All);
|
||||
let accounts: HashSet<PublicKey> = list
|
||||
.split_whitespace()
|
||||
.filter(|v| v.starts_with("npub1") && !v.ends_with("coop"))
|
||||
.filter_map(|i| PublicKey::from_bech32(i).ok())
|
||||
.collect();
|
||||
|
||||
accounts
|
||||
}
|
||||
57
crates/ui/src/views/app.rs
Normal file
57
crates/ui/src/views/app.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use components::theme::ActiveTheme;
|
||||
use gpui::*;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
use super::{chatspace::ChatSpaceView, setup::SetupView};
|
||||
|
||||
pub struct AppView {
|
||||
onboarding: Model<Option<AnyView>>, // TODO: create onboarding view
|
||||
setup: View<SetupView>,
|
||||
chat_space: View<ChatSpaceView>,
|
||||
}
|
||||
|
||||
impl AppView {
|
||||
pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView {
|
||||
// Onboarding model
|
||||
let onboarding = cx.new_model(|_| None);
|
||||
// Setup view
|
||||
let setup = cx.new_view(SetupView::new);
|
||||
// Chat Space view
|
||||
let chat_space = cx.new_view(ChatSpaceView::new);
|
||||
|
||||
cx.foreground_executor()
|
||||
.spawn(async move {
|
||||
// TODO: create onboarding view for the first time open app
|
||||
})
|
||||
.detach();
|
||||
|
||||
AppView {
|
||||
onboarding,
|
||||
setup,
|
||||
chat_space,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AppView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let mut content = div().size_full().flex().items_center().justify_center();
|
||||
|
||||
if cx.global::<AppState>().accounts.is_empty() {
|
||||
content = content.child(self.setup.clone())
|
||||
} else {
|
||||
content = content.child(self.chat_space.clone())
|
||||
}
|
||||
|
||||
if let Some(onboarding) = self.onboarding.read(cx).as_ref() {
|
||||
content = content.child(onboarding.clone())
|
||||
}
|
||||
|
||||
div()
|
||||
.bg(cx.theme().background)
|
||||
.text_color(rgb(0xFFFFFF))
|
||||
.size_full()
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
24
crates/ui/src/views/chatspace.rs
Normal file
24
crates/ui/src/views/chatspace.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use gpui::*;
|
||||
|
||||
pub struct ChatSpaceView {
|
||||
pub text: SharedString,
|
||||
}
|
||||
|
||||
impl ChatSpaceView {
|
||||
pub fn new(_cx: &mut ViewContext<'_, Self>) -> ChatSpaceView {
|
||||
ChatSpaceView {
|
||||
text: "chat".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ChatSpaceView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(format!("Hello, {}!", &self.text))
|
||||
}
|
||||
}
|
||||
3
crates/ui/src/views/mod.rs
Normal file
3
crates/ui/src/views/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod app;
|
||||
pub mod chatspace;
|
||||
pub mod setup;
|
||||
48
crates/ui/src/views/setup.rs
Normal file
48
crates/ui/src/views/setup.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use components::{
|
||||
input::{InputEvent, TextInput},
|
||||
label::Label,
|
||||
};
|
||||
use gpui::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
pub struct SetupView {
|
||||
input: View<TextInput>,
|
||||
}
|
||||
|
||||
impl SetupView {
|
||||
pub fn new(cx: &mut ViewContext<'_, Self>) -> SetupView {
|
||||
let input = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx);
|
||||
input.set_size(components::Size::Medium, cx);
|
||||
input
|
||||
});
|
||||
|
||||
cx.subscribe(&input, move |_, text_input, input_event, cx| {
|
||||
if let InputEvent::PressEnter = input_event {
|
||||
let content = text_input.read(cx).text().to_string();
|
||||
|
||||
if let Ok(public_key) = PublicKey::parse(content) {
|
||||
cx.global_mut::<AppState>().accounts.insert(public_key);
|
||||
cx.notify();
|
||||
};
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
SetupView { input }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SetupView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_1_3()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(Label::new("Private Key").text_sm())
|
||||
.child(self.input.clone())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user