diff --git a/assets/icons/loader.svg b/assets/icons/loader.svg
new file mode 100644
index 0000000..f6ff93f
--- /dev/null
+++ b/assets/icons/loader.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index 7370d9e..c747845 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -26,3 +26,4 @@ dirs.workspace = true
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
rust-embed = "8.5.0"
+smol = "1"
diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs
index 4096e93..625a0e5 100644
--- a/crates/app/src/constants.rs
+++ b/crates/app/src/constants.rs
@@ -1,3 +1,5 @@
pub const KEYRING_SERVICE: &str = "Coop Safe Storage";
pub const APP_NAME: &str = "coop";
pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83";
+pub const SUBSCRIPTION_ID: &str = "listen_new_giftwrap";
+pub const METADATA_DELAY: u64 = 150;
diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs
index 4bc998d..8350104 100644
--- a/crates/app/src/main.rs
+++ b/crates/app/src/main.rs
@@ -1,13 +1,17 @@
use asset::Assets;
use components::Root;
-use constants::{APP_NAME, FAKE_SIG};
use dirs::config_dir;
use gpui::*;
use nostr_sdk::prelude::*;
-use std::{fs, str::FromStr, sync::Arc, time::Duration};
-use tokio::sync::OnceCell;
+use std::{
+ fs,
+ str::FromStr,
+ sync::{Arc, OnceLock},
+ time::Duration,
+};
-use states::user::UserState;
+use constants::{APP_NAME, FAKE_SIG};
+use states::account::AccountState;
use ui::app::AppView;
pub mod asset;
@@ -18,77 +22,71 @@ pub mod utils;
actions!(main_menu, [Quit]);
-pub static CLIENT: OnceCell = OnceCell::const_new();
+static CLIENT: OnceLock = OnceLock::new();
-pub async fn get_client() -> &'static Client {
- CLIENT
- .get_or_init(|| async {
- // Setup app data folder
- let config_dir = config_dir().expect("Config directory not found");
- let _ = fs::create_dir_all(config_dir.join("Coop/"));
+pub fn initialize_client() {
+ // Setup app data folder
+ let config_dir = config_dir().expect("Config directory not found");
+ let _ = fs::create_dir_all(config_dir.join("Coop/"));
- // Setup database
- let lmdb = NostrLMDB::open(config_dir.join("Coop/nostr"))
- .expect("Database is NOT initialized");
+ // Setup database
+ let lmdb = NostrLMDB::open(config_dir.join("Coop/nostr")).expect("Database is NOT initialized");
- // Client options
- let opts = Options::new()
- .gossip(true)
- .max_avg_latency(Duration::from_secs(2));
+ // Client options
+ let opts = Options::new()
+ .gossip(true)
+ .max_avg_latency(Duration::from_secs(2));
- // Setup Nostr Client
- let client = ClientBuilder::default().database(lmdb).opts(opts).build();
+ // Setup Nostr Client
+ let client = ClientBuilder::default().database(lmdb).opts(opts).build();
- // Add some bootstrap relays
- let _ = client.add_relay("wss://relay.damus.io").await;
- let _ = client.add_relay("wss://relay.primal.net").await;
+ CLIENT.set(client).expect("Client is already initialized!");
+}
- let _ = client.add_discovery_relay("wss://directory.yabu.me").await;
- let _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
-
- // Connect to all relays
- client.connect().await;
-
- // Return client
- client
- })
- .await
+pub fn get_client() -> &'static Client {
+ CLIENT.get().expect("Client is NOT initialized!")
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
- // Initialize nostr client
- let _client = get_client().await;
+ // Initialize client
+ initialize_client();
+
+ // Get client
+ let client = get_client();
+ let mut notifications = client.notifications();
+
+ // Add some bootstrap relays
+ _ = client.add_relay("wss://relay.damus.io/").await;
+ _ = client.add_relay("wss://relay.primal.net/").await;
+ _ = client.add_relay("wss://nos.lol/").await;
+
+ _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
+ _ = client.add_discovery_relay("wss://directory.yabu.me/").await;
+
+ // Connect to all relays
+ _ = client.connect().await;
App::new()
.with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))
.run(move |cx| {
+ AccountState::set_global(cx);
+
// Initialize components
components::init(cx);
// Set quit action
cx.on_action(quit);
- // Set app state
- UserState::set_global(cx);
-
- // Refresh
- cx.refresh();
-
// Handle notifications
- cx.foreground_executor()
- .spawn(async move {
- let client = get_client().await;
-
- // Generate a fake signature for rumor event.
- // TODO: Find better way to save unsigned event to database.
- let fake_sig = Signature::from_str(FAKE_SIG).unwrap();
-
- client
- .handle_notifications(|notification| async {
+ cx.background_executor()
+ .spawn({
+ let sig = Signature::from_str(FAKE_SIG).unwrap();
+ async move {
+ while let Ok(notification) = notifications.recv().await {
#[allow(clippy::collapsible_match)]
if let RelayPoolNotification::Message { message, .. } = notification {
if let RelayMessage::Event { event, .. } = message {
@@ -96,36 +94,31 @@ async fn main() {
if let Ok(UnwrappedGift { rumor, .. }) =
client.unwrap_gift_wrap(&event).await
{
- println!("rumor: {}", rumor.as_json());
let mut rumor_clone = rumor.clone();
// Compute event id if not exist
rumor_clone.ensure_id();
- let ev = Event::new(
- rumor_clone.id.expect("System error"),
- rumor_clone.pubkey,
- rumor_clone.created_at,
- rumor_clone.kind,
- rumor_clone.tags,
- rumor_clone.content,
- fake_sig,
- );
+ if let Some(id) = rumor_clone.id {
+ let ev = Event::new(
+ id,
+ rumor_clone.pubkey,
+ rumor_clone.created_at,
+ rumor_clone.kind,
+ rumor_clone.tags,
+ rumor_clone.content,
+ sig,
+ );
- // Save rumor to database to further query
- if let Err(e) = client.database().save_event(&ev).await
- {
- println!("Error: {}", e)
+ // Save rumor to database to further query
+ _ = client.database().save_event(&ev).await
}
}
- } else if event.kind == Kind::Metadata {
- // TODO: handle metadata
}
}
}
- Ok(false)
- })
- .await
+ }
+ }
})
.detach();
diff --git a/crates/app/src/states/account.rs b/crates/app/src/states/account.rs
new file mode 100644
index 0000000..2982355
--- /dev/null
+++ b/crates/app/src/states/account.rs
@@ -0,0 +1,60 @@
+use async_utility::task::spawn;
+use gpui::*;
+use nostr_sdk::prelude::*;
+use std::time::Duration;
+
+use crate::{constants::SUBSCRIPTION_ID, get_client};
+
+pub struct AccountState {
+ pub in_use: Option,
+}
+
+impl Global for AccountState {}
+
+impl AccountState {
+ pub fn set_global(cx: &mut AppContext) {
+ cx.set_global(Self::new());
+
+ cx.observe_global::(|cx| {
+ let state = cx.global::();
+
+ if let Some(public_key) = state.in_use {
+ let client = get_client();
+
+ // Create a filter for getting all gift wrapped events send to current user
+ let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
+
+ // Subscription options
+ let opts = SubscribeAutoCloseOptions::default().filter(
+ FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(10)),
+ );
+
+ let subscription_id = SubscriptionId::new(SUBSCRIPTION_ID);
+
+ // Create a filter for getting new message
+ let new_message = Filter::new()
+ .kind(Kind::GiftWrap)
+ .pubkey(public_key)
+ .limit(0);
+
+ spawn(async move {
+ if client
+ .subscribe(vec![all_messages], Some(opts))
+ .await
+ .is_ok()
+ {
+ // Subscribe for new message
+ _ = client
+ .subscribe_with_id(subscription_id, vec![new_message], None)
+ .await
+ }
+ });
+ }
+ })
+ .detach();
+ }
+
+ fn new() -> Self {
+ Self { in_use: None }
+ }
+}
diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs
index 6a8ff5e..a4acb13 100644
--- a/crates/app/src/states/mod.rs
+++ b/crates/app/src/states/mod.rs
@@ -1,2 +1,2 @@
+pub mod account;
pub mod room;
-pub mod user;
diff --git a/crates/app/src/states/room.rs b/crates/app/src/states/room.rs
index 95c0cf8..2a972c9 100644
--- a/crates/app/src/states/room.rs
+++ b/crates/app/src/states/room.rs
@@ -1,35 +1,103 @@
+use components::theme::ActiveTheme;
use gpui::*;
use nostr_sdk::prelude::*;
+use prelude::FluentBuilder;
+
+use crate::get_client;
#[derive(Clone)]
-pub struct RoomLastMessage {
- pub content: Option,
- pub time: Timestamp,
+#[allow(dead_code)]
+struct RoomLastMessage {
+ content: Option,
+ time: Timestamp,
}
#[derive(Clone, IntoElement)]
pub struct Room {
- members: Vec,
- last_message: Option,
+ #[allow(dead_code)]
+ public_key: PublicKey,
+ metadata: Model