wip: onboarding
This commit is contained in:
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -3512,7 +3512,6 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
"common",
|
"common",
|
||||||
"dirs 5.0.1",
|
|
||||||
"flume 0.12.0",
|
"flume 0.12.0",
|
||||||
"futures",
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -3522,7 +3521,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
|
||||||
"person",
|
"person",
|
||||||
"reqwest_client",
|
"reqwest_client",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4015,6 +4013,20 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "note"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"flume 0.12.0",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"smallvec",
|
||||||
|
"state",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "7.0.0"
|
version = "7.0.0"
|
||||||
@@ -4315,12 +4327,6 @@ version = "1.21.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "oneshot"
|
|
||||||
version = "0.1.11"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oo7"
|
name = "oo7"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -4552,13 +4558,11 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
|
"flume 0.12.0",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
|
||||||
"state",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ dirs = "5.0"
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
oneshot = "0.1.10"
|
|
||||||
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
|
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
|
||||||
|
flume = { version = "0.12", default-features = false, features = ["async", "select"] }
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
|||||||
1
assets/icons/check.svg
Normal file
1
assets/icons/check.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M5 13L9 17L19 7" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
1
assets/icons/chevron-right.svg
Normal file
1
assets/icons/chevron-right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M9 6L15 12L9 18" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
@@ -1,6 +1,3 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -19,9 +16,6 @@ pub struct Account {
|
|||||||
/// Public Key of the account
|
/// Public Key of the account
|
||||||
public_key: Option<PublicKey>,
|
public_key: Option<PublicKey>,
|
||||||
|
|
||||||
/// Contact List of the account
|
|
||||||
pub contacts: Entity<HashSet<PublicKey>>,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
@@ -52,39 +46,7 @@ impl Account {
|
|||||||
|
|
||||||
/// Create a new account instance
|
/// Create a new account instance
|
||||||
fn new(cx: &mut Context<Self>) -> Self {
|
fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let contacts = cx.new(|_| HashSet::default());
|
|
||||||
|
|
||||||
// Collect command line arguments
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
let account = args.get(1).and_then(|s| Keys::parse(s).ok());
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
if let Some(keys) = account {
|
|
||||||
let public_key = keys.public_key();
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Set signer in background
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
// Set the signer
|
|
||||||
cx.background_executor()
|
|
||||||
.await_on_background(async move {
|
|
||||||
let client = client();
|
|
||||||
client.set_signer(keys).await;
|
|
||||||
|
|
||||||
log::info!("Signer is set");
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.public_key = Some(public_key);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Listen for public key set
|
// Listen for public key set
|
||||||
@@ -97,9 +59,8 @@ impl Account {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
contacts,
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_tasks: tasks,
|
_tasks: smallvec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +87,24 @@ impl Account {
|
|||||||
// Subscribe to the user's contact list
|
// Subscribe to the user's contact list
|
||||||
client.subscribe(filter, Some(opts)).await?;
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
log::info!("Subscribed to user metadata and contact list");
|
// Construct a filter to get the user's other metadata
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kinds(vec![
|
||||||
|
Kind::MuteList,
|
||||||
|
Kind::Bookmarks,
|
||||||
|
Kind::BookmarkSet,
|
||||||
|
Kind::SearchRelays,
|
||||||
|
Kind::BlockedRelays,
|
||||||
|
Kind::RelaySet,
|
||||||
|
Kind::Custom(10012),
|
||||||
|
])
|
||||||
|
.author(public_key)
|
||||||
|
.limit(24);
|
||||||
|
|
||||||
|
// Subscribe to the user's other metadata
|
||||||
|
client.subscribe(filter, Some(opts)).await?;
|
||||||
|
|
||||||
|
log::info!("Subscribed to user metadata");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
@@ -144,30 +122,4 @@ impl Account {
|
|||||||
// This method is only called when user is logged in, so unwrap safely
|
// This method is only called when user is logged in, so unwrap safely
|
||||||
self.public_key.unwrap()
|
self.public_key.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the contacts of the account from the database
|
|
||||||
pub fn load_contacts(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let task: Task<Result<HashSet<PublicKey>, Error>> = cx.background_spawn(async move {
|
|
||||||
let client = client();
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let contacts = client.database().contacts_public_keys(public_key).await?;
|
|
||||||
|
|
||||||
Ok(contacts)
|
|
||||||
});
|
|
||||||
|
|
||||||
self._tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
if let Ok(contacts) = task.await {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.contacts.update(cx, |this, cx| {
|
|
||||||
this.extend(contacts);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
/// Client (or application) name.
|
||||||
pub const CLIENT_NAME: &str = "Lume";
|
pub const CLIENT_NAME: &str = "Lume";
|
||||||
|
|
||||||
|
/// Application ID.
|
||||||
pub const APP_ID: &str = "su.reya.lume";
|
pub const APP_ID: &str = "su.reya.lume";
|
||||||
|
|
||||||
/// Bootstrap Relays.
|
/// Bootstrap Relays.
|
||||||
@@ -10,13 +13,6 @@ pub const BOOTSTRAP_RELAYS: [&str; 5] = [
|
|||||||
"wss://purplepag.es",
|
"wss://purplepag.es",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Search Relays.
|
|
||||||
pub const SEARCH_RELAYS: [&str; 3] = [
|
|
||||||
"wss://relay.nostr.band",
|
|
||||||
"wss://search.nos.today",
|
|
||||||
"wss://relay.noswhere.com",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Default relay for Nostr Connect
|
/// Default relay for Nostr Connect
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||||
|
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ anyhow.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
dirs.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
oneshot.workspace = true
|
flume.workspace = true
|
||||||
|
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
flume = { version = "0.12", default-features = false, features = ["async", "select"] }
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
|
use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions,
|
point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions,
|
||||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
|
||||||
@@ -40,11 +40,6 @@ fn main() {
|
|||||||
client.add_relay(url).await.ok();
|
client.add_relay(url).await.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add search relays to the relay pool
|
|
||||||
for url in SEARCH_RELAYS {
|
|
||||||
client.add_relay(url).await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to all added relays
|
// Connect to all added relays
|
||||||
client.connect().await;
|
client.connect().await;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar>
|
|||||||
cx.observe_global::<Theme>({
|
cx.observe_global::<Theme>({
|
||||||
let title = title.clone();
|
let title = title.clone();
|
||||||
let app_menu_bar = app_menu_bar.clone();
|
let app_menu_bar = app_menu_bar.clone();
|
||||||
|
|
||||||
move |cx| {
|
move |cx| {
|
||||||
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
|
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
|
||||||
}
|
}
|
||||||
@@ -26,6 +27,7 @@ pub fn init(title: impl Into<SharedString>, cx: &mut App) -> Entity<AppMenuBar>
|
|||||||
|
|
||||||
fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) {
|
fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuBar>, cx: &mut App) {
|
||||||
let mode = cx.theme().mode;
|
let mode = cx.theme().mode;
|
||||||
|
|
||||||
cx.set_menus(vec![
|
cx.set_menus(vec![
|
||||||
Menu {
|
Menu {
|
||||||
name: title.into(),
|
name: title.into(),
|
||||||
@@ -87,6 +89,7 @@ fn update_app_menu(title: impl Into<SharedString>, app_menu_bar: Entity<AppMenuB
|
|||||||
fn theme_menu(cx: &App) -> MenuItem {
|
fn theme_menu(cx: &App) -> MenuItem {
|
||||||
let themes = ThemeRegistry::global(cx).sorted_themes();
|
let themes = ThemeRegistry::global(cx).sorted_themes();
|
||||||
let current_name = cx.theme().theme_name();
|
let current_name = cx.theme().theme_name();
|
||||||
|
|
||||||
MenuItem::Submenu(Menu {
|
MenuItem::Submenu(Menu {
|
||||||
name: "Theme".into(),
|
name: "Theme".into(),
|
||||||
items: themes
|
items: themes
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod feed;
|
pub mod feed;
|
||||||
|
pub mod onboarding;
|
||||||
pub mod startup;
|
pub mod startup;
|
||||||
|
|||||||
45
crates/lume/src/panels/onboarding.rs
Normal file
45
crates/lume/src/panels/onboarding.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use gpui::{
|
||||||
|
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||||
|
ParentElement, Render, SharedString, Window,
|
||||||
|
};
|
||||||
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||||
|
cx.new(|cx| Onboarding::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Onboarding {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Onboarding {
|
||||||
|
fn new(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for Onboarding {
|
||||||
|
fn panel_name(&self) -> &'static str {
|
||||||
|
"Onboarding"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
SharedString::from("Onboarding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for Onboarding {}
|
||||||
|
|
||||||
|
impl Focusable for Onboarding {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Onboarding {
|
||||||
|
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
div().child("Onboarding")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
use account::Account;
|
|
||||||
use common::BOOTSTRAP_RELAYS;
|
use common::BOOTSTRAP_RELAYS;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
@@ -47,8 +46,7 @@ impl Focusable for Sidebar {
|
|||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let person = PersonRegistry::global(cx);
|
let person = PersonRegistry::global(cx);
|
||||||
let account = Account::global(cx);
|
let contacts = Vec::new();
|
||||||
let contacts = account.read(cx).contacts.read(cx);
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("sidebar-wrapper")
|
.id("sidebar-wrapper")
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
use std::collections::HashSet;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use account::Account;
|
use account::Account;
|
||||||
use anyhow::Error;
|
use common::{CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
|
||||||
use common::{BOOTSTRAP_RELAYS, CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
div, px, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
Render, Styled, Subscription, Task, Window,
|
Render, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle};
|
use gpui_component::dock::{DockArea, DockItem, DockPlacement, PanelStyle};
|
||||||
use gpui_component::{v_flex, Root, Theme};
|
use gpui_component::{v_flex, Root, Theme};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{client, StateEvent};
|
|
||||||
|
|
||||||
use crate::panels::feed::Feed;
|
use crate::panels::feed::Feed;
|
||||||
use crate::panels::startup;
|
use crate::panels::{onboarding, startup};
|
||||||
use crate::sidebar;
|
use crate::sidebar;
|
||||||
use crate::title_bar::AppTitleBar;
|
use crate::title_bar::AppTitleBar;
|
||||||
|
|
||||||
@@ -36,32 +32,23 @@ pub struct Workspace {
|
|||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
/// Background tasks
|
|
||||||
_tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let account = Account::global(cx);
|
|
||||||
|
|
||||||
// App's title bar
|
// App's title bar
|
||||||
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
|
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
|
||||||
|
|
||||||
// Dock area for the workspace.
|
// Dock area for the workspace.
|
||||||
let dock =
|
let dock = cx.new(|cx| {
|
||||||
cx.new(|cx| DockArea::new("dock", Some(1), window, cx).panel_style(PanelStyle::TabBar));
|
let startup = Arc::new(onboarding::init(window, cx));
|
||||||
|
let mut this = DockArea::new("dock", None, window, cx).panel_style(PanelStyle::TabBar);
|
||||||
// Channel for communication between Nostr and GPUI
|
this.set_center(DockItem::panel(startup), window, cx);
|
||||||
let (tx, rx) = flume::bounded::<StateEvent>(2048);
|
this
|
||||||
|
});
|
||||||
|
|
||||||
|
let account = Account::global(cx);
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
// Automatically sync theme with system appearance
|
|
||||||
subscriptions.push(window.observe_window_appearance(|window, cx| {
|
|
||||||
Theme::sync_system_appearance(Some(window), cx);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Observe account entity changes
|
// Observe account entity changes
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
@@ -72,98 +59,15 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle nostr notifications
|
// Automatically sync theme with system appearance
|
||||||
tasks.push(cx.background_spawn(async move {
|
subscriptions.push(window.observe_window_appearance(|window, cx| {
|
||||||
let client = client();
|
Theme::sync_system_appearance(Some(window), cx);
|
||||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
|
||||||
let mut notifications = client.notifications();
|
|
||||||
let mut processed_events: HashSet<EventId> = HashSet::default();
|
|
||||||
|
|
||||||
while let Ok(notification) = notifications.recv().await {
|
|
||||||
let RelayPoolNotification::Message { message, .. } = notification else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
match message {
|
|
||||||
RelayMessage::Event { event, .. } => {
|
|
||||||
// Skip if already processed
|
|
||||||
if !processed_events.insert(event.id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
match event.kind {
|
|
||||||
Kind::ContactList => {
|
|
||||||
// Get all public keys from the event
|
|
||||||
let public_keys: Vec<PublicKey> =
|
|
||||||
event.tags.public_keys().copied().collect();
|
|
||||||
|
|
||||||
// Construct a filter to get metadata for each public key
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::Metadata)
|
|
||||||
.limit(public_keys.len())
|
|
||||||
.authors(public_keys);
|
|
||||||
|
|
||||||
// Subscribe to metadata events in the bootstrap relays
|
|
||||||
client
|
|
||||||
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Notify GPUI of received contact list
|
|
||||||
tx.send_async(StateEvent::ReceivedContactList).await.ok();
|
|
||||||
}
|
|
||||||
Kind::Metadata => {
|
|
||||||
// Parse metadata from event, default if invalid
|
|
||||||
let metadata =
|
|
||||||
Metadata::from_json(&event.content).unwrap_or_default();
|
|
||||||
|
|
||||||
// Construct nostr profile with metadata and public key
|
|
||||||
let profile = Box::new(Profile::new(event.pubkey, metadata));
|
|
||||||
|
|
||||||
// Notify GPUI of received profile
|
|
||||||
tx.send_async(StateEvent::ReceivedProfile(profile))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RelayMessage::EndOfStoredEvents(_) => {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Handle state events
|
|
||||||
tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
while let Ok(event) = rx.recv_async().await {
|
|
||||||
cx.update(|_window, cx| match event {
|
|
||||||
StateEvent::ReceivedContactList => {
|
|
||||||
let account = Account::global(cx);
|
|
||||||
account.update(cx, |this, cx| {
|
|
||||||
this.load_contacts(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
StateEvent::ReceivedProfile(profile) => {
|
|
||||||
let person = PersonRegistry::global(cx);
|
|
||||||
person.update(cx, |this, cx| {
|
|
||||||
this.insert_or_update(&profile, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// Entity has been released, ignore any errors
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
title_bar,
|
title_bar,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_tasks: tasks,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
crates/note/Cargo.toml
Normal file
17
crates/note/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "note"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
flume.workspace = true
|
||||||
|
log.workspace = true
|
||||||
92
crates/note/src/lib.rs
Normal file
92
crates/note/src/lib.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::client;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
NoteRegistry::set_global(cx.new(NoteRegistry::new), cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GlobalNoteRegistry(Entity<NoteRegistry>);
|
||||||
|
|
||||||
|
impl Global for GlobalNoteRegistry {}
|
||||||
|
|
||||||
|
/// Note Registry
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NoteRegistry {
|
||||||
|
/// Collection of all notes
|
||||||
|
pub notes: HashMap<EventId, Event>,
|
||||||
|
|
||||||
|
/// Tasks for asynchronous operations
|
||||||
|
_tasks: SmallVec<[Task<()>; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NoteRegistry {
|
||||||
|
/// Retrieve the global note registry state
|
||||||
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
|
cx.global::<GlobalNoteRegistry>().0.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the global note registry instance
|
||||||
|
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalNoteRegistry(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new note registry instance
|
||||||
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Channel for communication between Nostr and GPUI
|
||||||
|
let (tx, rx) = flume::bounded::<Event>(2048);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Handle nostr notifications
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events: HashSet<EventId> = HashSet::default();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RelayMessage::Event { event, .. } = message {
|
||||||
|
// Skip if already processed
|
||||||
|
if !processed_events.insert(event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.kind == Kind::TextNote || event.kind == Kind::Repost {
|
||||||
|
tx.send_async(event.into_owned()).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Update GPUI state
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
while let Ok(event) = rx.recv_async().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.notes.insert(event.id, event);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
notes: HashMap::new(),
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, id: &EventId) -> Option<&Event> {
|
||||||
|
self.notes.get(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,5 @@ nostr-sdk.workspace = true
|
|||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
serde.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -20,7 +20,7 @@ pub struct PersonRegistry {
|
|||||||
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
pub persons: HashMap<PublicKey, Entity<Profile>>,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
_tasks: SmallVec<[Task<()>; 2]>,
|
_tasks: SmallVec<[Task<()>; 3]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PersonRegistry {
|
impl PersonRegistry {
|
||||||
@@ -38,6 +38,9 @@ impl PersonRegistry {
|
|||||||
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
|
||||||
let mut tasks = smallvec![];
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
// Channel for communication between Nostr and GPUI
|
||||||
|
let (tx, rx) = flume::bounded::<Profile>(1024);
|
||||||
|
|
||||||
tasks.push(
|
tasks.push(
|
||||||
// Load all user profiles from the database
|
// Load all user profiles from the database
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
@@ -57,6 +60,47 @@ impl PersonRegistry {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Handle nostr notifications
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let client = client();
|
||||||
|
let mut notifications = client.notifications();
|
||||||
|
let mut processed_events: HashSet<EventId> = HashSet::default();
|
||||||
|
|
||||||
|
while let Ok(notification) = notifications.recv().await {
|
||||||
|
let RelayPoolNotification::Message { message, .. } = notification else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let RelayMessage::Event { event, .. } = message {
|
||||||
|
// Skip if already processed
|
||||||
|
if !processed_events.insert(event.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.kind == Kind::Metadata {
|
||||||
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
|
let profile = Profile::new(event.pubkey, metadata);
|
||||||
|
|
||||||
|
tx.send_async(profile).await.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Update GPUI state
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
while let Ok(profile) = rx.recv_async().await {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.insert_or_update(&profile, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
persons: HashMap::new(),
|
persons: HashMap::new(),
|
||||||
_tasks: tasks,
|
_tasks: tasks,
|
||||||
@@ -110,8 +154,7 @@ impl PersonRegistry {
|
|||||||
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
|
||||||
self.persons
|
self.persons
|
||||||
.get(public_key)
|
.get(public_key)
|
||||||
.map(|e| e.read(cx))
|
.map(|e| e.read(cx).clone())
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
|
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user