diff --git a/Cargo.lock b/Cargo.lock
index 46eac26..6144faa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "account"
+version = "0.0.0"
+dependencies = [
+ "anyhow",
+ "common",
+ "global",
+ "gpui",
+ "log",
+ "nostr-sdk",
+ "smallvec",
+ "smol",
+ "ui",
+]
+
[[package]]
name = "addr2line"
version = "0.24.2"
@@ -1127,7 +1142,6 @@ version = "0.0.0"
dependencies = [
"anyhow",
"chrono",
- "dirs 5.0.1",
"global",
"gpui",
"itertools 0.13.0",
@@ -1176,6 +1190,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
name = "coop"
version = "0.1.4"
dependencies = [
+ "account",
"anyhow",
"chats",
"common",
@@ -1184,6 +1199,7 @@ dependencies = [
"global",
"gpui",
"itertools 0.13.0",
+ "keyring",
"log",
"nostr-connect",
"nostr-sdk",
@@ -1413,6 +1429,35 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
+[[package]]
+name = "dbus"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
+dependencies = [
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "dbus-secret-service"
+version = "4.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b"
+dependencies = [
+ "aes",
+ "block-padding",
+ "cbc",
+ "dbus",
+ "futures-util",
+ "hkdf",
+ "num",
+ "once_cell",
+ "rand 0.8.5",
+ "sha2",
+]
+
[[package]]
name = "derive_more"
version = "0.99.19"
@@ -2962,6 +3007,18 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "keyring"
+version = "4.0.0-rc.1"
+source = "git+https://github.com/hwchen/keyring-rs#9d1b02ff4c9fd1ff125c71f252c14b9ed7313fcb"
+dependencies = [
+ "byteorder",
+ "dbus-secret-service",
+ "log",
+ "security-framework",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "khronos-egl"
version = "6.0.0"
@@ -3009,6 +3066,15 @@ version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+[[package]]
+name = "libdbus-sys"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
+dependencies = [
+ "pkg-config",
+]
+
[[package]]
name = "libfuzzer-sys"
version = "0.4.9"
@@ -3378,7 +3444,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"aes",
"base64",
@@ -3403,7 +3469,7 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"async-utility",
"nostr",
@@ -3415,7 +3481,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"flatbuffers",
"lru",
@@ -3426,7 +3492,7 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"async-utility",
"heed",
@@ -3439,7 +3505,7 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3456,7 +3522,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.40.0"
-source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16"
+source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a"
dependencies = [
"async-utility",
"nostr",
diff --git a/Cargo.toml b/Cargo.toml
index 11282a2..732d90c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,9 +11,9 @@ gpui = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" }
# Nostr
-nostr-relay-builder = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17" }
-nostr-connect = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17" }
-nostr-sdk = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17", features = [
+nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" }
+nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
+nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
"lmdb",
"nip96",
"nip59",
@@ -35,6 +35,7 @@ anyhow = "1.0.44"
smallvec = "1.14.0"
rust-embed = "8.5.0"
log = "0.4"
+keyring = { git = "https://github.com/hwchen/keyring-rs" }
[profile.release]
strip = true
diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg
new file mode 100644
index 0000000..d0b5d88
--- /dev/null
+++ b/assets/icons/arrow-left.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg
new file mode 100644
index 0000000..05ef2da
--- /dev/null
+++ b/assets/icons/arrow-right.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml
new file mode 100644
index 0000000..ad9cff7
--- /dev/null
+++ b/crates/account/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "account"
+version = "0.0.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+ui = { path = "../ui" }
+common = { path = "../common" }
+global = { path = "../global" }
+
+gpui.workspace = true
+nostr-sdk.workspace = true
+anyhow.workspace = true
+smol.workspace = true
+smallvec.workspace = true
+log.workspace = true
diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs
new file mode 100644
index 0000000..3113a55
--- /dev/null
+++ b/crates/account/src/lib.rs
@@ -0,0 +1,166 @@
+use std::time::Duration;
+
+use anyhow::Error;
+use common::profile::NostrProfile;
+use global::{
+ constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
+ get_client,
+};
+use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
+use nostr_sdk::prelude::*;
+use ui::{notification::Notification, ContextModal};
+
+struct GlobalAccount(Entity);
+
+impl Global for GlobalAccount {}
+
+pub fn init(cx: &mut App) {
+ Account::set_global(cx.new(|_| Account { profile: None }), cx);
+}
+
+#[derive(Debug, Clone)]
+pub struct Account {
+ pub profile: Option,
+}
+
+impl Account {
+ pub fn global(cx: &App) -> Entity {
+ cx.global::().0.clone()
+ }
+
+ pub fn set_global(account: Entity, cx: &mut App) {
+ cx.set_global(GlobalAccount(account));
+ }
+
+ pub fn login(&mut self, signer: S, window: &mut Window, cx: &mut Context)
+ where
+ S: NostrSigner + 'static,
+ {
+ let task: Task> = cx.background_spawn(async move {
+ let client = get_client();
+ // Use user's signer for main signer
+ _ = client.set_signer(signer).await;
+
+ // Verify nostr signer and get public key
+ let signer = client.signer().await?;
+ let public_key = signer.get_public_key().await?;
+
+ // Fetch user's metadata
+ let metadata = client
+ .fetch_metadata(public_key, Duration::from_secs(2))
+ .await?
+ .unwrap_or_default();
+
+ Ok(NostrProfile::new(public_key, metadata))
+ });
+
+ cx.spawn_in(window, |this, mut cx| async move {
+ match task.await {
+ Ok(profile) => {
+ cx.update(|_, cx| {
+ this.update(cx, |this, cx| {
+ this.profile = Some(profile);
+ this.subscribe(cx);
+ cx.notify();
+ })
+ })
+ .ok();
+ }
+ Err(e) => {
+ cx.update(|window, cx| {
+ window.push_notification(Notification::error(e.to_string()), cx)
+ })
+ .ok();
+ }
+ }
+ })
+ .detach();
+ }
+
+ pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context) {
+ let client = get_client();
+ let keys = Keys::generate();
+
+ let task: Task> = cx.background_spawn(async move {
+ let public_key = keys.public_key();
+ // Update signer
+ client.set_signer(keys).await;
+ // Set metadata
+ client.set_metadata(&metadata).await?;
+
+ Ok(NostrProfile::new(public_key, metadata))
+ });
+
+ cx.spawn_in(window, |this, mut cx| async move {
+ if let Ok(profile) = task.await {
+ cx.update(|_, cx| {
+ this.update(cx, |this, cx| {
+ this.profile = Some(profile);
+ this.subscribe(cx);
+ cx.notify();
+ })
+ })
+ .ok();
+ } else {
+ cx.update(|window, cx| {
+ window.push_notification(Notification::error("Failed to create account."), cx)
+ })
+ .ok();
+ }
+ })
+ .detach();
+ }
+
+ pub fn subscribe(&self, cx: &Context) {
+ let Some(profile) = self.profile.as_ref() else {
+ return;
+ };
+
+ let client = get_client();
+ let user = profile.public_key;
+ let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
+
+ // Create a contact list filter
+ let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1);
+
+ // Create a user's data filter
+ let data = Filter::new()
+ .author(user)
+ .since(Timestamp::now())
+ .kinds(vec![
+ Kind::Metadata,
+ Kind::ContactList,
+ Kind::InboxRelays,
+ Kind::RelayList,
+ ]);
+
+ // Create a filter for getting all gift wrapped events send to current user
+ let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user);
+
+ // Create a filter to continuously receive new messages.
+ let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
+
+ let task: Task> = cx.background_spawn(async move {
+ // Only subscribe to the latest contact list
+ client.subscribe(contacts, Some(opts)).await?;
+
+ // Continuously receive new user's data since now
+ client.subscribe(data, None).await?;
+
+ let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
+ client.subscribe_with_id(sub_id, msg, Some(opts)).await?;
+
+ let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
+ client.subscribe_with_id(sub_id, new_msg, None).await?;
+
+ Ok(())
+ });
+
+ cx.spawn(|_, _| async move {
+ if let Err(e) = task.await {
+ log::error!("Error: {}", e);
+ }
+ })
+ .detach();
+ }
+}
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index a7fd1b4..4b73ad8 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -13,6 +13,7 @@ ui = { path = "../ui" }
common = { path = "../common" }
global = { path = "../global" }
chats = { path = "../chats" }
+account = { path = "../account" }
gpui.workspace = true
reqwest_client.workspace = true
@@ -29,7 +30,8 @@ log.workspace = true
smallvec.workspace = true
smol.workspace = true
oneshot.workspace = true
+keyring.workspace = true
rustls = "0.23.23"
-futures= "0.3"
+futures = "0.3"
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
diff --git a/crates/app/src/views/app.rs b/crates/app/src/chat_space.rs
similarity index 61%
rename from crates/app/src/views/app.rs
rename to crates/app/src/chat_space.rs
index c627d30..4868d48 100644
--- a/crates/app/src/views/app.rs
+++ b/crates/app/src/chat_space.rs
@@ -1,21 +1,23 @@
+use account::Account;
use global::get_client;
use gpui::{
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
- StyledImage, Window,
+ StyledImage, Subscription, Task, Window,
};
use serde::Deserialize;
+use smallvec::{smallvec, SmallVec};
use std::sync::Arc;
use ui::{
button::{Button, ButtonRounded, ButtonVariants},
- dock_area::{dock::DockPlacement, DockArea, DockItem},
+ dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
popup_menu::PopupMenuExt,
theme::{scale::ColorScaleStep, ActiveTheme, Appearance, Theme},
ContextModal, Icon, IconName, Root, Sizable, TitleBar,
};
-use super::{chat, contacts, onboarding, profile, relays, settings, sidebar, welcome};
-use crate::device::Device;
+use crate::views::{chat, contacts, profile, relays, settings, welcome};
+use crate::views::{onboarding, sidebar};
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub enum PanelKind {
@@ -43,25 +45,80 @@ impl_internal_actions!(dock, [AddPanel]);
// Account actions
actions!(account, [Logout]);
-pub fn init(window: &mut Window, cx: &mut App) -> Entity {
- AppView::new(window, cx)
+pub fn init(window: &mut Window, cx: &mut App) -> Entity {
+ ChatSpace::new(window, cx)
}
-pub struct AppView {
+pub struct ChatSpace {
+ titlebar: bool,
dock: Entity,
+ #[allow(unused)]
+ subscriptions: SmallVec<[Subscription; 1]>,
}
-impl AppView {
+impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity {
- // Initialize dock layout
+ let account = Account::global(cx);
let dock = cx.new(|cx| DockArea::new(window, cx));
- let weak_dock = dock.downgrade();
+ let titlebar = false;
- // Initialize left dock
- let left_panel = DockItem::panel(Arc::new(sidebar::init(window, cx)));
+ cx.new(|cx| {
+ let mut this = Self {
+ dock,
+ titlebar,
+ subscriptions: smallvec![cx.observe_in(
+ &account,
+ window,
+ |this: &mut ChatSpace, account, window, cx| {
+ if account.read(cx).profile.is_some() {
+ this.open_chats(window, cx);
+ } else {
+ this.open_onboarding(window, cx);
+ }
+ },
+ )],
+ };
- // Initial central dock
- let center_panel = DockItem::split_with_sizes(
+ if Account::global(cx).read(cx).profile.is_some() {
+ this.open_chats(window, cx);
+ } else {
+ this.open_onboarding(window, cx);
+ }
+
+ this
+ })
+ }
+
+ pub fn set_center_panel(panel: P, window: &mut Window, cx: &mut App) {
+ if let Some(Some(root)) = window.root::() {
+ if let Ok(chatspace) = root.read(cx).view().clone().downcast::() {
+ let panel = Arc::new(panel);
+ let center = DockItem::panel(panel);
+
+ chatspace.update(cx, |this, cx| {
+ this.dock.update(cx, |this, cx| {
+ this.set_center(center, window, cx);
+ });
+ });
+ }
+ }
+ }
+
+ fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context) {
+ let panel = Arc::new(onboarding::init(window, cx));
+ let center = DockItem::panel(panel);
+
+ self.dock.update(cx, |this, cx| {
+ this.set_center(center, window, cx);
+ });
+ }
+
+ fn open_chats(&mut self, window: &mut Window, cx: &mut Context) {
+ self.show_titlebar(cx);
+
+ let weak_dock = self.dock.downgrade();
+ let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
+ let center = DockItem::split_with_sizes(
Axis::Vertical,
vec![DockItem::tabs(
vec![Arc::new(welcome::init(window, cx))],
@@ -76,16 +133,18 @@ impl AppView {
cx,
);
- // Set default dock layout with left and central docks
- _ = weak_dock.update(cx, |view, cx| {
- view.set_left_dock(left_panel, Some(px(240.)), true, window, cx);
- view.set_center(center_panel, window, cx);
+ self.dock.update(cx, |this, cx| {
+ this.set_left_dock(left, Some(px(240.)), true, window, cx);
+ this.set_center(center, window, cx);
});
-
- cx.new(|_| Self { dock })
}
- fn render_mode_btn(&self, cx: &mut Context) -> impl IntoElement {
+ fn show_titlebar(&mut self, cx: &mut Context) {
+ self.titlebar = true;
+ cx.notify();
+ }
+
+ fn render_appearance_btn(&self, cx: &mut Context) -> impl IntoElement {
Button::new("appearance")
.xsmall()
.ghost()
@@ -111,16 +170,17 @@ impl AppView {
.xsmall()
.reverse()
.icon(Icon::new(IconName::ChevronDownSmall))
- .when_some(Device::global(cx), |this, account| {
- this.when_some(account.read(cx).profile(), |this, profile| {
+ .when_some(
+ Account::global(cx).read(cx).profile.as_ref(),
+ |this, profile| {
this.child(
img(profile.avatar.clone())
.size_5()
.rounded_full()
.object_fit(ObjectFit::Cover),
)
- })
- })
+ },
+ )
.popup_menu(move |this, _, _cx| {
this.menu(
"Profile",
@@ -218,21 +278,27 @@ impl AppView {
fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context) {
let client = get_client();
+ let reset: Task> = cx.background_spawn(async move {
+ client.reset().await;
+ Ok(())
+ });
- cx.background_spawn(async move {
- // Reset nostr client
- client.reset().await
+ cx.spawn_in(window, |_, mut cx| async move {
+ if reset.await.is_ok() {
+ cx.update(|_, cx| {
+ Account::global(cx).update(cx, |this, cx| {
+ this.profile = None;
+ cx.notify();
+ });
+ })
+ .ok();
+ };
})
.detach();
-
- Root::update(window, cx, |this, window, cx| {
- this.replace_view(onboarding::init(window, cx).into());
- cx.notify();
- });
}
}
-impl Render for AppView {
+impl Render for ChatSpace {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx);
@@ -246,23 +312,25 @@ impl Render for AppView {
.flex_col()
.size_full()
// Title Bar
- .child(
- TitleBar::new()
- // Left side
- .child(div())
- // Right side
- .child(
- div()
- .flex()
- .items_center()
- .justify_end()
- .gap_2()
- .px_2()
- .child(self.render_mode_btn(cx))
- .child(self.render_relays_btn(cx))
- .child(self.render_account_btn(cx)),
- ),
- )
+ .when(self.titlebar, |this| {
+ this.child(
+ TitleBar::new()
+ // Left side
+ .child(div())
+ // Right side
+ .child(
+ div()
+ .flex()
+ .items_center()
+ .justify_end()
+ .gap_2()
+ .px_2()
+ .child(self.render_appearance_btn(cx))
+ .child(self.render_relays_btn(cx))
+ .child(self.render_account_btn(cx)),
+ ),
+ )
+ })
// Dock
.child(self.dock.clone()),
)
diff --git a/crates/app/src/device.rs b/crates/app/src/device.rs
deleted file mode 100644
index 4d4a5c7..0000000
--- a/crates/app/src/device.rs
+++ /dev/null
@@ -1,916 +0,0 @@
-use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration};
-
-use anyhow::{anyhow, Error};
-use common::profile::NostrProfile;
-use global::{
- constants::{
- ALL_MESSAGES_SUB_ID, CLIENT_KEYRING, DEVICE_ANNOUNCEMENT_KIND, DEVICE_REQUEST_KIND,
- DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, MASTER_KEYRING, NEW_MESSAGE_SUB_ID,
- },
- get_app_name, get_client, get_device_keys, get_device_name, set_device_keys,
-};
-use gpui::{
- div, px, relative, App, AppContext, Context, Entity, Global, ParentElement, Styled, Task,
- Window,
-};
-use nostr_sdk::prelude::*;
-use smallvec::SmallVec;
-use ui::{
- button::{Button, ButtonRounded, ButtonVariants},
- indicator::Indicator,
- notification::Notification,
- theme::{scale::ColorScaleStep, ActiveTheme},
- ContextModal, Root, Sizable, StyledExt,
-};
-
-use crate::views::{app, onboarding, relays};
-
-struct GlobalDevice(Entity);
-
-impl Global for GlobalDevice {}
-
-#[derive(Debug, Default)]
-pub enum DeviceState {
- Master,
- Minion,
- #[default]
- None,
-}
-
-impl DeviceState {
- pub fn subscribe(&self, window: &mut Window, cx: &mut Context) {
- match self {
- Self::Master => {
- let client = get_client();
- let task: Task> = cx.background_spawn(async move {
- let signer = client.signer().await?;
- let public_key = signer.get_public_key().await?;
-
- let opts =
- SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
-
- let filter = Filter::new()
- .kind(Kind::Custom(DEVICE_REQUEST_KIND))
- .author(public_key)
- .limit(1);
-
- // Subscribe for the latest request
- client.subscribe(filter, Some(opts)).await?;
-
- let filter = Filter::new()
- .kind(Kind::Custom(DEVICE_REQUEST_KIND))
- .author(public_key)
- .since(Timestamp::now());
-
- // Subscribe for new device requests
- client.subscribe(filter, None).await?;
-
- Ok(())
- });
-
- cx.spawn_in(window, |_, _cx| async move {
- if let Err(err) = task.await {
- log::error!("Failed to subscribe for device requests: {}", err);
- }
- })
- .detach();
- }
- Self::Minion => {
- let client = get_client();
- let task: Task> = cx.background_spawn(async move {
- let signer = client.signer().await?;
- let public_key = signer.get_public_key().await?;
-
- let opts =
- SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
-
- let filter = Filter::new()
- .kind(Kind::Custom(DEVICE_RESPONSE_KIND))
- .author(public_key);
-
- // Getting all previous approvals
- client.subscribe(filter.clone(), Some(opts)).await?;
-
- // Continously receive the request approval
- client
- .subscribe(filter.since(Timestamp::now()), None)
- .await?;
-
- Ok(())
- });
-
- cx.spawn_in(window, |_, _cx| async move {
- if let Err(err) = task.await {
- log::error!("Failed to subscribe for device approval: {}", err);
- }
- })
- .detach();
- }
- _ => {}
- }
- }
-}
-
-/// Current Device (Client)
-///
-/// NIP-4e:
-#[derive(Debug)]
-pub struct Device {
- /// Profile (Metadata) of current user
- profile: Option,
- /// Client Keys
- client_keys: Arc,
- /// Device State
- state: Entity,
- requesters: Entity>,
- is_processing: bool,
-}
-
-pub fn init(window: &mut Window, cx: &App) {
- // Initialize client keys
- let read_keys = cx.read_credentials(CLIENT_KEYRING);
- let window_handle = window.window_handle();
-
- cx.spawn(|cx| async move {
- let client_keys = if let Ok(Some((_, secret))) = read_keys.await {
- let secret_key = SecretKey::from_slice(&secret).unwrap();
-
- Arc::new(Keys::new(secret_key))
- } else {
- // Generate new keys and save them to keyring
- let keys = Keys::generate();
-
- if let Ok(write_keys) = cx.update(|cx| {
- cx.write_credentials(
- CLIENT_KEYRING,
- keys.public_key.to_hex().as_str(),
- keys.secret_key().as_secret_bytes(),
- )
- }) {
- _ = write_keys.await;
- };
-
- Arc::new(keys)
- };
-
- cx.update(|cx| {
- let state = cx.new(|_| DeviceState::None);
- let weak_state = state.downgrade();
- let requesters = cx.new(|_| HashSet::new());
- let entity = cx.new(|_| Device {
- profile: None,
- is_processing: false,
- state,
- client_keys,
- requesters,
- });
-
- window_handle
- .update(cx, |_, window, cx| {
- // Open the onboarding view
- Root::update(window, cx, |this, window, cx| {
- this.replace_view(onboarding::init(window, cx).into());
- cx.notify();
- });
-
- // Observe the DeviceState changes
- if let Some(state) = weak_state.upgrade() {
- window
- .observe(&state, cx, |this, window, cx| {
- this.update(cx, |this, cx| {
- this.subscribe(window, cx);
- });
- })
- .detach();
- };
-
- // Observe the Device changes
- window
- .observe(&entity, cx, |this, window, cx| {
- this.update(cx, |this, cx| {
- this.on_device_change(window, cx);
- });
- })
- .detach();
- })
- .ok();
-
- Device::set_global(entity, cx)
- })
- .ok();
- })
- .detach();
-}
-
-impl Device {
- pub fn global(cx: &App) -> Option> {
- cx.try_global::().map(|model| model.0.clone())
- }
-
- pub fn set_global(device: Entity, cx: &mut App) {
- cx.set_global(GlobalDevice(device));
- }
-
- pub fn client_keys(&self) -> Arc {
- self.client_keys.clone()
- }
-
- pub fn profile(&self) -> Option<&NostrProfile> {
- self.profile.as_ref()
- }
-
- pub fn set_profile(&mut self, profile: NostrProfile, cx: &mut Context) {
- self.profile = Some(profile);
- cx.notify();
- }
-
- pub fn set_state(&mut self, state: DeviceState, cx: &mut Context) {
- self.state.update(cx, |this, cx| {
- *this = state;
- cx.notify();
- });
- }
-
- pub fn set_processing(&mut self, is_processing: bool, cx: &mut Context) {
- self.is_processing = is_processing;
- cx.notify();
- }
-
- pub fn add_requester(&mut self, public_key: PublicKey, cx: &mut Context) {
- self.requesters.update(cx, |this, cx| {
- this.insert(public_key);
- cx.notify();
- });
- }
-
- /// Login and set user signer
- pub fn login(&self, signer: T, cx: &mut Context) -> Task>
- where
- T: NostrSigner + 'static,
- {
- let client = get_client();
-
- // Set the user's signer as the main signer
- let login: Task> = cx.background_spawn(async move {
- // Use user's signer for main signer
- _ = client.set_signer(signer).await;
-
- // Verify nostr signer and get public key
- let signer = client.signer().await?;
- let public_key = signer.get_public_key().await?;
-
- // Fetch user's metadata
- let metadata = client
- .fetch_metadata(public_key, Duration::from_secs(2))
- .await?
- .unwrap_or_default();
-
- // Get user's inbox relays
- let filter = Filter::new()
- .kind(Kind::InboxRelays)
- .author(public_key)
- .limit(1);
-
- let relays = if let Some(event) = client
- .fetch_events(filter, Duration::from_secs(2))
- .await?
- .first_owned()
- {
- let relays = event
- .tags
- .filter_standardized(TagKind::Relay)
- .filter_map(|t| {
- if let TagStandard::Relay(url) = t {
- Some(url.to_owned())
- } else {
- None
- }
- })
- .collect::>();
-
- Some(relays)
- } else {
- None
- };
-
- let profile = NostrProfile::new(public_key, metadata).relays(relays);
-
- Ok(profile)
- });
-
- cx.spawn(|this, cx| async move {
- match login.await {
- Ok(user) => {
- cx.update(|cx| {
- this.update(cx, |this, cx| {
- this.profile = Some(user);
- cx.notify();
- })
- .ok();
- })
- .ok();
-
- Ok(())
- }
- Err(e) => Err(e),
- }
- })
- }
-
- /// This function is called whenever the device is changed
- fn on_device_change(&mut self, window: &mut Window, cx: &mut Context) {
- let Some(profile) = self.profile.as_ref() else {
- // User not logged in, render the Onboarding View
- Root::update(window, cx, |this, window, cx| {
- this.replace_view(onboarding::init(window, cx).into());
- cx.notify();
- });
-
- return;
- };
-
- // Replace the Onboarding View with the Dock View
- Root::update(window, cx, |this, window, cx| {
- this.replace_view(app::init(window, cx).into());
- cx.notify();
- });
-
- // Get the user's messaging relays
- // If it is empty, user must setup relays
- let ready = profile.messaging_relays.is_some();
-
- cx.spawn_in(window, |this, mut cx| async move {
- cx.update(|window, cx| {
- if !ready {
- this.update(cx, |this, cx| {
- this.render_setup_relays(window, cx);
- })
- .ok();
- } else {
- this.update(cx, |this, cx| {
- this.start_subscription(cx);
- })
- .ok();
- }
- })
- .ok();
- })
- .detach();
- }
-
- /// Initialize subscription for current user
- pub fn start_subscription(&self, cx: &Context) {
- if self.is_processing {
- return;
- }
-
- let Some(profile) = self.profile() else {
- return;
- };
-
- let user = profile.public_key;
- let client = get_client();
-
- let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
- let device_kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND);
-
- // Create a device announcement filter
- let device = Filter::new().kind(device_kind).author(user).limit(1);
-
- // Create a contact list filter
- let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1);
-
- // Create a user's data filter
- let data = Filter::new()
- .author(user)
- .since(Timestamp::now())
- .kinds(vec![
- Kind::Metadata,
- Kind::InboxRelays,
- Kind::RelayList,
- device_kind,
- ]);
-
- // Create a filter for getting all gift wrapped events send to current user
- let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user);
-
- // Create a filter to continuously receive new messages.
- let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
-
- let task: Task> = cx.background_spawn(async move {
- // Only subscribe to the latest device announcement
- let sub_id = SubscriptionId::new(DEVICE_SUB_ID);
- client.subscribe_with_id(sub_id, device, Some(opts)).await?;
-
- // Only subscribe to the latest contact list
- client.subscribe(contacts, Some(opts)).await?;
-
- // Continuously receive new user's data since now
- client.subscribe(data, None).await?;
-
- let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
- client.subscribe_with_id(sub_id, msg, Some(opts)).await?;
-
- let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
- client.subscribe_with_id(sub_id, new_msg, None).await?;
-
- Ok(())
- });
-
- cx.spawn(|_, _| async move {
- if let Err(e) = task.await {
- log::error!("Subscription error: {}", e);
- }
- })
- .detach();
- }
-
- /// Setup Device
- ///
- /// NIP-4e:
- pub fn setup_device(&mut self, window: &mut Window, cx: &mut Context) {
- let Some(profile) = self.profile().cloned() else {
- return;
- };
-
- // If processing, return early
- if self.is_processing {
- return;
- }
-
- // Only process if device keys are not set
- self.set_processing(true, cx);
-
- let client = get_client();
- let public_key = profile.public_key;
- let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND);
- let filter = Filter::new().kind(kind).author(public_key).limit(1);
-
- // Fetch device announcement events
- let fetch_announcement = cx.background_spawn(async move {
- if let Some(event) = client.database().query(filter).await?.first_owned() {
- Ok(event)
- } else {
- Err(anyhow!("Device Announcement not found."))
- }
- });
-
- cx.spawn_in(window, |this, mut cx| async move {
- // Device Keys has been set, no need to retrieve device announcement again
- if get_device_keys().await.is_some() {
- return;
- }
-
- match fetch_announcement.await {
- Ok(event) => {
- log::info!("Found a device announcement: {:?}", event);
-
- let n_tag = event
- .tags
- .find(TagKind::custom("n"))
- .and_then(|t| t.content())
- .map(|hex| hex.to_owned());
-
- let credentials_task =
- match cx.update(|_, cx| cx.read_credentials(MASTER_KEYRING)) {
- Ok(task) => task,
- Err(err) => {
- log::error!("Failed to read credentials: {:?}", err);
- log::info!("Trying to request keys from Master Device...");
-
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- this.request_master_keys(window, cx);
- })
- })
- .ok();
-
- return;
- }
- };
-
- match credentials_task.await {
- Ok(Some((pubkey, secret))) if n_tag.as_deref() == Some(&pubkey) => {
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- this.set_master_keys(secret, window, cx);
- })
- })
- .ok();
- }
- _ => {
- log::info!("This device is not the Master Device.");
- log::info!("Trying to request keys from Master Device...");
-
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- this.request_master_keys(window, cx);
- })
- })
- .ok();
- }
- }
- }
- Err(_) => {
- log::info!("Device Announcement not found.");
- log::info!("Appoint this device as Master Device.");
-
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- this.set_new_master_keys(window, cx);
- })
- .ok();
- })
- .ok();
- }
- }
- })
- .detach();
- }
-
- /// Create a new Master Keys, appointing this device as Master Device.
- ///
- /// NIP-4e:
- pub fn set_new_master_keys(&self, window: &mut Window, cx: &Context) {
- let client = get_client();
- let app_name = get_app_name();
-
- let task: Task, Error>> = cx.background_spawn(async move {
- let keys = Keys::generate();
- let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND);
- let client_tag = Tag::client(app_name);
- let pubkey_tag = Tag::custom(TagKind::custom("n"), vec![keys.public_key().to_hex()]);
-
- let event = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]);
-
- if let Err(e) = client.send_event_builder(event).await {
- log::error!("Failed to send Device Announcement: {}", e);
- } else {
- log::info!("Device Announcement has been sent");
- }
-
- Ok(Arc::new(keys))
- });
-
- cx.spawn_in(window, |this, mut cx| async move {
- if get_device_keys().await.is_some() {
- return;
- }
-
- if let Ok(keys) = task.await {
- // Update global state
- set_device_keys(keys.clone()).await;
-
- // Save keys
- if let Ok(task) = cx.update(|_, cx| {
- cx.write_credentials(
- MASTER_KEYRING,
- keys.public_key().to_hex().as_str(),
- keys.secret_key().as_secret_bytes(),
- )
- }) {
- if let Err(e) = task.await {
- log::error!("Failed to write device keys to keyring: {}", e);
- }
- };
-
- cx.update(|_, cx| {
- this.update(cx, |this, cx| {
- this.set_state(DeviceState::Master, cx);
- })
- .ok();
- })
- .ok();
- }
- })
- .detach();
- }
-
- /// Device already has Master Keys, re-appointing this device as Master Device.
- ///
- /// NIP-4e:
- pub fn set_master_keys(&self, secret: Vec, window: &mut Window, cx: &Context) {
- let Ok(secret_key) = SecretKey::from_slice(&secret) else {
- log::error!("Failed to parse secret key");
- return;
- };
- let keys = Arc::new(Keys::new(secret_key));
-
- cx.spawn_in(window, |this, mut cx| async move {
- log::info!("Re-appointing this device as Master Device.");
- set_device_keys(keys).await;
-
- cx.update(|_, cx| {
- this.update(cx, |this, cx| {
- this.set_state(DeviceState::Master, cx);
- })
- .ok();
- })
- .ok();
- })
- .detach();
- }
-
- /// Send a request to ask for device keys from the other Nostr client
- ///
- /// NIP-4e:
- pub fn request_master_keys(&self, window: &mut Window, cx: &Context) {
- let client = get_client();
- let app_name = get_app_name();
- let client_keys = self.client_keys.clone();
-
- let kind = Kind::Custom(DEVICE_REQUEST_KIND);
- let client_tag = Tag::client(app_name);
- let pubkey_tag = Tag::custom(
- TagKind::custom("pubkey"),
- vec![client_keys.public_key().to_hex()],
- );
-
- // Create a request event builder
- let builder = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]);
-
- let task: Task> = cx.background_spawn(async move {
- log::info!("Sent a request to ask for device keys from the other Nostr client");
-
- if let Err(e) = client.send_event_builder(builder).await {
- log::error!("Failed to send device keys request: {}", e);
- } else {
- log::info!("Waiting for response...");
- }
-
- Ok(())
- });
-
- cx.spawn_in(window, |this, mut cx| async move {
- if task.await.is_ok() {
- cx.update(|window, cx| {
- this.update(cx, |this, cx| {
- this.set_state(DeviceState::Minion, cx);
- this.render_waiting_modal(window, cx);
- })
- .ok();
- })
- .ok();
- }
- })
- .detach();
- }
-
- /// Received Device Keys approval from Master Device,
- ///
- /// NIP-4e:
- pub fn recv_approval(&self, event: Event, window: &mut Window, cx: &Context) {
- let local_signer = self.client_keys.clone();
-
- let task = cx.background_spawn(async move {
- if let Some(tag) = event
- .tags
- .find(TagKind::custom("P"))
- .and_then(|tag| tag.content())
- {
- if let Ok(public_key) = PublicKey::from_str(tag) {
- let secret = local_signer
- .nip44_decrypt(&public_key, &event.content)
- .await?;
-
- let keys = Arc::new(Keys::parse(&secret)?);
-
- // Update global state with new device keys
- set_device_keys(keys).await;
- log::info!("Received master keys");
-
- Ok(())
- } else {
- Err(anyhow!("Public Key is invalid"))
- }
- } else {
- Err(anyhow!("Failed to decrypt the Master Keys"))
- }
- });
-
- cx.spawn_in(window, |_, mut cx| async move {
- // No need to update if device keys are already available
- if get_device_keys().await.is_some() {
- return;
- }
-
- if let Err(e) = task.await {
- cx.update(|window, cx| {
- window.push_notification(
- Notification::error(format!("Failed to decrypt: {}", e)),
- cx,
- );
- })
- .ok();
- } else {
- cx.update(|window, cx| {
- window.close_all_modals(cx);
- window.push_notification(
- Notification::success("Device Keys request has been approved"),
- cx,
- );
- })
- .ok();
- }
- })
- .detach();
- }
-
- /// Received Master Keys request from other Nostr client
- ///
- /// NIP-4e:
- pub fn recv_request(&mut self, event: Event, window: &mut Window, cx: &mut Context) {
- let Some(target_pubkey) = event
- .tags
- .find(TagKind::custom("pubkey"))
- .and_then(|tag| tag.content())
- .and_then(|content| PublicKey::parse(content).ok())
- else {
- log::error!("Invalid public key.");
- return;
- };
-
- // Prevent processing duplicate requests
- if self.requesters.read(cx).contains(&target_pubkey) {
- return;
- }
-
- self.add_requester(target_pubkey, cx);
-
- let client = get_client();
- let read_keys = cx.read_credentials(MASTER_KEYRING);
- let local_signer = self.client_keys.clone();
-
- let device_name = event
- .tags
- .find(TagKind::Client)
- .and_then(|tag| tag.content())
- .unwrap_or("Other Device")
- .to_owned();
-
- let response = window.prompt(
- gpui::PromptLevel::Info,
- "Requesting Device Keys",
- Some(
- format!(
- "{} is requesting shared device keys stored in this device",
- device_name
- )
- .as_str(),
- ),
- &["Approve", "Deny"],
- cx,
- );
-
- cx.spawn_in(window, |_, cx| async move {
- match response.await {
- Ok(0) => {
- if let Ok(Some((_, secret))) = read_keys.await {
- let local_pubkey = local_signer.get_public_key().await?;
-
- // Get device's secret key
- let device_secret = SecretKey::from_slice(&secret)?;
- let device_secret_hex = device_secret.to_secret_hex();
-
- // Encrypt device's secret key by using NIP-44
- let content = local_signer
- .nip44_encrypt(&target_pubkey, &device_secret_hex)
- .await?;
-
- // Create pubkey tag for other device (lowercase p)
- let other_tag = Tag::public_key(target_pubkey);
-
- // Create pubkey tag for this device (uppercase P)
- let local_tag = Tag::custom(
- TagKind::SingleLetter(SingleLetterTag::uppercase(Alphabet::P)),
- vec![local_pubkey.to_hex()],
- );
-
- // Create event builder
- let kind = Kind::Custom(DEVICE_RESPONSE_KIND);
- let tags = vec![other_tag, local_tag];
- let builder = EventBuilder::new(kind, content).tags(tags);
-
- cx.background_spawn(async move {
- if let Err(err) = client.send_event_builder(builder).await {
- log::error!("Failed to send device keys to other client: {}", err);
- } else {
- log::info!("Sent device keys to other client");
- }
- })
- .await;
-
- Ok(())
- } else {
- Err(anyhow!("Device Keys not found"))
- }
- }
- _ => Ok(()),
- }
- })
- .detach();
- }
-
- /// Show setup relays modal
- ///
- /// NIP-17:
- pub fn render_setup_relays(&mut self, window: &mut Window, cx: &mut Context) {
- let relays = relays::init(window, cx);
-
- window.open_modal(cx, move |this, window, cx| {
- let is_loading = relays.read(cx).loading();
-
- this.keyboard(false)
- .closable(false)
- .width(px(430.))
- .title("Your Messaging Relays are not configured")
- .child(relays.clone())
- .footer(
- div()
- .p_2()
- .border_t_1()
- .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
- .child(
- Button::new("update_inbox_relays_btn")
- .label("Update")
- .primary()
- .bold()
- .rounded(ButtonRounded::Large)
- .w_full()
- .loading(is_loading)
- .on_click(window.listener_for(&relays, |this, _, window, cx| {
- this.update(window, cx);
- })),
- ),
- )
- });
- }
-
- /// Show waiting modal
- ///
- /// NIP-4e:
- pub fn render_waiting_modal(&mut self, window: &mut Window, cx: &mut Context) {
- window.open_modal(cx, move |this, _window, cx| {
- let msg = format!(
- "Please open {} and approve sharing device keys request.",
- get_device_name()
- );
-
- this.keyboard(false)
- .closable(false)
- .width(px(430.))
- .child(
- div()
- .flex()
- .items_center()
- .justify_center()
- .size_full()
- .p_4()
- .child(
- div()
- .flex()
- .flex_col()
- .items_center()
- .justify_center()
- .size_full()
- .child(
- div()
- .flex()
- .flex_col()
- .text_sm()
- .child(
- div()
- .font_semibold()
- .child("You're using a new device."),
- )
- .child(
- div()
- .text_color(
- cx.theme()
- .base
- .step(cx, ColorScaleStep::ELEVEN),
- )
- .line_height(relative(1.3))
- .child(msg),
- ),
- ),
- ),
- )
- .footer(
- div()
- .p_4()
- .border_t_1()
- .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
- .w_full()
- .flex()
- .gap_2()
- .items_center()
- .justify_center()
- .text_xs()
- .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
- .child(Indicator::new().small())
- .child("Waiting for approval ..."),
- )
- });
- }
-}
diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs
index 3848134..7a0709f 100644
--- a/crates/app/src/main.rs
+++ b/crates/app/src/main.rs
@@ -1,16 +1,11 @@
-use anyhow::anyhow;
use asset::Assets;
-use chats::registry::ChatRegistry;
-use device::Device;
+use chats::ChatRegistry;
use futures::{select, FutureExt};
#[cfg(not(target_os = "linux"))]
use global::constants::APP_NAME;
use global::{
- constants::{
- ALL_MESSAGES_SUB_ID, APP_ID, BOOTSTRAP_RELAYS, DEVICE_ANNOUNCEMENT_KIND,
- DEVICE_REQUEST_KIND, DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, NEW_MESSAGE_SUB_ID,
- },
- get_client, get_device_keys, set_device_name,
+ constants::{ALL_MESSAGES_SUB_ID, APP_ID, BOOTSTRAP_RELAYS, NEW_MESSAGE_SUB_ID},
+ get_client,
};
use gpui::{
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
@@ -21,16 +16,15 @@ use gpui::{point, SharedString, TitlebarOptions};
#[cfg(target_os = "linux")]
use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::{
- nips::nip59::UnwrappedGift, pool::prelude::ReqExitPolicy, Event, Filter, Keys, Kind, PublicKey,
- RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, TagKind,
+ pool::prelude::ReqExitPolicy, Event, Filter, Keys, Kind, PublicKey, RelayMessage,
+ RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId,
};
use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
use ui::{theme::Theme, Root};
-use views::startup;
pub(crate) mod asset;
-pub(crate) mod device;
+pub(crate) mod chat_space;
pub(crate) mod views;
actions!(coop, [Quit]);
@@ -39,12 +33,6 @@ actions!(coop, [Quit]);
enum Signal {
/// Receive event
Event(Event),
- /// Receive request master key event
- RequestMasterKey(Event),
- /// Receive approve master key event
- ReceiveMasterKey(Event),
- /// Receive announcement event
- ReceiveAnnouncement,
/// Receive EOSE
Eose,
}
@@ -121,7 +109,6 @@ fn main() {
let rng_keys = Keys::generate();
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
- let device_id = SubscriptionId::new(DEVICE_SUB_ID);
let mut notifications = client.notifications();
while let Ok(notification) = notifications.recv().await {
@@ -133,7 +120,7 @@ fn main() {
} => {
match event.kind {
Kind::GiftWrap => {
- if let Ok(gift) = handle_gift_wrap(&event).await {
+ if let Ok(gift) = client.unwrap_gift_wrap(&event).await {
// Sign the rumor with the generated keys,
// this event will be used for internal only,
// and NEVER send to relays.
@@ -161,45 +148,12 @@ fn main() {
handle_metadata(pubkeys).await;
}
- Kind::Custom(DEVICE_REQUEST_KIND) => {
- log::info!("Received device keys request");
-
- _ = event_tx
- .send(Signal::RequestMasterKey(event.into_owned()))
- .await;
- }
- Kind::Custom(DEVICE_RESPONSE_KIND) => {
- log::info!("Received device keys approval");
-
- _ = event_tx
- .send(Signal::ReceiveMasterKey(event.into_owned()))
- .await;
- }
- Kind::Custom(DEVICE_ANNOUNCEMENT_KIND) => {
- log::info!("Device Announcement received");
-
- if let Ok(signer) = client.signer().await {
- if let Ok(public_key) = signer.get_public_key().await {
- if event.pubkey == public_key {
- if let Some(tag) = event
- .tags
- .find(TagKind::custom("client"))
- .and_then(|tag| tag.content())
- {
- set_device_name(tag).await;
- }
- }
- }
- }
- }
_ => {}
}
}
RelayMessage::EndOfStoredEvents(subscription_id) => {
if all_id == *subscription_id {
_ = event_tx.send(Signal::Eose).await;
- } else if device_id == *subscription_id {
- _ = event_tx.send(Signal::ReceiveAnnouncement).await;
}
}
_ => {}
@@ -256,53 +210,26 @@ fn main() {
})
.detach();
- // Initialize components
- ui::init(cx);
-
- // Initialize chat global state
- chats::registry::init(cx);
-
- // Initialize device
- device::init(window, cx);
-
+ // Root Entity
cx.new(|cx| {
- let root = Root::new(startup::init(window, cx).into(), window, cx);
-
+ // Initialize components
+ ui::init(cx);
+ // Initialize chat state
+ chats::init(cx);
+ // Initialize account state
+ account::init(cx);
// Spawn a task to handle events from nostr channel
cx.spawn_in(window, |_, mut cx| async move {
+ let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap();
+
while let Ok(signal) = event_rx.recv().await {
- cx.update(|window, cx| {
+ cx.update(|_, cx| {
match signal {
Signal::Eose => {
- if let Some(chats) = ChatRegistry::global(cx) {
- chats.update(cx, |this, cx| this.load_chat_rooms(cx))
- }
+ chats.update(cx, |this, cx| this.load_chat_rooms(cx));
}
Signal::Event(event) => {
- if let Some(chats) = ChatRegistry::global(cx) {
- chats.update(cx, |this, cx| this.push_message(event, cx))
- }
- }
- Signal::ReceiveAnnouncement => {
- if let Some(device) = Device::global(cx) {
- device.update(cx, |this, cx| {
- this.setup_device(window, cx);
- });
- }
- }
- Signal::ReceiveMasterKey(event) => {
- if let Some(device) = Device::global(cx) {
- device.update(cx, |this, cx| {
- this.recv_approval(event, window, cx);
- });
- }
- }
- Signal::RequestMasterKey(event) => {
- if let Some(device) = Device::global(cx) {
- device.update(cx, |this, cx| {
- this.recv_request(event, window, cx);
- });
- }
+ chats.update(cx, |this, cx| this.push_message(event, cx));
}
};
})
@@ -311,43 +238,24 @@ fn main() {
})
.detach();
- root
+ Root::new(chat_space::init(window, cx).into(), window, cx)
})
})
.expect("Failed to open window. Please restart the application.");
});
}
-async fn handle_gift_wrap(gift_wrap: &Event) -> Result {
- let client = get_client();
-
- if let Some(device) = get_device_keys().await {
- // Try to unwrap with the device keys first
- match UnwrappedGift::from_gift_wrap(&device, gift_wrap).await {
- Ok(event) => Ok(event),
- Err(_) => {
- // Try to unwrap again with the user's signer
- let signer = client.signer().await?;
- let event = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;
- Ok(event)
- }
- }
- } else {
- Err(anyhow!("Signer not found"))
- }
-}
-
async fn handle_metadata(buffer: HashSet) {
let client = get_client();
let opts = SubscribeAutoCloseOptions::default()
.exit_policy(ReqExitPolicy::ExitOnEOSE)
- .idle_timeout(Some(Duration::from_secs(2)));
+ .idle_timeout(Some(Duration::from_secs(1)));
let filter = Filter::new()
.authors(buffer.iter().cloned())
- .limit(buffer.len() * 2)
- .kinds(vec![Kind::Metadata, Kind::Custom(DEVICE_ANNOUNCEMENT_KIND)]);
+ .limit(100)
+ .kinds(vec![Kind::Metadata, Kind::UserStatus]);
if let Err(e) = client.subscribe(filter, Some(opts)).await {
log::error!("Failed to sync metadata: {e}");
diff --git a/crates/app/src/views/chat.rs b/crates/app/src/views/chat.rs
index ca3d649..2b03dd4 100644
--- a/crates/app/src/views/chat.rs
+++ b/crates/app/src/views/chat.rs
@@ -1,6 +1,6 @@
use anyhow::anyhow;
use async_utility::task::spawn;
-use chats::{registry::ChatRegistry, room::Room};
+use chats::{room::Room, ChatRegistry};
use common::{
last_seen::LastSeen,
profile::NostrProfile,
@@ -37,14 +37,10 @@ pub fn init(
window: &mut Window,
cx: &mut App,
) -> Result>, anyhow::Error> {
- if let Some(chats) = ChatRegistry::global(cx) {
- if let Some(room) = chats.read(cx).get(id, cx) {
- Ok(Arc::new(Chat::new(id, room, window, cx)))
- } else {
- Err(anyhow!("Chat room is not exist"))
- }
+ if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) {
+ Ok(Arc::new(Chat::new(id, room, window, cx)))
} else {
- Err(anyhow!("Chat Registry is not initialized"))
+ Err(anyhow!("Chat room is not exist"))
}
}
diff --git a/crates/app/src/views/login.rs b/crates/app/src/views/login.rs
new file mode 100644
index 0000000..6bed937
--- /dev/null
+++ b/crates/app/src/views/login.rs
@@ -0,0 +1,433 @@
+use std::time::Duration;
+
+use account::Account;
+use common::utils::create_qr;
+use global::get_client_keys;
+use gpui::{
+ div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
+ EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled,
+ Subscription, Window,
+};
+use nostr_connect::prelude::*;
+use smallvec::{smallvec, SmallVec};
+use ui::{
+ button::{Button, ButtonVariants},
+ dock_area::panel::{Panel, PanelEvent},
+ input::{InputEvent, TextInput},
+ notification::Notification,
+ popup_menu::PopupMenu,
+ theme::{scale::ColorScaleStep, ActiveTheme},
+ ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
+};
+
+use crate::chat_space::ChatSpace;
+
+use super::onboarding;
+
+const INPUT_INVALID: &str = "You must provide a valid Private Key or Bunker.";
+
+pub fn init(window: &mut Window, cx: &mut App) -> Entity {
+ Login::new(window, cx)
+}
+
+pub struct Login {
+ // Inputs
+ key_input: Entity,
+ error_message: Entity