diff --git a/Cargo.lock b/Cargo.lock
index 12bbd401..be5cfa25 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3512,7 +3512,6 @@ dependencies = [
"anyhow",
"assets",
"common",
- "dirs 5.0.1",
"flume 0.12.0",
"futures",
"gpui",
@@ -3522,7 +3521,6 @@ dependencies = [
"log",
"nostr-connect",
"nostr-sdk",
- "oneshot",
"person",
"reqwest_client",
"serde",
@@ -4015,6 +4013,20 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "note"
+version = "0.0.1"
+dependencies = [
+ "anyhow",
+ "common",
+ "flume 0.12.0",
+ "gpui",
+ "log",
+ "nostr-sdk",
+ "smallvec",
+ "state",
+]
+
[[package]]
name = "notify"
version = "7.0.0"
@@ -4315,12 +4327,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
-[[package]]
-name = "oneshot"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
-
[[package]]
name = "oo7"
version = "0.5.0"
@@ -4552,13 +4558,11 @@ version = "0.0.1"
dependencies = [
"anyhow",
"common",
+ "flume 0.12.0",
"gpui",
"log",
"nostr-sdk",
- "serde",
- "serde_json",
"smallvec",
- "smol",
"state",
]
diff --git a/Cargo.toml b/Cargo.toml
index fb6815c5..8e98502c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,8 +28,8 @@ dirs = "5.0"
futures = "0.3"
itertools = "0.13.0"
log = "0.4"
-oneshot = "0.1.10"
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
+flume = { version = "0.12", default-features = false, features = ["async", "select"] }
rust-embed = "8.5.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diff --git a/assets/icons/check.svg b/assets/icons/check.svg
new file mode 100644
index 00000000..3861bd27
--- /dev/null
+++ b/assets/icons/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/chevron-right.svg b/assets/icons/chevron-right.svg
new file mode 100644
index 00000000..fab95dc6
--- /dev/null
+++ b/assets/icons/chevron-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs
index 3e9628fb..8cddf4f8 100644
--- a/crates/account/src/lib.rs
+++ b/crates/account/src/lib.rs
@@ -1,6 +1,3 @@
-use std::collections::HashSet;
-use std::env;
-
use anyhow::Error;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*;
@@ -19,9 +16,6 @@ pub struct Account {
/// Public Key of the account
public_key: Option,
- /// Contact List of the account
- pub contacts: Entity>,
-
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
@@ -52,39 +46,7 @@ impl Account {
/// Create a new account instance
fn new(cx: &mut Context) -> Self {
- let contacts = cx.new(|_| HashSet::default());
-
- // Collect command line arguments
- let args: Vec = env::args().collect();
- let account = args.get(1).and_then(|s| Keys::parse(s).ok());
-
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(
// Listen for public key set
@@ -97,9 +59,8 @@ impl Account {
Self {
public_key: None,
- contacts,
_subscriptions: subscriptions,
- _tasks: tasks,
+ _tasks: smallvec![],
}
}
@@ -126,7 +87,24 @@ impl Account {
// Subscribe to the user's contact list
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(())
});
@@ -144,30 +122,4 @@ impl Account {
// This method is only called when user is logged in, so unwrap safely
self.public_key.unwrap()
}
-
- /// Load the contacts of the account from the database
- pub fn load_contacts(&mut self, cx: &mut Context) {
- let task: Task, 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(())
- }));
- }
}
diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs
index 6fa9260c..68c87b76 100644
--- a/crates/common/src/constants.rs
+++ b/crates/common/src/constants.rs
@@ -1,4 +1,7 @@
+/// Client (or application) name.
pub const CLIENT_NAME: &str = "Lume";
+
+/// Application ID.
pub const APP_ID: &str = "su.reya.lume";
/// Bootstrap Relays.
@@ -10,13 +13,6 @@ pub const BOOTSTRAP_RELAYS: [&str; 5] = [
"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
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
diff --git a/crates/lume/Cargo.toml b/crates/lume/Cargo.toml
index f8cd87e3..2c03b368 100644
--- a/crates/lume/Cargo.toml
+++ b/crates/lume/Cargo.toml
@@ -27,12 +27,10 @@ anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
itertools.workspace = true
-dirs.workspace = true
log.workspace = true
smallvec.workspace = true
smol.workspace = true
futures.workspace = true
-oneshot.workspace = true
+flume.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
-flume = { version = "0.12", default-features = false, features = ["async", "select"] }
diff --git a/crates/lume/src/main.rs b/crates/lume/src/main.rs
index 4fb54078..8dc4ff08 100644
--- a/crates/lume/src/main.rs
+++ b/crates/lume/src/main.rs
@@ -1,7 +1,7 @@
use std::sync::Arc;
use assets::Assets;
-use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS};
+use common::{APP_ID, BOOTSTRAP_RELAYS, CLIENT_NAME};
use gpui::{
point, px, size, AppContext, Application, Bounds, SharedString, TitlebarOptions,
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowOptions,
@@ -40,11 +40,6 @@ fn main() {
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
client.connect().await;
})
diff --git a/crates/lume/src/menus.rs b/crates/lume/src/menus.rs
index 5287575c..43c6e849 100644
--- a/crates/lume/src/menus.rs
+++ b/crates/lume/src/menus.rs
@@ -15,6 +15,7 @@ pub fn init(title: impl Into, cx: &mut App) -> Entity
cx.observe_global::({
let title = title.clone();
let app_menu_bar = app_menu_bar.clone();
+
move |cx| {
update_app_menu(title.clone(), app_menu_bar.clone(), cx);
}
@@ -26,6 +27,7 @@ pub fn init(title: impl Into, cx: &mut App) -> Entity
fn update_app_menu(title: impl Into, app_menu_bar: Entity, cx: &mut App) {
let mode = cx.theme().mode;
+
cx.set_menus(vec![
Menu {
name: title.into(),
@@ -87,6 +89,7 @@ fn update_app_menu(title: impl Into, app_menu_bar: Entity MenuItem {
let themes = ThemeRegistry::global(cx).sorted_themes();
let current_name = cx.theme().theme_name();
+
MenuItem::Submenu(Menu {
name: "Theme".into(),
items: themes
diff --git a/crates/lume/src/panels/mod.rs b/crates/lume/src/panels/mod.rs
index 9e5237a6..cfb1bafa 100644
--- a/crates/lume/src/panels/mod.rs
+++ b/crates/lume/src/panels/mod.rs
@@ -1,2 +1,3 @@
pub mod feed;
+pub mod onboarding;
pub mod startup;
diff --git a/crates/lume/src/panels/onboarding.rs b/crates/lume/src/panels/onboarding.rs
new file mode 100644
index 00000000..7188123b
--- /dev/null
+++ b/crates/lume/src/panels/onboarding.rs
@@ -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 {
+ 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 {
+ 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) -> impl IntoElement {
+ SharedString::from("Onboarding")
+ }
+}
+
+impl EventEmitter 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) -> impl IntoElement {
+ div().child("Onboarding")
+ }
+}
diff --git a/crates/lume/src/sidebar/mod.rs b/crates/lume/src/sidebar/mod.rs
index 20eaff20..a4f6c42f 100644
--- a/crates/lume/src/sidebar/mod.rs
+++ b/crates/lume/src/sidebar/mod.rs
@@ -1,4 +1,3 @@
-use account::Account;
use common::BOOTSTRAP_RELAYS;
use gpui::{
div, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
@@ -47,8 +46,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
let person = PersonRegistry::global(cx);
- let account = Account::global(cx);
- let contacts = account.read(cx).contacts.read(cx);
+ let contacts = Vec::new();
v_flex()
.id("sidebar-wrapper")
diff --git a/crates/lume/src/workspace.rs b/crates/lume/src/workspace.rs
index 0cd914ea..671d3a3a 100644
--- a/crates/lume/src/workspace.rs
+++ b/crates/lume/src/workspace.rs
@@ -1,22 +1,18 @@
-use std::collections::HashSet;
use std::sync::Arc;
use account::Account;
-use anyhow::Error;
-use common::{BOOTSTRAP_RELAYS, CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
+use common::{CLIENT_NAME, DEFAULT_SIDEBAR_WIDTH};
use gpui::{
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::{v_flex, Root, Theme};
use nostr_sdk::prelude::*;
-use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
-use state::{client, StateEvent};
use crate::panels::feed::Feed;
-use crate::panels::startup;
+use crate::panels::{onboarding, startup};
use crate::sidebar;
use crate::title_bar::AppTitleBar;
@@ -36,32 +32,23 @@ pub struct Workspace {
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
-
- /// Background tasks
- _tasks: SmallVec<[Task>; 2]>,
}
impl Workspace {
pub fn new(window: &mut Window, cx: &mut Context) -> Self {
- let account = Account::global(cx);
-
// App's title bar
let title_bar = cx.new(|cx| AppTitleBar::new(CLIENT_NAME, window, cx));
// Dock area for the workspace.
- let dock =
- cx.new(|cx| DockArea::new("dock", Some(1), window, cx).panel_style(PanelStyle::TabBar));
-
- // Channel for communication between Nostr and GPUI
- let (tx, rx) = flume::bounded::(2048);
+ let dock = cx.new(|cx| {
+ let startup = Arc::new(onboarding::init(window, cx));
+ let mut this = DockArea::new("dock", None, window, cx).panel_style(PanelStyle::TabBar);
+ this.set_center(DockItem::panel(startup), window, cx);
+ this
+ });
+ let account = Account::global(cx);
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
subscriptions.push(
@@ -72,98 +59,15 @@ impl Workspace {
}),
);
- // Handle nostr notifications
- tasks.push(cx.background_spawn(async move {
- let client = client();
- let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
- let mut notifications = client.notifications();
- let mut processed_events: HashSet = 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 =
- 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(())
+ // Automatically sync theme with system appearance
+ subscriptions.push(window.observe_window_appearance(|window, cx| {
+ Theme::sync_system_appearance(Some(window), cx);
}));
Self {
dock,
title_bar,
_subscriptions: subscriptions,
- _tasks: tasks,
}
}
diff --git a/crates/note/Cargo.toml b/crates/note/Cargo.toml
new file mode 100644
index 00000000..049a90b4
--- /dev/null
+++ b/crates/note/Cargo.toml
@@ -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
diff --git a/crates/note/src/lib.rs b/crates/note/src/lib.rs
new file mode 100644
index 00000000..94d68291
--- /dev/null
+++ b/crates/note/src/lib.rs
@@ -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);
+
+impl Global for GlobalNoteRegistry {}
+
+/// Note Registry
+#[derive(Debug)]
+pub struct NoteRegistry {
+ /// Collection of all notes
+ pub notes: HashMap,
+
+ /// Tasks for asynchronous operations
+ _tasks: SmallVec<[Task<()>; 2]>,
+}
+
+impl NoteRegistry {
+ /// Retrieve the global note registry state
+ pub fn global(cx: &App) -> Entity {
+ cx.global::().0.clone()
+ }
+
+ /// Set the global note registry instance
+ pub(crate) fn set_global(state: Entity, cx: &mut App) {
+ cx.set_global(GlobalNoteRegistry(state));
+ }
+
+ /// Create a new note registry instance
+ pub(crate) fn new(cx: &mut Context) -> Self {
+ let mut tasks = smallvec![];
+
+ // Channel for communication between Nostr and GPUI
+ let (tx, rx) = flume::bounded::(2048);
+
+ tasks.push(
+ // Handle nostr notifications
+ cx.background_spawn(async move {
+ let client = client();
+ let mut notifications = client.notifications();
+ let mut processed_events: HashSet = 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)
+ }
+}
diff --git a/crates/person/Cargo.toml b/crates/person/Cargo.toml
index cd71d410..153b844c 100644
--- a/crates/person/Cargo.toml
+++ b/crates/person/Cargo.toml
@@ -13,7 +13,5 @@ nostr-sdk.workspace = true
anyhow.workspace = true
smallvec.workspace = true
-smol.workspace = true
+flume.workspace = true
log.workspace = true
-serde.workspace = true
-serde_json.workspace = true
diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs
index 5aa13223..be261af2 100644
--- a/crates/person/src/lib.rs
+++ b/crates/person/src/lib.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
use nostr_sdk::prelude::*;
@@ -20,7 +20,7 @@ pub struct PersonRegistry {
pub persons: HashMap>,
/// Tasks for asynchronous operations
- _tasks: SmallVec<[Task<()>; 2]>,
+ _tasks: SmallVec<[Task<()>; 3]>,
}
impl PersonRegistry {
@@ -38,6 +38,9 @@ impl PersonRegistry {
pub(crate) fn new(cx: &mut Context) -> Self {
let mut tasks = smallvec![];
+ // Channel for communication between Nostr and GPUI
+ let (tx, rx) = flume::bounded::(1024);
+
tasks.push(
// Load all user profiles from the database
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 = 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 {
persons: HashMap::new(),
_tasks: tasks,
@@ -110,8 +154,7 @@ impl PersonRegistry {
pub fn get(&self, public_key: &PublicKey, cx: &App) -> Profile {
self.persons
.get(public_key)
- .map(|e| e.read(cx))
- .cloned()
+ .map(|e| e.read(cx).clone())
.unwrap_or(Profile::new(public_key.to_owned(), Metadata::default()))
}
}